Polish boursecu UX: branding logos, home price stats, assets, and desktop footer.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,8 +5,7 @@
|
||||
>
|
||||
<div class="flex justify-between my-2 items-center">
|
||||
<div v-if="isBoursecuProject()" class="flex items-center gap-2">
|
||||
<img :src="projectLogo" alt="" class="h-8 w-auto object-contain" />
|
||||
<div class="color-transition font-bold text-primary">{{ appName }}</div>
|
||||
<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
|
||||
@@ -46,16 +45,13 @@
|
||||
import { computed } from 'vue'
|
||||
import { useWindowScroll } from '@vueuse/core'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { useConfigStore } from '@/store/configStore'
|
||||
import router from '@/router'
|
||||
import Icon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { getProjectNavLogo, isBoursecuProject } from '@/utils/config'
|
||||
import { getProjectMobileNavLogo, isBoursecuProject } from '@/utils/config'
|
||||
|
||||
const { y } = useWindowScroll()
|
||||
const userStore = useUserStore()
|
||||
const config = useConfigStore()
|
||||
const projectLogo = computed(() => getProjectNavLogo())
|
||||
const appName = computed(() => config.config?.name_fa || config.config?.name || 'بورس مس')
|
||||
const projectLogo = computed(() => getProjectMobileNavLogo())
|
||||
|
||||
defineEmits<{
|
||||
logout: []
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div :class="plain ? 'py-2' : 'bg-primary rounded-lg p-2 py-3 mt-2'">
|
||||
<div :class="rootClass">
|
||||
<div
|
||||
v-if="!boursecuLayout && meta?.has_count_based_products"
|
||||
:class="rowClass"
|
||||
class="flex justify-center w-full z-20"
|
||||
>
|
||||
محصولات:
|
||||
<div class="text-l mr-2 font-bold">{{ countBasedInventory }} عدد</div>
|
||||
<div class="text-l mr-2 font-bold">{{ usePriceWithoutType(countBasedInventory, false) }} عدد</div>
|
||||
</div>
|
||||
|
||||
<template v-if="boursecuAssetRows.length">
|
||||
@@ -94,7 +94,7 @@ const props = withDefaults(
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
openGoldModal: [metal: WalletMetalTotal]
|
||||
openGoldModal: [metal: WalletMetalTotal, modalText?: string]
|
||||
openRialModal: []
|
||||
}>()
|
||||
|
||||
@@ -103,6 +103,16 @@ 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])
|
||||
@@ -116,7 +126,7 @@ const countBasedInventory = computed(() =>
|
||||
)
|
||||
|
||||
function formatMetalTotal(metal: WalletMetalTotal) {
|
||||
return `${resolveSummaryMetalWeight(metal, inventoryCards.value)}${metal.unit}`
|
||||
return `${usePriceWithoutType(resolveSummaryMetalWeight(metal, inventoryCards.value), false)}${metal.unit}`
|
||||
}
|
||||
|
||||
function handleRowInfo(row: HomeAssetRow) {
|
||||
@@ -126,7 +136,16 @@ function handleRowInfo(row: HomeAssetRow) {
|
||||
}
|
||||
|
||||
if (row.metal) {
|
||||
emit('openGoldModal', row.metal)
|
||||
emit('openGoldModal', row.metal, row.modalText)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@boursecu-surface-border: #d4e3de;
|
||||
|
||||
.assets-section--boursecu {
|
||||
background-color: #eff5f3;
|
||||
border: 2px solid @boursecu-surface-border;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<Vue3Odometer
|
||||
:value="displayGoldVal"
|
||||
class="o-text !text-xl font-bold"
|
||||
style="font-family: var(--App-font-family); direction: ltr"
|
||||
style="font-family: iran-sans; direction: ltr"
|
||||
/>
|
||||
ریال
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div
|
||||
class="w-full flex justify-evenly rounded-lg py-2 home_appear_fade_in"
|
||||
:class="[
|
||||
layout === 'boursecu' ? 'mt-8 pt-2' : 'mt-6',
|
||||
isBoursecuLayout ? 'mt-8 pt-2' : 'mt-6',
|
||||
variant === 'mobile' ? 'pb-45' : 'mt-0 pb-0',
|
||||
]"
|
||||
>
|
||||
@@ -33,31 +33,28 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { usePriceWithoutType } from '../../../../utils/usePrice'
|
||||
import type { ChartMetalType } from '../composables/usePriceChart'
|
||||
import { resolveMetalDayRangePrices } from '../composables/usePriceChart'
|
||||
import { price } from '@/utils/price'
|
||||
import { getHomeLayoutId } from '../layouts/registry'
|
||||
import { resolveMetalDayRangePrices, usePriceChart } from '../composables/usePriceChart'
|
||||
|
||||
const props = withDefaults(
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
price: any
|
||||
layout?: 'default' | 'boursecu'
|
||||
variant?: 'mobile' | 'desktop'
|
||||
selectedMetal?: ChartMetalType
|
||||
maftolRatio?: number
|
||||
}>(),
|
||||
{
|
||||
layout: 'default',
|
||||
variant: 'mobile',
|
||||
selectedMetal: 'katod',
|
||||
maftolRatio: 0,
|
||||
}
|
||||
)
|
||||
|
||||
const { selectedChartMetal, maftolChartRatio } = usePriceChart()
|
||||
const isBoursecuLayout = computed(() => getHomeLayoutId() === 'boursecu')
|
||||
|
||||
const rangePrices = computed(() =>
|
||||
resolveMetalDayRangePrices({
|
||||
price: props.price,
|
||||
metal: props.selectedMetal,
|
||||
isBoursecuLayout: props.layout === 'boursecu',
|
||||
maftolRatio: props.maftolRatio,
|
||||
price: price.value,
|
||||
metal: selectedChartMetal.value,
|
||||
isBoursecuLayout: isBoursecuLayout.value,
|
||||
maftolRatio: maftolChartRatio.value,
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@@ -5,7 +5,12 @@ import { useUserStore } from '@/store/userStore'
|
||||
import { useWalletStore } from '@/store/walletStore'
|
||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
import type { WalletMetalTotal } from '@/models/walletAssets'
|
||||
import { formatMetalAssetsInfoText, formatRialAssetsInfoText, resolveAssetsRialValue } from '@/utils/productAssets'
|
||||
import {
|
||||
formatBoursecuMetalModalText,
|
||||
formatMetalAssetsInfoText,
|
||||
formatRialAssetsInfoText,
|
||||
resolveAssetsRialValue,
|
||||
} from '@/utils/productAssets'
|
||||
import { useConfigStore } from '@/store/configStore'
|
||||
import { getTradeBuyPrice, lastPrice, stopLastPricePolling } from '@/utils/price'
|
||||
import { getStoreClosedMessage, isStoreClosed } from '@/utils/storeHours'
|
||||
@@ -36,7 +41,7 @@ export interface HomePageContext {
|
||||
setChartMetal: (metal: ChartMetalType) => void
|
||||
isBoursecuHomeLayout: ReturnType<typeof computed<boolean>>
|
||||
handleBannerClick: (banner: { typeClick?: string; modalText?: string; link?: string }) => void
|
||||
openGoldModal: (metal: WalletMetalTotal) => void
|
||||
openGoldModal: (metal: WalletMetalTotal, modalText?: string) => void
|
||||
openRialModal: () => void
|
||||
}
|
||||
|
||||
@@ -142,9 +147,15 @@ export function useHomePage() {
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
function openGoldModal(metal: WalletMetalTotal) {
|
||||
function openGoldModal(metal: WalletMetalTotal, modalText?: string) {
|
||||
const title =
|
||||
modalText ??
|
||||
(isBoursecuHomeLayout.value
|
||||
? formatBoursecuMetalModalText(metal)
|
||||
: formatMetalAssetsInfoText(metal))
|
||||
|
||||
Modal.confirm({
|
||||
title: formatMetalAssetsInfoText(metal),
|
||||
title,
|
||||
okText: 'متوجه شدم',
|
||||
centered: true,
|
||||
cancelButtonProps: { style: { display: 'none' } },
|
||||
|
||||
@@ -21,8 +21,12 @@ export function isHomePageDataInitialized() {
|
||||
|
||||
export async function initHomePageData(force = false) {
|
||||
if (!force && initialized.value) {
|
||||
// Resume live price updates when returning to home without refetching datasets
|
||||
startLastPricePolling()
|
||||
|
||||
if (getHomeLayoutId() === 'boursecu') {
|
||||
void loadHomeMaftolOffset(false)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ref, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { getAllPrices } from '@/api/home'
|
||||
import type { Product } from '@/models/products'
|
||||
import { getProductsCatalog } from '@/composables/useProductsCatalog'
|
||||
import { getProductsCatalog, getProductsCatalogRef } from '@/composables/useProductsCatalog'
|
||||
import { getHomeLayoutId } from '../layouts/registry'
|
||||
import { getProductBuyPrice, resolvePriceOffsetRatio } from '@/utils/productPrice'
|
||||
import { isKatodMesGroup, isMaftolMesGroup } from '@/utils/productDealMethod'
|
||||
@@ -19,7 +19,27 @@ const rawChartPoints = ref<ChartPoint[]>([])
|
||||
const chartSelection = ref('DAILY')
|
||||
const chartLoading = ref(false)
|
||||
const selectedChartMetal = ref<ChartMetalType>('katod')
|
||||
const maftolChartRatio = ref(0)
|
||||
const catalogProducts = getProductsCatalogRef()
|
||||
|
||||
function resolveChartProducts(products: Product[]) {
|
||||
return {
|
||||
katod: findKatodProduct(products),
|
||||
maftol: findMaftolProduct(products),
|
||||
}
|
||||
}
|
||||
|
||||
/** نسبت مفتول به کاتد — reactive به قیمت لحظهای و کاتالوگ محصولات */
|
||||
const maftolChartRatio = computed(() => {
|
||||
if (getHomeLayoutId() !== 'boursecu') {
|
||||
return 0
|
||||
}
|
||||
|
||||
// lastPrice را track کن تا بعد از polling نسبت بهروز شود
|
||||
void lastPrice.value?.price_last
|
||||
|
||||
const { katod, maftol } = resolveChartProducts(catalogProducts.value)
|
||||
return deriveMaftolChartRatio(maftol, katod)
|
||||
})
|
||||
|
||||
let chartFetchPromise: Promise<void> | null = null
|
||||
let chartInitialized = false
|
||||
@@ -69,19 +89,25 @@ export function resolveMetalDayRangePrices(options: {
|
||||
}
|
||||
|
||||
function deriveMaftolChartRatio(maftolProduct?: Product, katodProduct?: Product) {
|
||||
const offsetRatio = resolvePriceOffsetRatio(maftolProduct?.price_offset_percent)
|
||||
if (offsetRatio) {
|
||||
return offsetRatio
|
||||
const maftolOffset = resolvePriceOffsetRatio(maftolProduct?.price_offset_percent)
|
||||
if (maftolOffset) {
|
||||
return maftolOffset
|
||||
}
|
||||
|
||||
if (!maftolProduct) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const basePrice = getTradeBuyPrice(lastPrice.value)
|
||||
const katodPrice = katodProduct ? getProductBuyPrice(katodProduct) : basePrice
|
||||
const maftolPrice = getProductBuyPrice(maftolProduct)
|
||||
const referencePrice = katodPrice || basePrice
|
||||
const liveBuy = getTradeBuyPrice(lastPrice.value)
|
||||
const katodLive = katodProduct ? getProductBuyPrice(katodProduct) : liveBuy
|
||||
const maftolLive = getProductBuyPrice(maftolProduct)
|
||||
|
||||
// وقتی فروشگاه بسته است قیمت لحظهای صفر میشود — از قیمت ثابت کاتالوگ استفاده کن
|
||||
const katodStatic = Number(katodProduct?.price ?? 0) + Number(katodProduct?.price_profit ?? 0)
|
||||
const maftolStatic = Number(maftolProduct.price ?? 0) + Number(maftolProduct.price_profit ?? 0)
|
||||
|
||||
const referencePrice = katodLive || liveBuy || katodStatic
|
||||
const maftolPrice = maftolLive || maftolStatic
|
||||
|
||||
if (!referencePrice || !maftolPrice || referencePrice === maftolPrice) {
|
||||
return 0
|
||||
@@ -168,12 +194,11 @@ async function fetchPrices(force = false) {
|
||||
|
||||
async function loadMaftolOffset(force = false) {
|
||||
if (getHomeLayoutId() !== 'boursecu') {
|
||||
maftolChartRatio.value = 0
|
||||
syncChartSeries()
|
||||
return
|
||||
}
|
||||
|
||||
if (!force && maftolOffsetLoaded) {
|
||||
if (!force && maftolOffsetLoaded && catalogProducts.value.length > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -183,15 +208,11 @@ async function loadMaftolOffset(force = false) {
|
||||
|
||||
maftolOffsetPromise = (async () => {
|
||||
try {
|
||||
const products = await getProductsCatalog(force)
|
||||
const katodProduct = findKatodProduct(products)
|
||||
const maftolProduct = findMaftolProduct(products)
|
||||
maftolChartRatio.value = deriveMaftolChartRatio(maftolProduct, katodProduct)
|
||||
await getProductsCatalog(force)
|
||||
syncChartSeries()
|
||||
maftolOffsetLoaded = true
|
||||
} catch (error) {
|
||||
console.error('Error loading maftol chart ratio:', error)
|
||||
maftolChartRatio.value = 0
|
||||
syncChartSeries()
|
||||
} finally {
|
||||
maftolOffsetPromise = null
|
||||
@@ -208,6 +229,10 @@ function updateChartData(timeline: string) {
|
||||
|
||||
function setChartMetal(metal: ChartMetalType) {
|
||||
selectedChartMetal.value = metal
|
||||
|
||||
if (metal === 'maftol' && getHomeLayoutId() === 'boursecu' && !catalogProducts.value.length) {
|
||||
void loadMaftolOffset(true)
|
||||
}
|
||||
}
|
||||
|
||||
export function usePriceChart() {
|
||||
@@ -237,7 +262,6 @@ export function resetHomePriceChart() {
|
||||
rawChartPoints.value = []
|
||||
chartSelection.value = 'DAILY'
|
||||
selectedChartMetal.value = 'katod'
|
||||
maftolChartRatio.value = 0
|
||||
chartInitialized = false
|
||||
chartFetchPromise = null
|
||||
maftolOffsetLoaded = false
|
||||
|
||||
@@ -8,7 +8,7 @@ import { buildBoursecuHomeAssetRows, resolveSummaryMetalWeight } from '@/utils/p
|
||||
import { useHomePageContext } from '../../composables/useHomePage'
|
||||
|
||||
const emit = defineEmits<{
|
||||
openGoldModal: [metal: WalletMetalTotal]
|
||||
openGoldModal: [metal: WalletMetalTotal, modalText?: string]
|
||||
openRialModal: []
|
||||
}>()
|
||||
|
||||
@@ -29,6 +29,7 @@ type MetricItem = {
|
||||
label: string
|
||||
value: string
|
||||
metal?: WalletMetalTotal
|
||||
modalText?: string
|
||||
showInfo?: boolean
|
||||
onInfo?: 'gold' | 'rial'
|
||||
}
|
||||
@@ -40,7 +41,7 @@ const metrics = computed<MetricItem[]>(() => {
|
||||
items.push({
|
||||
id: 'products',
|
||||
label: 'محصولات',
|
||||
value: `${countBasedInventory.value} عدد`,
|
||||
value: `${usePriceWithoutType(countBasedInventory.value, false)} عدد`,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -51,6 +52,7 @@ const metrics = computed<MetricItem[]>(() => {
|
||||
label: row.label,
|
||||
value: row.value,
|
||||
metal: row.metal,
|
||||
modalText: row.modalText,
|
||||
showInfo: row.info !== 'none',
|
||||
onInfo: row.info === 'rial' ? 'rial' : 'gold',
|
||||
})
|
||||
@@ -93,7 +95,7 @@ const metrics = computed<MetricItem[]>(() => {
|
||||
})
|
||||
|
||||
function formatMetalTotal(metal: WalletMetalTotal) {
|
||||
return `${resolveSummaryMetalWeight(metal, inventoryCards.value)}${metal.unit}`
|
||||
return `${usePriceWithoutType(resolveSummaryMetalWeight(metal, inventoryCards.value), false)}${metal.unit}`
|
||||
}
|
||||
|
||||
function handleInfo(metric: MetricItem) {
|
||||
@@ -102,7 +104,7 @@ function handleInfo(metric: MetricItem) {
|
||||
return
|
||||
}
|
||||
if (metric.metal) {
|
||||
emit('openGoldModal', metric.metal)
|
||||
emit('openGoldModal', metric.metal, metric.modalText)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<Vue3Odometer
|
||||
:value="displayGoldVal"
|
||||
class="dhome-hero-price__odometer o-text"
|
||||
style="font-family: var(--App-font-family)"
|
||||
style="font-family: iran-sans"
|
||||
/>
|
||||
<span class="dhome-hero-price__unit">ریال</span>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, toValue } from 'vue'
|
||||
import { useHomePageContext } from '../../composables/useHomePage'
|
||||
import { price } from '@/utils/price'
|
||||
import CurrentPrice from '../../components/CurrentPrice.vue'
|
||||
import PriceStats from '../../components/PriceStats.vue'
|
||||
import DesktopHomeCard from './DesktopHomeCard.vue'
|
||||
|
||||
const ctx = useHomePageContext()
|
||||
const statsLayout = computed(() => (toValue(ctx.isBoursecuHomeLayout) ? 'boursecu' : 'default'))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -17,7 +11,7 @@ const statsLayout = computed(() => (toValue(ctx.isBoursecuHomeLayout) ? 'boursec
|
||||
</DesktopHomeCard>
|
||||
|
||||
<DesktopHomeCard title="بازه قیمت امروز">
|
||||
<PriceStats variant="desktop" :price="price" :layout="statsLayout" />
|
||||
<PriceStats variant="desktop" />
|
||||
</DesktopHomeCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -52,5 +52,5 @@ function resolveSectionComponent(sectionId: HomeSectionId) {
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<HomeSkeleton v-if="loading" class="w-full" />
|
||||
<HomeSkeleton v-else class="w-full" />
|
||||
</template>
|
||||
|
||||
@@ -1,21 +1,7 @@
|
||||
<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'
|
||||
import { usePriceChart } from '../../composables/usePriceChart'
|
||||
|
||||
const ctx = inject(HOME_PAGE_CONTEXT_KEY)!
|
||||
const { maftolChartRatio } = usePriceChart()
|
||||
const layout = computed(() => (ctx.isBoursecuHomeLayout.value ? 'boursecu' : 'default'))
|
||||
const selectedMetal = computed(() => ctx.selectedChartMetal.value)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PriceStats
|
||||
:price="price"
|
||||
:layout="layout"
|
||||
:selected-metal="selectedMetal"
|
||||
:maftol-ratio="maftolChartRatio"
|
||||
/>
|
||||
<PriceStats />
|
||||
</template>
|
||||
|
||||
@@ -37,8 +37,8 @@ import pkg from '../../../../../package.json'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { useConfigStore } from '@/store/configStore'
|
||||
import {
|
||||
getProjectLoginLogo,
|
||||
getProjectLoginTaglineLines,
|
||||
getProjectSplashLogo,
|
||||
hasCustomLoginLogoLayout,
|
||||
isBoursecuProject,
|
||||
} from '@/utils/config'
|
||||
@@ -48,7 +48,7 @@ const isBoursecu = isBoursecuProject()
|
||||
const hasCustomLayout = hasCustomLoginLogoLayout()
|
||||
const version = pkg.version
|
||||
|
||||
const logo = computed(() => getProjectLoginLogo(config.config?.avatar))
|
||||
const logo = computed(() => getProjectSplashLogo(config.config?.avatar))
|
||||
|
||||
const taglineLines = computed(() => getProjectLoginTaglineLines())
|
||||
const tagline = computed(() => {
|
||||
|
||||
@@ -27,9 +27,9 @@
|
||||
|
||||
&--boursecu {
|
||||
.dauth-brand__logo {
|
||||
max-width: 300px;
|
||||
max-height: 110px;
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
max-height: 280px;
|
||||
width: min(100%, 280px);
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user