DashboardView ("میز کار"): Rebuilt from scratch with a new structure, distinct from the old card+grid layout but visually aligned with HomeView (same blurred bar, @ink/@wine palette, staged reveal animation on mount):
Single hero card (ink→wine gradient) showing balance, gold/fiat debt, and identity — no quick-action icons inside it anymore.
Live gold rate folded into a slim inline row at the bottom instead of a titled section.
Iterated shadow/spacing on the hero card per feedback (softer, less clipped).
DialogVant / DialogVantOneButton (shared modal components, used in 7+ call sites: Dashboard, Cart, SingleProduct, Register, App): Replaced Vant's default dialog chrome with the app's own rounded-card language — circular icon badge for the illustration, pill-shaped primary/secondary buttons. No call-site markup changed.
LoginView: Rebuilt from scratch — full-bleed dark hero (brand + step copy) overlapped by a floating white form sheet, animated crossfade transition between the mobile-number and OTP steps, same staged entrance animation as Home/Dashboard. All auth logic (API check, OTP stub, session login) unchanged.
202 lines
4.4 KiB
Vue
202 lines
4.4 KiB
Vue
<!--
|
|
Editorial hero for the home screen. Replaces the legacy swiper banner with a
|
|
self-contained, dependency-free slider: no swiper CSS to fight, no autoplay
|
|
jitter, and it keeps working when a slide has no artwork.
|
|
-->
|
|
<template>
|
|
<section class="showcase" v-if="slides.length">
|
|
<div class="showcase__frame">
|
|
<transition-group name="showcase-fade" tag="div" class="showcase__stage">
|
|
<article v-for="(slide, i) in slides" :key="slide.key" v-show="i === index" class="showcase__slide">
|
|
<img v-if="slide.image" :src="slide.image" alt="" class="showcase__art" />
|
|
<div class="showcase__scrim" aria-hidden="true"></div>
|
|
|
|
<div class="showcase__copy">
|
|
<span class="showcase__eyebrow">{{ slide.eyebrow }}</span>
|
|
<h2 class="showcase__title">{{ slide.title }}</h2>
|
|
<p class="showcase__sub">{{ slide.subtitle }}</p>
|
|
<button type="button" class="showcase__cta" @click="emit('select', slide)">
|
|
{{ slide.cta }}
|
|
<Icon icon="vuesax-linear:arrow-left-2" size="17" />
|
|
</button>
|
|
</div>
|
|
</article>
|
|
</transition-group>
|
|
</div>
|
|
|
|
<div class="showcase__dots" v-if="slides.length > 1">
|
|
<button
|
|
v-for="(slide, i) in slides"
|
|
:key="slide.key"
|
|
type="button"
|
|
class="showcase__dot"
|
|
:class="{ 'is-active': i === index }"
|
|
:aria-label="slide.title"
|
|
@click="go(i)"
|
|
/>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
|
import Icon from '../icon/Icon.vue'
|
|
|
|
export interface ShowcaseSlide {
|
|
key: string
|
|
eyebrow: string
|
|
title: string
|
|
subtitle: string
|
|
cta: string
|
|
image?: string
|
|
categoryId?: string
|
|
}
|
|
|
|
const props = defineProps<{ slides: ShowcaseSlide[] }>()
|
|
const emit = defineEmits<{ (e: 'select', slide: ShowcaseSlide): void }>()
|
|
|
|
const index = ref(0)
|
|
let timer: ReturnType<typeof setInterval> | undefined
|
|
|
|
function go(i: number) {
|
|
index.value = i
|
|
restart()
|
|
}
|
|
function restart() {
|
|
if (timer) clearInterval(timer)
|
|
if (props.slides.length > 1) {
|
|
timer = setInterval(() => {
|
|
index.value = (index.value + 1) % props.slides.length
|
|
}, 6000)
|
|
}
|
|
}
|
|
|
|
onMounted(restart)
|
|
onBeforeUnmount(() => {
|
|
if (timer) clearInterval(timer)
|
|
})
|
|
</script>
|
|
|
|
<style scoped lang="less">
|
|
.showcase__frame {
|
|
position: relative;
|
|
border-radius: 22px;
|
|
overflow: hidden;
|
|
background: @ink;
|
|
box-shadow: 0 26px 50px -34px rgba(20, 22, 26, 0.85);
|
|
}
|
|
|
|
.showcase__stage {
|
|
position: relative;
|
|
display: block;
|
|
aspect-ratio: 320 / 208;
|
|
}
|
|
|
|
.showcase__slide {
|
|
position: absolute;
|
|
inset: 0;
|
|
}
|
|
|
|
.showcase__art {
|
|
position: absolute;
|
|
inset: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
opacity: 0.5;
|
|
transform: scale(1.04);
|
|
}
|
|
|
|
.showcase__scrim {
|
|
position: absolute;
|
|
inset: 0;
|
|
background:
|
|
linear-gradient(255deg, rgba(122, 27, 51, 0.55) 0%, rgba(122, 27, 51, 0) 52%),
|
|
linear-gradient(to left, rgba(20, 22, 26, 0.2) 0%, rgba(20, 22, 26, 0.92) 72%);
|
|
}
|
|
|
|
.showcase__copy {
|
|
position: absolute;
|
|
inset: 0;
|
|
padding: 22px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
justify-content: center;
|
|
color: #fff;
|
|
}
|
|
|
|
.showcase__eyebrow {
|
|
font-size: 10.5px;
|
|
letter-spacing: 1.6px;
|
|
color: rgba(255, 255, 255, 0.62);
|
|
padding-bottom: 9px;
|
|
}
|
|
|
|
.showcase__title {
|
|
font-size: 22px;
|
|
font-weight: 800;
|
|
line-height: 1.35;
|
|
color: #fff;
|
|
margin: 0 0 7px;
|
|
}
|
|
|
|
.showcase__sub {
|
|
font-size: 12.5px;
|
|
line-height: 1.85;
|
|
color: rgba(255, 255, 255, 0.7);
|
|
margin: 0 0 16px;
|
|
max-width: 30ch;
|
|
}
|
|
|
|
.showcase__cta {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
border: 0;
|
|
cursor: pointer;
|
|
padding: 9px 16px;
|
|
border-radius: 999px;
|
|
background: #fff;
|
|
color: @ink;
|
|
font-size: 12.5px;
|
|
font-weight: 700;
|
|
font-family: inherit;
|
|
transition: transform 0.18s ease, background 0.18s ease;
|
|
}
|
|
.showcase__cta:active {
|
|
transform: scale(0.97);
|
|
}
|
|
|
|
.showcase__dots {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 6px;
|
|
padding-top: 12px;
|
|
}
|
|
|
|
.showcase__dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
padding: 0;
|
|
border: 0;
|
|
cursor: pointer;
|
|
border-radius: 999px;
|
|
background: @hairline;
|
|
transition: all 0.25s ease;
|
|
}
|
|
.showcase__dot.is-active {
|
|
width: 20px;
|
|
background: @wine;
|
|
}
|
|
|
|
.showcase-fade-enter-active,
|
|
.showcase-fade-leave-active {
|
|
transition: opacity 0.55s ease;
|
|
}
|
|
.showcase-fade-enter-from,
|
|
.showcase-fade-leave-to {
|
|
opacity: 0;
|
|
}
|
|
</style>
|