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:
2026-06-14 10:08:46 +03:30
co-authored by Cursor
parent 4404b4cb92
commit a5e458ed25
234 changed files with 18425 additions and 5503 deletions
@@ -0,0 +1,119 @@
<template>
<div class="dpr-new">
<div class="dpr-new__grid">
<section class="dpr-surface dpr-new__picker">
<h2 class="dpr-new__section-title">انتخاب محصول</h2>
<div class="dpr-new__picker-row">
<BasicSelect
v-model:value="selectedProduct"
:items="items"
class="w-full"
key-name="id"
:show-search="false"
value-name="title"
size="large"
placeholder="لطفا محصول را انتخاب کنید"
:has-tooltip="false"
/>
<BasicButton :disabled-b="selectedProduct == null" type="primary" @click="handleAdd">
افزودن
</BasicButton>
</div>
<p v-if="items?.length" class="dpr-new__hint">
{{ items.length }} محصول با موجودی قابل تحویل
</p>
</section>
<section class="dpr-surface dpr-new__cart">
<div class="dpr-new__cart-header">
<h2 class="dpr-new__section-title">سبد درخواست</h2>
<span class="dpr-new__cart-count">{{ selectedProducts.length }} مورد</span>
</div>
<div v-if="selectedProducts.length" class="dpr-new__cart-items">
<div
v-for="val in selectedProducts"
:key="val.product.id"
class="dpr-new__cart-item"
>
<PhysicalReceiveCartRow :item="val" variant="desktop" @remove="handleRemove" />
</div>
</div>
<div v-else class="dpr-new__cart-empty">
<BasicIcon icon="vuesax-linear:box" size="32" />
<p>محصولی به سبد اضافه نشده است.</p>
</div>
<div class="dpr-new__actions">
<BasicButton
:disabled-b="selectedProducts.length == 0"
class="w-full"
type="primary"
@click="handelFirstSubmit()"
>
ادامه و انتخاب آدرس
</BasicButton>
</div>
</section>
</div>
<BasicModal
v-model:visible="showAddress"
title="انتخاب آدرس"
width="520px"
destroy-on-close
custom-footer
>
<PhysicalReceiveAddressPicker
v-model:selected-address-id="selectedAddressID"
:addresses="address"
variant="desktop"
@add-address="addAddressVisible = true; showAddress = false"
@submit="handelsubmit"
/>
<template #footer>
<div></div>
</template>
</BasicModal>
<BasicModal
v-model:visible="addAddressVisible"
title="افزودن آدرس"
width="520px"
destroy-on-close
custom-footer
>
<New_EditAddress @onOk="handleAddress" />
<template #footer>
<div></div>
</template>
</BasicModal>
</div>
</template>
<script lang="ts" setup>
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import BasicSelect from '@/components/BasicPacks/BasicSelect.vue'
import BasicModal from '@/components/BasicPacks/BasicModal.vue'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import New_EditAddress from '@/views/base/address/components/New_EditAddress.vue'
import PhysicalReceiveCartRow from '../components/PhysicalReceiveCartRow.vue'
import PhysicalReceiveAddressPicker from '../components/PhysicalReceiveAddressPicker.vue'
import { useNewPhysicalRequestContext } from '../composables/useNewPhysicalRequestPage'
const {
items,
selectedProduct,
selectedProducts,
address,
selectedAddressID,
showAddress,
addAddressVisible,
handleAdd,
handleRemove,
handelFirstSubmit,
handelsubmit,
handleAddress,
} = useNewPhysicalRequestContext()
</script>
@@ -0,0 +1,32 @@
<template>
<header class="dpr-topbar">
<button type="button" class="dpr-topbar__back" @click="router.back()">
<BasicIcon icon="vuesax-linear:arrow-right-1" size="18" />
بازگشت
</button>
<div class="dpr-topbar__meta">
<p class="dpr-topbar__eyebrow">سفارش</p>
<h1 class="dpr-topbar__title">{{ pageTitle }}</h1>
<p class="dpr-topbar__subtitle">{{ pageSubtitle }}</p>
</div>
</header>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import router from '@/router'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
const route = useRoute()
const pageTitle = computed(() =>
route.name === 'list' ? 'دریافت فیزیکی' : 'درخواست جدید'
)
const pageSubtitle = computed(() =>
route.name === 'list'
? 'پیگیری درخواست‌های تحویل فیزیکی محصول'
: 'انتخاب محصول و ثبت آدرس تحویل'
)
</script>
@@ -0,0 +1,36 @@
<template>
<div class="dpr-list">
<div class="dpr-list__toolbar">
<span class="dpr-list__count">{{ requestItems.length }} درخواست</span>
<button type="button" class="dpr-list__add" @click="openAdd()">
<BasicIcon icon="vuesax-linear:add" size="16" />
درخواست جدید
</button>
</div>
<div v-if="requestItems.length" class="dpr-list__items">
<RequestItem
v-for="item in requestItems"
:key="item.id"
:item="item"
variant="desktop"
/>
</div>
<div v-else class="dpr-list__empty">
<BasicIcon icon="vuesax-linear:box" size="40" />
<p>هنوز درخواستی ثبت نشده است.</p>
<button type="button" class="dpr-list__empty-btn" @click="openAdd()">
ثبت اولین درخواست
</button>
</div>
</div>
</template>
<script setup lang="ts">
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import RequestItem from '../components/RequestItem.vue'
import { usePhysicalReceiveListContext } from '../composables/usePhysicalReceiveList'
const { requestItems, openAdd } = usePhysicalReceiveListContext()
</script>
@@ -0,0 +1,301 @@
/* Premium desktop physical receive */
.dpr {
min-height: 100%;
background: #f6f7f9;
color: #111827;
}
.dpr__container {
max-width: 1180px;
margin: 0 auto;
padding: 28px 36px 40px;
}
.dpr-topbar {
display: flex;
align-items: flex-start;
gap: 16px;
margin-bottom: 28px;
padding-bottom: 24px;
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
&__back {
display: inline-flex;
align-items: center;
gap: 8px;
border: none;
background: transparent;
color: #6b7280;
font-size: 13px;
font-weight: 500;
font-family: inherit;
cursor: pointer;
padding: 8px 0;
flex-shrink: 0;
&:hover {
color: #111827;
}
}
&__meta {
flex: 1;
min-width: 0;
}
&__eyebrow {
font-size: 12px;
font-weight: 500;
color: #9ca3af;
margin: 0 0 4px;
}
&__title {
font-size: 26px;
font-weight: 600;
letter-spacing: -0.03em;
color: #111827;
margin: 0;
line-height: 1.2;
}
&__subtitle {
font-size: 14px;
color: #6b7280;
margin: 6px 0 0;
line-height: 1.6;
}
}
.dpr-surface {
background: #fff;
border: 1px solid rgba(15, 23, 42, 0.06);
border-radius: 16px;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
}
.dpr-list {
padding: 24px;
background: #fff;
border: 1px solid rgba(15, 23, 42, 0.06);
border-radius: 16px;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
&__toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 20px;
}
&__count {
font-size: 14px;
color: #6b7280;
}
&__add {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 18px;
border: none;
border-radius: 10px;
background: @App-primary-color;
color: #fff;
font-size: 14px;
font-weight: 600;
font-family: inherit;
cursor: pointer;
&:hover {
filter: brightness(0.95);
}
}
&__items {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
&__empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
min-height: 240px;
padding: 32px 16px;
color: #6b7280;
text-align: center;
p {
margin: 0;
font-size: 15px;
}
}
&__empty-btn {
margin-top: 8px;
padding: 10px 20px;
border: none;
border-radius: 10px;
background: @App-primary-color;
color: #fff;
font-size: 14px;
font-weight: 600;
font-family: inherit;
cursor: pointer;
}
}
.dpr-item {
margin: 0 !important;
border-radius: 16px !important;
}
.dpr-new {
&__grid {
display: grid;
grid-template-columns: minmax(0, 380px) minmax(0, 1fr);
gap: 20px;
align-items: start;
}
&__picker,
&__cart {
padding: 24px;
}
&__section-title {
font-size: 16px;
font-weight: 600;
color: #111827;
margin: 0 0 16px;
}
&__picker-row {
display: flex;
gap: 10px;
align-items: flex-start;
}
&__hint {
margin: 12px 0 0;
font-size: 13px;
color: #9ca3af;
}
&__cart-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
}
&__cart-count {
font-size: 13px;
color: #9ca3af;
}
&__cart-items {
display: flex;
flex-direction: column;
gap: 12px;
max-height: 420px;
overflow-y: auto;
margin-bottom: 20px;
}
&__cart-item {
padding: 14px;
border-radius: 12px;
background: #f9fafb;
border: 1px solid rgba(15, 23, 42, 0.05);
}
&__cart-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
min-height: 200px;
color: #9ca3af;
text-align: center;
p {
margin: 0;
font-size: 14px;
}
}
&__actions {
padding-top: 8px;
border-top: 1px solid rgba(15, 23, 42, 0.06);
}
}
.dpr-cart-row__input {
width: min(100%, 240px);
min-width: 220px;
flex-shrink: 0;
.noname-input,
.ant-input-group-wrapper {
width: 100%;
}
.ant-input-group-addon {
min-width: 4.75rem;
padding-inline: 10px;
white-space: nowrap;
font-size: 12px;
font-weight: 600;
}
}
.dpr-address-picker {
&--desktop {
display: flex;
flex-direction: column;
gap: 16px;
}
&__list {
max-height: 320px;
overflow-y: auto;
}
&__item {
padding: 14px 16px;
margin-bottom: 10px;
border-radius: 12px;
border: 1px solid rgba(15, 23, 42, 0.08);
background: #f9fafb;
font-size: 14px;
}
&__actions {
display: flex;
gap: 10px;
padding-top: 8px;
> button:last-child {
flex: 1;
}
}
}
@media (max-width: 900px) {
.dpr-list__items {
grid-template-columns: 1fr;
}
.dpr-new__grid {
grid-template-columns: 1fr;
}
}