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,56 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useUserStore } from '@/store/userStore'
import { loading } from '@/utils'
import HomeSkeleton from '../components/HomeSkeleton.vue'
import { getHomeLayoutConfig } from './registry'
import { HOME_SECTION_COMPONENTS } from './sections'
import type { HomeLayoutConfig, HomeSectionId } from './types'
const props = defineProps<{
layoutConfig?: HomeLayoutConfig
}>()
const userStore = useUserStore()
const layoutConfig = computed(() => props.layoutConfig ?? getHomeLayoutConfig())
const outerSections = computed(() =>
(layoutConfig.value.outerSections ?? []).filter((section) => section.visible !== false)
)
const innerSections = computed(() =>
layoutConfig.value.sections.filter((section) => section.visible !== false)
)
function resolveSectionComponent(sectionId: HomeSectionId) {
return HOME_SECTION_COMPONENTS[sectionId]
}
</script>
<template>
<div v-if="userStore.userInfo && !loading">
<component
:is="resolveSectionComponent(section.id)"
v-for="section in outerSections"
:key="`outer-${section.id}`"
/>
<div v-if="layoutConfig.wrapperClass" :class="layoutConfig.wrapperClass">
<component
:is="resolveSectionComponent(section.id)"
v-for="section in innerSections"
:key="section.id"
/>
</div>
<template v-else>
<component
:is="resolveSectionComponent(section.id)"
v-for="section in innerSections"
:key="section.id"
/>
</template>
</div>
<HomeSkeleton v-if="loading" class="w-full" />
</template>
@@ -0,0 +1,21 @@
import type { HomeLayoutConfig } from '../types'
/**
* چیدمان اختصاصی boursecu — فقط وقتی home_layout=boursecu یا پروژه boursecu باشد.
* شامل: دارایی بدون بک‌گراند، گرید ۶ باکس، نمودار کاتد/مفتول، و ...
*/
export const boursecuHomeLayoutConfig: HomeLayoutConfig = {
id: 'boursecu',
wrapperClass: 'rounded-lg mt-5',
outerSections: [
{ id: 'assets' },
{ id: 'boursecuQuickActions' },
{ id: 'limitedPriceAlert' },
],
sections: [
{ id: 'marketClosed' },
{ id: 'currentPrice' },
{ id: 'priceChart' },
{ id: 'priceStats' },
],
}
@@ -0,0 +1,40 @@
import type { HomeQuickActionItem } from '../types'
export const boursecuQuickActions: HomeQuickActionItem[] = [
{
id: 'buySell',
label: 'خرید و فروش',
icon: 'vuesax-linear:money-receive',
route: { name: 'buyProduct' },
},
{
id: 'wallet',
label: 'واریز و برداشت',
icon: 'vuesax-linear:wallet-1',
route: { name: 'wallet' },
},
{
id: 'assets',
label: 'دارایی ها',
icon: 'vuesax-linear:cup',
route: { name: 'fund' },
},
{
id: 'transactions',
label: 'تراکنش ها',
icon: 'vuesax-linear:receipt-item',
route: { name: 'transactions' },
},
{
id: 'physicalReceive',
label: 'تحویل فیزیکی',
icon: 'vuesax-linear:direct-inbox',
route: { name: 'physicalReceive' },
},
{
id: 'convert',
label: 'تبدیل',
icon: 'vuesax-linear:refresh-circle',
route: { name: 'buyProduct' },
},
]
@@ -0,0 +1,19 @@
import type { HomeLayoutConfig } from '../types'
/** چیدمان پیش‌فرض — همه پروژه‌ها به‌جز boursecu */
export const defaultHomeLayoutConfig: HomeLayoutConfig = {
id: 'default',
wrapperClass: 'p-1 from-primary rounded-lg p-px to-transparent mt-5',
outerSections: [{ id: 'limitedPriceAlert' }],
sections: [
{ id: 'garoosiAssets' },
{ id: 'mainActions' },
{ id: 'referral' },
{ id: 'assets' },
{ id: 'marketClosed' },
{ id: 'currentPrice' },
{ id: 'banners' },
{ id: 'priceChart' },
{ id: 'priceStats' },
],
}
+47
View File
@@ -0,0 +1,47 @@
import type { Component } from 'vue'
import { getConfigApp, isBoursecuProject } from '@/utils/config'
import type { HomeLayoutConfig, HomeLayoutId } from './types'
import { defaultHomeLayoutConfig } from './configs/default'
import { boursecuHomeLayoutConfig } from './configs/boursecu'
import HomeLayoutRenderer from './HomeLayoutRenderer.vue'
const HOME_LAYOUT_CONFIGS: Record<HomeLayoutId, HomeLayoutConfig> = {
default: defaultHomeLayoutConfig,
boursecu: boursecuHomeLayoutConfig,
}
const HOME_LAYOUT_COMPONENTS: Record<HomeLayoutId, Component> = {
default: HomeLayoutRenderer,
boursecu: HomeLayoutRenderer,
}
export const DEFAULT_HOME_LAYOUT_ID: HomeLayoutId = 'default'
export function isHomeLayoutId(value: unknown): value is HomeLayoutId {
return typeof value === 'string' && value in HOME_LAYOUT_CONFIGS
}
export function getHomeLayoutId(): HomeLayoutId {
const adminLayout = getConfigApp('home_layout')
if (isHomeLayoutId(adminLayout)) {
return adminLayout
}
if (isBoursecuProject()) {
return 'boursecu'
}
return DEFAULT_HOME_LAYOUT_ID
}
export function getHomeLayoutConfig(layoutId = getHomeLayoutId()): HomeLayoutConfig {
return HOME_LAYOUT_CONFIGS[layoutId] ?? HOME_LAYOUT_CONFIGS[DEFAULT_HOME_LAYOUT_ID]
}
export function resolveHomeLayoutComponent(layoutId = getHomeLayoutId()): Component {
return HOME_LAYOUT_COMPONENTS[layoutId] ?? HOME_LAYOUT_COMPONENTS[DEFAULT_HOME_LAYOUT_ID]
}
export function registerHomeLayoutConfig(layoutId: HomeLayoutId, config: HomeLayoutConfig) {
HOME_LAYOUT_CONFIGS[layoutId] = config
}
@@ -0,0 +1,16 @@
<script setup lang="ts">
import { computed, inject } from 'vue'
import AssetsSection from '../../components/AssetsSection.vue'
import { HOME_PAGE_CONTEXT_KEY } from '../../composables/useHomePage'
const ctx = inject(HOME_PAGE_CONTEXT_KEY)!
const isPlainAssets = computed(() => ctx.isBoursecuHomeLayout.value)
</script>
<template>
<AssetsSection
v-if="!ctx.isGaroosi.value"
:plain="isPlainAssets"
@open-gold-modal="ctx.openGoldModal"
@open-rial-modal="ctx.openRialModal"
/>
</template>
@@ -0,0 +1,15 @@
<script setup lang="ts">
import { inject } from 'vue'
import BannerSlider from '../../components/BannerSlider.vue'
import { HOME_PAGE_CONTEXT_KEY } from '../../composables/useHomePage'
const ctx = inject(HOME_PAGE_CONTEXT_KEY)!
</script>
<template>
<BannerSlider
v-if="ctx.isGaroosi.value && ctx.banners.value?.length"
:banners="ctx.banners.value"
@bannerClick="ctx.handleBannerClick"
/>
</template>
@@ -0,0 +1,30 @@
<script setup lang="ts">
import router from '@/router'
import HomeQuickActionBox from '../../components/HomeQuickActionBox.vue'
import { boursecuQuickActions } from '../configs/boursecuQuickActions'
import type { HomeQuickActionItem } from '../types'
const props = withDefaults(
defineProps<{
actions?: HomeQuickActionItem[]
}>(),
{
actions: () => boursecuQuickActions,
}
)
function handleClick(action: HomeQuickActionItem) {
router.push(action.route)
}
</script>
<template>
<div class="grid grid-cols-3 gap-3 mt-4">
<HomeQuickActionBox
v-for="action in props.actions"
:key="action.id"
:action="action"
@click="handleClick(action)"
/>
</div>
</template>
@@ -0,0 +1,7 @@
<script setup lang="ts">
import CurrentPrice from '../../components/CurrentPrice.vue'
</script>
<template>
<CurrentPrice />
</template>
@@ -0,0 +1,23 @@
<script setup lang="ts">
import { inject } from 'vue'
import { useUserStore } from '@/store/userStore'
import { usePriceWithoutType } from '@/utils/usePrice'
import AssetItem from '../../components/AssetItem.vue'
import { HOME_PAGE_CONTEXT_KEY } from '../../composables/useHomePage'
const ctx = inject(HOME_PAGE_CONTEXT_KEY)!
const userStore = useUserStore()
</script>
<template>
<div v-if="ctx.isGaroosi.value" class="flex justify-between px-2 mb-2">
<AssetItem
label="دارایی طلایی:"
:value="`${(userStore?.userInfo?.wallet?.count_gold ?? 0).toFixed(3)} گرم`"
/>
<AssetItem
label="دارایی ریالی:"
:value="`${usePriceWithoutType(userStore?.userInfo?.wallet?.count_cash ?? 0)} ریال`"
/>
</div>
</template>
@@ -0,0 +1,28 @@
<script setup lang="ts">
import { computed, inject } from 'vue'
import { useUserStore } from '@/store/userStore'
import { usePriceWithoutType } from '@/utils/usePrice'
import Icon from '@/components/BasicPacks/BasicIcon.vue'
import { HOME_PAGE_CONTEXT_KEY } from '../../composables/useHomePage'
const ctx = inject(HOME_PAGE_CONTEXT_KEY)!
const userStore = useUserStore()
const isVisible = computed(
() =>
ctx.limitedPrice.value !== undefined &&
userStore.userInfo?.trade_active !== 1
)
</script>
<template>
<div
v-if="isVisible"
class="text-red text-xs flex gap-1 items-center justify-center mb-2"
>
<Icon icon="vuesax-linear:info-circle" size="16" />
حداقل مبلغ اولیه برای ثبت سفارش
{{ usePriceWithoutType(Number(ctx.limitedPrice.value?.value ?? 0)) }}
ریال می باشد.
</div>
</template>
@@ -0,0 +1,20 @@
<script setup lang="ts">
import { inject } from 'vue'
import ActionButton from '../../components/ActionButton.vue'
import { HOME_PAGE_CONTEXT_KEY } from '../../composables/useHomePage'
const ctx = inject(HOME_PAGE_CONTEXT_KEY)!
</script>
<template>
<div class="p-2 py-3 rounded-lg bg-tertiary">
<div class="flex justify-between">
<ActionButton
v-for="action in ctx.mainActions.value"
:key="action.name"
:action="action"
@click="action.handler"
/>
</div>
</div>
</template>
@@ -0,0 +1,14 @@
<script setup lang="ts">
import { inject } from 'vue'
import MarketClosedMessage from '../../components/MarketClosedMessage.vue'
import { HOME_PAGE_CONTEXT_KEY } from '../../composables/useHomePage'
const ctx = inject(HOME_PAGE_CONTEXT_KEY)!
</script>
<template>
<MarketClosedMessage
v-if="ctx.showClosedMessage.value"
:message="ctx.closedMessage.value as string"
/>
</template>
@@ -0,0 +1,22 @@
<script setup lang="ts">
import { computed, inject, toValue } from 'vue'
import PriceChart from '../../components/PriceChart.vue'
import { HOME_PAGE_CONTEXT_KEY } from '../../composables/useHomePage'
const ctx = inject(HOME_PAGE_CONTEXT_KEY)!
const layout = computed(() => (toValue(ctx.isBoursecuHomeLayout) ? 'boursecu' : 'default'))
const priceChartSeries = computed(() => toValue(ctx.priceChartSeries))
const chartSelection = computed(() => toValue(ctx.chartSelection))
const selectedChartMetal = computed(() => toValue(ctx.selectedChartMetal))
</script>
<template>
<PriceChart
:price-chart-series="priceChartSeries"
:layout="layout"
:chart-selection="chartSelection"
:selected-metal="selectedChartMetal"
@update-data="ctx.updateChartData"
@update:selected-metal="ctx.setChartMetal"
/>
</template>
@@ -0,0 +1,13 @@
<script setup lang="ts">
import { computed, inject } from 'vue'
import PriceStats from '../../components/PriceStats.vue'
import { price } from '@/utils/price'
import { HOME_PAGE_CONTEXT_KEY } from '../../composables/useHomePage'
const ctx = inject(HOME_PAGE_CONTEXT_KEY)!
const layout = computed(() => (ctx.isBoursecuHomeLayout.value ? 'boursecu' : 'default'))
</script>
<template>
<PriceStats :price="price" :layout="layout" />
</template>
@@ -0,0 +1,11 @@
<script setup lang="ts">
import { inject } from 'vue'
import ReferralSection from '../../components/ReferralSection.vue'
import { HOME_PAGE_CONTEXT_KEY } from '../../composables/useHomePage'
const ctx = inject(HOME_PAGE_CONTEXT_KEY)!
</script>
<template>
<ReferralSection v-if="ctx.isGaroosi.value" />
</template>
@@ -0,0 +1,31 @@
import type { Component } from 'vue'
import type { HomeSectionId } from '../types'
import LimitedPriceAlertSection from './LimitedPriceAlertSection.vue'
import GaroosiAssetsSection from './GaroosiAssetsSection.vue'
import MainActionsSection from './MainActionsSection.vue'
import BoursecuQuickActionsSection from './BoursecuQuickActionsSection.vue'
import ReferralSectionBlock from './ReferralSectionBlock.vue'
import AssetsSectionBlock from './AssetsSectionBlock.vue'
import MarketClosedSection from './MarketClosedSection.vue'
import CurrentPriceSection from './CurrentPriceSection.vue'
import BannersSection from './BannersSection.vue'
import PriceChartSection from './PriceChartSection.vue'
import PriceStatsSection from './PriceStatsSection.vue'
export const HOME_SECTION_COMPONENTS: Record<HomeSectionId, Component> = {
limitedPriceAlert: LimitedPriceAlertSection,
garoosiAssets: GaroosiAssetsSection,
mainActions: MainActionsSection,
boursecuQuickActions: BoursecuQuickActionsSection,
referral: ReferralSectionBlock,
assets: AssetsSectionBlock,
marketClosed: MarketClosedSection,
currentPrice: CurrentPriceSection,
banners: BannersSection,
priceChart: PriceChartSection,
priceStats: PriceStatsSection,
}
export function registerHomeSection(id: HomeSectionId, component: Component) {
HOME_SECTION_COMPONENTS[id] = component
}
+41
View File
@@ -0,0 +1,41 @@
/** چیدمان‌های صفحه اصلی — هر پروژه/ادمین می‌تواند یکی را انتخاب کند */
export type HomeLayoutId = 'default' | 'boursecu'
export type HomeSectionId =
| 'limitedPriceAlert'
| 'garoosiAssets'
| 'mainActions'
| 'boursecuQuickActions'
| 'referral'
| 'assets'
| 'marketClosed'
| 'currentPrice'
| 'banners'
| 'priceChart'
| 'priceStats'
export interface HomeQuickActionItem {
id: string
label: string
icon: string
route: { name: string; query?: Record<string, string> }
}
export interface HomeLayoutSectionConfig {
id: HomeSectionId
visible?: boolean
}
export interface HomeLayoutConfig {
id: HomeLayoutId
wrapperClass?: string
outerSections?: HomeLayoutSectionConfig[]
sections: HomeLayoutSectionConfig[]
}
export interface HomeActionItem {
name: string
icon: string
label: string
handler: () => void
}