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:
@@ -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' },
|
||||
],
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user