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,79 @@
<template>
<div class="dauth" :class="{ 'dauth--boursecu': isBoursecu }">
<div class="dauth__layout">
<aside class="dauth-brand" :class="{ 'dauth-brand--boursecu': isBoursecu }">
<div class="dauth-brand__pattern" />
<div class="dauth-brand__content">
<img
:src="logo"
alt=""
class="dauth-brand__logo"
:class="{ 'dauth-brand__logo--round': !hasCustomLayout }"
>
<h1 class="dauth-brand__title">{{ brandTitle }}</h1>
<p v-if="tagline" class="dauth-brand__tagline">{{ tagline }}</p>
<ul class="dauth-brand__features">
<li v-for="feature in features" :key="feature" class="dauth-brand__feature">
<span class="dauth-brand__feature-icon">
<BasicIcon icon="vuesax-linear:tick-circle" size="16" />
</span>
{{ feature }}
</li>
</ul>
</div>
<div class="dauth-brand__footer">نسخه {{ version }}</div>
</aside>
<main class="dauth-form-panel">
<slot />
</main>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import pkg from '../../../../../package.json'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import { useConfigStore } from '@/store/configStore'
import {
getProjectLoginLogo,
getProjectLoginTaglineLines,
hasCustomLoginLogoLayout,
isBoursecuProject,
} from '@/utils/config'
const config = useConfigStore()
const isBoursecu = isBoursecuProject()
const hasCustomLayout = hasCustomLoginLogoLayout()
const version = pkg.version
const logo = computed(() => getProjectLoginLogo(config.config?.avatar))
const taglineLines = computed(() => getProjectLoginTaglineLines())
const tagline = computed(() => {
if (taglineLines.value?.length) {
return taglineLines.value.join(' ')
}
return 'پلتفرم امن و سریع برای مدیریت دارایی و معاملات شما'
})
const brandTitle = computed(() => config.config?.name_fa ?? 'خوش آمدید')
const features = computed(() => {
if (isBoursecu) {
return [
'قیمت لحظه‌ای و نمودار بازار',
'مدیریت دارایی و کیف پول',
'معاملات سریع و امن',
]
}
return [
'ورود امن با پیامک یا رمز عبور',
'مدیریت یکپارچه دارایی‌ها',
'دسترسی سریع به خدمات',
]
})
</script>
<style lang="less" src="../styles/desktop-auth.less"></style>
@@ -0,0 +1,204 @@
<script setup lang="ts">
import { mobileValidation, required } from '@/utils/inputRules'
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import OtpInput from 'vue3-otp-input'
import { useConfigStore } from '@/store/configStore'
import { useLoginPageContext } from '../../composables/useLoginPage'
import DesktopPwaGuideModal from './DesktopPwaGuideModal.vue'
import router from '@/router'
const emit = defineEmits<{
register: [val: boolean]
}>()
const config = useConfigStore()
const {
time,
enableSendAgainCode,
stepTwoVerify,
formRef,
refOtpInput,
isSMS,
isRegister,
showPWAInstallGuide,
userFormState,
loading,
submitLabel,
showFingerprint,
setIsSMS,
reSendCode,
enterFingerPrint,
handleOnChange,
handleOnComplete,
phoneVerification,
editPhoneNumber,
installWebApp,
proceedWithInstall,
} = useLoginPageContext().loginForm
function goToEducate() {
router.push({ name: 'educate' })
}
function goToAboutUs() {
router.push('/aboutUs')
}
</script>
<template>
<div class="dauth-card">
<header class="dauth-card__header">
<p class="dauth-card__eyebrow">حساب کاربری</p>
<h2 class="dauth-card__title">ورود</h2>
<p class="dauth-card__subtitle">برای ادامه، روش ورود خود را انتخاب کنید</p>
</header>
<div class="dauth-toggle">
<button
type="button"
class="dauth-toggle__btn"
:class="{ 'dauth-toggle__btn--active': isSMS }"
@click="setIsSMS(true)"
>
ورود با پیامک
</button>
<button
type="button"
class="dauth-toggle__btn"
:class="{ 'dauth-toggle__btn--active': !isSMS }"
@click="setIsSMS(false)"
>
ورود با کلمه عبور
</button>
</div>
<a-form
ref="formRef"
:model="userFormState"
layout="vertical"
class="dauth-form"
name="desktop-login"
@finish="phoneVerification"
>
<a-form-item
v-if="!stepTwoVerify"
:rules="[required('شماره تلفن'), mobileValidation('شماره تلفن')]"
:validate-trigger="['change', 'blur']"
label="شماره تلفن"
name="mobile"
>
<BasicInput
v-model:value="userFormState.mobile"
place-holder="۰۹۱۲۳۴۵۶۷۸۹"
size="large"
inputmode="numeric"
type="tel"
/>
</a-form-item>
<a-form-item
v-if="!isSMS"
:rules="[required('کلمه عبور')]"
label="کلمه عبور"
name="password"
>
<AInputPassword
v-model:value="userFormState.password"
place-holder="کلمه عبور"
size="large"
/>
</a-form-item>
<template v-if="isSMS && stepTwoVerify">
<AFormItem
:rules="[
{ required: true, message: 'کد ارسال شده را وارد نمایید' },
{ min: 4, max: 4, message: 'کد ارسال شده را وارد نمایید' },
]"
class="error-token"
name="token"
>
<OtpInput
ref="refOtpInput"
:is-input-num="true"
:num-inputs="4"
:should-auto-focus="true"
autocomplete="one-time-code"
class="dauth-otp justify-center"
input-classes="otp-input"
separator=""
style="direction: ltr"
value=""
@on-change="handleOnChange"
@on-complete="handleOnComplete"
/>
</AFormItem>
<div class="dauth-phone-edit">
<span class="dauth-phone-edit__number">{{ userFormState.mobile }}</span>
<button type="button" class="dauth-phone-edit__btn" @click="editPhoneNumber">
<span>ویرایش شماره</span>
<BasicIcon icon="vuesax-linear:edit" size="16" />
</button>
</div>
<div class="dauth-resend">
<AButton :disabled="!enableSendAgainCode" class="w-full" type="dashed" @click="reSendCode">
<span v-if="!enableSendAgainCode">
ارسال مجدد کد در <span class="text-primary mx-1">{{ time }}</span> دیگر
</span>
<span v-else>ارسال مجدد کد تایید</span>
</AButton>
</div>
</template>
<div class="dauth-actions">
<BasicButton :disabled="loading" class="w-full" html-type="submit" type="primary">
<a-spin v-if="loading" size="small" />
<span v-else>{{ submitLabel }}</span>
</BasicButton>
<BasicButton
v-if="showFingerprint"
:disabled="loading"
class="w-full"
@click="enterFingerPrint"
>
ورود با اثر انگشت
</BasicButton>
<template v-if="config.config?.name === 'garoosi'">
<div class="dauth-actions-row">
<a
class="dauth-link-btn dauth-link-btn--primary"
download="Garoosi.apk"
href="https://garoosigholaktala.ir/apk/Garoosi.apk"
>
دانلود اپ اندروید
</a>
<button type="button" class="dauth-link-btn" @click="installWebApp">
نصب نسخه وب اپ
</button>
</div>
<button type="button" class="dauth-link-btn" @click="goToEducate">
آموزش کار با نرم افزار
</button>
<button type="button" class="dauth-link-btn" @click="goToAboutUs">
درباره ما
</button>
</template>
</div>
<div v-if="isRegister" class="dauth-switch">
<span>حساب کاربری ندارید؟</span>
<button type="button" class="dauth-switch__link" @click="emit('register', true)">
ثبت نام کنید
</button>
</div>
</a-form>
<DesktopPwaGuideModal v-model:show="showPWAInstallGuide" @confirm="proceedWithInstall" />
</div>
</template>
@@ -0,0 +1,66 @@
<script setup lang="ts">
import BasicModal from '@/components/BasicPacks/BasicModal.vue'
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import shareAppleIcon from '@/assets/img/share-apple.png'
import addToHomeScreenIcon from '@/assets/img/Add-to-homeScreen.png'
defineProps<{
show: boolean
}>()
const emit = defineEmits<{
'update:show': [value: boolean]
confirm: []
}>()
function close() {
emit('update:show', false)
}
function confirm() {
emit('confirm')
close()
}
</script>
<template>
<BasicModal
:visible="show"
:custom-footer="true"
destroy-on-close
@update:visible="emit('update:show', $event)"
>
<h2 class="text-base font-semibold text-center mb-2 text-gray-800">
وب اپلیکیشن گروسی را به صفحه اصلی خود اضافه کنید
</h2>
<div class="dauth-pwa-steps">
<div class="dauth-pwa-step">
<div class="dauth-pwa-step__icon">
<img :src="shareAppleIcon" alt="Share" />
</div>
<p class="dauth-pwa-step__text">روی دکمه Share در نوار پایین کلیک کنید.</p>
</div>
<div class="dauth-pwa-step">
<div class="dauth-pwa-step__icon">
<img :src="addToHomeScreenIcon" alt="Add to Home Screen" />
</div>
<p class="dauth-pwa-step__text">
در منوی باز شده در قسمت پایین گزینه Add to Home Screen را انتخاب کنید.
</p>
</div>
<div class="dauth-pwa-step">
<div class="dauth-pwa-step__icon">
<span class="text-blue-600 font-bold text-lg">Add</span>
</div>
<p class="dauth-pwa-step__text">در مرحله آخر در قسمت بالا روی Add کلیک کنید.</p>
</div>
</div>
<BasicButton class="w-full mt-4" type="primary" @click="confirm">
متوجه شدم
</BasicButton>
</BasicModal>
</template>
@@ -0,0 +1,144 @@
<script setup lang="ts">
import { mobileValidation, required } from '@/utils/inputRules'
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import DialogVantOneButton from '@/components/DialogVantOneButton.vue'
import registerDelayImage from '@/assets/img/registerDelay.svg'
import { useConfigStore } from '@/store/configStore'
import { isBoursecuProject } from '@/utils/config'
import { useLoginPageContext } from '../../composables/useLoginPage'
import router from '@/router'
const emit = defineEmits<{
register: [val: boolean]
}>()
const config = useConfigStore()
const {
registeredInfo,
disableForm,
userFormState,
formRef,
loading,
registerAccount,
dismissSuccess,
} = useLoginPageContext().registerForm
function goToEducate() {
router.push({ name: 'educate' })
}
function goToAboutUs() {
router.push('/aboutUs')
}
function handleSuccessDismiss() {
dismissSuccess()
emit('register', false)
}
</script>
<template>
<div class="dauth-card">
<header class="dauth-card__header">
<p class="dauth-card__eyebrow">حساب جدید</p>
<h2 class="dauth-card__title">ثبت نام</h2>
<p class="dauth-card__subtitle">اطلاعات خود را برای ایجاد حساب وارد کنید</p>
</header>
<a-form
ref="formRef"
:model="userFormState"
layout="vertical"
class="dauth-form"
name="desktop-register"
@finish="registerAccount"
>
<a-form-item :rules="[required()]" label="نام" name="fname">
<BasicInput v-model:value="userFormState.fname" place-holder="نام" size="large" />
</a-form-item>
<a-form-item :rules="[required()]" label="نام خانوادگی" name="lname">
<BasicInput
v-model:value="userFormState.lname"
place-holder="نام خانوادگی"
size="large"
/>
</a-form-item>
<a-form-item
:rules="[required(), mobileValidation('شماره تلفن')]"
label="شماره تلفن"
name="mobile"
>
<BasicInput
v-model:value="userFormState.mobile"
place-holder="۰۹۱۲۳۴۵۶۷۸۹"
size="large"
inputmode="numeric"
/>
</a-form-item>
<a-form-item :rules="[required()]" label="کلمه عبور" name="password">
<AInputPassword
v-model:value="userFormState.password"
place-holder="کلمه عبور"
size="large"
/>
</a-form-item>
<a-form-item
:rules="isBoursecuProject() ? [] : [required()]"
label="کد معرف"
name="referral_id"
>
<BasicInput
v-model:value="userFormState.referral_id"
:disabled="disableForm"
place-holder="کد معرف (اختیاری)"
size="large"
/>
</a-form-item>
<div class="dauth-actions">
<BasicButton :disabled="loading" class="w-full" html-type="submit" type="primary">
<a-spin v-if="loading" size="small" />
<span v-else>ثبت نام</span>
</BasicButton>
<template v-if="config.config?.name === 'garoosi'">
<button type="button" class="dauth-link-btn" @click="goToEducate">
آموزش کار با نرم افزار
</button>
<button type="button" class="dauth-link-btn" @click="goToAboutUs">
درباره ما
</button>
</template>
</div>
<div class="dauth-switch">
<span>حساب کاربری دارید؟</span>
<button type="button" class="dauth-switch__link" @click="emit('register', false)">
وارد شوید
</button>
</div>
</a-form>
<DialogVantOneButton
v-model:show="registeredInfo"
:closeOnClickOverlay="false"
:timeOut="-1"
>
<template #img>
<img :src="registerDelayImage" alt="" class="opacity-80" />
</template>
<template #title>
<div class="p-5 text-center text-sm">
ثبت نام شما با موفقیت انجام شد. شما میتوانید با اطلاعات خود، وارد اپلیکیشن شوید.
</div>
</template>
<template #button>
<div class="text-center" @click="handleSuccessDismiss">متوجه شدم</div>
</template>
</DialogVantOneButton>
</div>
</template>