Files
gold_land_user/src/pages/MarketPage.vue
T
safavimohadeseh78andCursor 8a311c1fd5
Frontend CI / unit (test, type-check, build) (push) Failing after 1m25s
Frontend CI / browser E2E (frontend + pinned backend) (push) Skipped
feat: redesign home, market, wallet, and settings screens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-26 14:54:44 +03:30

423 lines
9.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>