change login-page design

This commit is contained in:
2026-10-06 13:38:35 +03:30
parent ab22c4dd63
commit cf8a46ec9c
35 changed files with 1377 additions and 188 deletions
@@ -26,3 +26,10 @@
- 2026-09-09 | نام منوی کاربر | `navigation.ts` + `MobileBottomNav.vue` | «تنظیمات» → «امکانات بیشتر» (دسکتاپ+موبایل)
- 2026-09-12 | دکمه دارایی‌ها خانه | `boursecuQuickActions` + `useHomePage.openAssetsSheet` | قبلاً `fund` می‌رفت و با خاموش بودن قلک گارد بی‌صدا برمی‌گرداند؛ حالا شیت BalanceSheet
- 2026-09-09 | ماشین حساب | `useUserPage.ts` + `GoldDayCalculatorModal.vue` | «قیمت روز طلا» → «ماشین حساب»؛ برچسب فلز/واحد از settingsStore (metalTypeLabel + tradeWeightUnit) نه هاردکد طلا/گرم
- 2026-10-05 | ????? ????? ??????? | desktop/components/DesktopAuthShell.vue + desktop/styles/desktop-auth.less | ????? stretch? ??? ???????? bleed? tagline ? form-links ?? margin-top:auto ???????? support ??? ???? ?? __top
- 2026-10-05 | نمای کلی پروژه | `boursecu/BOURSECU.md` | چهار ریپو: user:3100، admin:3600، server:8000، landing:3200؛ فلز مس از settings
- 2026-10-05 | ???? ????? ????? | DesktopAuthShell.vue + desktop-auth.less | status ???? form-column__foot ??? ???????? body/top ?? margin-top:auto ???????? tagline ??????? foot
- 2026-10-05 | ??? ????? ?????? | home/components/AppHeader.vue + guestHome.ts | ???? ????? ???? ?????login? ????????? ????? ?????/??/???? ?? guest ????
- 2026-10-05 | FAB ???????? ????? ?????? | DesktopAuthShell.vue + useDraggableFloatingButton | ??????: ????? ???? ????????? ??????: ???? inline ??? ????
- 2026-10-05 | ?????? ?? ??? ????? | boutUs/components/AboutUsContentPanel.vue | ??? ?????? ???? ??? ???? ?? ????? ???? ?? ??? ??
- 2026-10-06 | بیلد آموزش ENOENT 1.png | EducateContentPanel.vue + assets/img/1.png,2.png,4.png + banner1/2 | فایل‌ها در گیت بودند و از دیسک پاک شده بودند؛ git restore
+4 -3
View File
@@ -32,7 +32,7 @@
</div>
</AConfigProvider>
<div
v-if="showSupportButton && !showSplash && route.name !== 'support' && !isTvBoardRoute"
v-if="showSupportButton && !showSplash && route.name !== 'support' && route.name !== 'login' && !isTvBoardRoute"
id="support-button"
ref="supportButtonEl"
class="fixed z-[5100] cursor-grab select-none rounded-full"
@@ -71,7 +71,7 @@
</div>
</div>
<div
v-if="!showSplash && !isDesktop && !isTvBoardRoute"
v-if="!showSplash && !isDesktop && !isTvBoardRoute && !isLoginRoute"
class="app-credit-footer app-credit-footer--mobile"
:class="{ 'app-credit-footer--login': showLoginCreditExtras }"
>
@@ -157,7 +157,8 @@ const showDesktopFloatingCredit = computed(() => {
if (!isDesktop.value) return false
// پشتیبانی خودش سایدبار دارد؛ float روی دکمه ارسال می‌افتد
if (route.name === 'support') return false
return route.name === 'login' || !!route.meta.hideMenu
if (route.name === 'login') return false
return !!route.meta.hideMenu
})
const isLoginRoute = computed(() => route.name === 'login')
+10 -1
View File
@@ -14,6 +14,7 @@ import { initPriceRealtimeChannel } from '@/utils/priceSocket'
import { initMobileScrollRestoration } from '@/layouts/composables/useMobileScrollReset'
import { initMobileInputKeyboardScroll } from '@/utils/mobileInputKeyboardScroll'
import { isServerUnavailableError } from '@/utils/serverUnavailable'
import { isGuestHomeQuery } from '@/utils/guestHome'
type BootWindow = Window & {
__BOOT_TIMEOUT__?: number
@@ -132,7 +133,15 @@ export async function bootstrapApp() {
await router.isReady()
if (!getToken() && router.currentRoute.value.name !== 'login' && router.currentRoute.value.name !== 'tvMonitor' && router.currentRoute.value.name !== 'aboutSoftware') {
const bootRoute = router.currentRoute.value
const guestHomeBoot = bootRoute.name === 'home' && isGuestHomeQuery(bootRoute.query)
if (
!getToken()
&& bootRoute.name !== 'login'
&& bootRoute.name !== 'tvMonitor'
&& bootRoute.name !== 'aboutSoftware'
&& !guestHomeBoot
) {
await router.replace({ name: 'login' })
}
+13 -4
View File
@@ -7,11 +7,16 @@ import type { NavigationGuardNext } from 'vue-router'
import { useAuthStore } from '@/store/useAuthStore'
import { recoverMobileViewportAfterAuth } from '@/layouts/composables/useMobileScrollReset'
import { getFingerPosition, setFingerPosition } from '@/app/authBoot'
import { isGuestHomeQuery } from '@/utils/guestHome'
import type { RouteLocationNormalized } from 'vue-router'
const PUBLIC_ROUTE_NAMES = ['login', '404', 'educate', 'aboutUs', 'aboutSoftware', 'tvMonitor'] as const
function isPublicRoute(name: string | symbol | null | undefined) {
return PUBLIC_ROUTE_NAMES.includes(name as (typeof PUBLIC_ROUTE_NAMES)[number])
function isPublicRoute(to: RouteLocationNormalized) {
if (to.name === 'home' && isGuestHomeQuery(to.query)) {
return true
}
return PUBLIC_ROUTE_NAMES.includes(to.name as (typeof PUBLIC_ROUTE_NAMES)[number])
}
function goToLogin(next: NavigationGuardNext) {
@@ -71,7 +76,7 @@ export function setupRouterGuards() {
const authenticated = await resolveSession()
if (!authenticated) {
if (isPublicRoute(to.name)) {
if (isPublicRoute(to)) {
return next()
}
return goToLogin(next)
@@ -192,7 +197,11 @@ export async function ensureAuthenticatedRoute() {
if (!token) {
userStore.userInfo = undefined
if (router.currentRoute.value.name !== 'login') {
const current = router.currentRoute.value
if (current.name === 'home' && isGuestHomeQuery(current.query)) {
return false
}
if (current.name !== 'login') {
await router.replace({ name: 'login' })
}
return false
Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 830 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 912 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 929 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

+2 -1
View File
@@ -1,5 +1,6 @@
import { computed, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { isGuestHomeActive } from '@/utils/guestHome'
export function useAppNavigation() {
const router = useRouter()
@@ -7,7 +8,7 @@ export function useAppNavigation() {
const popVisible = ref(false)
const currentRoute = computed(() => route.name)
const hideMenu = computed(() => !!route.meta.hideMenu)
const hideMenu = computed(() => !!route.meta.hideMenu || isGuestHomeActive(route))
/** فقط ناو پایین موبایل — سایدبار دسکتاپ را مخفی نمی‌کند */
const hideBottomNav = computed(
() => !!route.meta.hideBottomNav || !!route.meta.hideMenu,
+19
View File
@@ -0,0 +1,19 @@
import { getToken } from '@/utils'
import router from '@/router'
import type { LocationQuery, RouteLocationNormalizedLoaded } from 'vue-router'
export const GUEST_HOME_QUERY = { guest: '1' } as const
export function isGuestHomeQuery(query: LocationQuery | Record<string, unknown>) {
const value = query.guest
return value === '1' || value === 1
}
export function isGuestHomeActive(route?: RouteLocationNormalizedLoaded) {
const current = route ?? router.currentRoute.value
return current.name === 'home' && isGuestHomeQuery(current.query) && !getToken()
}
export function guestHomeRoute() {
return { name: 'home' as const, query: { ...GUEST_HOME_QUERY } }
}
+13 -1
View File
@@ -17,6 +17,10 @@ type DraggableFloatingOptions = {
/** فاصله از پایینِ نوار منو + montzar (px) — مثل تصویر */
defaultBottomGap?: number
defaultRightOffset?: number
/** عرض کل پنجره به‌جای فریم موبایل ۴۲۶ */
fullViewport?: boolean
/** نوار پایین اپ را در موقعیت پیش‌فرض لحاظ نکن */
ignoreBottomChrome?: boolean
}
function readBottomChromeHeight(): number {
@@ -49,6 +53,8 @@ export function useDraggableFloatingButton(
const height = options.height ?? 44
const defaultBottomGap = options.defaultBottomGap ?? 12
const defaultRightOffset = options.defaultRightOffset ?? 16
const fullViewport = options.fullViewport === true
const ignoreBottomChrome = options.ignoreBottomChrome === true
const pos = ref<FabPosition>({ x: 0, y: 0 })
const dragging = ref(false)
@@ -68,6 +74,7 @@ export function useDraggableFloatingButton(
}))
function getViewportWidth() {
if (fullViewport) return window.innerWidth
return Math.min(window.innerWidth, 426)
}
@@ -78,11 +85,16 @@ export function useDraggableFloatingButton(
return Math.max(0, (vw - contentWidth) / 2)
}
function getBottomChrome() {
if (ignoreBottomChrome) return 0
return readBottomChromeHeight()
}
function getDefaultPosition(): FabPosition {
const vh = window.innerHeight
const offsetX = getHorizontalOffset()
const contentWidth = getViewportWidth()
const bottomChrome = readBottomChromeHeight()
const bottomChrome = getBottomChrome()
return {
x: offsetX + contentWidth - width - defaultRightOffset,
@@ -17,10 +17,10 @@
<h2 :class="variant === 'desktop' ? 'daboutus-content__title' : 'text-base font-bold mb-3'">درباره ما</h2>
<div :class="variant === 'desktop' ? 'daboutus-content__paragraph' : 'mt-3'">
گروسی گالری بستری امن و آنلاین برای خرید و فروش طلای آبشده فراهم کرده تا کاربران بتوانند سرمایه‌های خرد خود را به راحتی پس‌انداز و مدیریت کنند.
بورس مس بستری امن و آنلاین برای خرید و فروش مس فراهم کرده تا کاربران بتوانند معاملات کاتد، مفتول و سیم کابل را ساده، سریع و قابل اعتماد انجام دهند.
</div>
<div :class="variant === 'desktop' ? 'daboutus-content__paragraph' : 'mt-3'">
هدف ما ایجاد تجربه‌ای سریع، شفاف و کاربرپسند در معاملات طلای آبشده است.
هدف ما ایجاد تجربه‌ای شفاف و کاربرپسند در معاملات مس، با پشتوانه تولیدکنندگان واقعی است.
</div>
</div>
+16 -2
View File
@@ -48,6 +48,7 @@
</span>
</button>
<button
v-if="!isGuestHome"
type="button"
class="mhome-header-btn"
@click="router.push({ name: 'user' })"
@@ -55,6 +56,16 @@
<Icon icon="vuesax-linear:user" size="18" />
</button>
<button
v-if="isGuestHome"
type="button"
class="mhome-header-btn mhome-header-btn--logout"
@click="router.push({ name: 'login' })"
>
<Icon icon="vuesax-linear:login" size="18" />
<span>ورود</span>
</button>
<button
v-else
type="button"
class="mhome-header-btn mhome-header-btn--logout"
@click="$emit('logout')"
@@ -82,6 +93,7 @@ import {
initPwaInstallPromptListener,
} from '@/modules/pwa/pwaInstall'
import { unreadSupportCount, isUserSupportChatEnabled } from '@/utils/chat'
import { isGuestHomeActive } from '@/utils/guestHome'
const headerRef = ref<HTMLElement | null>(null)
const isHeaderScrolled = ref(false)
@@ -89,13 +101,15 @@ const canNativeInstall = ref(false)
const route = useRoute()
const projectLogo = computed(() => getProjectMobileNavLogo())
const chatEnabled = computed(() => isUserSupportChatEnabled())
const isGuestHome = computed(() => isGuestHomeActive(route))
const chatEnabled = computed(() => !isGuestHome.value && isUserSupportChatEnabled())
const chatUnreadCount = computed(() => unreadSupportCount.value)
const {
enabled: notificationEnabled,
enabled: notificationEnabledRaw,
unreadCount,
clearUnreadLocally,
} = useAppNotificationBadge()
const notificationEnabled = computed(() => !isGuestHome.value && notificationEnabledRaw.value)
const { tryInstallOrOpenGuide, showInstallEntry } = usePwaInstall()
function refreshCanInstall() {
@@ -1,6 +1,7 @@
import { computed, h, inject, onUnmounted, provide, ref, type InjectionKey } from 'vue'
import router from '@/router'
import { getToken, removeToken } from '@/utils'
import { isGuestHomeActive } from '@/utils/guestHome'
import { useUserStore } from '@/store/userStore'
import { useWalletStore } from '@/store/walletStore'
import { useWalletAssets } from '@/composables/useWalletAssets'
@@ -266,6 +267,11 @@ export function useHomePageContext() {
}
async function ensureHomeUserAndData() {
if (isGuestHomeActive()) {
await initHomePageData(false, { guest: true })
return
}
const authed = await ensureAuthenticatedRoute()
if (!authed) {
return
@@ -20,7 +20,9 @@ export function isHomePageDataInitialized() {
return initialized.value
}
export async function initHomePageData(force = false) {
export async function initHomePageData(force = false, options?: { guest?: boolean }) {
const guest = options?.guest === true
if (!force && initialized.value) {
startLastPricePolling()
void loadHomeMaftolOffset(false)
@@ -36,14 +38,18 @@ export async function initHomePageData(force = false) {
try {
const { loadWalletAssets } = useWalletAssets()
await Promise.all([
loadHomeFunds(force),
loadWalletAssets(force),
const sharedTasks = [
getLastPrices(),
fetchHomeChartPrices(force),
startLastPricePolling(),
loadHomeBanners(force),
])
]
await Promise.all(
guest
? sharedTasks
: [...sharedTasks, loadHomeFunds(force), loadWalletAssets(force)],
)
await loadHomeMaftolOffset(force)
@@ -1,12 +1,21 @@
<template>
<header class="dhome-topbar">
<div>
<p class="dhome-topbar__eyebrow">نمای کلی</p>
<h1 class="dhome-topbar__title">{{ userStore.userInfo?.name ?? 'کاربر' }}</h1>
<p class="dhome-topbar__eyebrow">{{ guestMode ? 'بازار مس' : 'نمای کلی' }}</p>
<h1 class="dhome-topbar__title">{{ guestMode ? 'قیمت لحظه‌ای مس' : (userStore.userInfo?.name ?? 'کاربر') }}</h1>
</div>
<div class="dhome-topbar__actions">
<button type="button" class="dhome-btn-ghost" @click="router.push({ name: 'user' })">
<button
v-if="guestMode"
type="button"
class="dhome-btn-ghost"
@click="router.push({ name: 'login' })"
>
<BasicIcon icon="vuesax-linear:login" size="16" />
<span>ورود</span>
</button>
<button v-if="!guestMode" type="button" class="dhome-btn-ghost" @click="router.push({ name: 'user' })">
<BasicIcon icon="vuesax-linear:user" size="16" />
<span>حساب کاربری</span>
</button>
@@ -21,7 +30,7 @@
<span>{{ canNativeInstall ? 'نصب وب اپلیکیشن' : 'آموزش نصب' }}</span>
</button>
<button
v-if="notificationEnabled"
v-if="!guestMode && notificationEnabled"
type="button"
class="dhome-btn-ghost dhome-btn-ghost--notify"
aria-label="اعلان"
@@ -36,7 +45,7 @@
<span>اعلان</span>
</button>
<button
v-if="chatEnabled"
v-if="!guestMode && chatEnabled"
type="button"
class="dhome-btn-ghost dhome-btn-ghost--notify"
aria-label="چت پشتیبانی"
@@ -54,7 +63,7 @@
<BasicIcon icon="vuesax-linear:refresh-2" size="16" />
</button>
<button
v-if="showLogout"
v-if="!guestMode && showLogout"
type="button"
class="dhome-btn-ghost"
@click="$emit('logout')"
@@ -84,8 +93,9 @@ import router from '@/router'
withDefaults(
defineProps<{
showLogout?: boolean
guestMode?: boolean
}>(),
{ showLogout: true },
{ showLogout: true, guestMode: false },
)
defineEmits<{
@@ -3,6 +3,7 @@ import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/store/userStore'
import { getToken } from '@/utils'
import { isGuestHomeActive } from '@/utils/guestHome'
import { useHomePageContext } from '../../composables/useHomePage'
import HomeSkeleton from '../../components/HomeSkeleton.vue'
import DesktopHomeShell from '../components/DesktopHomeShell.vue'
@@ -22,18 +23,20 @@ const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const ctx = useHomePageContext()
const showContent = computed(() => Boolean(userInfo.value))
const isGuestHome = computed(() => isGuestHomeActive())
const showContent = computed(() => Boolean(userInfo.value) || isGuestHome.value)
const showSkeleton = computed(() => Boolean(getToken()) && !userInfo.value)
</script>
<template>
<DesktopHomeShell>
<DesktopHomeTopbar @logout="$emit('logout')" />
<DesktopHomeTopbar :guest-mode="isGuestHome" @logout="$emit('logout')" />
<HomeSkeleton v-if="showSkeleton" />
<template v-if="showContent">
<DesktopAssetsMetrics
v-if="!isGuestHome"
@open-gold-modal="ctx.openGoldModal"
@open-rial-modal="ctx.openRialModal"
/>
@@ -47,7 +50,7 @@ const showSkeleton = computed(() => Boolean(getToken()) && !userInfo.value)
<section class="dhome-main-grid">
<DesktopChartSection />
<DesktopQuickActionsPanel />
<DesktopQuickActionsPanel v-if="!isGuestHome" />
</section>
</template>
</DesktopHomeShell>
@@ -3,6 +3,7 @@ import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/store/userStore'
import { getToken } from '@/utils'
import { isGuestHomeActive } from '@/utils/guestHome'
import HomeSkeleton from '../components/HomeSkeleton.vue'
import { getHomeLayoutConfig } from './registry'
import { HOME_SECTION_COMPONENTS } from './sections'
@@ -16,8 +17,15 @@ const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const layoutConfig = computed(() => props.layoutConfig ?? getHomeLayoutConfig())
const isGuestHome = computed(() => isGuestHomeActive())
const outerSections = computed(() =>
(layoutConfig.value.outerSections ?? []).filter((section) => section.visible !== false)
(layoutConfig.value.outerSections ?? [])
.filter((section) => section.visible !== false)
.filter((section) => {
if (!isGuestHome.value) return true
return section.id !== 'assets' && section.id !== 'boursecuQuickActions'
}),
)
const innerSections = computed(() =>
@@ -29,7 +37,7 @@ const tailSections = computed(() =>
)
/** محتوا با userInfo — داده‌های API در پس‌زمینه لود می‌شوند */
const showContent = computed(() => Boolean(userInfo.value))
const showContent = computed(() => Boolean(userInfo.value) || isGuestHome.value)
const showSkeleton = computed(() => Boolean(getToken()) && !userInfo.value)
function resolveSectionComponent(sectionId: HomeSectionId) {
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

+61 -32
View File
@@ -81,13 +81,26 @@
<!-- <transition name="fade" mode="out-in">-->
<a-form-item
v-if="!stepTwoVerify"
:rules="[required('شماره تلفن'), mobileValidation('شماره تلفن')]"
:rules="[required(mobileFieldLabel), mobileValidation(mobileFieldLabel)]"
:validate-trigger="['change', 'blur']"
class="home_appear_fade_in_down"
label="شماره تلفن"
:label="mobileFieldLabel"
name="mobile"
>
<a-input
v-if="variant === 'desktop'"
v-model:value="userFormState.mobile"
size="large"
inputmode="numeric"
type="tel"
placeholder="شماره موبایل"
>
<template #prefix>
<BasicIcon icon="vuesax-linear:mobile" size="18" />
</template>
</a-input>
<BasicInput
v-else
v-model:value="userFormState.mobile"
place-holder="شماره تلفن"
size="large"
@@ -164,7 +177,7 @@
<div v-if="variant === 'desktop'" class="dauth-actions">
<BasicButton
:disabled="loading"
class="w-full dauth-actions__submit"
class="w-full dauth-actions__submit dauth-btn-stack"
html-type="submit"
type="primary"
>
@@ -172,36 +185,43 @@
<span v-else>{{ !isSMS ? 'ورود' : stepTwoVerify ? 'ورود' : 'ارسال کد' }}</span>
</BasicButton>
<BasicButton
v-if="getToken() && userStore.userInfo?.finger_print"
<button
v-if="!isTWA()"
type="button"
class="dauth-btn-stack"
:disabled="loading"
class="w-full"
@click="enterFingerPrint"
@click="installWebApp"
>
ورود با اثر انگشت
</BasicButton>
<button type="button" class="dauth-link-btn dauth-link-btn--primary" @click="goToAboutSoftware">
درباره نرم‌افزار
<span>نصب وب اپلیکیشن</span>
</button>
<div v-if="!isTWA()" class="mt-1 flex flex-col gap-2">
<a
v-if="!!setting.getSettingByKey('apk_download_link')"
class="dauth-link-btn dauth-link-btn--primary"
:href="setting.getSettingByKey('apk_download_link')"
>
دانلود اپ اندروید
</a>
<button type="button" class="dauth-link-btn dauth-link-btn--primary w-full" @click="installWebApp">
نصب نسخه وب اپ
</button>
</div>
<!-- <a
v-if="!isTWA() && apkDownloadHref"
class="dauth-btn-stack dauth-btn-stack&#45;&#45;link"
:href="apkDownloadHref"
>
<span>دانلود اپ اندروید</span>
<BasicIcon icon="vuesax-linear:android" size="20" />
</a>-->
<button
v-else-if="!isTWA()"
type="button"
class="dauth-btn-stack"
@click="onApkDownloadClick"
>
<span>نصب وب اپلیکیشن</span>
<BasicIcon icon="vuesax-linear:android" size="20" />
</button>
<button type="button" class="dauth-btn-stack" :disabled="loading" @click="goToGuestHome">
<span>قیمت لحظه‌ای مس</span>
<!-- <BasicIcon icon="vuesax-linear:global" size="20" />-->
</button>
</div>
<div
v-if="isRegister && variant === 'desktop'"
v-if="variant === 'desktop' && !isRegister"
class="dauth-switch"
>
<span>حساب کاربری ندارید؟</span>
@@ -242,12 +262,9 @@
دانلود اپ اندروید
</a>
<BasicButton type="primary" class="w-full py-2" html-type="button" @click="installWebApp">
<div>نصب نسخه وب اپ</div>
<div>نصب وب اپلیکیشن</div>
</BasicButton>
</div>
<BasicButton class="mt-2 w-full" @click="goToAboutSoftware">
<div>درباره نرم‌افزار</div>
</BasicButton>
<div
v-if="isRegister"
class="cursor-pointer flex gap-1 justify-center mt-5 text-xs "
@@ -309,8 +326,9 @@ import { usePwaInstall } from '@/composables/usePwaInstall'
import {getProducts} from "@/api/products";
import {getSettings} from "@/api/configuration";
import { LOGIN_PAGE_CONTEXT_KEY } from '../composables/useLoginPage'
import { guestHomeRoute } from '@/utils/guestHome'
withDefaults(
const props = withDefaults(
defineProps<{
variant?: 'mobile' | 'desktop'
}>(),
@@ -323,6 +341,17 @@ defineEmits<{
const loginPageContext = inject(LOGIN_PAGE_CONTEXT_KEY, null)
const isRegister = computed(() => unref(loginPageContext?.loginForm?.isRegister) ?? false)
const mobileFieldLabel = computed(() => (props.variant === 'desktop' ? 'شماره موبایل' : 'شماره تلفن'))
const apkDownloadHref = computed(() => {
const link = setting.getSettingByKey('apk_download_link')
return link ? String(link) : ''
})
function onApkDownloadClick() {
if (!apkDownloadHref.value) {
message.info('لینک دانلود اندروید در تنظیمات تعریف نشده است.')
}
}
const config=useConfigStore()
const { time, onFinish, reset } = useCountdownTimer('00:02:00', 'mm:ss')
@@ -465,8 +494,8 @@ async function goToAboutUs(){
await router.push('/aboutUs')
}
async function goToAboutSoftware() {
await router.push({ name: 'aboutSoftware' })
async function goToGuestHome() {
await router.push(guestHomeRoute())
}
const { tryInstallOrOpenGuide: installWebApp } = usePwaInstall()
+18 -8
View File
@@ -100,8 +100,13 @@
<a-spin v-if="loading" size="small" />
<span v-else>ثبت نام</span>
</BasicButton>
<button type="button" class="dauth-link-btn dauth-link-btn--primary" @click="goToAboutSoftware">
درباره نرم‌افزار
<button
v-if="!isTWA()"
type="button"
class="dauth-btn-stack"
@click="installWebApp"
>
<span>نصب وب اپلیکیشن</span>
</button>
</div>
@@ -124,8 +129,14 @@
<div v-else>ثبت نام</div>
</div>
</BasicButton>
<BasicButton class="mt-2 w-full" @click="goToAboutSoftware">
<div>درباره نرم‌افزار</div>
<BasicButton
v-if="!isTWA()"
class="mt-2 w-full"
type="primary"
html-type="button"
@click="installWebApp"
>
<div>نصب وب اپلیکیشن</div>
</BasicButton>
<div
class="cursor-pointer flex gap-1 justify-center mt-5 text-xs"
@@ -221,8 +232,9 @@ import { registerCustomer } from '@/api/login'
import router from '@/router'
import {useConfigStore} from "@/store/configStore";
import {useSettingsStore} from '@/store/settingsStore'
import {getProjectLoginLogo, getProjectLoginTaglineLines, hasCustomLoginLogoLayout} from "@/utils/config";
import { getProjectLoginLogo, getProjectLoginTaglineLines, hasCustomLoginLogoLayout, isTWA } from "@/utils/config";
import { normalizeAuthFormFields } from '@/utils/persianDigits'
import { usePwaInstall } from '@/composables/usePwaInstall'
withDefaults(
defineProps<{
@@ -311,9 +323,7 @@ async function goToAboutUs(){
await router.push('/aboutUs')
}
async function goToAboutSoftware() {
await router.push({ name: 'aboutSoftware' })
}
const { tryInstallOrOpenGuide: installWebApp } = usePwaInstall()
const goToEducate = () => {
router.push({ name: "educate" })
+1 -1
View File
@@ -13,7 +13,7 @@ const page = useLoginPageContext()
<header v-if="!page.enterType" class="dauth-card__header">
<p class="dauth-card__eyebrow">حساب کاربری</p>
<h2 class="dauth-card__title">ورود</h2>
<p class="dauth-card__subtitle">برای ادامه، روش ورود خود را انتخاب کنید</p>
<p class="dauth-card__subtitle">برای ادامه، رمز ورود خود را انتخاب کنید.</p>
</header>
<header v-else class="dauth-card__header">
<p class="dauth-card__eyebrow">حساب جدید</p>
@@ -1,66 +1,167 @@
<template>
<div class="dauth dauth--boursecu">
<div class="dauth__layout">
<main class="dauth-form-panel">
<slot />
</main>
<div class="dauth-page-logo">
<img :src="pageLogo" alt="بورس مس" class="dauth-page-logo__img">
</div>
<aside class="dauth-brand dauth-brand--boursecu">
<div class="dauth-brand__pattern" />
<div class="dauth-brand__content">
<img
:src="logo"
alt=""
class="dauth-brand__logo"
>
<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" />
<div class="dauth__layout">
<div class="dauth-visual-col">
<img
:src="copperProducts"
alt=""
class="dauth-visual-col__products"
loading="lazy"
>
</div>
<section class="dauth-center-col" aria-label="معرفی بورس مس">
<div class="dauth-center-col__body">
<h1 class="dauth-center-col__headline">
<span class="dauth-center-col__headline-line">به دنیای معاملات مس</span>
<span class="dauth-center-col__headline-accent">خوش آمدید</span>
</h1>
<p class="dauth-center-col__lead">
بورس مس مسیر خرید و فروش آنلاین مس را ساده، سریع و قابل اعتماد کرده است؛ با پشتوانه تولیدکنندگان واقعی.
</p>
<ul class="dauth-center-col__features">
<li v-for="item in featureItems" :key="item.title" class="dauth-center-col__feature">
<span class="dauth-center-col__feature-icon" aria-hidden="true">
<BasicIcon :icon="item.icon" size="22" />
</span>
{{ feature }}
<div class="dauth-center-col__feature-text">
<p class="dauth-center-col__feature-title">{{ item.title }}</p>
<p class="dauth-center-col__feature-desc">{{ item.desc }}</p>
</div>
</li>
</ul>
</div>
<div class="dauth-brand__footer">نسخه {{ version }}</div>
</aside>
<p class="dauth-center-col__tagline">
<span class="dauth-center-col__tagline-line" aria-hidden="true" />
<span>مس، آینده‌ای مطمئن و...</span>
</p>
</section>
<main class="dauth-form-panel">
<div class="dauth-form-column">
<div class="dauth-form-column__top">
<slot />
</div>
<div class="dauth-form-column__foot">
<nav class="dauth-form-links" aria-label="پیوندهای صفحه ورود">
<span class="dauth-form-links__text">معاملات آتی</span>
<span class="dauth-form-links__sep" aria-hidden="true">|</span>
<span class="dauth-form-links__text">دسترسی به بازار</span>
<span class="dauth-form-links__sep" aria-hidden="true">|</span>
<button type="button" class="dauth-form-links__link dauth-form-links__link--accent" @click="goAboutUs">
درباره مس
</button>
<span class="dauth-form-links__sep" aria-hidden="true">|</span>
<span class="dauth-form-links__text">پشتیبانی</span>
</nav>
<div class="dauth-page-footer__status">
<span class="dauth-page-footer__status-icon" aria-hidden="true">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M4.5 16.2L9 12.2L11.6 14.6L17.2 7.8"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M14.2 7.8H17.2V10.8"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
<span>پلتفرم معاملات مس، در دسترس است</span>
</div>
</div>
</div>
</main>
</div>
<button
type="button"
class="dauth-form-support dauth-form-support--fab"
:class="{ 'dauth-form-support--fab-dragging': supportDragging }"
:style="supportFabStyle"
title="پشتیبانی"
@pointerdown="onSupportPointerDown"
@pointermove="onSupportPointerMove"
@pointerup="onSupportFabPointerUp"
@pointercancel="onSupportPointerCancel"
>
<BasicIcon icon="vuesax-linear:headphone" size="16" />
<span>پشتیبانی</span>
</button>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import pkg from '../../../../../package.json'
import { getDisplayAppVersion } from '@/config/config-cache'
import router from '@/router'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import { useConfigStore } from '@/store/configStore'
import {
getProjectDesktopLoginBrandLogo,
getProjectLoginTaglineLines,
} from '@/utils/config'
import { getProjectLoginLogo } from '@/utils/config'
import { useDraggableFloatingButton } from '@/utils/useDraggableFloatingButton'
import copperProducts from '../../assets/img/login-copper-hero.jpg'
const config = useConfigStore()
const version = getDisplayAppVersion(pkg.version)
const pageLogo = getProjectLoginLogo()
const logo = computed(() => getProjectDesktopLoginBrandLogo(config.config?.avatar))
const taglineLines = computed(() => getProjectLoginTaglineLines())
const tagline = computed(() => {
if (taglineLines.value?.length) {
return taglineLines.value.join(' ')
}
return 'پلتفرم امن و سریع برای مدیریت دارایی و معاملات شما'
const {
dragging: supportDragging,
fabStyle: supportFabStyle,
handlePointerDown: onSupportPointerDown,
handlePointerMove: onSupportPointerMove,
handlePointerUp: supportPointerUp,
handlePointerCancel: onSupportPointerCancel,
} = useDraggableFloatingButton('boursecu_login_support_fab_v2', {
width: 108,
height: 40,
defaultBottomGap: 28,
defaultRightOffset: 24,
fullViewport: true,
ignoreBottomChrome: true,
})
const brandTitle = computed(() => config.config?.name_fa ?? 'خوش آمدید')
const features = [
'قیمت لحظه‌ای و نمودار بازار',
'مدیریت دارایی و کیف پول',
'معاملات سریع و امن',
const featureItems = [
{
icon: 'vuesax-linear:box',
title: 'خرید و فروش کاتد، مفتول و سیم کابل',
desc: 'با پشتوانه تولیدکننده واقعی',
},
{
icon: 'vuesax-linear:refresh-circle',
title: 'قابلیت تبدیل محصول',
desc: 'از کاتد و مفتول به محصول نهایی (سیم و کابل)',
},
{
icon: 'vuesax-linear:truck-tick',
title: 'قابلیت تحویل فیزیکی',
desc: 'به‌صورت مطمئن و برنامه‌ریزی‌شده',
},
{
icon: 'vuesax-linear:shield-tick',
title: 'قابلیت خرید اعتباری',
desc: 'با شرایط منعطف و مطمئن',
},
]
function onSupportClick() {
void router.push({ name: 'support' })
}
function onSupportFabPointerUp(event: PointerEvent) {
const clicked = supportPointerUp(event)
if (!clicked) return
onSupportClick()
}
function goAboutUs() {
void router.push({ name: 'aboutUs' })
}
</script>
<style lang="less" src="../styles/desktop-auth.less"></style>
@@ -44,10 +44,6 @@ function goToEducate() {
function goToAboutUs() {
router.push('/aboutUs')
}
function goToAboutSoftware() {
router.push({ name: 'aboutSoftware' })
}
</script>
<template>
@@ -166,23 +162,23 @@ function goToAboutSoftware() {
>
ورود با اثر انگشت
</BasicButton>
<button type="button" class="dauth-link-btn" @click="goToAboutSoftware">
درباره نرم‌افزار
<button
v-if="!isTWA()"
type="button"
class="dauth-btn-stack"
@click="installWebApp"
>
<span>نصب وب اپلیکیشن</span>
</button>
<div v-if="!isTWA()" class="dauth-actions-row flex flex-col gap-2">
<a
v-if="!!setting.getSettingByKey('apk_download_link')"
class="dauth-link-btn dauth-link-btn--primary"
:href="setting.getSettingByKey('apk_download_link')"
>
دانلود اپ اندروید
</a>
<BasicButton html-type="button" type="primary" class="w-full" @click.prevent="installWebApp">
نصب نسخه وب اپ
</BasicButton>
</div>
<a
v-if="!isTWA() && !!setting.getSettingByKey('apk_download_link')"
class="dauth-link-btn dauth-link-btn--primary"
:href="setting.getSettingByKey('apk_download_link')"
>
دانلود اپ اندروید
</a>
</div>
<div v-if="isRegister" class="dauth-switch">
@@ -9,6 +9,8 @@ import { useConfigStore } from '@/store/configStore'
import { useSettingsStore } from '@/store/settingsStore'
import { useLoginPageContext } from '../../composables/useLoginPage'
import router from '@/router'
import { isTWA } from '@/utils/config'
import { usePwaInstall } from '@/composables/usePwaInstall'
const emit = defineEmits<{
register: [val: boolean]
@@ -16,6 +18,7 @@ const emit = defineEmits<{
const config = useConfigStore()
const settingsStore = useSettingsStore()
const { tryInstallOrOpenGuide: installWebApp } = usePwaInstall()
const referralIdRules = computed(() =>
settingsStore.isReferralRequired ? [required('کد معرف')] : [],
)
@@ -40,10 +43,6 @@ function goToAboutUs() {
router.push('/aboutUs')
}
function goToAboutSoftware() {
router.push({ name: 'aboutSoftware' })
}
function handleSuccessDismiss() {
dismissSuccess()
emit('register', false)
@@ -119,8 +118,13 @@ function handleSuccessDismiss() {
<a-spin v-if="loading" size="small" />
<span v-else>ثبت نام</span>
</BasicButton>
<button type="button" class="dauth-link-btn" @click="goToAboutSoftware">
درباره نرم‌افزار
<button
v-if="!isTWA()"
type="button"
class="dauth-btn-stack"
@click="installWebApp"
>
<span>نصب وب اپلیکیشن</span>
</button>
</div>
@@ -2,6 +2,12 @@
.dauth {
min-height: 100vh;
/* Force exact viewport sizing to avoid page scrolling */
height: 100vh;
width: 100vw;
overflow: hidden;
overscroll-behavior: none;
box-sizing: border-box;
background: #f6f7f9;
display: flex;
align-items: stretch;
@@ -489,6 +495,895 @@
}
}
/* ─── Bourse Mes login mockup (۳ ناحیه: تصویر | متن | کارت) ─── */
@boursecu-navy: #0b192e;
@boursecu-navy-deep: #0a1628;
@boursecu-icon-bg: #152a45;
@boursecu-copper: #d4843f;
@boursecu-copper-soft: #e8a04a;
@boursecu-btn: #1a3554;
.dauth--boursecu {
position: relative;
min-height: 100vh;
height: 100vh;
width: 100%;
overflow: hidden;
background: @boursecu-navy;
color: #fff;
display: flex;
flex-direction: column;
.dauth-page-logo {
position: absolute;
top: clamp(20px, 3.2vh, 36px);
left: clamp(28px, 3vw, 52px);
z-index: 6;
&__img {
display: block;
width: clamp(132px, 12.5vw, 172px);
height: auto;
max-height: clamp(112px, 16vh, 152px);
object-fit: contain;
}
}
.dauth__layout {
flex: 1 1 auto;
height: 100%;
min-height: 0;
display: grid;
direction: ltr;
/* تصویر چپ | متن وسط | کارت راست — موکاپ */
grid-template-columns: minmax(220px, 0.82fr) minmax(340px, 1.4fr) minmax(380px, 440px);
grid-template-rows: minmax(0, 1fr);
gap: clamp(8px, 1.4vw, 24px);
align-items: stretch;
padding:
clamp(88px, 11vh, 108px)
clamp(28px, 3.2vw, 52px)
clamp(88px, 11vh, 112px)
0;
}
/* تصویر: پایین‌چپ، بزرگ، bleed به لبه چپ/پایین */
.dauth-visual-col {
direction: ltr;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-start;
align-self: stretch;
min-height: 0;
height: auto;
padding: 0;
margin: 0;
overflow: visible;
pointer-events: none;
&__products {
display: block;
width: clamp(460px, 50vw, 740px);
max-width: none;
min-width: 380px;
max-height: min(66vh, 580px);
height: auto;
object-fit: contain;
object-position: left bottom;
margin: 0;
margin-inline-start: -10%;
margin-bottom: -28px;
transform: translateX(-8%);
mix-blend-mode: lighten;
pointer-events: none;
user-select: none;
}
}
/* متن: بالا هم‌تراز کارت؛ شعار پایین هم‌تراز لینک‌ها */
.dauth-center-col {
direction: rtl;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-self: stretch;
min-height: 0;
height: auto;
padding-top: 0;
padding-inline: clamp(4px, 1.2vw, 20px) clamp(4px, 1vw, 12px);
max-width: none;
width: 100%;
&__body {
display: flex;
flex-direction: column;
flex: 0 1 auto;
min-height: 0;
/* با tagline که margin-top:auto دارد → محتوا وسط‌چین عمودی مثل موکاپ */
margin-top: auto;
}
&__headline {
margin: 0 0 12px;
display: flex;
flex-direction: column;
gap: 2px;
font-weight: 700;
line-height: 1.3;
}
&__headline-line {
font-size: clamp(24px, 2.3vw, 32px);
color: #ffffff;
letter-spacing: -0.02em;
}
&__headline-accent {
font-size: clamp(32px, 3.2vw, 44px);
color: @boursecu-copper-soft;
letter-spacing: -0.02em;
font-weight: 800;
}
&__lead {
margin: 0 0 22px;
max-width: 520px;
font-size: 13px;
line-height: 1.9;
color: rgba(255, 255, 255, 0.64);
}
&__features {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 16px;
}
&__feature {
display: flex;
align-items: flex-start;
gap: 12px;
}
&__feature-icon {
display: flex;
align-items: center;
justify-content: center;
width: 42px;
height: 42px;
border-radius: 8px;
background: @boursecu-icon-bg;
border: 1px solid rgba(232, 160, 74, 0.4);
color: @boursecu-copper-soft;
flex-shrink: 0;
}
&__feature-title {
margin: 0;
font-size: 13px;
font-weight: 600;
color: #ffffff;
line-height: 1.5;
}
&__feature-desc {
margin: 2px 0 0;
font-size: 11px;
line-height: 1.5;
color: rgba(255, 255, 255, 0.48);
}
&__tagline {
display: inline-flex;
align-items: center;
gap: 10px;
width: fit-content;
max-width: 100%;
margin-top: auto;
/* هم‌تراز لینک‌ها؛ استاتوس زیر لینک‌هاست */
margin-bottom: calc(10px + 36px);
margin-inline-start: clamp(20px, 3.5vw, 48px);
padding-top: 0;
flex-shrink: 0;
font-size: 13px;
font-weight: 500;
line-height: 1.45;
color: rgba(255, 255, 255, 0.78);
}
&__tagline-line {
width: 40px;
height: 2px;
background: @boursecu-copper-soft;
border-radius: 1px;
flex-shrink: 0;
}
}
/* کارت+پشتیبانی وسط؛ فوتر (لینک+استاتوس) پایین راست — هم‌تراز شعار */
.dauth-form-panel {
direction: rtl;
background: transparent;
padding: 0 !important;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start; /* RTL: لبه راست ستون */
align-self: stretch;
min-height: 0;
height: auto;
width: 100%;
}
.dauth-form-column {
width: 100%;
max-width: 420px;
flex: 1 1 auto;
min-height: 0;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 0;
}
.dauth-form-column__top {
display: flex;
flex-direction: column;
gap: 14px;
flex: 0 0 auto;
margin-top: auto;
}
.dauth-form-column__foot {
display: flex;
flex-direction: column;
align-items: flex-start; /* RTL: راست‌چین زیر کارت */
gap: 10px;
flex-shrink: 0;
margin-top: auto;
margin-bottom: 8px;
margin-inline-start: 0;
padding-top: 0;
}
.dauth-form-support {
display: none;
align-items: center;
justify-content: center;
gap: 6px;
padding: 7px 16px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.45);
background: rgba(12, 28, 48, 0.94);
color: #ffffff;
font-size: 12px;
font-weight: 600;
font-family: inherit;
transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
&:hover {
background: rgba(10, 22, 40, 0.75);
border-color: rgba(255, 255, 255, 0.55);
}
&--fab {
position: fixed;
z-index: 5200;
display: inline-flex;
margin: 0;
min-height: 40px;
padding: 8px 14px;
cursor: grab;
user-select: none;
touch-action: none;
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.28);
}
&--fab-dragging {
cursor: grabbing;
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.36);
opacity: 0.96;
}
}
.dauth-form-links {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
gap: 8px;
margin: 0;
padding: 0;
font-size: 12px;
line-height: 1.45;
color: rgba(255, 255, 255, 0.78);
&__link {
border: none;
background: transparent;
padding: 0;
font: inherit;
color: rgba(255, 255, 255, 0.82);
cursor: pointer;
transition: color 0.15s ease;
&:hover,
&--accent {
color: @boursecu-copper-soft;
}
}
&__text {
color: rgba(255, 255, 255, 0.72);
cursor: default;
user-select: none;
}
&__sep {
color: rgba(255, 255, 255, 0.28);
user-select: none;
}
}
.dauth-card {
width: 100%;
max-width: 420px;
border-radius: 14px;
padding: 24px 26px 20px;
box-shadow: none !important;
margin: 0;
}
.dauth-card__eyebrow {
color: #9ca3af;
font-size: 11px;
margin-bottom: 4px;
}
.dauth-card__title {
font-size: 26px;
font-weight: 700;
color: #0f172a;
}
.dauth-card__subtitle {
font-size: 12px;
color: #6b7280;
margin-top: 6px;
}
.dauth-toggle {
background: #eceff3;
border-radius: 12px;
padding: 5px;
gap: 6px;
margin-bottom: 20px;
&__btn {
border-radius: 10px;
padding: 10px 10px;
font-size: 12px;
font-weight: 600;
color: #4b5563;
&--active {
background: @boursecu-btn;
color: #ffffff;
box-shadow: 0 4px 12px rgba(15, 35, 60, 0.25);
}
}
}
.dauth-form {
:deep(.ant-form-item-label > label) {
font-size: 12px;
font-weight: 600;
color: #111827;
}
:deep(.ant-input),
:deep(.ant-input-affix-wrapper) {
border-radius: 12px;
min-height: 46px;
border-color: #d1d5db;
padding-inline: 14px;
font-size: 14px;
&:hover,
&:focus,
&-focused {
border-color: @boursecu-btn;
box-shadow: 0 0 0 2px rgba(26, 53, 84, 0.12);
}
}
:deep(.ant-input-affix-wrapper .ant-input-prefix) {
color: #9ca3af;
margin-inline-end: 8px;
}
}
.dauth-actions {
gap: 9px;
margin-top: 2px;
&__submit,
:deep(.noname-base-button.dauth-actions__submit) {
min-height: 44px;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
background: @boursecu-btn !important;
border-color: @boursecu-btn !important;
}
}
.dauth-btn-stack {
width: 100%;
min-height: 44px;
border-radius: 10px;
border: 1px solid @boursecu-btn;
background: @boursecu-btn;
color: #fff;
font-size: 14px;
font-weight: 600;
font-family: inherit;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
cursor: pointer;
text-decoration: none;
box-sizing: border-box;
padding: 0 14px;
transition: opacity 0.15s ease, background 0.15s ease;
&:hover:not(:disabled):not(.dauth-btn-stack--disabled) {
opacity: 0.94;
color: #fff;
}
&:disabled,
&--disabled {
opacity: 0.55;
cursor: not-allowed;
pointer-events: none;
}
&--link {
color: #fff;
}
}
.dauth-link-btn--primary {
background: @boursecu-btn;
border-color: @boursecu-btn;
min-height: 44px;
border-radius: 10px;
font-size: 14px;
}
.dauth-switch {
margin-top: 14px;
font-size: 12px;
color: #6b7280;
&__link {
color: @boursecu-copper;
font-weight: 700;
}
}
.dauth-page-footer__status {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px 6px 10px;
border-radius: 999px;
border: 1px solid rgba(148, 180, 220, 0.28);
background: rgba(12, 28, 48, 0.92);
color: rgba(255, 255, 255, 0.9);
font-size: 11px;
white-space: nowrap;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.22);
}
.dauth-page-footer__status-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 999px;
background: rgba(8, 18, 32, 0.85);
color: @boursecu-copper-soft;
flex-shrink: 0;
}
}
/* دسکتاپ متوسط: جمع‌وجورتر بدون کوچک‌کردن زیاد عکس */
@media (max-width: 1360px) and (min-width: 1101px) {
.dauth--boursecu {
.dauth__layout {
grid-template-columns: minmax(220px, 0.8fr) minmax(300px, 1.3fr) minmax(360px, 420px);
gap: clamp(6px, 1vw, 16px);
padding-bottom: clamp(72px, 9vh, 96px);
}
.dauth-visual-col__products {
width: clamp(420px, 46vw, 640px);
min-width: 360px;
max-height: min(60vh, 520px);
margin-inline-start: -8%;
transform: translateX(-6%);
}
.dauth-form-column__foot {
gap: 10px;
}
.dauth-center-col {
&__headline-line,
&__headline-accent {
font-size: clamp(24px, 2.2vw, 30px);
}
&__lead {
font-size: 12px;
margin-bottom: 16px;
}
&__features {
gap: 12px;
}
}
}
}
/* ارتفاع کوتاه دسکتاپ */
@media (max-height: 780px) and (min-width: 768px) {
.dauth--boursecu {
.dauth__layout {
padding-top: clamp(72px, 9vh, 92px);
padding-bottom: clamp(28px, 4vh, 44px);
}
.dauth-visual-col__products {
max-height: min(48vh, 360px);
margin-bottom: -16px;
}
.dauth-center-col {
&__lead {
margin-bottom: 14px;
}
&__features {
gap: 10px;
}
&__feature-icon {
width: 36px;
height: 36px;
}
}
}
}
/* ─── تبلت عریض (≈768–1100): دو ستون — برند چپ، فرم راست ─── */
@media (max-width: 1100px) and (min-width: 768px) {
.dauth--boursecu {
.dauth-page-logo {
top: 20px;
left: 24px;
&__img {
width: clamp(110px, 14vw, 140px);
max-height: 120px;
}
}
.dauth__layout {
direction: ltr;
grid-template-columns: minmax(0, 1fr) minmax(340px, 420px);
grid-template-rows: auto minmax(0, 1fr);
grid-template-areas:
'brand form'
'visual form';
align-items: stretch;
gap: 16px 28px;
padding: 96px 28px 20px 0;
min-height: calc(100vh - 64px);
height: auto;
}
.dauth-center-col {
grid-area: brand;
order: unset;
min-height: auto;
height: auto;
max-width: none;
margin: 0;
padding: 8px 12px 0 20px;
justify-content: flex-start;
&__headline-line,
&__headline-accent {
font-size: clamp(24px, 3vw, 30px);
}
&__lead {
font-size: 13px;
margin-bottom: 18px;
}
&__features {
gap: 14px;
}
&__tagline {
margin-top: 20px;
padding-top: 0;
}
}
.dauth-visual-col {
grid-area: visual;
order: unset;
min-height: auto;
align-items: flex-start;
justify-content: flex-end;
padding-bottom: 0;
&__products {
width: clamp(360px, 44vw, 520px);
min-width: 320px;
max-height: min(46vh, 380px);
margin-inline-start: -4%;
margin-bottom: -8px;
transform: translateX(-4%);
mix-blend-mode: lighten;
}
}
.dauth-form-panel {
grid-area: form;
order: unset;
justify-content: flex-start;
align-items: flex-start;
align-self: stretch;
padding-inline-end: 8px;
width: 100%;
height: auto;
}
.dauth-form-column {
max-width: 400px;
margin: 0;
width: 100%;
flex: 1 1 auto;
height: auto;
}
.dauth-center-col__body {
margin-top: 0;
}
.dauth-form-column__top {
margin-top: 0;
}
.dauth-form-column__foot {
margin-top: auto;
}
.dauth-card {
box-shadow: none !important;
padding: 22px 22px 18px;
}
}
}
/* ─── موبایل (<768): لوگو + کارت + پشتیبانی، وسط‌چین عمودی ─── */
@media (max-width: 767px) {
.dauth--boursecu {
min-height: 100dvh;
display: flex;
flex-direction: column;
justify-content: center;
padding: 12px 0;
.dauth-page-logo {
position: relative;
top: auto;
left: auto;
transform: none;
display: flex;
justify-content: center;
padding: 0 16px 12px;
z-index: 5;
flex-shrink: 0;
&__img {
width: 104px;
max-height: 96px;
}
}
.dauth__layout {
display: flex;
flex-direction: column;
direction: rtl;
flex: 0 1 auto;
gap: 12px;
padding: 0 16px;
min-height: 0;
width: 100%;
}
.dauth-form-panel {
order: 1;
width: 100%;
justify-content: center;
padding: 0;
}
.dauth-form-column {
max-width: none;
width: 100%;
height: auto;
gap: 0;
}
.dauth-form-column__top {
gap: 12px;
margin-top: 0;
}
.dauth-form-column__foot {
margin-top: 10px;
margin-bottom: 0;
margin-inline-start: 0;
padding-top: 0;
align-items: center;
gap: 8px;
}
.dauth-page-footer__status {
display: none;
}
.dauth-card {
max-width: none;
width: 100%;
border-radius: 14px;
padding: 20px 16px 16px;
box-shadow: none !important;
}
.dauth-card__header {
margin-bottom: 18px;
}
.dauth-card__title {
font-size: 22px;
}
.dauth-card__subtitle {
font-size: 12px;
margin-top: 4px;
}
.dauth-toggle {
margin-bottom: 14px;
padding: 4px;
gap: 4px;
&__btn {
padding: 10px 6px;
font-size: 12px;
}
}
.dauth-form {
&.ant-form-vertical :deep(.ant-form-item) {
margin-bottom: 14px;
}
:deep(.ant-input),
:deep(.ant-input-affix-wrapper) {
min-height: 44px;
}
}
.dauth-actions {
gap: 8px;
margin-top: 0;
}
.dauth-btn-stack,
.dauth-actions__submit,
:deep(.noname-base-button.dauth-actions__submit) {
min-height: 44px;
border-radius: 10px;
font-size: 13px;
}
.dauth-form-links {
display: flex;
justify-content: center;
width: 100%;
gap: 6px 8px;
font-size: 11px;
line-height: 1.6;
text-align: center;
flex-wrap: wrap;
margin: 0;
padding: 0;
}
.dauth-center-col {
display: flex;
order: 2;
min-height: auto;
height: auto;
max-width: none;
width: 100%;
padding: 4px 0 0;
justify-content: center;
align-items: center;
&__body {
display: none;
margin-top: 0;
}
&__tagline {
display: inline-flex;
justify-content: center;
margin: 8px 0 0;
padding-top: 0;
font-size: 12px;
}
}
.dauth-visual-col {
display: none;
}
.dauth-switch {
margin-top: 12px;
font-size: 11px;
}
}
}
@media (max-width: 400px) {
.dauth--boursecu {
.dauth-page-logo {
&__img {
width: 88px;
max-height: 80px;
}
}
.dauth-form-links {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px 10px;
justify-items: center;
&__sep {
display: none;
}
}
}
}
.login-form--desktop {
:deep(.ant-form-item) {
margin-bottom: 18px;
+78 -31
View File
@@ -1,12 +1,11 @@
<template>
<div class="login-page-root">
<!-- Desktop: premium split-panel -->
<DesktopAuthShell v-show="isDesktop">
<DesktopAuthShell>
<div class="dauth-card">
<header v-if="!loginPage.enterType" class="dauth-card__header">
<p class="dauth-card__eyebrow">حساب کاربری</p>
<h2 class="dauth-card__title">ورود</h2>
<p class="dauth-card__subtitle">برای ادامه، روش ورود خود را انتخاب کنید</p>
<p class="dauth-card__subtitle">برای ادامه، رمز ورود خود را انتخاب کنید.</p>
</header>
<header v-else class="dauth-card__header">
<p class="dauth-card__eyebrow">حساب جدید</p>
@@ -14,53 +13,101 @@
<p class="dauth-card__subtitle">اطلاعات خود را برای ایجاد حساب وارد کنید</p>
</header>
<Login v-if="!loginPage.enterType" variant="desktop" @register="loginPage.setEnterType" />
<Register v-else variant="desktop" @register="loginPage.setEnterType" />
<Login
v-if="!loginPage.enterType"
variant="desktop"
@register="loginPage.setEnterType"
/>
<Register
v-else
variant="desktop"
@register="loginPage.setEnterType"
/>
</div>
</DesktopAuthShell>
<!-- Mobile: original layout -->
<div v-show="!isDesktop" class="login-page flex flex-col items-center">
<div class="login-page__content z-1000 w-full p-5">
<Login v-if="!loginPage.enterType" @register="loginPage.setEnterType" />
<Register v-else @register="loginPage.setEnterType" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted } from 'vue'
import { useLoginPage } from './composables/useLoginPage'
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
import Login from './components/Login.vue'
import Register from './components/Register.vue'
import DesktopAuthShell from './desktop/components/DesktopAuthShell.vue'
const isDesktop = useDesktopBreakpoint()
const loginPage = useLoginPage()
onMounted(() => {
document.documentElement.classList.add('login-page-active')
})
onUnmounted(() => {
document.documentElement.classList.remove('login-page-active')
})
</script>
<style scoped>
.login-page-root {
width: 100%;
height: 100%;
max-height: 100dvh;
overflow: hidden;
}
.login-page {
height: 100%;
max-height: calc(100dvh - var(--app-credit-footer-offset, 28px));
min-height: 100vh;
min-height: 100dvh;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
}
.login-page__content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
</style>
<style lang="less">
/* موبایل لاگین: تمام‌عرض — بدون فریم ۴۲۶ و حاشیه سفید دوطرف */
@media screen and (max-width: 767.98px) {
html.login-page-active {
body {
display: block !important;
justify-content: unset !important;
background: #0b192e !important;
}
#app {
display: block !important;
width: 100% !important;
max-width: none !important;
background: #0b192e !important;
}
.desktopConfig,
.app-main,
.login-page-root,
.dauth,
.dauth--boursecu {
width: 100% !important;
max-width: none !important;
margin: 0 !important;
left: auto !important;
right: auto !important;
transform: none !important;
}
.desktopConfig,
.app-main,
.login-page-root {
min-height: 100dvh !important;
height: auto !important;
max-height: none !important;
overflow-x: hidden !important;
overflow-y: auto !important;
background: #0b192e !important;
}
.dauth--boursecu {
flex: 1 1 auto;
min-height: 100dvh !important;
}
.app-credit-footer--mobile,
.app-credit-footer--login {
display: none !important;
}
}
}
</style>
+17 -15
View File
@@ -1,26 +1,28 @@
<template>
<div class="login-page flex items-center justify-center">
<div class="z-1000 w-full p-5">
<Login v-if="!page.enterType" key="login" @register="page.setEnterType" />
<Register v-else key="register" @register="page.setEnterType" />
<DesktopAuthShell>
<div class="dauth-card">
<header v-if="!page.enterType" class="dauth-card__header">
<p class="dauth-card__eyebrow">حساب کاربری</p>
<h2 class="dauth-card__title">ورود</h2>
<p class="dauth-card__subtitle">برای ادامه، رمز ورود خود را انتخاب کنید.</p>
</header>
<header v-else class="dauth-card__header">
<p class="dauth-card__eyebrow">حساب جدید</p>
<h2 class="dauth-card__title">ثبت نام</h2>
<p class="dauth-card__subtitle">اطلاعات خود را برای ایجاد حساب وارد کنید</p>
</header>
<Login v-if="!page.enterType" variant="desktop" @register="page.setEnterType" />
<Register v-else variant="desktop" @register="page.setEnterType" />
</div>
</div>
</DesktopAuthShell>
</template>
<script lang="ts" setup>
import Login from '../components/Login.vue'
import Register from '../components/Register.vue'
import DesktopAuthShell from '../desktop/components/DesktopAuthShell.vue'
import { useLoginPageContext } from '../composables/useLoginPage'
const page = useLoginPageContext()
</script>
<style scoped>
.login-page {
min-height: calc(100dvh - var(--app-credit-footer-offset, 28px));
max-height: calc(100dvh - var(--app-credit-footer-offset, 28px));
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
}
</style>