423 lines
9.6 KiB
Vue
423 lines
9.6 KiB
Vue
<script setup lang="ts">
|
||
import { ref, computed } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { Minus, Plus, TrendingUp } from 'lucide-vue-next'
|
||
import PageBackButton from '@/components/common/PageBackButton.vue'
|
||
import MarketChart from '@/components/market/MarketChart.vue'
|
||
import { useWalletStore } from '@/stores/wallet'
|
||
import { useMarketStore, type TimeRange } from '@/stores/market'
|
||
import { formatNumber, formatDecimal, formatPercent } from '@/utils/storage'
|
||
import { tomansForGoldPurchase, tomansForGoldSale } from '@/utils/goldTrade'
|
||
import { useToast } from '@/composables/useToast'
|
||
import { illustrations } from '@/assets/illustrations'
|
||
|
||
const router = useRouter()
|
||
const walletStore = useWalletStore()
|
||
const marketStore = useMarketStore()
|
||
const toast = useToast()
|
||
|
||
const timeRanges: { key: TimeRange; label: string }[] = [
|
||
{ key: '1d', label: '۱ روز' },
|
||
{ key: '7d', label: '۱ هفته' },
|
||
{ key: '30d', label: '۱ ماه' },
|
||
{ key: '90d', label: '۳ ماه' },
|
||
{ key: '1y', label: '۱ سال' },
|
||
]
|
||
|
||
const tradeMode = ref<'buy' | 'sell'>('buy')
|
||
const tradeGrams = ref(0.1)
|
||
const submitting = ref(false)
|
||
|
||
const tradeEstimate = computed(() => {
|
||
const grams = Number(tradeGrams.value)
|
||
if (!Number.isFinite(grams) || grams <= 0) return null
|
||
return tradeMode.value === 'buy' ? tomansForGoldPurchase(grams) : tomansForGoldSale(grams)
|
||
})
|
||
|
||
const gramLabel = computed(() => formatDecimal(tradeGrams.value, 1).replace('٫', '/').replace('.', '/'))
|
||
|
||
function stepGrams(direction: -1 | 1) {
|
||
const next = Math.round((tradeGrams.value + direction * 0.1) * 10) / 10
|
||
tradeGrams.value = Math.min(100, Math.max(0.1, next))
|
||
}
|
||
|
||
async function submitTrade() {
|
||
if (submitting.value) return
|
||
submitting.value = true
|
||
try {
|
||
const result =
|
||
tradeMode.value === 'buy'
|
||
? await walletStore.buyGold(tradeGrams.value, marketStore.currentPrice)
|
||
: await walletStore.sellGold(tradeGrams.value, marketStore.currentPrice)
|
||
result.success ? toast.success(result.message) : toast.error(result.message)
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="market-page">
|
||
<section class="market-hero">
|
||
<img :src="illustrations.marketHero" alt="" class="market-hero__img" />
|
||
<header class="market-header">
|
||
<PageBackButton @click="router.push('/')" />
|
||
<div class="market-header__copy" />
|
||
<span class="market-header__spacer" aria-hidden="true" />
|
||
</header>
|
||
</section>
|
||
|
||
<div class="market-body">
|
||
<section class="price-card">
|
||
<div class="price-card__top">
|
||
<img :src="illustrations.goldBarsStack" alt="" class="price-card__gold" />
|
||
<div class="price-card__main">
|
||
<p class="price-card__label">قیمت هر گرم طلای آموزشی</p>
|
||
<p class="price-card__amount">{{ formatNumber(marketStore.currentPrice) }}</p>
|
||
<p class="price-card__unit">سکه آموزشی</p>
|
||
</div>
|
||
<div class="price-card__change" :class="{ 'price-card__change--down': marketStore.dailyChange < 0 }">
|
||
<span>
|
||
<TrendingUp :size="14" />
|
||
{{ formatPercent(marketStore.dailyChange) }}
|
||
</span>
|
||
<small>در ۲۴ ساعت گذشته</small>
|
||
</div>
|
||
</div>
|
||
|
||
<MarketChart />
|
||
|
||
<div class="time-filters">
|
||
<button
|
||
v-for="range in timeRanges"
|
||
:key="range.key"
|
||
type="button"
|
||
class="time-filter"
|
||
:class="{ 'time-filter--active': marketStore.selectedRange === range.key }"
|
||
@click="marketStore.setRange(range.key)"
|
||
>
|
||
{{ range.label }}
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="trade-switch">
|
||
<button
|
||
type="button"
|
||
class="trade-switch__btn"
|
||
:class="{ 'trade-switch__btn--active': tradeMode === 'buy' }"
|
||
@click="tradeMode = 'buy'"
|
||
>
|
||
خرید طلا
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="trade-switch__btn"
|
||
:class="{ 'trade-switch__btn--active': tradeMode === 'sell' }"
|
||
@click="tradeMode = 'sell'"
|
||
>
|
||
فروش طلا
|
||
</button>
|
||
</div>
|
||
|
||
<div class="amount-card">
|
||
<span>مقدار (گرم)</span>
|
||
<div class="amount-card__stepper">
|
||
<button type="button" aria-label="کم کردن" @click="stepGrams(-1)"><Minus :size="18" /></button>
|
||
<strong>{{ gramLabel }}</strong>
|
||
<button type="button" aria-label="زیاد کردن" @click="stepGrams(1)"><Plus :size="18" /></button>
|
||
</div>
|
||
</div>
|
||
|
||
<p class="estimate">
|
||
<span>مبلغ تقریبی</span>
|
||
<strong>{{ tradeEstimate == null ? '—' : formatNumber(tradeEstimate) }}</strong>
|
||
</p>
|
||
|
||
<button type="button" class="trade-submit" :disabled="submitting" @click="submitTrade">
|
||
{{ tradeMode === 'buy' ? 'خرید طلا' : 'فروش طلا' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.market-page {
|
||
position: relative;
|
||
min-height: 100dvh;
|
||
background: transparent;
|
||
padding-bottom: calc(var(--nav-height) + var(--nav-bottom-offset) + env(safe-area-inset-bottom, 0px) + 16px);
|
||
}
|
||
|
||
.market-hero {
|
||
position: relative;
|
||
height: 230px;
|
||
}
|
||
|
||
.market-hero__img {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100dvh;
|
||
object-fit: cover;
|
||
object-position: center top;
|
||
z-index: 0;
|
||
}
|
||
|
||
.market-header {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 1;
|
||
display: grid;
|
||
grid-template-columns: 40px 1fr 40px;
|
||
align-items: start;
|
||
padding: 8px 10px 0;
|
||
direction: rtl;
|
||
}
|
||
|
||
.market-header__copy {
|
||
text-align: center;
|
||
}
|
||
|
||
.market-header__copy h1 {
|
||
margin: 0;
|
||
color: #fff;
|
||
font-size: 1.2rem;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.market-header__copy p {
|
||
margin: 4px 0 0;
|
||
color: rgba(255, 255, 255, 0.88);
|
||
font-size: 0.75rem;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.market-header__spacer {
|
||
width: 40px;
|
||
}
|
||
|
||
.market-body {
|
||
position: relative;
|
||
z-index: 1;
|
||
margin-top: -28px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
padding: 0 14px;
|
||
}
|
||
|
||
.price-card {
|
||
position: relative;
|
||
padding: 14px 14px 10px;
|
||
border-radius: 18px;
|
||
background: #171c36;
|
||
border: 1px solid #3a446e;
|
||
}
|
||
|
||
.price-card::before {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 0;
|
||
border-radius: inherit;
|
||
padding: 1.5px;
|
||
background: linear-gradient(
|
||
90deg,
|
||
transparent 0%,
|
||
rgba(240, 208, 120, 0.2) 14%,
|
||
#f6d98a 50%,
|
||
rgba(240, 208, 120, 0.2) 86%,
|
||
transparent 100%
|
||
);
|
||
-webkit-mask:
|
||
linear-gradient(#000 0 0) content-box,
|
||
linear-gradient(#000 0 0);
|
||
-webkit-mask-composite: xor;
|
||
mask:
|
||
linear-gradient(#000 0 0) content-box,
|
||
linear-gradient(#000 0 0);
|
||
mask-composite: exclude;
|
||
clip-path: inset(0 0 80% 0);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.price-card__top {
|
||
display: grid;
|
||
grid-template-columns: 54px 1fr auto;
|
||
gap: 10px;
|
||
align-items: center;
|
||
direction: rtl;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.price-card__gold {
|
||
width: 54px;
|
||
height: 42px;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.price-card__label {
|
||
margin: 0;
|
||
color: rgba(220, 228, 245, 0.82);
|
||
font-size: 0.75rem;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.price-card__amount {
|
||
margin: 2px 0 0;
|
||
color: #fff;
|
||
font-size: 1.45rem;
|
||
font-weight: 800;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.price-card__unit {
|
||
margin: 0;
|
||
color: rgba(220, 228, 245, 0.7);
|
||
font-size: 0.75rem;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.price-card__change {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 4px;
|
||
direction: ltr;
|
||
color: #2fce86;
|
||
font-size: 0.82rem;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.price-card__change span {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.price-card__change small {
|
||
color: rgba(220, 228, 245, 0.62);
|
||
font-size: 0.68rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.price-card__change--down {
|
||
color: #ff6b6b;
|
||
}
|
||
|
||
.time-filters {
|
||
display: flex;
|
||
direction: rtl;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.time-filter {
|
||
flex: 0 0 auto;
|
||
height: 28px;
|
||
padding: 0 8px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
background: transparent;
|
||
color: #9aa3c2;
|
||
font-family: inherit;
|
||
font-size: 0.75rem;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.time-filter--active {
|
||
background: linear-gradient(180deg, #f8d56a 0%, #e2aa2a 100%);
|
||
color: #2a2416;
|
||
padding: 0 12px;
|
||
}
|
||
|
||
.trade-switch {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 10px;
|
||
direction: rtl;
|
||
}
|
||
|
||
.trade-switch__btn {
|
||
height: 46px;
|
||
border-radius: 14px;
|
||
border: 1.5px solid #7c5cbf;
|
||
background: transparent;
|
||
color: #d9ccff;
|
||
font-family: inherit;
|
||
font-size: 0.95rem;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.trade-switch__btn--active {
|
||
border-color: transparent;
|
||
background: #7b46ef;
|
||
color: #fff;
|
||
}
|
||
|
||
.amount-card,
|
||
.estimate {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
direction: rtl;
|
||
min-height: 58px;
|
||
padding: 0 14px;
|
||
border-radius: 16px;
|
||
background: #171c36;
|
||
border: 1px solid #3a446e;
|
||
color: #fff;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.amount-card__stepper {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.amount-card__stepper button {
|
||
display: grid;
|
||
place-items: center;
|
||
width: 32px;
|
||
height: 32px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
background: #2a2454;
|
||
color: #fff;
|
||
}
|
||
|
||
.amount-card__stepper strong {
|
||
min-width: 36px;
|
||
text-align: center;
|
||
font-size: 1.05rem;
|
||
}
|
||
|
||
.estimate strong {
|
||
font-size: 1.25rem;
|
||
}
|
||
|
||
.trade-submit {
|
||
height: 52px;
|
||
border: none;
|
||
border-radius: 16px;
|
||
background: linear-gradient(180deg, #f8d56e 0%, #e7b02e 100%);
|
||
color: #1b2433;
|
||
font-family: inherit;
|
||
font-size: 1.05rem;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.trade-submit:active {
|
||
transform: translateY(1px);
|
||
}
|
||
|
||
.trade-submit:disabled {
|
||
opacity: 0.7;
|
||
}
|
||
</style>
|