Split desktop and mobile layouts with shared page data so APIs fetch once per context.
Add layout shells, desktop views, module-level page caches, and UI fixes for auth, home, wallet, transactions, address, and related pages. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div>
|
||||
<MobileOverflowMenu
|
||||
:visible="popVisible"
|
||||
:overflow-items="overflowItems"
|
||||
@close="closeOverflowMenu"
|
||||
@navigate="navigateTo"
|
||||
/>
|
||||
|
||||
<div :class="['main-with-bottom-chrome', { 'main-with-bottom-chrome--no-nav': hideMenu }]">
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition :name="route.meta.transition || 'fade'">
|
||||
<component :is="Component" class="main-with-bottom-chrome-inner" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
|
||||
<MobileBottomNav
|
||||
v-if="!hideMenu"
|
||||
:is-boursecu="isBoursecu"
|
||||
:nav-logo="navLogo"
|
||||
:nav-item-class="navItemClass"
|
||||
@navigate="navigateTo"
|
||||
@toggle-overflow="toggleOverflowMenu"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { useConfigStore } from '@/store/configStore'
|
||||
import { getProjectNavLogo, isBoursecuProject } from '@/utils/config'
|
||||
import { SECONDARY_NAV } from '../config/navigation'
|
||||
import { useAppNavigation } from '../composables/useAppNavigation'
|
||||
import MobileBottomNav from './MobileBottomNav.vue'
|
||||
import MobileOverflowMenu from './MobileOverflowMenu.vue'
|
||||
|
||||
const config = useConfigStore()
|
||||
const isBoursecu = isBoursecuProject()
|
||||
const navLogo = computed(() => getProjectNavLogo(config.config?.avatar))
|
||||
|
||||
const {
|
||||
popVisible,
|
||||
hideMenu,
|
||||
navItemClass,
|
||||
navigateTo,
|
||||
toggleOverflowMenu,
|
||||
closeOverflowMenu,
|
||||
} = useAppNavigation()
|
||||
|
||||
const overflowItems = [
|
||||
{ id: 'user', routeName: 'user', label: 'حساب کاربری', icon: 'vuesax-linear:user' },
|
||||
...SECONDARY_NAV,
|
||||
]
|
||||
</script>
|
||||
|
||||
<style lang="less" src="./mobile-layout.less"></style>
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="bottom-appbar w-auto mx-auto bg-white">
|
||||
<div class="tabs bottom-0">
|
||||
<div
|
||||
v-if="isBoursecu"
|
||||
:class="navItemClass('buyProduct')"
|
||||
class="tab"
|
||||
@click="$emit('navigate', 'buyProduct')"
|
||||
>
|
||||
<Icon icon="vuesax-linear:box" />
|
||||
<div class="text-xs mt-1 font-bold">سفارش ها</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:class="navItemClass('home')"
|
||||
class="tab"
|
||||
@click="$emit('navigate', 'home')"
|
||||
>
|
||||
<Icon icon="vuesax-linear:home" />
|
||||
<div class="text-xs mt-1 font-bold">صفحه اصلی</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:class="navItemClass('transactions')"
|
||||
class="tab"
|
||||
@click="$emit('navigate', 'transactions')"
|
||||
>
|
||||
<Icon icon="vuesax-linear:receipt-item" />
|
||||
<div class="text-xs mt-1 font-bold">{{ isBoursecu ? 'تراکنش ها' : 'گزارش' }}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isBoursecu"
|
||||
:class="navItemClass('home')"
|
||||
class="tab"
|
||||
@click="$emit('navigate', 'home')"
|
||||
>
|
||||
<Icon icon="vuesax-linear:home" />
|
||||
<div class="text-xs mt-1 font-bold">صفحه اصلی</div>
|
||||
</div>
|
||||
<div v-else class="main_in" @click="$emit('navigate', 'buyProduct')">
|
||||
<div
|
||||
class="circle_in"
|
||||
:class="isWhiteNavLogo ? 'is-white-nav-logo' : 'text-white'"
|
||||
>
|
||||
<div class="circle_into" @click.stop="$emit('toggle-overflow')">
|
||||
<div
|
||||
v-if="projectName === 'garoosi'"
|
||||
class="p-2 text-tertiary flex flex-col justify-center items-center"
|
||||
>
|
||||
<Icon icon="vuesax-linear:coin" />
|
||||
<div class="text-[9px] font-bold">محصولات</div>
|
||||
</div>
|
||||
<img
|
||||
v-else
|
||||
class="p-2"
|
||||
:src="navLogo"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:class="navItemClass('wallet')"
|
||||
class="tab"
|
||||
@click="$emit('navigate', 'wallet')"
|
||||
>
|
||||
<Icon icon="vuesax-linear:wallet-1" />
|
||||
<div class="text-xs mt-1 font-bold">کیف پول</div>
|
||||
</div>
|
||||
<div
|
||||
:class="navItemClass('user')"
|
||||
class="tab"
|
||||
@click="$emit('navigate', 'user')"
|
||||
>
|
||||
<Icon icon="vuesax-linear:setting-2" />
|
||||
<div class="text-xs mt-1 font-bold">تنظیمات</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isBoursecu" class="main">
|
||||
<div class="circle" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import Icon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { getConfigApp } from '@/utils/config'
|
||||
|
||||
defineProps<{
|
||||
isBoursecu: boolean
|
||||
navLogo: string
|
||||
navItemClass: (routeName: string) => string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
navigate: [routeName: string]
|
||||
'toggle-overflow': []
|
||||
}>()
|
||||
|
||||
const projectName = getConfigApp('name')
|
||||
const whiteNavLogoProjects = ['goldenramin']
|
||||
const isWhiteNavLogo = computed(() => whiteNavLogoProjects.includes(projectName))
|
||||
</script>
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition>
|
||||
<div
|
||||
v-if="visible"
|
||||
class="top-0 bottom-0 bg-primary wrap-fix z-2000 opacity-60"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
</Transition>
|
||||
<Transition name="slide-fade">
|
||||
<div v-if="visible" class="wrap-fix bottom-11vh z-2000">
|
||||
<div class="relative flex flex-col items-center">
|
||||
<div
|
||||
class="flex items-center justify-center bg-white rounded-full w-[30px] h-[30px] mb-2"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<BasicIcon icon="vuesax-linear:close-circle" />
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<BasicButton
|
||||
v-for="item in overflowItems"
|
||||
:key="item.id"
|
||||
icon-size="16"
|
||||
class="!font-bold w-15rem"
|
||||
@click="$emit('navigate', item.routeName)"
|
||||
>
|
||||
<div class="flex gap-2 items-center">
|
||||
<BasicIcon :icon="item.icon" size="16" class="absolute right-2" />
|
||||
<div>{{ item.label }}</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import type { AppNavItem } from '../types'
|
||||
|
||||
defineProps<{
|
||||
visible: boolean
|
||||
overflowItems: AppNavItem[]
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
close: []
|
||||
navigate: [routeName: string]
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,145 @@
|
||||
.bottom-appbar {
|
||||
z-index: var(--app-z-nav, 5000);
|
||||
height: var(--app-nav-height, 80px);
|
||||
position: fixed;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
background: #fff;
|
||||
bottom: var(--app-credit-footer-offset, 28px);
|
||||
right: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
max-width: 100vw;
|
||||
box-sizing: border-box;
|
||||
|
||||
@media screen and (min-width: 426px) {
|
||||
width: 426px;
|
||||
max-width: 426px;
|
||||
left: 50%;
|
||||
right: auto;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
justify-content: space-between;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
z-index: 400;
|
||||
box-shadow: 0 -2px 4px -2px #0000001a;
|
||||
|
||||
.tab {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
flex-flow: column;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.main_in {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
flex-shrink: 0;
|
||||
align-self: center;
|
||||
|
||||
.circle_in {
|
||||
margin-top: -10px;
|
||||
border-radius: 50%;
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
flex-shrink: 0;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
flex-wrap: wrap;
|
||||
background-color: @App-primary-color;
|
||||
|
||||
.circle_into {
|
||||
overflow: hidden;
|
||||
@apply bg-primary;
|
||||
border-radius: 50%;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
flex-wrap: wrap;
|
||||
border: white 1px solid;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.circle_in.is-white-nav-logo {
|
||||
.circle_into {
|
||||
background: #fff !important;
|
||||
border-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
height: 70px;
|
||||
width: 70px;
|
||||
position: relative;
|
||||
|
||||
.circle {
|
||||
border-radius: 50%;
|
||||
height: 70px;
|
||||
width: 70px;
|
||||
z-index: 20;
|
||||
box-shadow: 0 -5px 4px -2px #0000001a;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-enter-active,
|
||||
.v-leave-active {
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.v-enter-from,
|
||||
.v-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-fade-enter-active {
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.slide-fade-leave-active {
|
||||
transition: all 0.4s cubic-bezier(1, 0.5, 0.8, 1);
|
||||
}
|
||||
|
||||
.slide-fade-enter-from,
|
||||
.slide-fade-leave-to {
|
||||
transform: translateX(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.main-with-bottom-chrome {
|
||||
height: calc(100vh - 72px - var(--app-credit-footer-offset, 28px));
|
||||
}
|
||||
|
||||
.main-with-bottom-chrome-inner {
|
||||
height: calc(100vh - 72px - var(--app-credit-footer-offset, 28px));
|
||||
}
|
||||
|
||||
.main-with-bottom-chrome--no-nav,
|
||||
.main-with-bottom-chrome--no-nav .main-with-bottom-chrome-inner {
|
||||
height: calc(100vh - var(--app-credit-footer-offset, 28px));
|
||||
}
|
||||
Reference in New Issue
Block a user