Merge remote-tracking branch 'origin/modular' into modular
This commit is contained in:
@@ -9,15 +9,13 @@
|
||||
<div class="text-l mr-2 font-bold">{{ usePriceWithoutType(countBasedInventory, false) }} عدد</div>
|
||||
</div>
|
||||
|
||||
<PortfolioOverview v-if="!boursecuLayout" variant="mobile" />
|
||||
|
||||
<BoursecuAssetsSummary
|
||||
v-else-if="boursecuLayout"
|
||||
v-if="boursecuLayout"
|
||||
variant="mobile"
|
||||
icon-color="primary"
|
||||
/>
|
||||
|
||||
<template v-else-if="showLegacyRows && meta?.single_metal && primaryMetalTotal">
|
||||
<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>
|
||||
@@ -29,6 +27,8 @@
|
||||
/>
|
||||
</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>
|
||||
@@ -41,7 +41,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="showLegacyRows && !boursecuLayout">
|
||||
<template v-else-if="!boursecuLayout">
|
||||
<div
|
||||
v-for="(metal, index) in metalTotals"
|
||||
:key="metal.metal_id"
|
||||
@@ -59,6 +59,8 @@
|
||||
/>
|
||||
</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>
|
||||
@@ -77,7 +79,7 @@
|
||||
import { computed } from 'vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import BoursecuAssetsSummary from './BoursecuAssetsSummary.vue'
|
||||
import PortfolioOverview from './PortfolioOverview.vue'
|
||||
import PortfolioProfitInsight from './PortfolioProfitInsight.vue'
|
||||
import { formatWeightAmount, usePriceWithoutType } from '@/utils/usePrice'
|
||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
import type { WalletMetalTotal } from '@/models/walletAssets'
|
||||
@@ -119,8 +121,6 @@ const inventoryCards = computed(() => assetsSummary.value?.inventory_cards ?? []
|
||||
const countBasedInventory = computed(() =>
|
||||
(assetsSummary.value?.count_products ?? []).reduce((sum, item) => sum + Number(item.quantity ?? 0), 0)
|
||||
)
|
||||
const showLegacyRows = computed(() => !assetsSummary.value?.portfolio)
|
||||
|
||||
function formatMetalTotal(metal: WalletMetalTotal) {
|
||||
return `${formatWeightAmount(resolveSummaryMetalWeight(metal, inventoryCards.value))}${metal.unit}`
|
||||
}
|
||||
|
||||
@@ -16,9 +16,21 @@
|
||||
'portfolio-overview__profit--down': profitTone === 'loss',
|
||||
}"
|
||||
>
|
||||
<span class="portfolio-overview__profit-amount">{{ profitText }}</span>
|
||||
<span v-if="profitPercent" class="portfolio-overview__profit-percent">{{ profitPercent }}</span>
|
||||
<span class="portfolio-overview__profit-hint">سود/زیان تحققنیافته طلا</span>
|
||||
<div class="portfolio-overview__profit-main">
|
||||
<span class="portfolio-overview__profit-amount">{{ profitText }}</span>
|
||||
<span v-if="profitPercent" class="portfolio-overview__profit-percent">{{ profitPercent }}</span>
|
||||
</div>
|
||||
<div class="portfolio-overview__profit-label">
|
||||
<span class="portfolio-overview__profit-hint">سود/زیان تحققنیافته طلا</span>
|
||||
<button
|
||||
type="button"
|
||||
class="portfolio-overview__profit-info"
|
||||
aria-label="اطلاعات بیشتر"
|
||||
@click="openProfitInfo"
|
||||
>
|
||||
<BasicIcon icon="vuesax-linear:info-circle" size="14" color="tertiary" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="portfolio-overview__grid">
|
||||
@@ -36,17 +48,16 @@
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<p v-if="hasProfitInsight && averageBuyPrice" class="portfolio-overview__footnote">
|
||||
بر اساس میانگین خرید {{ usePriceWithoutType(averageBuyPrice) }} ریال و قیمت فعلی
|
||||
{{ usePriceWithoutType(liveGoldPrice) }} ریال
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, h } from 'vue'
|
||||
import { Modal } from 'ant-design-vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
import {
|
||||
formatPortfolioProfitInfoText,
|
||||
formatPortfolioProfitLabel,
|
||||
formatPortfolioProfitPercent,
|
||||
formatPortfolioProfitText,
|
||||
@@ -79,8 +90,6 @@ const metalRial = computed(() => {
|
||||
return resolveAssetsRialValue(assetsSummary.value, getTradeBuyPrice(lastPrice.value))
|
||||
})
|
||||
const goldWeight = computed(() => Number(portfolio.value?.gold_weight ?? 0))
|
||||
const averageBuyPrice = computed(() => portfolio.value?.average_buy_price ?? null)
|
||||
const liveGoldPrice = computed(() => Number(portfolio.value?.live_gold_price ?? 0))
|
||||
|
||||
const hasProfitInsight = computed(() => portfolio.value?.unrealized_profit_rial != null)
|
||||
const profitTone = computed(() => formatPortfolioProfitLabel(portfolio.value?.unrealized_profit_rial))
|
||||
@@ -89,6 +98,33 @@ const profitPercent = computed(() =>
|
||||
formatPortfolioProfitPercent(portfolio.value?.unrealized_profit_percent)
|
||||
)
|
||||
|
||||
function renderModalLines(text: string) {
|
||||
return h(
|
||||
'div',
|
||||
{ style: { lineHeight: '1.9' } },
|
||||
text.split('\n').map((line, index) =>
|
||||
h(
|
||||
'div',
|
||||
{ key: index, style: index > 0 ? { marginTop: '6px' } : undefined },
|
||||
line
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function openProfitInfo() {
|
||||
const p = portfolio.value
|
||||
if (!p) return
|
||||
|
||||
Modal.confirm({
|
||||
title: 'سود/زیان تحققنیافته طلا',
|
||||
content: renderModalLines(formatPortfolioProfitInfoText(p, weightUnit.value)),
|
||||
okText: 'متوجه شدم',
|
||||
centered: true,
|
||||
cancelButtonProps: { style: { display: 'none' } },
|
||||
})
|
||||
}
|
||||
|
||||
const rootClass = computed(() => [
|
||||
'portfolio-overview',
|
||||
`portfolio-overview--${props.variant}`,
|
||||
@@ -146,17 +182,46 @@ const rootClass = computed(() => [
|
||||
|
||||
.portfolio-overview__profit {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 10px;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 12px;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.portfolio-overview__profit-main {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.portfolio-overview__profit-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.portfolio-overview__profit-info {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
line-height: 0;
|
||||
opacity: 0.85;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.portfolio-overview--desktop .portfolio-overview__profit {
|
||||
background: #f9fafb;
|
||||
}
|
||||
@@ -244,16 +309,4 @@ const rootClass = computed(() => [
|
||||
color: #9ca3af;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.portfolio-overview__footnote {
|
||||
margin: 10px 0 0;
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.portfolio-overview--desktop .portfolio-overview__footnote {
|
||||
color: #6b7280;
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="hasProfitInsight"
|
||||
class="portfolio-profit"
|
||||
:class="[
|
||||
`portfolio-profit--${variant}`,
|
||||
{
|
||||
'portfolio-profit--up': profitTone === 'profit',
|
||||
'portfolio-profit--down': profitTone === 'loss',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<div class="portfolio-profit__main">
|
||||
<span class="portfolio-profit__amount">{{ profitText }}</span>
|
||||
<span v-if="profitPercent" class="portfolio-profit__percent">{{ profitPercent }}</span>
|
||||
</div>
|
||||
|
||||
<div class="portfolio-profit__label-row">
|
||||
<span class="portfolio-profit__hint">سود/زیان تحققنیافته طلا</span>
|
||||
<button
|
||||
type="button"
|
||||
class="portfolio-profit__info"
|
||||
aria-label="اطلاعات بیشتر"
|
||||
@click="openInfoModal"
|
||||
>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:info-circle"
|
||||
:size="variant === 'desktop' ? 14 : 16"
|
||||
:color="infoIconColor"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h } from 'vue'
|
||||
import { Modal } from 'ant-design-vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
import {
|
||||
formatPortfolioProfitInfoText,
|
||||
formatPortfolioProfitLabel,
|
||||
formatPortfolioProfitPercent,
|
||||
formatPortfolioProfitText,
|
||||
} from '@/utils/productAssets'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
variant?: 'mobile' | 'desktop'
|
||||
}>(),
|
||||
{ variant: 'mobile' }
|
||||
)
|
||||
|
||||
const { assetsSummary } = useWalletAssets()
|
||||
|
||||
const portfolio = computed(() => assetsSummary.value?.portfolio)
|
||||
const weightUnit = computed(() => assetsSummary.value?.meta?.weight_unit || 'گرم')
|
||||
|
||||
const hasProfitInsight = computed(() => portfolio.value?.unrealized_profit_rial != null)
|
||||
const profitTone = computed(() => formatPortfolioProfitLabel(portfolio.value?.unrealized_profit_rial))
|
||||
const profitText = computed(() => formatPortfolioProfitText(portfolio.value?.unrealized_profit_rial))
|
||||
const profitPercent = computed(() =>
|
||||
formatPortfolioProfitPercent(portfolio.value?.unrealized_profit_percent)
|
||||
)
|
||||
const infoIconColor = computed(() => (props.variant === 'desktop' ? 'tertiary' : 'tertiary'))
|
||||
|
||||
function renderModalLines(text: string) {
|
||||
return h(
|
||||
'div',
|
||||
{ style: { lineHeight: '1.9' } },
|
||||
text.split('\n').map((line, index) =>
|
||||
h(
|
||||
'div',
|
||||
{ key: index, style: index > 0 ? { marginTop: '6px' } : undefined },
|
||||
line
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function openInfoModal() {
|
||||
const p = portfolio.value
|
||||
if (!p) return
|
||||
|
||||
Modal.confirm({
|
||||
title: 'سود/زیان تحققنیافته طلا',
|
||||
content: renderModalLines(formatPortfolioProfitInfoText(p, weightUnit.value)),
|
||||
okText: 'متوجه شدم',
|
||||
centered: true,
|
||||
cancelButtonProps: { style: { display: 'none' } },
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.portfolio-profit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 12px;
|
||||
margin-top: 10px;
|
||||
background: linear-gradient(145deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.06));
|
||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.portfolio-profit--desktop {
|
||||
align-items: flex-start;
|
||||
margin-top: 0;
|
||||
margin-bottom: 12px;
|
||||
background: linear-gradient(145deg, #f9fafb, #f3f4f6);
|
||||
border: 1px solid rgba(15, 23, 42, 0.06);
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04);
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
.portfolio-profit__main {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.portfolio-profit--desktop .portfolio-profit__main {
|
||||
justify-content: flex-start;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.portfolio-profit--up {
|
||||
color: #bbf7d0;
|
||||
border-color: rgba(187, 247, 208, 0.35);
|
||||
}
|
||||
|
||||
.portfolio-profit--desktop.portfolio-profit--up {
|
||||
color: #027a48;
|
||||
background: linear-gradient(145deg, #ecfdf3, #d1fae5);
|
||||
border-color: rgba(2, 122, 72, 0.12);
|
||||
}
|
||||
|
||||
.portfolio-profit--down {
|
||||
color: #fecaca;
|
||||
border-color: rgba(254, 202, 202, 0.35);
|
||||
}
|
||||
|
||||
.portfolio-profit--desktop.portfolio-profit--down {
|
||||
color: #b42318;
|
||||
background: linear-gradient(145deg, #fef3f2, #fee4e2);
|
||||
border-color: rgba(180, 35, 24, 0.12);
|
||||
}
|
||||
|
||||
.portfolio-profit__percent {
|
||||
direction: ltr;
|
||||
unicode-bidi: isolate;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: 0.92em;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.portfolio-profit__label-row {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.portfolio-profit--desktop .portfolio-profit__label-row {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.portfolio-profit__hint {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
opacity: 0.88;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.portfolio-profit--desktop .portfolio-profit__hint {
|
||||
color: #6b7280;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.portfolio-profit__info {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
opacity: 0.85;
|
||||
line-height: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed } from 'vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import BoursecuAssetsSummary from '../../components/BoursecuAssetsSummary.vue'
|
||||
import PortfolioOverview from '../../components/PortfolioOverview.vue'
|
||||
import PortfolioProfitInsight from '../../components/PortfolioProfitInsight.vue'
|
||||
import { formatWeightAmount, usePriceWithoutType } from '@/utils/usePrice'
|
||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
import type { WalletMetalTotal } from '@/models/walletAssets'
|
||||
@@ -25,7 +25,6 @@ const cashWallet = computed(() => assetsSummary.value?.cash_wallet ?? 0)
|
||||
const countBasedInventory = computed(() =>
|
||||
(assetsSummary.value?.count_products ?? []).reduce((sum, item) => sum + Number(item.quantity ?? 0), 0),
|
||||
)
|
||||
const showLegacyMetrics = computed(() => !assetsSummary.value?.portfolio)
|
||||
|
||||
type MetricItem = {
|
||||
id: string
|
||||
@@ -104,9 +103,9 @@ function handleInfo(metric: MetricItem) {
|
||||
/>
|
||||
|
||||
<template v-else>
|
||||
<PortfolioOverview variant="desktop" />
|
||||
<PortfolioProfitInsight variant="desktop" />
|
||||
|
||||
<div v-if="showLegacyMetrics" class="dhome-metrics">
|
||||
<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>
|
||||
|
||||
@@ -37,13 +37,7 @@ const showSkeleton = computed(() => Boolean(getToken()) && !userInfo.value)
|
||||
<HomeSkeleton v-if="showSkeleton" />
|
||||
|
||||
<template v-if="showContent">
|
||||
<DesktopAssetsMetrics
|
||||
v-if="!ctx.isGaroosi.value"
|
||||
@open-gold-modal="ctx.openGoldModal"
|
||||
@open-rial-modal="ctx.openRialModal"
|
||||
/>
|
||||
|
||||
<section class="dhome-hero-row">
|
||||
<section v-if="ctx.isGaroosi.value" class="dhome-hero-row">
|
||||
<DesktopLivePrice />
|
||||
<DesktopPriceRange />
|
||||
</section>
|
||||
@@ -74,6 +68,17 @@ const showSkeleton = computed(() => Boolean(getToken()) && !userInfo.value)
|
||||
|
||||
<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"
|
||||
|
||||
@@ -24,6 +24,10 @@ const innerSections = computed(() =>
|
||||
layoutConfig.value.sections.filter((section) => section.visible !== false)
|
||||
)
|
||||
|
||||
const tailSections = computed(() =>
|
||||
(layoutConfig.value.tailSections ?? []).filter((section) => section.visible !== false)
|
||||
)
|
||||
|
||||
/** محتوا با userInfo — دادههای API در پسزمینه لود میشوند */
|
||||
const showContent = computed(() => Boolean(userInfo.value))
|
||||
const showSkeleton = computed(() => Boolean(getToken()) && !userInfo.value)
|
||||
@@ -56,6 +60,12 @@ function resolveSectionComponent(sectionId: HomeSectionId) {
|
||||
:key="section.id"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<component
|
||||
:is="resolveSectionComponent(section.id)"
|
||||
v-for="section in tailSections"
|
||||
:key="`tail-${section.id}`"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<HomeSkeleton v-else-if="showSkeleton" class="w-full" />
|
||||
|
||||
@@ -3,12 +3,14 @@ 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',
|
||||
wrapperClass: 'p-1 shadow-lg from-primary rounded-lg p-px to-transparent mt-5',
|
||||
sections: [
|
||||
{ id: 'garoosiAssets' },
|
||||
{ id: 'assets' },
|
||||
{ id: 'mainActions' },
|
||||
{ id: 'referral' },
|
||||
{ id: 'assets' },
|
||||
],
|
||||
tailSections: [
|
||||
{ id: 'marketClosed' },
|
||||
{ id: 'currentPrice' },
|
||||
{ id: 'banners' },
|
||||
|
||||
@@ -31,6 +31,8 @@ export interface HomeLayoutConfig {
|
||||
wrapperClass?: string
|
||||
outerSections?: HomeLayoutSectionConfig[]
|
||||
sections: HomeLayoutSectionConfig[]
|
||||
/** بعد از wrapper — مثل قیمت و نمودار در UI قدیمی */
|
||||
tailSections?: HomeLayoutSectionConfig[]
|
||||
}
|
||||
|
||||
export interface HomeActionItem {
|
||||
|
||||
Reference in New Issue
Block a user