Redesign orders/cart/search flows and fix RTL/auth edge cases in user-app

- Fix silent redirect-to-home for unauthenticated users on protected routes;
  now redirects to login with a return path
- Restyle OrderCard, SearchInput, Filters, DropDown, and product card image
  sizing/centering to match the app's LESS-token design language
- Fix ant-design Slider handle placement/drag under RTL and seed a valid
  default range so handles actually render
- Fix chip-row edge padding and top spacing across Orders/Cart/Search lists
- Add pull-to-refresh to the dashboard and fix its header/padding to match
  the fixed PageBar pattern
- Remove the floating WhatsApp button from index.html
This commit is contained in:
s.esmaeili
2026-09-16 01:15:16 +03:30
parent 0e5f9ccd7b
commit e756fbaf85
30 changed files with 890 additions and 365 deletions
+15 -6
View File
@@ -32,10 +32,12 @@
<Spinner :spinning="cartData.loadingPage" tip="درحال دریافت اطلاعات">
<container class="cart__body">
<!-- Step 1: the basket -->
<template v-if="cartData.step === 1">
<PullRefresh v-if="cartData.step === 1" v-model="refreshing" @refresh="onRefresh">
<template #pulling><Loading size="18" /></template>
<template #loosing><Loading size="18" /></template>
<template #loading><Loading size="18" /></template>
<template #success><span /></template>
<template v-if="cart.items.length">
<SectionHead title="اقلام سبد" :subtitle="`${cart.items.length} قلم`" />
<div class="cart__items">
<CartItem
v-for="item in cart.items"
@@ -58,7 +60,7 @@
دیدن محصولات
</button>
</section>
</template>
</PullRefresh>
<!-- Step 2: submitted -->
<section v-else class="cart__done">
@@ -141,12 +143,12 @@
import { onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Drawer, Form, FormItem, Textarea } from 'ant-design-vue'
import { Loading, PullRefresh } from 'vant'
import dialogEmpty4 from '../assets/img/empty/dialogEmpty4.svg'
import loginDialogImage from '../assets/img/empty/dialogEmpty1.svg'
import Icon from '../components/icon/Icon.vue'
import PageBar from '../components/ui/PageBar.vue'
import Spinner from '../components/Spinner.vue'
import SectionHead from '../components/home/SectionHead.vue'
import DialogVant from '../components/DialogVant.vue'
import CartItem from '../components/product/CartItem.vue'
import { useCartStore, type CartItem as CartItemType } from '../stores/cartStore'
@@ -158,6 +160,7 @@ const cart = useCartStore()
const session = useSessionStore()
const loginNotify = ref(false)
const refreshing = ref(false)
const cartData = reactive({
loadingPage: false,
@@ -213,6 +216,11 @@ const submitOrder = async () => {
}
}
async function onRefresh() {
await cart.init()
refreshing.value = false
}
onMounted(async () => {
await cart.init()
})
@@ -225,7 +233,7 @@ onMounted(async () => {
}
.cart__body {
padding-top: 16px;
padding-top: 76px;
padding-bottom: 150px;
}
@@ -233,6 +241,7 @@ onMounted(async () => {
display: flex;
flex-direction: column;
gap: 11px;
padding-top: 16px;
}
.cart__empty {
+23 -29
View File
@@ -35,6 +35,11 @@
</div>
</header>
<PullRefresh v-model="refreshing" @refresh="onRefresh">
<template #pulling><Loading size="18" /></template>
<template #loosing><Loading size="18" /></template>
<template #loading><Loading size="18" /></template>
<template #success><span /></template>
<container class="home__body">
<RateStrip :quote="goldPrice" class="home__rate" />
@@ -85,11 +90,6 @@
<p>هنوز محصولی در مجموعه ثبت نشده است.</p>
</section>
<section class="home__section">
<SectionHead title="حساب من" subtitle="موجودی و دسترسی‌های شما" />
<AccountStrip :customer="customer" @go="onAccountNav" @login="goLogin" />
</section>
<div class="home__trust">
<div class="home__trustItem">
<Icon icon="vuesax-linear:shield-tick" size="18" />
@@ -105,6 +105,7 @@
</div>
</div>
</container>
</PullRefresh>
<Drawer
v-model:visible="productDrawer"
@@ -123,6 +124,7 @@
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Drawer } from 'ant-design-vue'
import { Loading, PullRefresh } from 'vant'
import Icon from '../components/icon/Icon.vue'
import SingleProduct from '../components/product/SingleProduct.vue'
import RateStrip from '../components/home/RateStrip.vue'
@@ -131,25 +133,22 @@ import type { ShowcaseSlide } from '../components/home/ShowcaseSlider.vue'
import CategoryRail from '../components/home/CategoryRail.vue'
import ShowcaseCard from '../components/home/ShowcaseCard.vue'
import SectionHead from '../components/home/SectionHead.vue'
import AccountStrip from '../components/home/AccountStrip.vue'
import { useConfigStore } from '../stores/configStore'
import type { HomeCategory } from '../stores/configStore'
import { useCartStore } from '../stores/cartStore'
import { useFiltersStore } from '../stores/filtersStore'
import { useSessionStore } from '../stores/session'
import { api } from '../api/client'
import type { CustomerDetail, GoldPriceQuote, ProductSummary } from '../api/client'
import type { GoldPriceQuote, ProductSummary } from '../api/client'
const router = useRouter()
const memory = useConfigStore()
const cart = useCartStore()
const filters = useFiltersStore()
const session = useSessionStore()
const loading = ref(true)
const refreshing = ref(false)
const products = ref<ProductSummary[]>([])
const goldPrice = ref<GoldPriceQuote | null>(null)
const customer = ref<CustomerDetail | null>(null)
const productDrawer = ref(false)
const activeProductId = ref<string | null>(null)
@@ -203,28 +202,18 @@ function onSlide(slide: ShowcaseSlide) {
router.push({ name: 'search' })
}
function onAccountNav(routeName: string) {
router.push({ name: routeName })
}
function goSearch() {
router.push({ name: 'search' })
}
function goNewest() {
router.push({ name: 'newestProduct' })
}
function goLogin() {
router.push({ name: 'login' })
}
onMounted(async () => {
async function loadData() {
try {
const [list] = await Promise.all([api.listProducts(), memory.init()])
products.value = list
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
try {
@@ -232,14 +221,16 @@ onMounted(async () => {
} catch {
goldPrice.value = null
}
}
if (session.customerId) {
try {
customer.value = await api.getCustomer(session.customerId)
} catch {
customer.value = null
}
}
async function onRefresh() {
await loadData()
refreshing.value = false
}
onMounted(async () => {
await loadData()
loading.value = false
})
</script>
@@ -250,8 +241,10 @@ onMounted(async () => {
}
.home__bar {
position: sticky;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 20;
background: rgba(255, 255, 255, 0.88);
backdrop-filter: blur(14px);
@@ -303,6 +296,7 @@ onMounted(async () => {
}
.home__body {
padding-top: 62px;
padding-bottom: 90px;
}
+1 -1
View File
@@ -82,7 +82,7 @@ const pull = async () => {
}
.newest__body {
padding-top: 16px;
padding-top: 76px;
padding-bottom: 90px;
}
@@ -72,7 +72,7 @@ onMounted(async () => {
}
.coll__body {
padding-top: 16px;
padding-top: 76px;
padding-bottom: 90px;
}
+32 -7
View File
@@ -62,7 +62,12 @@
@change="hideFilters"
/>
<container v-show="!showFilter" class="search__body">
<PullRefresh v-show="!showFilter" v-model="refreshing" @refresh="onRefresh">
<template #pulling><Loading size="18" /></template>
<template #loosing><Loading size="18" /></template>
<template #loading><Loading size="18" /></template>
<template #success><span /></template>
<container class="search__body">
<div v-if="availableCollections.length" class="search__collections">
<button
v-for="item in availableCollections"
@@ -92,7 +97,7 @@
reaches the end — the same incremental behaviour the previous
ScrollFetchSearch-based list had. -->
<div v-if="hasMore" ref="sentinel" class="search__more">
<Spinner :spinning="true" :indicator="true" tip="" cls="my-[10px]" />
<Spinner :spinning="true" :indicator="true" :fixed="false" tip="" cls="my-[10px]" />
</div>
<section v-else-if="!loadingPage" class="search__empty">
@@ -100,12 +105,14 @@
<p>محصولی با این مشخصات پیدا نشد.</p>
</section>
</container>
</PullRefresh>
</Spinner>
</div>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { PullRefresh } from 'vant'
import Icon from '../../components/icon/Icon.vue'
import PageBar from '../../components/ui/PageBar.vue'
import Spinner from '../../components/Spinner.vue'
@@ -121,6 +128,7 @@ const filters = useFiltersStore()
const nav = useNavStore()
const loadingPage = ref(false)
const refreshing = ref(false)
const allProducts = ref<ProductSummary[]>([])
const showFilter = ref(false)
const filterKeys = ref<string[]>([])
@@ -203,6 +211,11 @@ async function loadProducts() {
}
}
async function onRefresh() {
await loadProducts()
refreshing.value = false
}
function search(value: string | undefined) {
filters.query.text = value
}
@@ -245,18 +258,26 @@ onMounted(loadProducts)
.search__barExtra {
max-width: 768px;
margin: 0 auto;
padding: 0 16px 11px;
padding: 0 16px 0;
}
.search__chips {
display: flex;
gap: 8px;
gap: 4px;
overflow-x: auto;
margin: 11px -16px 0;
padding: 0 16px;
margin: 8px 0px 8px -16px;
// `padding` alone on an RTL flex scroller can get its edge padding
// collapsed by the browser once content overflows — explicit spacer
// elements hold the same 16px gap reliably at both scroll extremes.
scroll-snap-type: x proximity;
}
.search__chips::before,
.search__chips::after {
content: '';
flex: 0 0 16px;
}
.search__chip {
scroll-snap-align: start;
flex: 0 0 auto;
@@ -292,10 +313,14 @@ onMounted(loadProducts)
}
.search__body {
padding-top: 16px;
padding-top: 160px;
padding-bottom: 90px;
}
.search__body > *:first-child {
margin-top: 16px;
}
.search__collections {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
+17 -4
View File
@@ -48,6 +48,7 @@
</div>
</header>
<PullRefresh v-model="refreshing" @refresh="onRefresh">
<container class="dash__body">
<!-- Signed in: real position from GET /customers/:id -->
<template v-if="customer">
@@ -117,6 +118,7 @@
<RateStrip :quote="goldPrice" />
</section>
</container>
</PullRefresh>
<DialogVant :timeOut="-1" v-model:show="data.notify" :closeOnClickOverlay="true">
<template v-slot:img>
@@ -150,6 +152,7 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { PullRefresh } from 'vant'
import Icon from '../../components/icon/Icon.vue'
import SectionHead from '../../components/home/SectionHead.vue'
import RateStrip from '../../components/home/RateStrip.vue'
@@ -165,6 +168,7 @@ const session = useSessionStore()
const customer = ref<CustomerDetail | null>(null)
const goldPrice = ref<GoldPriceQuote | null>(null)
const refreshing = ref(false)
// The ledger stores the gram position as a signed figure — positive means the
// customer owes gold. Labelled the same way AccountStrip does on the home
@@ -201,7 +205,7 @@ function logOut() {
router.push({ name: 'home' })
}
onMounted(async () => {
async function loadDashboard() {
if (session.customerId) {
customer.value = await api.getCustomer(session.customerId)
}
@@ -210,7 +214,14 @@ onMounted(async () => {
} catch {
goldPrice.value = null
}
})
}
async function onRefresh() {
await loadDashboard()
refreshing.value = false
}
onMounted(loadDashboard)
</script>
<style scoped lang="less">
@@ -220,8 +231,10 @@ onMounted(async () => {
}
.dash__bar {
position: sticky;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 20;
background: rgba(255, 255, 255, 0.88);
backdrop-filter: blur(14px);
@@ -269,7 +282,7 @@ onMounted(async () => {
}
.dash__body {
padding-top: 16px;
padding-top: 76px;
padding-bottom: 90px;
}
@@ -69,7 +69,7 @@ onMounted(async () => {
}
.favs__body {
padding-top: 16px;
padding-top: 76px;
padding-bottom: 90px;
}
+5 -3
View File
@@ -84,7 +84,7 @@
<script setup lang="ts">
import { computed, nextTick, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import { Form, FormItem, Input } from 'ant-design-vue'
import Icon from '../../components/icon/Icon.vue'
import PageBar from '../../components/ui/PageBar.vue'
@@ -96,6 +96,7 @@ import { api, type CustomerSummary } from '../../api/client'
const emit = defineEmits<{ (e: 'register', value: boolean): void }>()
const router = useRouter()
const route = useRoute()
const session = useSessionStore()
const formModel = reactive({ username: undefined as string | undefined })
@@ -152,7 +153,8 @@ const sendToken = async (_otpCode: string) => {
body.loading = true
if (!matchedCustomer) return
session.login(matchedCustomer.id, matchedCustomer.mobile)
await router.push({ name: 'home' })
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : undefined
await router.push(redirect ?? { name: 'home' })
notify.text = 'با موفقیت وارد شدید'
notify.show = true
} finally {
@@ -172,7 +174,7 @@ const btnText = computed(() => (body.step === 1 ? 'دریافت رمز عبور'
}
.auth__body {
padding-top: 26px;
padding-top: 86px;
padding-bottom: 90px;
}
@@ -0,0 +1,51 @@
<!--
New nav destination (bottom nav redesign) — no legacy equivalent and no
notifications backend exists in apps/api. Renders the same dashed
empty-state pattern used elsewhere (see FavoritesView) rather than
fabricating fake notification data.
-->
<template>
<div class="notif">
<PageBar title="اعلان‌ها" back-to="home" />
<container class="notif__body">
<section class="notif__empty">
<Icon icon="vuesax-linear:notification" size="26" />
<p>اعلانی برای نمایش وجود ندارد.</p>
</section>
</container>
</div>
</template>
<script setup lang="ts">
import Icon from '../../components/icon/Icon.vue'
import PageBar from '../../components/ui/PageBar.vue'
</script>
<style scoped lang="less">
.notif {
background: @canvas;
min-height: 100vh;
}
.notif__body {
padding-top: 76px;
padding-bottom: 90px;
}
.notif__empty {
margin-top: 8px;
padding: 38px 20px;
border: 1px dashed @hairline;
border-radius: 18px;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
color: @inkFaint;
}
.notif__empty p {
margin: 0;
font-size: 12.5px;
}
</style>
+13 -6
View File
@@ -48,9 +48,9 @@
</PageBar>
<Spinner :spinning="loading" tip="درحال دریافت اطلاعات">
<PullRefresh v-model="refreshing" @refresh="onRefresh">
<container class="ord__body">
<template v-if="filteredOrders.length">
<SectionHead title="سفارش‌های شما" :subtitle="`${filteredOrders.length} سفارش`" />
<div class="ord__list">
<OrderCard v-for="item in filteredOrders" :key="item.id" :value="item" @view="openDetail" />
</div>
@@ -61,6 +61,7 @@
<p>سفارشی با این مشخصات پیدا نشد.</p>
</section>
</container>
</PullRefresh>
</Spinner>
<Drawer
@@ -144,10 +145,10 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { Drawer } from 'ant-design-vue'
import { PullRefresh } from 'vant'
import Icon from '../../components/icon/Icon.vue'
import PageBar from '../../components/ui/PageBar.vue'
import Spinner from '../../components/Spinner.vue'
import SectionHead from '../../components/home/SectionHead.vue'
import SearchInput from '../../components/search/SearchInput.vue'
import OrderCard from '../../components/product/OrderCard.vue'
import OrderItemCard from '../../components/product/OrderItemCard.vue'
@@ -177,6 +178,7 @@ const tabs = [
const activeTab = ref('ALL')
const loading = ref(false)
const refreshing = ref(false)
const allOrders = ref<OrderDetail[]>([])
const searchTerm = ref<string | undefined>(undefined)
@@ -213,6 +215,11 @@ function openDetail(item: OrderDetail) {
viewData.model = true
}
async function onRefresh() {
await loadOrders()
refreshing.value = false
}
onMounted(loadOrders)
</script>
@@ -225,15 +232,14 @@ onMounted(loadOrders)
.ord__barExtra {
max-width: 768px;
margin: 0 auto;
padding: 0 16px 11px;
padding: 0 16px 0;
}
.ord__chips {
display: flex;
gap: 8px;
overflow-x: auto;
margin: 11px -16px 0;
padding: 0 16px;
margin: 8px 0px 8px -16px;
scroll-snap-type: x proximity;
}
@@ -264,13 +270,14 @@ onMounted(loadOrders)
}
.ord__body {
padding-top: 16px;
padding-top: 160px;
padding-bottom: 90px;
}
.ord__list {
display: flex;
flex-direction: column;
padding-top: 16px;
gap: 11px;
}
+1 -1
View File
@@ -103,7 +103,7 @@ onMounted(async () => {
}
.prof__body {
padding-top: 20px;
padding-top: 80px;
padding-bottom: 90px;
}
@@ -132,7 +132,7 @@ const register = async () => {
}
.auth__body {
padding-top: 26px;
padding-top: 86px;
padding-bottom: 90px;
}
@@ -157,7 +157,7 @@ onMounted(async () => {
}
.tx__body {
padding-top: 16px;
padding-top: 76px;
padding-bottom: 90px;
}