feat: redesign home, market, wallet, and settings screens
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 153 KiB |
@@ -11,12 +11,15 @@ import goldBarsStack from './gold-bars-stack.png'
|
||||
import coinLarge from './coin-large.png'
|
||||
import coinsStack from './coins-stack.png'
|
||||
import cashBundle from './cash-bundle.png'
|
||||
import homeHeroBg from './home-hero-bg.png'
|
||||
import homeHeroBg from './home-hero-bg.jpg'
|
||||
import marketHero from './market-hero.jpg'
|
||||
import walletHero from './wallet-hero.jpg'
|
||||
import gamepadIcon from './gamepad-icon.png'
|
||||
import targetIcon from './target-icon.png'
|
||||
import cartIcon from './cart-icon.png'
|
||||
import friendsIcon from './friends-icon.png'
|
||||
import challengeBanner from './challenge-banner.png'
|
||||
import nextMissionBanner from './next-mission-banner.webp'
|
||||
import gameGoldChart from './game-gold-chart.png'
|
||||
import gameInflation from './game-inflation.png'
|
||||
import gamePortfolio from './game-portfolio.png'
|
||||
@@ -45,11 +48,14 @@ export const illustrations = {
|
||||
coinsStack,
|
||||
cashBundle,
|
||||
homeHeroBg,
|
||||
marketHero,
|
||||
walletHero,
|
||||
gamepadIcon,
|
||||
targetIcon,
|
||||
cartIcon,
|
||||
friendsIcon,
|
||||
challengeBanner,
|
||||
nextMissionBanner,
|
||||
gameGoldChart,
|
||||
gameInflation,
|
||||
gamePortfolio,
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
@@ -25,9 +25,9 @@ function createFillGradient(chart: Chart) {
|
||||
if (!chartArea) return 'rgba(232, 196, 92, 0.2)'
|
||||
|
||||
const gradient = ctx.createLinearGradient(0, chartArea.top, 0, chartArea.bottom)
|
||||
gradient.addColorStop(0, 'rgba(244, 214, 120, 0.55)')
|
||||
gradient.addColorStop(0.55, 'rgba(118, 75, 190, 0.28)')
|
||||
gradient.addColorStop(1, 'rgba(118, 75, 190, 0)')
|
||||
gradient.addColorStop(0, 'rgba(232, 184, 74, 0.45)')
|
||||
gradient.addColorStop(0.45, 'rgba(232, 184, 74, 0.12)')
|
||||
gradient.addColorStop(1, 'rgba(232, 184, 74, 0)')
|
||||
return gradient
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ const chartData = computed(() => ({
|
||||
datasets: [
|
||||
{
|
||||
data: marketStore.priceHistory,
|
||||
borderColor: '#f4d678',
|
||||
borderColor: '#e8c15a',
|
||||
backgroundColor: (context: ScriptableContext<'line'>) => createFillGradient(context.chart),
|
||||
fill: true,
|
||||
tension: 0.35,
|
||||
@@ -100,11 +100,19 @@ const chartOptions = computed(() => {
|
||||
border: { display: false },
|
||||
},
|
||||
y: {
|
||||
display: false,
|
||||
display: true,
|
||||
position: 'right',
|
||||
min: domain.min,
|
||||
max: domain.max,
|
||||
grid: { display: false },
|
||||
border: { display: false },
|
||||
ticks: {
|
||||
color: '#8e98b8',
|
||||
maxTicksLimit: 4,
|
||||
padding: 6,
|
||||
font: { size: 10, family: 'Vazirmatn' },
|
||||
callback: (value: string | number) => formatNumber(Number(value)),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -119,7 +127,7 @@ const chartOptions = computed(() => {
|
||||
|
||||
<style scoped>
|
||||
.market-chart {
|
||||
height: 196px;
|
||||
height: 148px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
margin-top: 4px;
|
||||
|
||||
@@ -24,13 +24,6 @@ const iconConfig = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const isCoinIncome = computed(
|
||||
() => props.transaction.type === 'income' && props.transaction.unit === 'سکه',
|
||||
)
|
||||
|
||||
const isTomanExpense = computed(
|
||||
() => props.transaction.type === 'expense' && props.transaction.unit === 'تومان',
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -49,35 +42,12 @@ const isTomanExpense = computed(
|
||||
|
||||
<div
|
||||
class="tx-card__amount"
|
||||
:class="{
|
||||
'tx-card__amount--coin': isCoinIncome,
|
||||
'tx-card__amount--expense': isTomanExpense,
|
||||
'tx-card__amount--income': transaction.type === 'income' && !isCoinIncome,
|
||||
}"
|
||||
:class="transaction.type === 'income' ? 'tx-card__amount--income' : 'tx-card__amount--expense'"
|
||||
>
|
||||
<template v-if="isCoinIncome">
|
||||
<span class="tx-card__amount-value">
|
||||
+{{ formatNumber(transaction.amount) }} {{ transaction.unit }}
|
||||
</span>
|
||||
<img
|
||||
:src="illustrations.coinLarge"
|
||||
alt=""
|
||||
class="tx-card__amount-coin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="isTomanExpense">
|
||||
<span class="tx-card__amount-value">
|
||||
−{{ formatNumber(transaction.amount) }}
|
||||
</span>
|
||||
<span class="tx-card__amount-badge" aria-hidden="true" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="tx-card__amount-value">
|
||||
{{ transaction.type === 'income' ? '+' : '−' }}{{ formatNumber(transaction.amount) }}
|
||||
{{ transaction.unit }}
|
||||
</span>
|
||||
</template>
|
||||
<span class="tx-card__amount-value">
|
||||
{{ transaction.type === 'income' ? '+' : '−' }}{{ formatNumber(transaction.amount) }}
|
||||
</span>
|
||||
<span class="tx-card__amount-unit">سکه آموزشی</span>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
@@ -87,11 +57,12 @@ const isTomanExpense = computed(
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
direction: ltr;
|
||||
direction: rtl;
|
||||
min-height: 72px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 18px;
|
||||
background: #151830;
|
||||
border-radius: 16px;
|
||||
background: #171c36;
|
||||
border: 1px solid #3a446e;
|
||||
}
|
||||
|
||||
.tx-card__icon {
|
||||
@@ -162,11 +133,12 @@ const isTomanExpense = computed(
|
||||
}
|
||||
|
||||
.tx-card__amount {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
direction: ltr;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.tx-card__amount-value {
|
||||
@@ -176,16 +148,18 @@ const isTomanExpense = computed(
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tx-card__amount--coin .tx-card__amount-value {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tx-card__amount--expense .tx-card__amount-value {
|
||||
color: #ff6b7a;
|
||||
color: #ff5d6c;
|
||||
}
|
||||
|
||||
.tx-card__amount--income .tx-card__amount-value {
|
||||
color: var(--green);
|
||||
color: #2fce86;
|
||||
}
|
||||
|
||||
.tx-card__amount-unit {
|
||||
color: #8b95ab;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tx-card__amount-coin {
|
||||
|
||||
@@ -15,5 +15,6 @@ import BottomNavigation from '@/components/common/BottomNavigation.vue'
|
||||
min-height: 100dvh;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
overflow-y: visible;
|
||||
}
|
||||
</style>
|
||||
|
||||
+202
-47
@@ -1,14 +1,79 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import HomeHeader from '@/components/home/HomeHeader.vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useWalletStore } from '@/stores/wallet'
|
||||
import { formatNumber, formatDecimal } from '@/utils/storage'
|
||||
import { illustrations } from '@/assets/illustrations'
|
||||
import { goldLandApi } from '@/modules/gold-land/services/goldLand.api'
|
||||
import type { LevelItem } from '@/modules/gold-land/types/goldLand.types'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const walletStore = useWalletStore()
|
||||
const levels = ref<LevelItem[]>([])
|
||||
|
||||
const nextLevel = computed(() => {
|
||||
return (
|
||||
levels.value.find((level) => level.status === 'in_progress') ??
|
||||
levels.value.find((level) => level.status === 'available') ??
|
||||
null
|
||||
)
|
||||
})
|
||||
|
||||
const nextMissionTitle = computed(() => {
|
||||
const level = nextLevel.value
|
||||
if (!level) return 'همهٔ مأموریتهای قلعه را تمام کردی.'
|
||||
return level.description || level.title
|
||||
})
|
||||
|
||||
const nextMissionLines = computed(() => {
|
||||
const words = nextMissionTitle.value.trim().split(/\s+/)
|
||||
if (words.length < 2) return [nextMissionTitle.value, '']
|
||||
const total = nextMissionTitle.value.length
|
||||
let best = 1
|
||||
let bestDiff = Number.POSITIVE_INFINITY
|
||||
let used = 0
|
||||
for (let index = 0; index < words.length - 1; index += 1) {
|
||||
used += words[index].length + (index > 0 ? 1 : 0)
|
||||
const diff = Math.abs(used - total / 2)
|
||||
if (diff < bestDiff) {
|
||||
bestDiff = diff
|
||||
best = index + 1
|
||||
}
|
||||
}
|
||||
return [words.slice(0, best).join(' '), words.slice(best).join(' ')]
|
||||
})
|
||||
|
||||
const nextMissionNumber = computed(() => {
|
||||
const level = nextLevel.value
|
||||
if (!level) return ''
|
||||
return String(level.id).replace(/\d/g, (digit) => '۰۱۲۳۴۵۶۷۸۹'[Number(digit)] ?? digit)
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const progress = await goldLandApi.getProgress()
|
||||
levels.value = progress.levels
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
|
||||
async function continueMission() {
|
||||
const level = nextLevel.value
|
||||
if (!level) {
|
||||
router.push('/map')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await goldLandApi.startLevel(level.id)
|
||||
router.push(`/gold-land/levels/${level.id}`)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const quickActions = [
|
||||
{ label: 'چالش آموزشی', icon: null, image: illustrations.targetIcon, route: '/challenge', color: '#f4ae21' },
|
||||
@@ -20,16 +85,13 @@ const quickActions = [
|
||||
|
||||
<template>
|
||||
<div class="home-page">
|
||||
<!-- Hero -->
|
||||
<section class="hero card-enter">
|
||||
<img
|
||||
:src="illustrations.homeHeroBg"
|
||||
alt=""
|
||||
class="hero__bg"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div class="hero__fade" />
|
||||
|
||||
<img
|
||||
:src="illustrations.homeHeroBg"
|
||||
alt=""
|
||||
class="home-page__bg"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div class="home-page__content">
|
||||
<HomeHeader />
|
||||
|
||||
<div class="hero__stats">
|
||||
@@ -48,6 +110,20 @@ const quickActions = [
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="home-page__stack">
|
||||
<section class="mission-banner card-enter" style="animation-delay:0.04s">
|
||||
<img
|
||||
:src="illustrations.nextMissionBanner"
|
||||
alt=""
|
||||
class="mission-banner__art"
|
||||
/>
|
||||
<p class="mission-banner__title">
|
||||
<span>{{ nextMissionLines[0] }}</span>
|
||||
<span v-if="nextMissionLines[1]">{{ nextMissionLines[1] }}</span>
|
||||
</p>
|
||||
<span class="mission-banner__kind">{{ nextMissionNumber }}</span>
|
||||
<button type="button" class="mission-banner__go" :aria-label="`ادامه ${nextMissionTitle}`" @click="continueMission" />
|
||||
</section>
|
||||
|
||||
<!-- Quick actions -->
|
||||
@@ -90,25 +166,19 @@ const quickActions = [
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.home-page {
|
||||
min-height: 100dvh;
|
||||
padding: 0 0 calc(var(--nav-height) + var(--nav-bottom-offset) + env(safe-area-inset-bottom, 0px) + 16px);
|
||||
background: #040817;
|
||||
}
|
||||
|
||||
/* ── Hero ── */
|
||||
.hero {
|
||||
position: relative;
|
||||
height: 320px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 14px;
|
||||
min-height: 100dvh;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.hero__bg {
|
||||
.home-page__bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
@@ -118,18 +188,23 @@ const quickActions = [
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.hero__fade {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 72px;
|
||||
background: linear-gradient(180deg, transparent 0%, #040817 100%);
|
||||
.home-page__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100dvh;
|
||||
padding: 0 0 calc(var(--nav-height) + var(--nav-bottom-offset) + env(safe-area-inset-bottom, 0px) + 12px);
|
||||
}
|
||||
|
||||
.hero :deep(.home-header) {
|
||||
.home-page__stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.home-page__content :deep(.home-header) {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
@@ -194,13 +269,81 @@ const quickActions = [
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.mission-banner {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin: 0 14px;
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
direction: rtl;
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.mission-banner__art {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.mission-banner__title {
|
||||
position: absolute;
|
||||
top: 34%;
|
||||
right: 5%;
|
||||
width: 52%;
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.35;
|
||||
text-align: right;
|
||||
text-shadow: 0 2px 0 rgba(30, 0, 60, 0.55);
|
||||
}
|
||||
|
||||
.mission-banner__kind {
|
||||
position: absolute;
|
||||
left: 6.4%;
|
||||
top: 52.7%;
|
||||
width: 5.6%;
|
||||
height: 7%;
|
||||
z-index: 2;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: none;
|
||||
color: #f7e7c8;
|
||||
font-size: 3.2cqi;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
text-shadow: 0 1px 0 rgba(40, 16, 0, 0.5);
|
||||
}
|
||||
|
||||
.mission-banner__go {
|
||||
position: absolute;
|
||||
left: 3%;
|
||||
right: 3%;
|
||||
bottom: 4%;
|
||||
z-index: 1;
|
||||
height: 23%;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ── Quick actions ── */
|
||||
.quick-actions {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
padding: 0 14px;
|
||||
margin-bottom: 14px;
|
||||
gap: 10px;
|
||||
padding: 4px 14px 0;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
@@ -212,17 +355,15 @@ const quickActions = [
|
||||
gap: 8px;
|
||||
min-height: 86px;
|
||||
padding: 12px 4px 10px;
|
||||
border-radius: 16px;
|
||||
background: #1a1540;
|
||||
border: 1px solid rgba(118, 75, 190, 0.35);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.05),
|
||||
0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
transition: transform var(--transition-fast);
|
||||
border: none;
|
||||
border-radius: 18px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.22) 0%, rgba(255, 255, 255, 0) 38%),
|
||||
linear-gradient(180deg, #4a348c 0%, #2a1760 52%, #1a0d40 100%);
|
||||
}
|
||||
|
||||
.quick-action:active {
|
||||
transform: scale(0.96);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.quick-action__icon {
|
||||
@@ -237,6 +378,9 @@ const quickActions = [
|
||||
object-fit: contain;
|
||||
background: transparent;
|
||||
display: block;
|
||||
filter:
|
||||
drop-shadow(0 3px 0 rgba(8, 4, 20, 0.55))
|
||||
drop-shadow(0 8px 6px rgba(0, 0, 0, 0.35));
|
||||
}
|
||||
|
||||
.quick-action__label {
|
||||
@@ -245,11 +389,13 @@ const quickActions = [
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
line-height: 1.25;
|
||||
text-shadow: 0 2px 0 rgba(8, 4, 20, 0.65);
|
||||
}
|
||||
|
||||
/* ── Challenge banner ── */
|
||||
.challenge {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
@@ -257,10 +403,14 @@ const quickActions = [
|
||||
margin: 0 14px;
|
||||
min-height: 128px;
|
||||
padding: 16px;
|
||||
border-radius: 20px;
|
||||
border-radius: 22px;
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
box-shadow:
|
||||
0 8px 0 #14081f,
|
||||
0 16px 22px rgba(0, 0, 0, 0.4),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.22);
|
||||
}
|
||||
|
||||
.challenge__bg {
|
||||
@@ -287,6 +437,8 @@ const quickActions = [
|
||||
text-align: right;
|
||||
margin-right: 0;
|
||||
margin-left: auto;
|
||||
padding: 10px 12px 12px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.challenge__title {
|
||||
@@ -296,15 +448,19 @@ const quickActions = [
|
||||
line-height: 1.2;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
text-shadow:
|
||||
0 2px 0 rgba(70, 20, 10, 0.45),
|
||||
0 6px 10px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.challenge__text {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
line-height: 1.55;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
text-shadow: 0 2px 0 rgba(20, 6, 40, 0.7);
|
||||
}
|
||||
|
||||
.challenge__btn {
|
||||
@@ -318,12 +474,11 @@ const quickActions = [
|
||||
font-size: 0.875rem;
|
||||
font-weight: 800;
|
||||
color: #3a2200;
|
||||
background: linear-gradient(180deg, #ffd55e 0%, #f4ae21 55%, #e09810 100%);
|
||||
background: linear-gradient(180deg, #ffe38a 0%, #f6c043 48%, #d8920c 100%);
|
||||
cursor: pointer;
|
||||
transition: transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.challenge__btn:active {
|
||||
transform: scale(0.96);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -167,18 +167,20 @@ onActivated(loadProgress)
|
||||
.map-page {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
height: calc(
|
||||
min-height: calc(
|
||||
100dvh - var(--nav-height) - var(--nav-bottom-offset) - env(safe-area-inset-bottom, 0px)
|
||||
);
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
height: auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.map-scroll {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
min-height: inherit;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.map-header {
|
||||
|
||||
@@ -149,9 +149,10 @@ function forgotPassword() {
|
||||
.login-screen__frame {
|
||||
position: relative;
|
||||
width: min(100%, 430px);
|
||||
height: 100dvh;
|
||||
max-height: 100dvh;
|
||||
overflow: hidden;
|
||||
min-height: 100dvh;
|
||||
height: auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: visible;
|
||||
background: #071427;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -160,8 +161,9 @@ function forgotPassword() {
|
||||
.login-screen__hero {
|
||||
width: 100%;
|
||||
flex: 1 1 auto;
|
||||
height: 0;
|
||||
min-height: 0;
|
||||
height: auto;
|
||||
min-height: 180px;
|
||||
max-height: 52vh;
|
||||
object-fit: cover;
|
||||
object-position: top center;
|
||||
display: block;
|
||||
|
||||
+321
-428
@@ -1,529 +1,422 @@
|
||||
<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 { TrendingUp, Heart } from 'lucide-vue-next'
|
||||
import AppModal from '@/components/common/AppModal.vue'
|
||||
import MarketChart from '@/components/market/MarketChart.vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useWalletStore } from '@/stores/wallet'
|
||||
import { useMarketStore, type TimeRange } from '@/stores/market'
|
||||
import { formatNumber, formatDecimal, formatCurrency, formatPercent } from '@/utils/storage'
|
||||
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 userStore = useUserStore()
|
||||
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: '7d', label: '۱ هفته' },
|
||||
{ key: '30d', label: '۱ ماه' },
|
||||
{ key: '90d', label: '۳ ماه' },
|
||||
{ key: '1y', label: '۱ سال' },
|
||||
]
|
||||
|
||||
const showBuyModal = ref(false)
|
||||
const showSellModal = ref(false)
|
||||
const tradeGrams = ref(1)
|
||||
const tradeBuyEstimate = computed(() => {
|
||||
const g = Number(tradeGrams.value)
|
||||
if (!Number.isFinite(g) || g <= 0) return null
|
||||
return tomansForGoldPurchase(g)
|
||||
})
|
||||
const tradeSellEstimate = computed(() => {
|
||||
const g = Number(tradeGrams.value)
|
||||
if (!Number.isFinite(g) || g <= 0) return null
|
||||
return tomansForGoldSale(g)
|
||||
})
|
||||
const assetValue = computed(() => walletStore.goldGrams * marketStore.currentPrice)
|
||||
const tradeMode = ref<'buy' | 'sell'>('buy')
|
||||
const tradeGrams = ref(0.1)
|
||||
const submitting = ref(false)
|
||||
|
||||
function openBuy() { tradeGrams.value = 1; showBuyModal.value = true }
|
||||
function openSell() { tradeGrams.value = 1; showSellModal.value = true }
|
||||
const tradeEstimate = computed(() => {
|
||||
const grams = Number(tradeGrams.value)
|
||||
if (!Number.isFinite(grams) || grams <= 0) return null
|
||||
return tradeMode.value === 'buy' ? tomansForGoldPurchase(grams) : tomansForGoldSale(grams)
|
||||
})
|
||||
|
||||
async function confirmBuy() {
|
||||
const result = await walletStore.buyGold(tradeGrams.value, marketStore.currentPrice)
|
||||
result.success ? (toast.success(result.message), showBuyModal.value = false) : toast.error(result.message)
|
||||
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 confirmSell() {
|
||||
const result = await walletStore.sellGold(tradeGrams.value, marketStore.currentPrice)
|
||||
result.success ? (toast.success(result.message), showSellModal.value = false) : toast.error(result.message)
|
||||
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">
|
||||
<!-- Header -->
|
||||
<header class="market-header card-enter">
|
||||
<PageBackButton @click="router.push('/')" />
|
||||
<div class="market-header__title">
|
||||
<img :src="illustrations.goldBar" alt="" class="market-header__title-icon" aria-hidden="true" />
|
||||
<h1>بازار طلا</h1>
|
||||
</div>
|
||||
<div class="market-header__coins">
|
||||
<img :src="illustrations.coinLarge" alt="" class="market-header__coin-icon" aria-hidden="true" />
|
||||
<span>{{ formatNumber(userStore.coins) }}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Price + Chart -->
|
||||
<section class="price-section card-enter" style="animation-delay:0.04s">
|
||||
<p class="price-section__label">قیمت هر گرم طلا</p>
|
||||
<p class="price-section__amount">{{ formatNumber(marketStore.currentPrice) }}</p>
|
||||
<div class="price-section__meta">
|
||||
<span class="price-section__unit">تومان</span>
|
||||
<span class="price-section__change">
|
||||
<TrendingUp :size="14" stroke-width="2.5" />
|
||||
{{ formatPercent(marketStore.dailyChange) }} (امروز)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<MarketChart />
|
||||
|
||||
<div class="time-filters">
|
||||
<button
|
||||
v-for="range in timeRanges"
|
||||
:key="range.key"
|
||||
class="time-filter"
|
||||
:class="{ 'time-filter--active': marketStore.selectedRange === range.key }"
|
||||
@click="marketStore.setRange(range.key)"
|
||||
>
|
||||
{{ range.label }}
|
||||
</button>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<!-- Buy / Sell -->
|
||||
<div class="trade-buttons card-enter" style="animation-delay:0.08s">
|
||||
<button type="button" class="trade-btn trade-btn--buy" @click="openBuy">خرید</button>
|
||||
<button type="button" class="trade-btn trade-btn--sell" @click="openSell">فروش</button>
|
||||
</div>
|
||||
|
||||
<!-- Assets -->
|
||||
<section class="asset-card card-enter" style="animation-delay:0.12s">
|
||||
<div class="asset-card__info">
|
||||
<div class="asset-card__title">
|
||||
<Heart :size="14" fill="#f4ae21" color="#f4ae21" aria-hidden="true" />
|
||||
<span>دارایی من</span>
|
||||
<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>
|
||||
<p class="asset-card__grams">
|
||||
{{ formatDecimal(walletStore.goldGrams) }}
|
||||
<span class="asset-card__unit">گرم</span>
|
||||
</p>
|
||||
<p class="asset-card__value">
|
||||
<img :src="illustrations.coinLarge" alt="" class="asset-card__coin" aria-hidden="true" />
|
||||
<span>{{ formatNumber(assetValue) }}</span>
|
||||
<span class="asset-card__value-unit">تومان</span>
|
||||
</p>
|
||||
</div>
|
||||
<img
|
||||
:src="illustrations.treasureChest"
|
||||
alt=""
|
||||
class="asset-card__chest"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<AppModal :open="showBuyModal" title="خرید طلا" @close="showBuyModal = false">
|
||||
<div class="trade-form">
|
||||
<label class="trade-form__label">مقدار (گرم)</label>
|
||||
<input
|
||||
v-model.number="tradeGrams"
|
||||
type="number"
|
||||
min="0.000001"
|
||||
step="0.000001"
|
||||
class="trade-form__input"
|
||||
dir="ltr"
|
||||
/>
|
||||
<p class="trade-form__cost">
|
||||
<template v-if="tradeBuyEstimate != null">
|
||||
برآورد هزینه (خرید به بالا): {{ formatCurrency(tradeBuyEstimate) }}
|
||||
</template>
|
||||
<template v-else>مقدار نامعتبر</template>
|
||||
</p>
|
||||
<p class="trade-form__hint">مبلغ نهایی پس از تأیید از سرور ثبت میشود.</p>
|
||||
<p class="trade-form__balance">موجودی: {{ formatCurrency(walletStore.cash) }}</p>
|
||||
</div>
|
||||
<template #footer>
|
||||
<button type="button" class="trade-btn trade-btn--buy trade-btn--modal" @click="confirmBuy">
|
||||
تأیید خرید
|
||||
</button>
|
||||
</template>
|
||||
</AppModal>
|
||||
<MarketChart />
|
||||
|
||||
<AppModal :open="showSellModal" title="فروش طلا" @close="showSellModal = false">
|
||||
<div class="trade-form">
|
||||
<label class="trade-form__label">مقدار (گرم)</label>
|
||||
<input
|
||||
v-model.number="tradeGrams"
|
||||
type="number"
|
||||
min="0.000001"
|
||||
step="0.000001"
|
||||
class="trade-form__input"
|
||||
dir="ltr"
|
||||
/>
|
||||
<p class="trade-form__cost">
|
||||
<template v-if="tradeSellEstimate != null">
|
||||
برآورد دریافتی (فروش به پایین): {{ formatCurrency(tradeSellEstimate) }}
|
||||
</template>
|
||||
<template v-else>مقدار نامعتبر</template>
|
||||
</p>
|
||||
<p class="trade-form__hint">مبلغ نهایی پس از تأیید از سرور ثبت میشود.</p>
|
||||
<p class="trade-form__balance">طلای موجود: {{ formatDecimal(walletStore.goldGrams) }} گرم</p>
|
||||
</div>
|
||||
<template #footer>
|
||||
<button type="button" class="trade-btn trade-btn--sell trade-btn--modal" @click="confirmSell">
|
||||
تأیید فروش
|
||||
<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>
|
||||
</template>
|
||||
</AppModal>
|
||||
<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;
|
||||
padding: 0 var(--page-padding) calc(var(--nav-height) + var(--nav-bottom-offset) + env(safe-area-inset-bottom, 0px) + 16px);
|
||||
}
|
||||
|
||||
/* ── Header ── */
|
||||
.market-header {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr auto;
|
||||
align-items: center;
|
||||
direction: rtl;
|
||||
gap: 8px;
|
||||
height: 52px;
|
||||
margin-bottom: 18px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.market-header :deep(.page-back-btn) {
|
||||
grid-column: 1;
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.market-header__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.market-header__title-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.market-header__title h1 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 800;
|
||||
color: var(--text-main);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.market-header__coins {
|
||||
grid-column: 3;
|
||||
justify-self: end;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 34px;
|
||||
padding: 5px 12px 5px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 800;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.market-header__coin-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* ── Price section ── */
|
||||
.price-section {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px 14px 14px;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(174, 185, 210, 0.28);
|
||||
border-radius: 22px;
|
||||
padding-bottom: calc(var(--nav-height) + var(--nav-bottom-offset) + env(safe-area-inset-bottom, 0px) + 16px);
|
||||
}
|
||||
|
||||
.price-section__label {
|
||||
text-align: center;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 8px;
|
||||
.market-hero {
|
||||
position: relative;
|
||||
height: 230px;
|
||||
}
|
||||
|
||||
.price-section__amount {
|
||||
.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;
|
||||
font-size: clamp(1.75rem, 7vw, 2rem);
|
||||
}
|
||||
|
||||
.market-header__copy h1 {
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 800;
|
||||
color: var(--gold);
|
||||
line-height: 1.1;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.price-section__meta {
|
||||
.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;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
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;
|
||||
margin-bottom: 6px;
|
||||
color: #2fce86;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.price-section__unit {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.price-section__change {
|
||||
.price-card__change span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
color: #32d74b;
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.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;
|
||||
gap: 6px;
|
||||
margin-top: 12px;
|
||||
direction: rtl;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.time-filter {
|
||||
flex: 1;
|
||||
min-height: 34px;
|
||||
padding: 7px 4px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: var(--text-muted);
|
||||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||
transition: all var(--transition-fast);
|
||||
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: rgba(255, 255, 255, 0.08);
|
||||
color: var(--text-main);
|
||||
border-color: rgba(174, 185, 210, 0.35);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
background: linear-gradient(180deg, #f8d56a 0%, #e2aa2a 100%);
|
||||
color: #2a2416;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
/* ── Trade buttons ── */
|
||||
.trade-buttons {
|
||||
position: relative;
|
||||
.trade-switch {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
direction: ltr;
|
||||
margin-bottom: 22px;
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.trade-buttons::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: -8px;
|
||||
transform: translateX(-50%);
|
||||
width: 110px;
|
||||
height: 2px;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
rgba(241, 140, 22, 0.08) 12%,
|
||||
rgba(241, 140, 22, 0.22) 28%,
|
||||
rgba(241, 140, 22, 0.9) 50%,
|
||||
rgba(241, 140, 22, 0.22) 72%,
|
||||
rgba(241, 140, 22, 0.08) 88%,
|
||||
transparent 100%
|
||||
);
|
||||
box-shadow: 0 0 8px rgba(241, 140, 22, 0.55);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.trade-btn {
|
||||
min-height: 52px;
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
.trade-switch__btn {
|
||||
height: 46px;
|
||||
border-radius: 14px;
|
||||
border: 1.5px solid #7c5cbf;
|
||||
background: transparent;
|
||||
color: #d9ccff;
|
||||
font-family: inherit;
|
||||
font-size: 1.0625rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.trade-switch__btn--active {
|
||||
border-color: transparent;
|
||||
background: #7b46ef;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: transform var(--transition-fast), filter var(--transition-fast);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.trade-btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, transparent 48%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.trade-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.trade-btn--buy {
|
||||
background: linear-gradient(180deg, #34d058 0%, #22b54b 52%, #138d36 100%);
|
||||
}
|
||||
|
||||
.trade-btn--sell {
|
||||
background: linear-gradient(180deg, #ff5060 0%, #e63443 52%, #b8172d 100%);
|
||||
}
|
||||
|
||||
.trade-btn--modal {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ── Asset card ── */
|
||||
.asset-card {
|
||||
.amount-card,
|
||||
.estimate {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-height: 118px;
|
||||
padding: 16px 18px;
|
||||
background: #1a223a;
|
||||
border: 1px solid rgba(174, 185, 210, 0.28);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.asset-card__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
direction: rtl;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.asset-card__title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.asset-card__grams {
|
||||
font-size: 1.625rem;
|
||||
min-height: 58px;
|
||||
padding: 0 14px;
|
||||
border-radius: 16px;
|
||||
background: #171c36;
|
||||
border: 1px solid #3a446e;
|
||||
color: #fff;
|
||||
font-weight: 800;
|
||||
color: var(--text-main);
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.asset-card__unit {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.asset-card__value {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.asset-card__coin {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.asset-card__value-unit {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.asset-card__chest {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
/* ── Trade modal form ── */
|
||||
.trade-form {
|
||||
.amount-card__stepper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.trade-form__label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
.amount-card__stepper button {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: #2a2454;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.trade-form__input {
|
||||
padding: 12px 14px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid rgba(118, 75, 190, 0.45);
|
||||
background: #040817;
|
||||
color: var(--text-main);
|
||||
.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: 1rem;
|
||||
font-weight: 700;
|
||||
appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.trade-form__input::-webkit-outer-spin-button,
|
||||
.trade-form__input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.trade-form__cost {
|
||||
font-size: 1.0625rem;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 800;
|
||||
color: var(--gold-light);
|
||||
}
|
||||
|
||||
.trade-form__hint {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
.trade-submit:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.trade-form__balance {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-muted);
|
||||
.trade-submit:disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
|
||||
+227
-81
@@ -1,29 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Camera, ChevronDown, Eye, EyeOff } from 'lucide-vue-next'
|
||||
import PageBackButton from '@/components/common/PageBackButton.vue'
|
||||
import PrimaryButton from '@/components/common/PrimaryButton.vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { api } from '@/api/client'
|
||||
import { runMeMutation } from '@/api/sync'
|
||||
import { profileAssets } from '@/assets/profile'
|
||||
import { loadFromStorage, saveToStorage } from '@/utils/storage'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const authStore = useAuthStore()
|
||||
const toast = useToast()
|
||||
|
||||
const ages = [8, 9, 10, 11, 12, 13, 14, 15, 16]
|
||||
const displayName = ref(userStore.name)
|
||||
const email = ref(loadFromStorage('settings-email', ''))
|
||||
const age = ref(loadFromStorage('settings-age', 11))
|
||||
const currentPassword = ref('')
|
||||
const newPassword = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const showCurrent = ref(false)
|
||||
const showNew = ref(false)
|
||||
const showConfirm = ref(false)
|
||||
const saving = ref(false)
|
||||
|
||||
function fa(value: number) {
|
||||
return String(value).replace(/\d/g, (digit) => '۰۱۲۳۴۵۶۷۸۹'[Number(digit)] ?? digit)
|
||||
}
|
||||
|
||||
async function saveProfile() {
|
||||
const name = displayName.value.trim()
|
||||
if (!name) {
|
||||
toast.error('نام نمایشی را وارد کنید')
|
||||
toast.error('نام کاربری را وارد کنید')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -53,6 +65,8 @@ async function saveProfile() {
|
||||
try {
|
||||
await runMeMutation(() => api.updateProfile(name))
|
||||
authStore.displayName = name
|
||||
saveToStorage('settings-email', email.value.trim())
|
||||
saveToStorage('settings-age', age.value)
|
||||
|
||||
if (wantsPasswordChange) {
|
||||
await api.updatePassword(currentPassword.value, newPassword.value)
|
||||
@@ -61,7 +75,7 @@ async function saveProfile() {
|
||||
confirmPassword.value = ''
|
||||
}
|
||||
|
||||
toast.success(wantsPasswordChange ? 'تنظیمات و رمز عبور ذخیره شد' : 'تنظیمات ذخیره شد')
|
||||
toast.success(wantsPasswordChange ? 'تنظیمات و رمز عبور ذخیره شد' : 'تغییرات ذخیره شد')
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'خطا در ذخیره')
|
||||
} finally {
|
||||
@@ -78,94 +92,125 @@ async function saveProfile() {
|
||||
<span class="settings-header__spacer" aria-hidden="true" />
|
||||
</header>
|
||||
|
||||
<section class="settings-section card-enter">
|
||||
<h2 class="settings-section__title">حساب کاربری</h2>
|
||||
<section class="settings-card">
|
||||
<h2 class="settings-card__title">اطلاعات حساب</h2>
|
||||
|
||||
<label class="settings-field">
|
||||
<span class="settings-field__label">نام نمایشی</span>
|
||||
<input v-model="displayName" type="text" class="settings-field__input" />
|
||||
</label>
|
||||
<div class="settings-avatar">
|
||||
<img :src="profileAssets.avatar" alt="" class="settings-avatar__img" />
|
||||
<span class="settings-avatar__camera" aria-hidden="true">
|
||||
<Camera :size="14" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<label class="settings-field">
|
||||
<span class="settings-field__label">نام کاربری</span>
|
||||
<input v-model="displayName" type="text" class="settings-field__input" placeholder="نام کاربری" />
|
||||
</label>
|
||||
|
||||
<label class="settings-field">
|
||||
<span class="settings-field__label">ایمیل</span>
|
||||
<input
|
||||
:value="authStore.username ?? '—'"
|
||||
type="text"
|
||||
class="settings-field__input settings-field__input--readonly"
|
||||
readonly
|
||||
v-model="email"
|
||||
type="email"
|
||||
class="settings-field__input settings-field__input--ltr"
|
||||
placeholder="email@example.com"
|
||||
dir="ltr"
|
||||
autocomplete="email"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="settings-field">
|
||||
<span class="settings-field__label">سن</span>
|
||||
<span class="settings-field__select-wrap">
|
||||
<select v-model.number="age" class="settings-field__input settings-field__input--select">
|
||||
<option v-for="item in ages" :key="item" :value="item">{{ fa(item) }} سال</option>
|
||||
</select>
|
||||
<ChevronDown class="settings-field__chevron" :size="18" aria-hidden="true" />
|
||||
</span>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="settings-card">
|
||||
<h2 class="settings-card__title">تغییر رمز عبور</h2>
|
||||
|
||||
<label class="settings-field">
|
||||
<span class="settings-field__label">رمز عبور فعلی</span>
|
||||
<input
|
||||
v-model="currentPassword"
|
||||
type="password"
|
||||
class="settings-field__input"
|
||||
autocomplete="current-password"
|
||||
placeholder="••••••"
|
||||
dir="ltr"
|
||||
/>
|
||||
<span class="settings-field__secret">
|
||||
<input
|
||||
v-model="currentPassword"
|
||||
:type="showCurrent ? 'text' : 'password'"
|
||||
class="settings-field__input"
|
||||
autocomplete="current-password"
|
||||
placeholder="رمز عبور فعلی"
|
||||
/>
|
||||
<button type="button" class="settings-field__eye" @click="showCurrent = !showCurrent">
|
||||
<EyeOff v-if="showCurrent" :size="18" />
|
||||
<Eye v-else :size="18" />
|
||||
</button>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="settings-field">
|
||||
<span class="settings-field__label">رمز عبور جدید</span>
|
||||
<input
|
||||
v-model="newPassword"
|
||||
type="password"
|
||||
class="settings-field__input"
|
||||
autocomplete="new-password"
|
||||
placeholder="حداقل ۶ کاراکتر"
|
||||
dir="ltr"
|
||||
/>
|
||||
<span class="settings-field__secret">
|
||||
<input
|
||||
v-model="newPassword"
|
||||
:type="showNew ? 'text' : 'password'"
|
||||
class="settings-field__input"
|
||||
autocomplete="new-password"
|
||||
placeholder="رمز عبور جدید"
|
||||
/>
|
||||
<button type="button" class="settings-field__eye" @click="showNew = !showNew">
|
||||
<EyeOff v-if="showNew" :size="18" />
|
||||
<Eye v-else :size="18" />
|
||||
</button>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="settings-field">
|
||||
<span class="settings-field__label">تکرار رمز عبور جدید</span>
|
||||
<input
|
||||
v-model="confirmPassword"
|
||||
type="password"
|
||||
class="settings-field__input"
|
||||
autocomplete="new-password"
|
||||
placeholder="••••••"
|
||||
dir="ltr"
|
||||
/>
|
||||
<span class="settings-field__secret">
|
||||
<input
|
||||
v-model="confirmPassword"
|
||||
:type="showConfirm ? 'text' : 'password'"
|
||||
class="settings-field__input"
|
||||
autocomplete="new-password"
|
||||
placeholder="تکرار رمز عبور جدید"
|
||||
/>
|
||||
<button type="button" class="settings-field__eye" @click="showConfirm = !showConfirm">
|
||||
<EyeOff v-if="showConfirm" :size="18" />
|
||||
<Eye v-else :size="18" />
|
||||
</button>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<PrimaryButton variant="gold" full-width :disabled="saving" @click="saveProfile">
|
||||
{{ saving ? 'در حال ذخیره...' : 'ذخیره تغییرات' }}
|
||||
</PrimaryButton>
|
||||
</section>
|
||||
|
||||
<section class="settings-section card-enter" style="animation-delay:0.06s">
|
||||
<h2 class="settings-section__title">اپلیکیشن</h2>
|
||||
<p class="settings-note">سایر تنظیمات بهزودی اضافه میشوند.</p>
|
||||
</section>
|
||||
<button type="button" class="settings-save" :disabled="saving" @click="saveProfile">
|
||||
{{ saving ? 'در حال ذخیره...' : 'ذخیره تغییرات' }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.settings-page {
|
||||
min-height: 100dvh;
|
||||
padding: 0 var(--page-padding) calc(env(safe-area-inset-bottom, 0px) + 24px);
|
||||
padding: 8px 16px calc(var(--nav-height) + var(--nav-bottom-offset) + env(safe-area-inset-bottom, 0px) + 20px);
|
||||
background: #040817;
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr 40px;
|
||||
align-items: center;
|
||||
direction: rtl;
|
||||
min-height: 52px;
|
||||
margin-bottom: 18px;
|
||||
padding-top: 4px;
|
||||
min-height: 48px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.settings-header__title {
|
||||
grid-column: 2;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-size: 1.1875rem;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
}
|
||||
@@ -175,62 +220,163 @@ async function saveProfile() {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
padding: 18px 16px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 18px;
|
||||
background: #0d1730;
|
||||
border: 1px solid rgba(85, 53, 142, 0.25);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.04),
|
||||
0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
.settings-card {
|
||||
margin-top: 14px;
|
||||
padding: 18px 16px 8px;
|
||||
border-radius: 22px;
|
||||
background: #151830;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.settings-section__title {
|
||||
font-size: 0.9375rem;
|
||||
.settings-card__title {
|
||||
margin: 0 0 14px;
|
||||
text-align: center;
|
||||
font-size: 1rem;
|
||||
font-weight: 800;
|
||||
color: var(--gold-light);
|
||||
margin-bottom: 14px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.settings-avatar {
|
||||
position: relative;
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
.settings-avatar__img {
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 3px solid rgba(255, 255, 255, 0.85);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
.settings-avatar__camera {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 2px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
background: #151830;
|
||||
color: #d7e4ff;
|
||||
border: 2px solid rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.settings-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.settings-field__label {
|
||||
font-size: 0.8125rem;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
color: rgba(210, 224, 248, 0.82);
|
||||
}
|
||||
|
||||
.settings-field__input {
|
||||
width: 100%;
|
||||
min-height: 46px;
|
||||
padding: 11px 14px;
|
||||
height: 48px;
|
||||
padding: 0 14px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(118, 75, 190, 0.45);
|
||||
border: 1px solid rgba(150, 180, 230, 0.16);
|
||||
background: #040817;
|
||||
color: var(--text-main);
|
||||
color: #fff;
|
||||
font-family: inherit;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
text-align: right;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.settings-field__input::placeholder {
|
||||
color: rgba(180, 198, 226, 0.45);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.settings-field__input:focus {
|
||||
border-color: rgba(244, 174, 33, 0.65);
|
||||
border-color: rgba(246, 196, 83, 0.7);
|
||||
}
|
||||
|
||||
.settings-field__input--readonly {
|
||||
opacity: 0.75;
|
||||
.settings-field__input:-webkit-autofill,
|
||||
.settings-field__input:-webkit-autofill:hover,
|
||||
.settings-field__input:-webkit-autofill:focus {
|
||||
-webkit-text-fill-color: #fff;
|
||||
caret-color: #fff;
|
||||
box-shadow: 0 0 0 1000px #040817 inset;
|
||||
transition: background-color 9999s ease-out;
|
||||
}
|
||||
|
||||
.settings-note {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.6;
|
||||
.settings-field__input--ltr {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.settings-field__select-wrap,
|
||||
.settings-field__secret {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.settings-field__input--select {
|
||||
appearance: none;
|
||||
padding-left: 36px;
|
||||
}
|
||||
|
||||
.settings-field__chevron {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: rgba(210, 224, 248, 0.8);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.settings-field__secret .settings-field__input {
|
||||
padding-left: 42px;
|
||||
}
|
||||
|
||||
.settings-field__eye {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: rgba(210, 224, 248, 0.85);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.settings-save {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 52px;
|
||||
margin-top: 18px;
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(180deg, #ffe38a 0%, #f6c043 46%, #e3a31a 100%);
|
||||
color: #1b2433;
|
||||
font-family: inherit;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.settings-save:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.settings-save:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: default;
|
||||
}
|
||||
</style>
|
||||
|
||||
+124
-42
@@ -1,52 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import PageTitle from '@/components/common/PageTitle.vue'
|
||||
import FinancialStatCard from '@/components/common/FinancialStatCard.vue'
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { SlidersHorizontal } from 'lucide-vue-next'
|
||||
import TransactionItem from '@/components/wallet/TransactionItem.vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useWalletStore } from '@/stores/wallet'
|
||||
import { formatNumber, formatDecimal, formatCurrency } from '@/utils/storage'
|
||||
import { formatNumber, formatDecimal } from '@/utils/storage'
|
||||
import { illustrations } from '@/assets/illustrations'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const walletStore = useWalletStore()
|
||||
|
||||
const goldLabel = computed(() => formatDecimal(walletStore.goldGrams, 1).replace('٫', '/').replace('.', '/'))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-content wallet-page">
|
||||
<PageTitle title="کیف پول" />
|
||||
<img :src="illustrations.walletHero" alt="" class="wallet-page__bg" />
|
||||
|
||||
<div class="balance-stack">
|
||||
<FinancialStatCard
|
||||
label="تومان مجازی"
|
||||
:value="formatCurrency(walletStore.cash)"
|
||||
:illustration="illustrations.cashBundle"
|
||||
variant="green"
|
||||
large
|
||||
class="card-enter"
|
||||
/>
|
||||
<FinancialStatCard
|
||||
label="طلای من"
|
||||
:value="formatDecimal(walletStore.goldGrams)"
|
||||
unit="گرم"
|
||||
:illustration="illustrations.goldBar"
|
||||
variant="gold"
|
||||
large
|
||||
class="card-enter"
|
||||
style="animation-delay:0.05s"
|
||||
/>
|
||||
<FinancialStatCard
|
||||
label="سکهها"
|
||||
:value="formatNumber(userStore.coins)"
|
||||
:illustration="illustrations.coinLarge"
|
||||
variant="purple"
|
||||
large
|
||||
class="card-enter"
|
||||
style="animation-delay:0.1s"
|
||||
/>
|
||||
<div class="wallet-balances">
|
||||
<article class="balance-card">
|
||||
<img :src="illustrations.goldBarsStack" alt="" class="balance-card__icon" />
|
||||
<p class="balance-card__label">موجودی طلای آموزشی</p>
|
||||
<p class="balance-card__value">{{ goldLabel }}</p>
|
||||
</article>
|
||||
<article class="balance-card">
|
||||
<img :src="illustrations.coinLarge" alt="" class="balance-card__icon" />
|
||||
<p class="balance-card__label">موجودی سکه آموزشی</p>
|
||||
<p class="balance-card__value">{{ formatNumber(userStore.coins) }}</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<section class="wallet-history card-enter" style="animation-delay:0.14s">
|
||||
<h2 class="wallet-history__title">تاریخچه اخیر</h2>
|
||||
<div class="wallet-actions">
|
||||
<button type="button" class="wallet-actions__sell" @click="router.push('/market')">فروش طلا</button>
|
||||
<button type="button" class="wallet-actions__buy" @click="router.push('/market')">خرید طلا</button>
|
||||
</div>
|
||||
|
||||
<section class="wallet-history">
|
||||
<header class="wallet-history__head">
|
||||
<h2>تاریخچه تراکنشها</h2>
|
||||
<SlidersHorizontal :size="18" aria-hidden="true" />
|
||||
</header>
|
||||
<div class="wallet-history__list">
|
||||
<TransactionItem
|
||||
v-for="tx in walletStore.transactions"
|
||||
@@ -60,24 +55,111 @@ const walletStore = useWalletStore()
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.balance-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.wallet-page {
|
||||
position: relative;
|
||||
background: transparent;
|
||||
padding-top: 380px;
|
||||
}
|
||||
|
||||
.wallet-page__bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center top;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wallet-page > :not(.wallet-page__bg) {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.wallet-balances {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.wallet-history {
|
||||
.balance-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 132px;
|
||||
padding: 14px 8px 12px;
|
||||
border-radius: 18px;
|
||||
background: #171c36;
|
||||
border: 1px solid #3a446e;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wallet-history__title {
|
||||
.balance-card__icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.balance-card__label {
|
||||
margin: 0;
|
||||
color: rgba(220, 228, 245, 0.78);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.balance-card__value {
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
font-size: 1.35rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.wallet-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 0.85fr 1.25fr;
|
||||
gap: 10px;
|
||||
direction: rtl;
|
||||
margin: 12px 0 18px;
|
||||
}
|
||||
|
||||
.wallet-actions__buy,
|
||||
.wallet-actions__sell {
|
||||
height: 48px;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wallet-actions__buy {
|
||||
background: linear-gradient(180deg, #f8d56e 0%, #e7b02e 100%);
|
||||
color: #1b2433;
|
||||
}
|
||||
|
||||
.wallet-actions__sell {
|
||||
background: #7b46ef;
|
||||
color: #fff;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.wallet-history__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
direction: rtl;
|
||||
margin-bottom: 12px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.wallet-history__head h2 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.wallet-history__list {
|
||||
|
||||
+1
-6
@@ -30,6 +30,7 @@ const router = createRouter({
|
||||
{ path: 'map', name: 'map', component: () => import('@/pages/LearningMapPage.vue'), meta: { title: 'سرزمین طلا', nav: true } },
|
||||
{ path: 'wallet', name: 'wallet', component: () => import('@/pages/WalletPage.vue'), meta: { title: 'کیف پول', nav: true } },
|
||||
{ path: 'profile', name: 'profile', component: () => import('@/pages/ProfilePage.vue'), meta: { title: 'پروفایل', nav: true } },
|
||||
{ path: 'settings', name: 'settings', component: () => import('@/pages/SettingsPage.vue'), meta: { title: 'تنظیمات', nav: true } },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -62,12 +63,6 @@ const router = createRouter({
|
||||
component: () => import('@/pages/GoldPredictionGamePage.vue'),
|
||||
meta: { title: 'حدس قیمت طلا', nav: false, requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
component: () => import('@/pages/SettingsPage.vue'),
|
||||
meta: { title: 'تنظیمات', nav: false, requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/gold-land/levels/:levelId',
|
||||
name: 'gold-land-level',
|
||||
|
||||
@@ -15,7 +15,7 @@ html {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
scrollbar-gutter: stable;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(174, 185, 210, 0.28) #040817;
|
||||
}
|
||||
@@ -28,7 +28,7 @@ body {
|
||||
background: #040817;
|
||||
color: var(--text-main);
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior-y: none;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@@ -65,6 +65,7 @@ body {
|
||||
background: #040817;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
overflow-y: visible;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user