ui changes

This commit is contained in:
2026-07-02 17:29:09 +03:30
parent b99c2b9e4a
commit eb5fda197e
3 changed files with 241 additions and 68 deletions
+42
View File
@@ -426,6 +426,48 @@ export function formatPortfolioProfitPercent(percent?: number | null): string {
return `${sign}${value.toFixed(2)}٪` return `${sign}${value.toFixed(2)}٪`
} }
/** متن توضیح مودال سود/زیان تحقق‌نیافته طلا */
export function formatPortfolioProfitInfoText(
portfolio: {
average_buy_price?: number | null
live_gold_price?: number | null
cost_basis_rial?: number | null
gold_weight?: number | null
gold_rial_value?: number | null
unrealized_profit_rial?: number | null
unrealized_profit_percent?: number | null
},
weightUnit = 'گرم'
): string {
const lines: string[] = []
if (portfolio.average_buy_price != null && portfolio.average_buy_price > 0) {
lines.push(`میانگین قیمت خرید: ${formatRialNumber(portfolio.average_buy_price)} ریال`)
}
if (portfolio.live_gold_price != null && portfolio.live_gold_price > 0) {
lines.push(`قیمت فعلی طلا: ${formatRialNumber(portfolio.live_gold_price)} ریال`)
}
if (portfolio.gold_weight != null && portfolio.gold_weight > 0) {
lines.push(`وزن طلای شما: ${formatWeightAmount(portfolio.gold_weight)} ${weightUnit}`)
}
if (portfolio.cost_basis_rial != null && portfolio.cost_basis_rial > 0) {
lines.push(`بهای تمام‌شده طلا: ${formatRialNumber(portfolio.cost_basis_rial)} ریال`)
}
if (portfolio.gold_rial_value != null && portfolio.gold_rial_value > 0) {
lines.push(`ارزش فعلی طلا: ${formatRialNumber(portfolio.gold_rial_value)} ریال`)
}
if (portfolio.unrealized_profit_rial != null) {
const pct = formatPortfolioProfitPercent(portfolio.unrealized_profit_percent)
lines.push(
`سود/زیان تحقق‌نیافته: ${formatPortfolioProfitText(portfolio.unrealized_profit_rial)}${pct ? ` (${pct})` : ''}`
)
}
lines.push('محاسبه بر اساس میانگین وزنی قیمت خرید و قیمت لحظه‌ای بازار است.')
return lines.join('\n')
}
/** متن توضیح مودال دارایی ریالی */ /** متن توضیح مودال دارایی ریالی */
export function formatRialAssetsInfoText( export function formatRialAssetsInfoText(
cashWallet: number, cashWallet: number,
@@ -16,9 +16,21 @@
'portfolio-overview__profit--down': profitTone === 'loss', 'portfolio-overview__profit--down': profitTone === 'loss',
}" }"
> >
<span class="portfolio-overview__profit-amount">{{ profitText }}</span> <div class="portfolio-overview__profit-main">
<span v-if="profitPercent" class="portfolio-overview__profit-percent">{{ profitPercent }}</span> <span class="portfolio-overview__profit-amount">{{ profitText }}</span>
<span class="portfolio-overview__profit-hint">سود/زیان تحقق‌نیافته طلا</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>
<div class="portfolio-overview__grid"> <div class="portfolio-overview__grid">
@@ -36,17 +48,16 @@
</article> </article>
</div> </div>
<p v-if="hasProfitInsight && averageBuyPrice" class="portfolio-overview__footnote">
بر اساس میانگین خرید {{ usePriceWithoutType(averageBuyPrice) }} ریال و قیمت فعلی
{{ usePriceWithoutType(liveGoldPrice) }} ریال
</p>
</section> </section>
</template> </template>
<script setup lang="ts"> <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 { useWalletAssets } from '@/composables/useWalletAssets'
import { import {
formatPortfolioProfitInfoText,
formatPortfolioProfitLabel, formatPortfolioProfitLabel,
formatPortfolioProfitPercent, formatPortfolioProfitPercent,
formatPortfolioProfitText, formatPortfolioProfitText,
@@ -79,8 +90,6 @@ const metalRial = computed(() => {
return resolveAssetsRialValue(assetsSummary.value, getTradeBuyPrice(lastPrice.value)) return resolveAssetsRialValue(assetsSummary.value, getTradeBuyPrice(lastPrice.value))
}) })
const goldWeight = computed(() => Number(portfolio.value?.gold_weight ?? 0)) 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 hasProfitInsight = computed(() => portfolio.value?.unrealized_profit_rial != null)
const profitTone = computed(() => formatPortfolioProfitLabel(portfolio.value?.unrealized_profit_rial)) const profitTone = computed(() => formatPortfolioProfitLabel(portfolio.value?.unrealized_profit_rial))
@@ -89,6 +98,33 @@ const profitPercent = computed(() =>
formatPortfolioProfitPercent(portfolio.value?.unrealized_profit_percent) 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(() => [ const rootClass = computed(() => [
'portfolio-overview', 'portfolio-overview',
`portfolio-overview--${props.variant}`, `portfolio-overview--${props.variant}`,
@@ -146,17 +182,46 @@ const rootClass = computed(() => [
.portfolio-overview__profit { .portfolio-overview__profit {
display: flex; display: flex;
flex-wrap: wrap; flex-direction: column;
align-items: center; gap: 6px;
gap: 8px; padding: 10px 12px;
padding: 8px 10px; border-radius: 12px;
border-radius: 10px;
margin-bottom: 12px; margin-bottom: 12px;
background: rgba(255, 255, 255, 0.12); background: rgba(255, 255, 255, 0.12);
font-size: 13px; font-size: 13px;
font-weight: 700; 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 { .portfolio-overview--desktop .portfolio-overview__profit {
background: #f9fafb; background: #f9fafb;
} }
@@ -244,16 +309,4 @@ const rootClass = computed(() => [
color: #9ca3af; color: #9ca3af;
opacity: 1; 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> </style>
@@ -10,25 +10,40 @@
}, },
]" ]"
> >
<span class="portfolio-profit__amount">{{ profitText }}</span> <div class="portfolio-profit__main">
<span v-if="profitPercent" class="portfolio-profit__percent">{{ profitPercent }}</span> <span class="portfolio-profit__amount">{{ profitText }}</span>
<span class="portfolio-profit__hint">سود/زیان تحقق‌نیافته طلا</span> <span v-if="profitPercent" class="portfolio-profit__percent">{{ profitPercent }}</span>
<p v-if="averageBuyPrice" class="portfolio-profit__footnote"> </div>
بر اساس میانگین خرید {{ usePriceWithoutType(averageBuyPrice) }} ریال و قیمت فعلی
{{ usePriceWithoutType(liveGoldPrice) }} ریال <div class="portfolio-profit__label-row">
</p> <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> </div>
</template> </template>
<script setup lang="ts"> <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 { useWalletAssets } from '@/composables/useWalletAssets'
import { import {
formatPortfolioProfitInfoText,
formatPortfolioProfitLabel, formatPortfolioProfitLabel,
formatPortfolioProfitPercent, formatPortfolioProfitPercent,
formatPortfolioProfitText, formatPortfolioProfitText,
} from '@/utils/productAssets' } from '@/utils/productAssets'
import { usePriceWithoutType } from '@/utils/usePrice'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@@ -40,8 +55,7 @@ const props = withDefaults(
const { assetsSummary } = useWalletAssets() const { assetsSummary } = useWalletAssets()
const portfolio = computed(() => assetsSummary.value?.portfolio) const portfolio = computed(() => assetsSummary.value?.portfolio)
const averageBuyPrice = computed(() => portfolio.value?.average_buy_price ?? null) const weightUnit = computed(() => assetsSummary.value?.meta?.weight_unit || 'گرم')
const liveGoldPrice = computed(() => Number(portfolio.value?.live_gold_price ?? 0))
const hasProfitInsight = computed(() => portfolio.value?.unrealized_profit_rial != null) const hasProfitInsight = computed(() => portfolio.value?.unrealized_profit_rial != null)
const profitTone = computed(() => formatPortfolioProfitLabel(portfolio.value?.unrealized_profit_rial)) const profitTone = computed(() => formatPortfolioProfitLabel(portfolio.value?.unrealized_profit_rial))
@@ -49,81 +63,145 @@ const profitText = computed(() => formatPortfolioProfitText(portfolio.value?.unr
const profitPercent = computed(() => const profitPercent = computed(() =>
formatPortfolioProfitPercent(portfolio.value?.unrealized_profit_percent) 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> </script>
<style scoped lang="less"> <style scoped lang="less">
.portfolio-profit { .portfolio-profit {
display: flex; display: flex;
flex-wrap: wrap; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; gap: 6px;
gap: 6px 8px; padding: 10px 14px;
padding: 8px 10px; border-radius: 12px;
border-radius: 10px; margin-top: 10px;
margin-top: 8px; background: linear-gradient(145deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.06));
background: rgba(255, 255, 255, 0.12); border: 1px solid rgba(255, 255, 255, 0.22);
font-size: 13px; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.1);
font-weight: 700; backdrop-filter: blur(6px);
} }
.portfolio-profit--desktop { .portfolio-profit--desktop {
justify-content: flex-start; align-items: flex-start;
margin-top: 0; margin-top: 0;
margin-bottom: 12px; margin-bottom: 12px;
background: #f9fafb; 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 { .portfolio-profit--up {
color: #bbf7d0; color: #bbf7d0;
border-color: rgba(187, 247, 208, 0.35);
} }
.portfolio-profit--desktop.portfolio-profit--up { .portfolio-profit--desktop.portfolio-profit--up {
color: #027a48; color: #027a48;
background: #ecfdf3; background: linear-gradient(145deg, #ecfdf3, #d1fae5);
border-color: rgba(2, 122, 72, 0.12);
} }
.portfolio-profit--down { .portfolio-profit--down {
color: #fecaca; color: #fecaca;
border-color: rgba(254, 202, 202, 0.35);
} }
.portfolio-profit--desktop.portfolio-profit--down { .portfolio-profit--desktop.portfolio-profit--down {
color: #b42318; color: #b42318;
background: #fef3f2; background: linear-gradient(145deg, #fef3f2, #fee4e2);
border-color: rgba(180, 35, 24, 0.12);
} }
.portfolio-profit__percent { .portfolio-profit__percent {
direction: ltr; direction: ltr;
unicode-bidi: isolate; unicode-bidi: isolate;
font-variant-numeric: tabular-nums; 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 { .portfolio-profit__hint {
width: 100%;
text-align: center;
font-size: 11px; font-size: 11px;
font-weight: 500; font-weight: 500;
opacity: 0.85; opacity: 0.88;
white-space: nowrap;
} }
.portfolio-profit--desktop .portfolio-profit__hint { .portfolio-profit--desktop .portfolio-profit__hint {
text-align: right;
color: #6b7280; color: #6b7280;
opacity: 1; opacity: 1;
} }
.portfolio-profit__footnote { .portfolio-profit__info {
width: 100%; display: inline-flex;
margin: 4px 0 0; align-items: center;
font-size: 10px; justify-content: center;
font-weight: 500; padding: 0;
line-height: 1.5; margin: 0;
opacity: 0.75; border: none;
text-align: center; background: transparent;
} cursor: pointer;
opacity: 0.85;
line-height: 0;
transition: opacity 0.15s ease;
.portfolio-profit--desktop .portfolio-profit__footnote { &:hover {
text-align: right; opacity: 1;
color: #6b7280; }
opacity: 1;
} }
</style> </style>