Make boursecu branch boursecu-only and simplify app loading.

Remove multi-tenant branches and default views, show logo-only boot and splash screens, and polish boursecu transactions tab styling.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 12:35:42 +02:00
co-authored by Cursor
parent 2affe61dd3
commit 59cceaf581
76 changed files with 211 additions and 2932 deletions
+2 -13
View File
@@ -5,21 +5,12 @@
:class="{ 'app-header--scrolled': isHeaderScrolled }"
>
<div class="flex justify-between my-2 items-center">
<div v-if="isBoursecuProject()" class="flex items-center gap-2">
<div class="flex items-center gap-2">
<img :src="projectLogo" alt="" class="h-10 w-auto max-w-[9rem] object-contain" />
</div>
<div v-else class="flex items-center gap-2" @click.stop="router.push({ name: 'user' })">
<div class="z-100000 rounded-full border-1 text-primary border-primary">
<Icon icon="vuesax-linear:user" size="20" />
</div>
<div class="text-primary color-transition font-bold">
{{ userStore.userInfo?.name }}
</div>
</div>
<div class="flex items-center gap-2">
<button
v-if="isBoursecuProject()"
type="button"
class="mhome-header-btn"
@click="router.push({ name: 'user' })"
@@ -42,16 +33,14 @@
<script setup lang="ts">
import { computed, nextTick, onActivated, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useUserStore } from '@/store/userStore'
import router from '@/router'
import Icon from '@/components/BasicPacks/BasicIcon.vue'
import { getProjectMobileNavLogo, isBoursecuProject } from '@/utils/config'
import { getProjectMobileNavLogo } from '@/utils/config'
const headerRef = ref<HTMLElement | null>(null)
const isHeaderScrolled = ref(false)
const route = useRoute()
const userStore = useUserStore()
const projectLogo = computed(() => getProjectMobileNavLogo())
let intersectionObserver: IntersectionObserver | null = null
@@ -1,127 +1,18 @@
<template>
<div :class="rootClass">
<div
v-if="!boursecuLayout && meta?.has_count_based_products"
:class="rowClass"
class="flex justify-center w-full z-20 mb-2"
>
محصولات:
<div class="text-l mr-2 font-bold">{{ usePriceWithoutType(countBasedInventory, false) }} عدد</div>
</div>
<div class="assets-section assets-section--boursecu rounded-lg px-3 py-3 mt-2">
<BoursecuAssetsSummary
v-if="boursecuLayout"
variant="mobile"
icon-color="primary"
/>
<template v-else-if="meta?.single_metal && primaryMetalTotal">
<div :class="[rowClass, meta?.has_count_based_products ? 'mt-2' : '']" class="flex justify-center w-full z-20">
{{ primaryMetalTotal.label }}:
<div class="text-l mr-2 font-bold">{{ formatMetalTotal(primaryMetalTotal) }}</div>
<BasicIcon
icon="vuesax-linear:info-circle"
size="18"
:color="iconColor"
@click="$emit('openGoldModal', primaryMetalTotal)"
/>
</div>
<PortfolioProfitInsight variant="mobile" />
<div :class="[rowClass, 'mt-2']" class="flex justify-center w-full z-20">
دارایی ریالی:
<div class="text-l mr-2 font-bold">{{ usePriceWithoutType(cashWallet) }} ریال</div>
<BasicIcon
icon="vuesax-linear:info-circle"
size="18"
:color="iconColor"
@click="$emit('openRialModal')"
/>
</div>
</template>
<template v-else-if="!boursecuLayout">
<div
v-for="(metal, index) in metalTotals"
:key="metal.metal_id"
:class="[rowClass, index > 0 || meta?.has_count_based_products ? 'mt-2' : '']"
class="flex justify-center w-full z-20"
>
{{ metal.label }}:
<div class="text-l mr-2 font-bold">{{ formatMetalTotal(metal) }}</div>
<BasicIcon
v-if="metal.metal_id === metalTotals[0]?.metal_id"
icon="vuesax-linear:info-circle"
size="18"
:color="iconColor"
@click="$emit('openGoldModal', metal)"
/>
</div>
<PortfolioProfitInsight variant="mobile" />
<div :class="[rowClass, 'mt-2']" class="flex justify-center w-full z-20">
دارایی ریالی:
<div class="text-l mr-2 font-bold">{{ usePriceWithoutType(cashWallet) }} ریال</div>
<BasicIcon
icon="vuesax-linear:info-circle"
size="18"
:color="iconColor"
@click="$emit('openRialModal')"
/>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import BoursecuAssetsSummary from './BoursecuAssetsSummary.vue'
import PortfolioProfitInsight from './PortfolioProfitInsight.vue'
import { formatWeightAmount, usePriceWithoutType } from '@/utils/usePrice'
import { useWalletAssets } from '@/composables/useWalletAssets'
import type { WalletMetalTotal } from '@/models/walletAssets'
import { resolveSummaryMetalWeight } from '@/utils/productAssets'
const props = withDefaults(
defineProps<{
plain?: boolean
boursecuLayout?: boolean
}>(),
{ plain: false, boursecuLayout: false }
)
defineEmits<{
openGoldModal: [metal: WalletMetalTotal, modalText?: string, modalTitle?: string, modalContent?: string]
openRialModal: []
}>()
const { assetsSummary } = useWalletAssets()
const rowClass = computed(() => (props.plain ? 'text-primary' : 'text-tertiary'))
const iconColor = computed(() => (props.plain ? 'primary' : 'tertiary'))
const rootClass = computed(() => {
if (props.boursecuLayout) {
return 'assets-section assets-section--boursecu rounded-lg px-3 py-3 mt-2'
}
if (props.plain) {
return 'py-2'
}
return 'bg-primary rounded-lg p-2 py-3 mt-2'
})
const meta = computed(() => assetsSummary.value?.meta)
const metalTotals = computed(() => assetsSummary.value?.metal_totals ?? [])
const primaryMetalTotal = computed(() => metalTotals.value[0])
const cashWallet = computed(() => assetsSummary.value?.cash_wallet ?? 0)
const inventoryCards = computed(() => assetsSummary.value?.inventory_cards ?? [])
const countBasedInventory = computed(() =>
(assetsSummary.value?.count_products ?? []).reduce((sum, item) => sum + Number(item.quantity ?? 0), 0)
)
function formatMetalTotal(metal: WalletMetalTotal) {
return `${formatWeightAmount(resolveSummaryMetalWeight(metal, inventoryCards.value))}${metal.unit}`
}
</script>
@@ -4,12 +4,12 @@
:class="containerClass"
>
<template v-if="displayState !== 'open'">
<div class="status-box" :class="isBoursecu ? 'status-box--boursecu' : 'status-box--default'">
<div class="status-box status-box--boursecu">
<div class="status-box__icon-wrap">
<Icon
:icon="displayState === 'closed' ? 'vuesax-linear:lock' : 'vuesax-linear:info-circle'"
size="22"
:color="isBoursecu ? '#ffffff' : '#64748b'"
color="#ffffff"
/>
</div>
<div class="status-box__title">{{ statusTitle }}</div>
@@ -18,8 +18,8 @@
</template>
<template v-else>
<div class="price-label" :class="isBoursecu ? 'price-label--boursecu' : 'text-gray-600 text-[12px] ml-2 w-full text-center'">
{{ priceLabel }}
<div class="price-label price-label--boursecu">
قیمت لحظه‌ای کاتد مس
</div>
<div class="w-full items-center flex justify-center">
<div v-if="price" class="flex gap-1 text-xs items-center">
@@ -38,11 +38,10 @@
</div>
<Vue3Odometer
:value="displayGoldVal"
class="o-text !text-xl font-bold"
:class="isBoursecu ? 'price-odometer--boursecu' : ''"
class="o-text !text-xl font-bold price-odometer--boursecu"
style="direction: ltr"
/>
<span :class="isBoursecu ? 'price-unit--boursecu' : ''">ریال</span>
<span class="price-unit--boursecu">ریال</span>
</div>
</div>
</template>
@@ -52,9 +51,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import Icon from '@/components/BasicPacks/BasicIcon.vue'
import { useConfigStore } from '@/store/configStore'
import { useUserStore } from '@/store/userStore'
import { isBoursecuProject } from '@/utils/config'
import { getTradeBuyPrice, lastPrice, percentPriceChanged, price } from '@/utils/price'
import {
NO_QUOTE_MESSAGE,
@@ -65,13 +62,7 @@ import Vue3Odometer from 'vue3-odometer'
import 'odometer/themes/odometer-theme-default.css'
import { formatSignedPercentChange } from '@/utils/usePrice'
const config = useConfigStore()
const userStore = useUserStore()
const isBoursecu = isBoursecuProject()
const priceLabel = computed(() =>
isBoursecu ? 'قیمت لحظه‌ای کاتد مس' : `قیمت خرید از ${config.config.name_fa}`
)
const displayState = computed(() => getPriceDisplayState())
@@ -91,9 +82,6 @@ const statusHint = computed(() => {
})
const containerClass = computed(() => {
if (!isBoursecu) {
return 'border-gray-200 border-1'
}
if (displayState.value !== 'open') {
return 'boursecu-price-box boursecu-price-box--closed'
}
@@ -190,18 +178,6 @@ void price
color: rgba(255, 255, 255, 0.78);
}
.status-box--default .status-box__icon-wrap {
background: #f1f5f9;
}
.status-box--default .status-box__title {
color: #334155;
}
.status-box--default .status-box__hint {
color: #64748b;
}
.price-change-badge {
display: inline-flex;
align-items: center;
@@ -1,6 +1,5 @@
<script setup lang="ts">
import Icon from '@/components/BasicPacks/BasicIcon.vue'
import { isBoursecuProject } from '@/utils/config'
import type { HomeQuickActionItem } from '../layouts/types'
defineProps<{
@@ -10,15 +9,12 @@ defineProps<{
defineEmits<{
click: []
}>()
const isBoursecu = isBoursecuProject()
</script>
<template>
<button
type="button"
class="home-quick-action-box flex flex-col items-center justify-center gap-2 rounded-xl border-2 p-1 min-h-[96px] w-full"
:class="{ 'home-quick-action-box--boursecu': isBoursecu }"
class="home-quick-action-box home-quick-action-box--boursecu flex flex-col items-center justify-center gap-2 rounded-xl border-2 p-1 min-h-[96px] w-full"
@click="$emit('click')"
>
<div class="flex h-12 w-12 shrink-0 items-center justify-center">
@@ -126,7 +126,6 @@ const { width: chartWidth } = useElementSize(chartContainerRef)
const seriesLength = computed(() => props.priceChartSeries?.[0]?.data?.length ?? 0)
const amplifyKatodChart = computed(() =>
shouldAmplifyKatodMesChart({
isBoursecu: props.layout === 'boursecu',
metalTypeSetting: settingsStore.metalTypeSettingValue,
})
)
@@ -141,7 +140,6 @@ const katodDayRange = computed(() => {
return resolveMetalDayRangePrices({
price: price.value,
metal: props.selectedMetal,
isBoursecuLayout: true,
metalRatio: selectedMetalChartRatio.value,
})
})
@@ -2,7 +2,7 @@
<div
class="w-full flex justify-evenly rounded-lg py-2 home_appear_fade_in"
:class="[
isBoursecuLayout ? 'mt-8 pt-2' : 'mt-6',
'mt-8 pt-2',
variant === 'mobile' ? 'pb-20' : 'mt-0 pb-0',
]"
>
@@ -34,7 +34,6 @@
import { computed } from 'vue'
import { usePriceWithoutType } from '../../../../utils/usePrice'
import { price } from '@/utils/price'
import { getHomeLayoutId } from '../layouts/registry'
import { resolveMetalDayRangePrices, usePriceChart } from '../composables/usePriceChart'
withDefaults(
@@ -47,13 +46,11 @@ withDefaults(
)
const { selectedChartMetal, selectedMetalChartRatio } = usePriceChart()
const isBoursecuLayout = computed(() => getHomeLayoutId() === 'boursecu')
const rangePrices = computed(() =>
resolveMetalDayRangePrices({
price: price.value,
metal: selectedChartMetal.value,
isBoursecuLayout: isBoursecuLayout.value,
metalRatio: selectedMetalChartRatio.value,
})
)
+4 -27
View File
@@ -7,20 +7,16 @@ import { useWalletAssets } from '@/composables/useWalletAssets'
import type { WalletMetalTotal } from '@/models/walletAssets'
import {
formatBoursecuMetalModalText,
formatMetalAssetsInfoText,
formatRialAssetsInfoText,
resolveAssetsRialValue,
} from '@/utils/productAssets'
import { useConfigStore } from '@/store/configStore'
import { useSettingsStore } from '@/store/settingsStore'
import { getTradeBuyPrice, lastPrice, stopLastPricePolling } from '@/utils/price'
import { getStoreClosedMessage, isStoreClosed } from '@/utils/storeHours'
import { message, Modal } from 'ant-design-vue'
import { getReferralShareText, getReferralShareTitle, shareReferralText } from '@/utils/referralShare'
import { useBanners } from './useBanners'
import { usePriceChart, type ChartMetalType } from './usePriceChart'
import type { HomeActionItem } from '../layouts/types'
import { getHomeLayoutId } from '../layouts/registry'
import { initHomePageData, resetHomePageData, useHomePageLoading } from './useHomePageData'
import { ensureAuthenticatedRoute } from '@/app/setupRouter'
import { resetWalletPageData } from '../../wallet/composables/useWalletPageData'
@@ -30,7 +26,6 @@ import { resetProductsPageData } from '../../products/composables/useProductsPag
export interface HomePageContext {
homePageLoading: ReturnType<typeof useHomePageLoading>['homePageLoading']
isGaroosi: ReturnType<typeof computed<boolean>>
bannerEnabled: ReturnType<typeof computed<boolean>>
showClosedMessage: ReturnType<typeof computed<boolean>>
closedMessage: ReturnType<typeof computed>
@@ -41,7 +36,6 @@ export interface HomePageContext {
updateChartData: ReturnType<typeof usePriceChart>['updateChartData']
selectedChartMetal: ReturnType<typeof usePriceChart>['selectedChartMetal']
setChartMetal: (metal: ChartMetalType) => void
isBoursecuHomeLayout: ReturnType<typeof computed<boolean>>
handleBannerClick: (banner: { typeClick?: string; modalText?: string; link?: string }) => void
openGoldModal: (
metal: WalletMetalTotal,
@@ -57,7 +51,6 @@ export const HOME_PAGE_CONTEXT_KEY: InjectionKey<HomePageContext> = Symbol('home
export function useHomePage() {
const userStore = useUserStore()
const walletStore = useWalletStore()
const configStore = useConfigStore()
const settingsStore = useSettingsStore()
const { assetsSummary, clearWalletAssets } = useWalletAssets()
@@ -76,9 +69,7 @@ export function useHomePage() {
selectedChartMetal,
setChartMetal,
} = usePriceChart()
const isBoursecuHomeLayout = computed(() => getHomeLayoutId() === 'boursecu')
const isGaroosi = computed(() => configStore.config?.name === 'garoosi')
const bannerEnabled = computed(() => settingsStore.isSettingTruthy('banner_enabled'))
const closedMessage = computed(() => userStore.getValueByKey('closed_message'))
const marketClosedText = computed(() => {
@@ -100,7 +91,7 @@ export function useHomePage() {
{
name: 'deposit',
icon: 'vuesax-linear:card-add',
label: isGaroosi.value ? 'واریز و برداشت' : 'واریز نقدی',
label: 'واریز نقدی',
handler: () => router.push({ name: 'wallet', query: { state: 'deposit' } }),
},
{
@@ -114,21 +105,11 @@ export function useHomePage() {
{
name: 'assets',
icon: 'vuesax-linear:cup',
label: isGaroosi.value ? 'دعوت از دوستان' : 'دارایی ها',
handler: () => (isGaroosi.value ? shareCode() : router.push({ name: 'fund' })),
label: 'دارایی ها',
handler: () => router.push({ name: 'fund' }),
},
])
async function shareCode() {
const text = getReferralShareText({
userName: userStore.userInfo?.name,
projectName: configStore.config.name,
referralId: userStore.userInfo?.referral_id,
})
await shareReferralText(text, getReferralShareTitle(configStore.config.name))
}
function copyCode() {
const code = userStore.userInfo?.referral_id
if (code) {
@@ -178,9 +159,7 @@ export function useHomePage() {
const title =
modalTitle ??
modalText ??
(isBoursecuHomeLayout.value
? formatBoursecuMetalModalText(metal)
: formatMetalAssetsInfoText(metal))
formatBoursecuMetalModalText(metal)
Modal.confirm({
title,
@@ -230,7 +209,6 @@ export function useHomePage() {
const context: HomePageContext = {
homePageLoading,
isGaroosi,
bannerEnabled,
showClosedMessage,
closedMessage: marketClosedText,
@@ -241,7 +219,6 @@ export function useHomePage() {
updateChartData,
selectedChartMetal,
setChartMetal,
isBoursecuHomeLayout,
handleBannerClick,
openGoldModal,
openRialModal,
@@ -2,7 +2,6 @@ import { ref } from 'vue'
import { getLastPrices, startLastPricePolling, stopLastPricePolling } from '@/utils/price'
import { useWalletAssets } from '@/composables/useWalletAssets'
import { clearProductsCatalog } from '@/composables/useProductsCatalog'
import { getHomeLayoutId } from '../layouts/registry'
import { fetchHomeChartPrices, loadHomeMaftolOffset, resetHomePriceChart } from './usePriceChart'
import { loadHomeBanners, resetHomeBanners } from './useBanners'
import { loadHomeFunds, resetHomeFunds } from './useUserFunds'
@@ -24,10 +23,7 @@ export function isHomePageDataInitialized() {
export async function initHomePageData(force = false) {
if (!force && initialized.value) {
startLastPricePolling()
if (getHomeLayoutId() === 'boursecu') {
void loadHomeMaftolOffset(false)
}
void loadHomeMaftolOffset(false)
return
}
@@ -49,9 +45,7 @@ export async function initHomePageData(force = false) {
loadHomeBanners(force),
])
if (getHomeLayoutId() === 'boursecu') {
await loadHomeMaftolOffset(force)
}
await loadHomeMaftolOffset(force)
initialized.value = true
} catch (error) {
@@ -2,7 +2,6 @@ import { computed, ref, watch } from 'vue'
import { getAllPrices } from '@/api/home'
import type { Product } from '@/models/products'
import { getProductsCatalog, getProductsCatalogRef } from '@/composables/useProductsCatalog'
import { getHomeLayoutId } from '../layouts/registry'
import { getProductBuyPrice, resolvePriceOffsetRatio } from '@/utils/productPrice'
import { isKatodMesGroup, isMaftolMesGroup, isShamshMesGroup } from '@/utils/productDealMethod'
import { getTradeBuyPrice, getTradeSellPrice, lastPrice } from '@/utils/price'
@@ -31,10 +30,6 @@ function resolveChartProducts(products: Product[]) {
/** نسبت مفتول به کاتد — reactive به قیمت لحظه‌ای و کاتالوگ محصولات */
const maftolChartRatio = computed(() => {
if (getHomeLayoutId() !== 'boursecu') {
return 0
}
void lastPrice.value?.price_last
const { katod, maftol } = resolveChartProducts(catalogProducts.value)
@@ -43,10 +38,6 @@ const maftolChartRatio = computed(() => {
/** نسبت شمش مس به کاتد */
const shamshChartRatio = computed(() => {
if (getHomeLayoutId() !== 'boursecu') {
return 0
}
void lastPrice.value?.price_last
const { katod, shamsh } = resolveChartProducts(catalogProducts.value)
@@ -100,13 +91,12 @@ function applyChartRatio(basePrice: number, ratio: number) {
export function resolveMetalDayRangePrices(options: {
price: { up?: { buy?: number; sell?: number }; buy?: number; sell?: number; price_last?: number } | null | undefined
metal: ChartMetalType
isBoursecuLayout: boolean
metalRatio: number
}) {
const baseHigh = options.price?.up?.buy ?? getTradeBuyPrice(options.price as Parameters<typeof getTradeBuyPrice>[0])
const baseLow = options.price?.up?.sell ?? getTradeSellPrice(options.price as Parameters<typeof getTradeSellPrice>[0])
if (!options.isBoursecuLayout || options.metal === 'katod') {
if (options.metal === 'katod') {
return { high: baseHigh, low: baseLow }
}
@@ -176,7 +166,7 @@ function buildChartPoints(items: Array<{ timestamp: number; value: number }>): C
function syncChartSeries() {
const ratio = selectedMetalChartRatio.value
const data =
getHomeLayoutId() === 'boursecu' && selectedChartMetal.value !== 'katod'
selectedChartMetal.value !== 'katod'
? rawChartPoints.value.map(
([timestamp, value]) =>
[timestamp, applyChartRatio(value, ratio)] as ChartPoint
@@ -221,11 +211,6 @@ async function fetchPrices(force = false) {
}
async function loadMetalChartOffsets(force = false) {
if (getHomeLayoutId() !== 'boursecu') {
syncChartSeries()
return
}
if (!force && metalOffsetLoaded && catalogProducts.value.length > 0) {
return
}
@@ -258,7 +243,7 @@ function updateChartData(timeline: string) {
function setChartMetal(metal: ChartMetalType) {
selectedChartMetal.value = metal
if (metal !== 'katod' && getHomeLayoutId() === 'boursecu' && !catalogProducts.value.length) {
if (metal !== 'katod' && !catalogProducts.value.length) {
void loadMetalChartOffsets(true)
}
}
@@ -1,17 +1,11 @@
<template>
<BoursecuDesktopHome v-if="layoutId === 'boursecu'" @logout="$emit('logout')" />
<DefaultDesktopHome v-else @logout="$emit('logout')" />
<BoursecuDesktopHome @logout="$emit('logout')" />
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { getHomeLayoutId } from '../layouts/registry'
import BoursecuDesktopHome from './layouts/BoursecuDesktopHome.vue'
import DefaultDesktopHome from './layouts/DefaultDesktopHome.vue'
defineEmits<{
logout: []
}>()
const layoutId = computed(() => getHomeLayoutId())
</script>
@@ -13,6 +13,7 @@ defineEmits<{
<DesktopHomeCard title="دارایی‌های من">
<AssetsSection
plain
boursecu-layout
class="home-desktop-assets"
@open-gold-modal="$emit('openGoldModal', $event)"
@open-rial-modal="$emit('openRialModal')"
@@ -1,126 +1,11 @@
<script setup lang="ts">
import { computed } from 'vue'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import BoursecuAssetsSummary from '../../components/BoursecuAssetsSummary.vue'
import PortfolioProfitInsight from '../../components/PortfolioProfitInsight.vue'
import { formatWeightAmount, usePriceWithoutType } from '@/utils/usePrice'
import { useWalletAssets } from '@/composables/useWalletAssets'
import type { WalletMetalTotal } from '@/models/walletAssets'
import { resolveSummaryMetalWeight } from '@/utils/productAssets'
import { useHomePageContext } from '../../composables/useHomePage'
const emit = defineEmits<{
openGoldModal: [metal: WalletMetalTotal, modalText?: string, modalTitle?: string, modalContent?: string]
openRialModal: []
}>()
const { assetsSummary } = useWalletAssets()
const ctx = useHomePageContext()
const isBoursecuLayout = computed(() => ctx.isBoursecuHomeLayout.value)
const meta = computed(() => assetsSummary.value?.meta)
const metalTotals = computed(() => assetsSummary.value?.metal_totals ?? [])
const inventoryCards = computed(() => assetsSummary.value?.inventory_cards ?? [])
const cashWallet = computed(() => assetsSummary.value?.cash_wallet ?? 0)
const countBasedInventory = computed(() =>
(assetsSummary.value?.count_products ?? []).reduce((sum, item) => sum + Number(item.quantity ?? 0), 0),
)
type MetricItem = {
id: string
label: string
value: string
metal?: WalletMetalTotal
showInfo?: boolean
onInfo?: 'gold' | 'rial'
}
const metrics = computed<MetricItem[]>(() => {
const items: MetricItem[] = []
if (meta.value?.has_count_based_products) {
items.push({
id: 'products',
label: 'محصولات',
value: `${usePriceWithoutType(countBasedInventory.value, false)} عدد`,
})
}
if (meta.value?.single_metal && metalTotals.value[0]) {
const metal = metalTotals.value[0]
items.push({
id: `metal-${metal.metal_id}`,
label: metal.label,
value: formatMetalTotal(metal),
metal,
showInfo: true,
onInfo: 'gold',
})
} else {
metalTotals.value.forEach((metal, index) => {
items.push({
id: `metal-${metal.metal_id}`,
label: metal.label,
value: formatMetalTotal(metal),
metal,
showInfo: index === 0,
onInfo: 'gold',
})
})
}
items.push({
id: 'rial',
label: 'دارایی ریالی',
value: `${usePriceWithoutType(cashWallet.value)} ریال`,
showInfo: true,
onInfo: 'rial',
})
return items
})
function formatMetalTotal(metal: WalletMetalTotal) {
return `${formatWeightAmount(resolveSummaryMetalWeight(metal, inventoryCards.value))}${metal.unit}`
}
function handleInfo(metric: MetricItem) {
if (metric.onInfo === 'rial') {
emit('openRialModal')
return
}
if (metric.metal) {
emit('openGoldModal', metric.metal)
}
}
</script>
<template>
<BoursecuAssetsSummary
v-if="isBoursecuLayout"
variant="desktop"
icon-color="tertiary"
/>
<template v-else>
<PortfolioProfitInsight variant="desktop" />
<div class="dhome-metrics">
<article v-for="metric in metrics" :key="metric.id" class="dhome-metric-tile">
<div class="dhome-metric-tile__head">
<span class="dhome-metric-tile__label">{{ metric.label }}</span>
<button
v-if="metric.showInfo"
type="button"
class="dhome-metric-tile__info"
aria-label="اطلاعات بیشتر"
@click="handleInfo(metric)"
>
<BasicIcon icon="vuesax-linear:info-circle" size="14" />
</button>
</div>
<p class="dhome-metric-tile__value">{{ metric.value }}</p>
</article>
</div>
</template>
</template>
@@ -6,7 +6,7 @@ import DesktopHomeCard from '../components/DesktopHomeCard.vue'
const ctx = useHomePageContext()
const chartLayout = computed(() => (toValue(ctx.isBoursecuHomeLayout) ? 'boursecu' : 'default'))
const chartLayout = 'boursecu' as const
const priceChartSeries = computed(() => toValue(ctx.priceChartSeries))
const chartSelection = computed(() => toValue(ctx.chartSelection))
const selectedChartMetal = computed(() => toValue(ctx.selectedChartMetal))
@@ -15,7 +15,7 @@ const props = withDefaults(
const ctx = useHomePageContext()
const chartLayout = computed(() => (toValue(ctx.isBoursecuHomeLayout) ? 'boursecu' : 'default'))
const chartLayout = 'boursecu' as const
const priceChartSeries = computed(() => toValue(ctx.priceChartSeries))
const chartSelection = computed(() => toValue(ctx.chartSelection))
const selectedChartMetal = computed(() => toValue(ctx.selectedChartMetal))
@@ -1,30 +0,0 @@
<script setup lang="ts">
import { inject } from 'vue'
import { useUserStore } from '@/store/userStore'
import { usePriceWithoutType } from '@/utils/usePrice'
import DesktopHomeSection from './DesktopHomeSection.vue'
import { HOME_PAGE_CONTEXT_KEY } from '../../composables/useHomePage'
const ctx = inject(HOME_PAGE_CONTEXT_KEY)!
const userStore = useUserStore()
</script>
<template>
<DesktopHomeSection v-if="ctx.isGaroosi.value" title="دارایی‌ها">
<div class="dhome-garoosi-assets">
<div class="dhome-garoosi-assets__item">
<span class="dhome-garoosi-assets__label">دارایی طلایی</span>
<span class="dhome-garoosi-assets__value">
{{ (userStore?.userInfo?.wallet?.count_gold ?? 0).toFixed(3) }} گرم
</span>
</div>
<div class="dhome-garoosi-assets__divider" />
<div class="dhome-garoosi-assets__item">
<span class="dhome-garoosi-assets__label">دارایی ریالی</span>
<span class="dhome-garoosi-assets__value">
{{ usePriceWithoutType(userStore?.userInfo?.wallet?.count_cash ?? 0) }} ریال
</span>
</div>
</div>
</DesktopHomeSection>
</template>
@@ -1,17 +0,0 @@
<script setup lang="ts">
import { inject } from 'vue'
import { isBoursecuProject } from '@/utils/config'
import { HOME_PAGE_CONTEXT_KEY } from '../../composables/useHomePage'
const ctx = inject(HOME_PAGE_CONTEXT_KEY)!
const isBoursecu = isBoursecuProject()
</script>
<template>
<div v-if="!isBoursecu && ctx.showClosedMessage.value" class="dhome-alerts">
<div
class="dhome-alert dhome-alert--info"
v-html="ctx.closedMessage.value"
/>
</div>
</template>
@@ -1,15 +1,9 @@
<template>
<div class="dhome" :class="{ 'dhome--boursecu': isBoursecu }">
<div class="dhome dhome--boursecu">
<div class="dhome__container">
<slot />
</div>
</div>
</template>
<script lang="ts" setup>
import { isBoursecuProject } from '@/utils/config'
const isBoursecu = isBoursecuProject()
</script>
<style lang="less" src="../styles/desktop-home.less"></style>
@@ -2,17 +2,17 @@
<div
class="dhome-surface dhome-hero-price"
:class="{
'dhome-hero-price--boursecu-open': isBoursecu && displayState === 'open',
'dhome-hero-price--boursecu-closed': isBoursecu && displayState !== 'open',
'dhome-hero-price--boursecu-open': displayState === 'open',
'dhome-hero-price--boursecu-closed': displayState !== 'open',
}"
>
<template v-if="displayState !== 'open'">
<div class="dhome-hero-price__closed" :class="{ 'dhome-hero-price__closed--default': !isBoursecu }">
<div class="dhome-hero-price__closed">
<div class="dhome-hero-price__closed-icon">
<BasicIcon
:icon="displayState === 'closed' ? 'vuesax-linear:lock' : 'vuesax-linear:info-circle'"
size="22"
:color="isBoursecu ? '#ffffff' : '#64748b'"
color="#ffffff"
/>
</div>
<p class="dhome-hero-price__closed-title">{{ statusTitle }}</p>
@@ -21,10 +21,9 @@
</template>
<template v-else>
<span class="dhome-label" :class="{ 'dhome-label--boursecu': isBoursecu }">
{{ isBoursecu ? 'قیمت لحظه‌ای کاتد مس' : 'قیمت لحظه‌ای' }}
<span class="dhome-label dhome-label--boursecu">
قیمت لحظه‌ای کاتد مس
</span>
<p v-if="!isBoursecu" class="dhome-hero-price__brand">{{ config.config?.name_fa }}</p>
<div class="dhome-hero-price__value-row">
<span
v-if="percentChange !== 0"
@@ -39,10 +38,9 @@
</span>
<Vue3Odometer
:value="displayGoldVal"
class="dhome-hero-price__odometer o-text"
:class="{ 'dhome-hero-price__odometer--boursecu': isBoursecu }"
class="dhome-hero-price__odometer o-text dhome-hero-price__odometer--boursecu"
/>
<span class="dhome-hero-price__unit" :class="{ 'dhome-hero-price__unit--boursecu': isBoursecu }">ریال</span>
<span class="dhome-hero-price__unit dhome-hero-price__unit--boursecu">ریال</span>
</div>
</template>
</div>
@@ -53,9 +51,7 @@ import { computed } from 'vue'
import Vue3Odometer from 'vue3-odometer'
import 'odometer/themes/odometer-theme-default.css'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import { useConfigStore } from '@/store/configStore'
import { useUserStore } from '@/store/userStore'
import { isBoursecuProject } from '@/utils/config'
import { getTradeBuyPrice, lastPrice, percentPriceChanged } from '@/utils/price'
import {
NO_QUOTE_MESSAGE,
@@ -64,9 +60,7 @@ import {
} from '@/utils/storeHours'
import { formatSignedPercentChange } from '@/utils/usePrice'
const config = useConfigStore()
const userStore = useUserStore()
const isBoursecu = isBoursecuProject()
const displayState = computed(() => getPriceDisplayState())
@@ -152,18 +146,4 @@ const percentChange = computed(() => {
line-height: 1.7;
color: rgba(255, 255, 255, 0.78);
}
.dhome-hero-price__closed--default {
.dhome-hero-price__closed-icon {
background: #f1f5f9;
}
.dhome-hero-price__closed-title {
color: #334155;
}
.dhome-hero-price__closed-hint {
color: #64748b;
}
}
</style>
@@ -2,17 +2,14 @@
import { computed } from 'vue'
import { price } from '@/utils/price'
import { usePriceWithoutType } from '@/utils/usePrice'
import { getHomeLayoutId } from '../../layouts/registry'
import { resolveMetalDayRangePrices, usePriceChart } from '../../composables/usePriceChart'
const { selectedChartMetal, selectedMetalChartRatio } = usePriceChart()
const isBoursecuLayout = computed(() => getHomeLayoutId() === 'boursecu')
const rangePrices = computed(() =>
resolveMetalDayRangePrices({
price: price.value,
metal: selectedChartMetal.value,
isBoursecuLayout: isBoursecuLayout.value,
metalRatio: selectedMetalChartRatio.value,
})
)
@@ -1,14 +0,0 @@
<script setup lang="ts">
import { inject } from 'vue'
import DesktopHomeSection from './DesktopHomeSection.vue'
import ReferralSection from '../../components/ReferralSection.vue'
import { HOME_PAGE_CONTEXT_KEY } from '../../composables/useHomePage'
const ctx = inject(HOME_PAGE_CONTEXT_KEY)!
</script>
<template>
<DesktopHomeSection v-if="ctx.isGaroosi.value" title="دعوت از دوستان">
<ReferralSection />
</DesktopHomeSection>
</template>
@@ -7,7 +7,6 @@ import { useHomePageContext } from '../../composables/useHomePage'
import HomeSkeleton from '../../components/HomeSkeleton.vue'
import DesktopHomeShell from '../components/DesktopHomeShell.vue'
import DesktopHomeTopbar from '../components/DesktopHomeTopbar.vue'
import DesktopHomeAlerts from '../components/DesktopHomeAlerts.vue'
import DesktopLivePrice from '../components/DesktopLivePrice.vue'
import DesktopPriceRange from '../components/DesktopPriceRange.vue'
import DesktopAssetsMetrics from '../components/DesktopAssetsMetrics.vue'
@@ -29,7 +28,6 @@ const showSkeleton = computed(() => Boolean(getToken()) && !userInfo.value)
<template>
<DesktopHomeShell>
<DesktopHomeTopbar @logout="$emit('logout')" />
<DesktopHomeAlerts />
<HomeSkeleton v-if="showSkeleton" />
@@ -1,98 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/store/userStore'
import { getToken } from '@/utils'
import { useHomePageContext } from '../../composables/useHomePage'
import HomeSkeleton from '../../components/HomeSkeleton.vue'
import DesktopHomeShell from '../components/DesktopHomeShell.vue'
import DesktopHomeTopbar from '../components/DesktopHomeTopbar.vue'
import DesktopHomeAlerts from '../components/DesktopHomeAlerts.vue'
import DesktopLivePrice from '../components/DesktopLivePrice.vue'
import DesktopPriceRange from '../components/DesktopPriceRange.vue'
import DesktopAssetsMetrics from '../components/DesktopAssetsMetrics.vue'
import DesktopChartSection from '../components/DesktopChartSection.vue'
import DesktopDefaultActions from '../components/DesktopDefaultActions.vue'
import DesktopGaroosiAssets from '../components/DesktopGaroosiAssets.vue'
import DesktopReferralPanel from '../components/DesktopReferralPanel.vue'
import DesktopBannersPanel from '../components/DesktopBannersPanel.vue'
defineEmits<{
logout: []
}>()
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const ctx = useHomePageContext()
const showContent = computed(() => Boolean(userInfo.value))
const showSkeleton = computed(() => Boolean(getToken()) && !userInfo.value)
</script>
<template>
<DesktopHomeShell>
<DesktopHomeTopbar @logout="$emit('logout')" />
<DesktopHomeAlerts />
<HomeSkeleton v-if="showSkeleton" />
<template v-if="showContent">
<section v-if="ctx.isGaroosi.value" class="dhome-hero-row">
<DesktopLivePrice />
<DesktopPriceRange />
</section>
<div v-if="ctx.isGaroosi.value" class="dhome-default-grid">
<div class="dhome-default-grid__main">
<DesktopDefaultActions />
<section
v-if="ctx.bannerEnabled.value && ctx.banners.value?.length"
class="dhome-surface dhome-chart-banner-combined"
>
<header class="dhome-section__header">
<h2 class="dhome-section__title">نمودار قیمت</h2>
</header>
<div class="dhome-chart-banner-combined__grid">
<DesktopChartSection embedded />
<DesktopBannersPanel embedded />
</div>
</section>
<DesktopChartSection v-else />
</div>
<aside class="dhome-default-grid__side">
<DesktopGaroosiAssets />
<DesktopReferralPanel />
</aside>
</div>
<template v-else>
<DesktopDefaultActions />
<DesktopAssetsMetrics
@open-gold-modal="ctx.openGoldModal"
@open-rial-modal="ctx.openRialModal"
/>
<section class="dhome-hero-row">
<DesktopLivePrice />
<DesktopPriceRange />
</section>
<section
v-if="ctx.bannerEnabled.value && ctx.banners.value?.length"
class="dhome-surface dhome-chart-banner-combined"
>
<header class="dhome-section__header">
<h2 class="dhome-section__title">نمودار قیمت</h2>
</header>
<div class="dhome-chart-banner-combined__grid">
<DesktopChartSection embedded />
<DesktopBannersPanel embedded />
</div>
</section>
<DesktopChartSection v-else />
</template>
</template>
</DesktopHomeShell>
</template>
File diff suppressed because it is too large Load Diff
@@ -1,9 +1,5 @@
import type { HomeLayoutConfig } from '../types'
/**
* چیدمان اختصاصی boursecu — فقط وقتی home_layout=boursecu یا پروژه boursecu باشد.
* شامل: دارایی بدون بک‌گراند، گرید ۶ باکس، نمودار کاتد/مفتول، و ...
*/
export const boursecuHomeLayoutConfig: HomeLayoutConfig = {
id: 'boursecu',
wrapperClass: 'rounded-lg mt-5',
@@ -13,7 +9,6 @@ export const boursecuHomeLayoutConfig: HomeLayoutConfig = {
{ id: 'boursecuQuickActions' },
],
sections: [
{ id: 'marketClosed' },
{ id: 'priceChart' },
{ id: 'priceStats' },
],
@@ -1,20 +0,0 @@
import type { HomeLayoutConfig } from '../types'
/** چیدمان پیش‌فرض — همه پروژه‌ها به‌جز boursecu */
export const defaultHomeLayoutConfig: HomeLayoutConfig = {
id: 'default',
wrapperClass: 'p-1 shadow-lg from-primary rounded-lg p-px to-transparent mt-5',
sections: [
{ id: 'garoosiAssets' },
{ id: 'mainActions' },
{ id: 'referral' },
{ id: 'assets' },
],
tailSections: [
{ id: 'marketClosed' },
{ id: 'currentPrice' },
{ id: 'banners' },
{ id: 'priceChart' },
{ id: 'priceStats' },
],
}
+3 -15
View File
@@ -1,13 +1,10 @@
import { defineAsyncComponent, 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'
export const DEFAULT_HOME_LAYOUT_ID: HomeLayoutId = 'default'
export const DEFAULT_HOME_LAYOUT_ID: HomeLayoutId = 'boursecu'
const HOME_LAYOUT_CONFIGS: Record<HomeLayoutId, HomeLayoutConfig> = {
default: defaultHomeLayoutConfig,
boursecu: boursecuHomeLayoutConfig,
}
@@ -26,20 +23,11 @@ export function isHomeLayoutId(value: unknown): value is HomeLayoutId {
}
export function getHomeLayoutId(): HomeLayoutId {
const adminLayout = getConfigApp('home_layout')
if (isHomeLayoutId(adminLayout)) {
return adminLayout
}
if (isBoursecuProject()) {
return 'boursecu'
}
return DEFAULT_HOME_LAYOUT_ID
return 'boursecu'
}
export function getHomeLayoutConfig(layoutId = getHomeLayoutId()): HomeLayoutConfig {
return HOME_LAYOUT_CONFIGS[layoutId] ?? HOME_LAYOUT_CONFIGS[DEFAULT_HOME_LAYOUT_ID]
return HOME_LAYOUT_CONFIGS[layoutId] ?? boursecuHomeLayoutConfig
}
export function resolveHomeLayoutComponent(_layoutId = getHomeLayoutId()): Component {
@@ -1,16 +1,15 @@
<script setup lang="ts">
import { computed, inject } from 'vue'
import { 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"
:boursecu-layout="isPlainAssets"
plain
boursecu-layout
@open-gold-modal="ctx.openGoldModal"
@open-rial-modal="ctx.openRialModal"
/>
@@ -1,23 +0,0 @@
<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>
@@ -1,15 +0,0 @@
<script setup lang="ts">
import { inject } from 'vue'
import { isBoursecuProject } from '@/utils/config'
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="!isBoursecuProject() && ctx.showClosedMessage.value"
:message="ctx.closedMessage.value as string"
/>
</template>
@@ -4,7 +4,7 @@ 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 layout = 'boursecu' as const
const priceChartSeries = computed(() => toValue(ctx.priceChartSeries))
const chartSelection = computed(() => toValue(ctx.chartSelection))
const selectedChartMetal = computed(() => toValue(ctx.selectedChartMetal))
@@ -1,11 +0,0 @@
<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>
@@ -1,23 +1,17 @@
import type { Component } from 'vue'
import type { HomeSectionId } from '../types'
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> = {
garoosiAssets: GaroosiAssetsSection,
mainActions: MainActionsSection,
boursecuQuickActions: BoursecuQuickActionsSection,
referral: ReferralSectionBlock,
assets: AssetsSectionBlock,
marketClosed: MarketClosedSection,
currentPrice: CurrentPriceSection,
banners: BannersSection,
priceChart: PriceChartSection,
+2 -6
View File
@@ -1,13 +1,10 @@
/** چیدمان‌های صفحه اصلی — هر پروژه/ادمین می‌تواند یکی را انتخاب کند */
export type HomeLayoutId = 'default' | 'boursecu'
/** چیدمان صفحه اصلی boursecu */
export type HomeLayoutId = 'boursecu'
export type HomeSectionId =
| 'garoosiAssets'
| 'mainActions'
| 'boursecuQuickActions'
| 'referral'
| 'assets'
| 'marketClosed'
| 'currentPrice'
| 'banners'
| 'priceChart'
@@ -31,7 +28,6 @@ export interface HomeLayoutConfig {
wrapperClass?: string
outerSections?: HomeLayoutSectionConfig[]
sections: HomeLayoutSectionConfig[]
/** بعد از wrapper — مثل قیمت و نمودار در UI قدیمی */
tailSections?: HomeLayoutSectionConfig[]
}