Polish boursecu UX: branding logos, home price stats, assets, and desktop footer.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Vendored
+1
-1
@@ -14,6 +14,7 @@ declare module 'vue' {
|
|||||||
AInput: typeof import('ant-design-vue/es')['Input']
|
AInput: typeof import('ant-design-vue/es')['Input']
|
||||||
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
||||||
AModal: typeof import('ant-design-vue/es')['Modal']
|
AModal: typeof import('ant-design-vue/es')['Modal']
|
||||||
|
AppCreditLink: typeof import('./src/components/AppCreditLink.vue')['default']
|
||||||
ARadio: typeof import('ant-design-vue/es')['Radio']
|
ARadio: typeof import('ant-design-vue/es')['Radio']
|
||||||
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
|
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
|
||||||
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
||||||
@@ -69,6 +70,5 @@ declare module 'vue' {
|
|||||||
TransactionSkeleton: typeof import('./src/components/TransactionSkeleton.vue')['default']
|
TransactionSkeleton: typeof import('./src/components/TransactionSkeleton.vue')['default']
|
||||||
VanActionSheet: typeof import('vant/es')['ActionSheet']
|
VanActionSheet: typeof import('vant/es')['ActionSheet']
|
||||||
VanDialog: typeof import('vant/es')['Dialog']
|
VanDialog: typeof import('vant/es')['Dialog']
|
||||||
VanField: typeof import('vant/es')['Field']
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+120
-22
@@ -27,10 +27,13 @@
|
|||||||
</template>
|
</template>
|
||||||
<div class="self-center splash_appear_fade_in">
|
<div class="self-center splash_appear_fade_in">
|
||||||
<img
|
<img
|
||||||
:src="getProjectLoginLogo(config?.config?.avatar)"
|
:src="getProjectSplashLogo(config?.config?.avatar)"
|
||||||
:class="hasCustomLoginLogoLayout()
|
:class="isBoursecuProject()
|
||||||
? 'w-[80vw] max-w-[320px] h-auto object-contain'
|
? 'w-[min(72vw,280px)] h-auto object-contain'
|
||||||
: 'h-[20vh] object-contain'"
|
: hasCustomLoginLogoLayout()
|
||||||
|
? 'w-[80vw] max-w-[320px] h-auto object-contain'
|
||||||
|
: 'h-[20vh] object-contain'"
|
||||||
|
alt=""
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="fixed w-full bottom-5 home_appear_fade_in_up">
|
<div class="fixed w-full bottom-5 home_appear_fade_in_up">
|
||||||
@@ -90,20 +93,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!showSplash"
|
v-if="!showSplash && !isDesktop"
|
||||||
class="app-credit-footer flex bg-white justify-center text-[9px]"
|
class="app-credit-footer app-credit-footer--mobile"
|
||||||
>
|
>
|
||||||
<a
|
<AppCreditLink variant="mobile" />
|
||||||
href="https://montzar.ir"
|
</div>
|
||||||
target="_blank"
|
<div
|
||||||
rel="noopener noreferrer"
|
v-if="!showSplash && showDesktopFloatingCredit"
|
||||||
class="flex items-center gap-1 text-inherit no-underline"
|
class="app-credit-footer app-credit-footer--desktop-float"
|
||||||
>
|
>
|
||||||
<div class="flex justify-center py-1 items-center">
|
<AppCreditLink variant="float" />
|
||||||
<div>توسعه و طراحی توسط</div>
|
|
||||||
<img src="/logo-montzar.png" alt="" class="h-3" />
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -118,19 +117,28 @@ import img_bottomGradiant from '@/views/login/assets/img/gr_bottom_white.svg'
|
|||||||
import {DotLottieVue} from '@lottiefiles/dotlottie-vue'
|
import {DotLottieVue} from '@lottiefiles/dotlottie-vue'
|
||||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||||
import {useConfigStore} from "@/store/configStore";
|
import {useConfigStore} from "@/store/configStore";
|
||||||
import {getProjectFontFamily, getProjectLoginLogo, getProjectSplashBackground, getProjectBrandRed, hasCustomLoginLogoLayout, isBoursecuProject, resetAppSurfaceBackground} from "@/utils/config";
|
import {getProjectFontFamily, getProjectSplashBackground, getProjectSplashLogo, getProjectBrandRed, hasCustomLoginLogoLayout, isBoursecuProject, resetAppSurfaceBackground} from "@/utils/config";
|
||||||
import {ConfigProvider as AConfigProvider} from 'ant-design-vue'
|
import {ConfigProvider as AConfigProvider} from 'ant-design-vue'
|
||||||
import BasicIcon from "@/components/BasicPacks/BasicIcon.vue";
|
import BasicIcon from "@/components/BasicPacks/BasicIcon.vue";
|
||||||
import { useApplyDesktopLayoutClass } from '@/layouts/composables/useAppLayout'
|
import { useApplyDesktopLayoutClass, useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||||
import {
|
import {
|
||||||
buildSupportMessengerUrl,
|
buildSupportMessengerUrl,
|
||||||
loadSupportMessengerFromSettings,
|
loadSupportMessengerFromSettings,
|
||||||
supportMessengerSettings,
|
supportMessengerSettings,
|
||||||
} from '@/utils/supportMessenger'
|
} from '@/utils/supportMessenger'
|
||||||
import {useDraggableFloatingButton} from '@/utils/useDraggableFloatingButton'
|
import { useDraggableFloatingButton } from '@/utils/useDraggableFloatingButton'
|
||||||
|
import AppCreditLink from '@/components/AppCreditLink.vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
useApplyDesktopLayoutClass()
|
useApplyDesktopLayoutClass()
|
||||||
|
|
||||||
|
const isDesktop = useDesktopBreakpoint()
|
||||||
|
const route = useRoute()
|
||||||
|
const showDesktopFloatingCredit = computed(() => {
|
||||||
|
if (!isDesktop.value) return false
|
||||||
|
return route.name === 'login' || !!route.meta.hideMenu
|
||||||
|
})
|
||||||
|
|
||||||
const {offlineReady, needRefresh, updateServiceWorker} = useRegisterSW()
|
const {offlineReady, needRefresh, updateServiceWorker} = useRegisterSW()
|
||||||
const config = useConfigStore()
|
const config = useConfigStore()
|
||||||
const splashIndicator = computed(() => h(LoadingOutlined, {
|
const splashIndicator = computed(() => h(LoadingOutlined, {
|
||||||
@@ -310,16 +318,106 @@ function update() {
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
z-index: 1000000;
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-credit-footer--mobile {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 426px;
|
max-width: 426px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
z-index: 1000000;
|
|
||||||
min-height: var(--app-credit-bar-inner);
|
min-height: var(--app-credit-bar-inner);
|
||||||
padding-bottom: var(--app-safe-bottom);
|
padding-bottom: var(--app-safe-bottom);
|
||||||
box-sizing: content-box;
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 -2px 4px -2px #0000001a;
|
box-shadow: 0 -2px 4px -2px #0000001a;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-credit-footer--desktop-float {
|
||||||
|
left: auto;
|
||||||
|
right: 20px;
|
||||||
|
bottom: 16px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-credit-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__logo {
|
||||||
|
width: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--mobile {
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 0;
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
|
.app-credit-link__logo {
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--sidebar {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 10px;
|
||||||
|
|
||||||
|
.app-credit-link__logo {
|
||||||
|
height: 15px;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--float {
|
||||||
|
gap: 6px;
|
||||||
|
padding: 7px 14px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #6b7280;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||||
|
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.06);
|
||||||
|
|
||||||
|
.app-credit-link__logo {
|
||||||
|
height: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html.project-boursecu {
|
||||||
|
.app-credit-link--sidebar {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
|
||||||
|
.app-credit-link__logo {
|
||||||
|
filter: brightness(1.15);
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-credit-link--float {
|
||||||
|
background: var(--App-brand-nav, #133050);
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.updateButton .van-dialog__confirm .van-button__text {
|
.updateButton .van-dialog__confirm .van-button__text {
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<a
|
||||||
|
href="https://montzar.ir"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="app-credit-link"
|
||||||
|
:class="`app-credit-link--${variant}`"
|
||||||
|
>
|
||||||
|
<span class="app-credit-link__text">توسعه و طراحی توسط</span>
|
||||||
|
<img src="/logo-montzar.png" alt="montzar" class="app-credit-link__logo" />
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
variant?: 'mobile' | 'sidebar' | 'float'
|
||||||
|
}>(),
|
||||||
|
{ variant: 'mobile' },
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@@ -1,13 +1,18 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
import { getProducts } from '@/api/products'
|
import { getProducts } from '@/api/products'
|
||||||
import type { Product } from '@/models/products'
|
import type { Product } from '@/models/products'
|
||||||
|
|
||||||
/** Shared product catalog — dedupes getProducts() across wallet assets and chart offset. */
|
/** Shared product catalog — dedupes getProducts() across wallet assets and chart offset. */
|
||||||
let catalogCache: Product[] | null = null
|
const catalogProducts = ref<Product[]>([])
|
||||||
let catalogPromise: Promise<Product[]> | null = null
|
let catalogPromise: Promise<Product[]> | null = null
|
||||||
|
|
||||||
|
export function getProductsCatalogRef() {
|
||||||
|
return catalogProducts
|
||||||
|
}
|
||||||
|
|
||||||
export function getProductsCatalog(force = false): Promise<Product[]> {
|
export function getProductsCatalog(force = false): Promise<Product[]> {
|
||||||
if (!force && catalogCache) {
|
if (!force && catalogProducts.value.length > 0) {
|
||||||
return Promise.resolve(catalogCache)
|
return Promise.resolve(catalogProducts.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!force && catalogPromise) {
|
if (!force && catalogPromise) {
|
||||||
@@ -16,8 +21,8 @@ export function getProductsCatalog(force = false): Promise<Product[]> {
|
|||||||
|
|
||||||
catalogPromise = getProducts()
|
catalogPromise = getProducts()
|
||||||
.then((products) => {
|
.then((products) => {
|
||||||
catalogCache = products ?? []
|
catalogProducts.value = products ?? []
|
||||||
return catalogCache
|
return catalogProducts.value
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
catalogPromise = null
|
catalogPromise = null
|
||||||
@@ -27,6 +32,6 @@ export function getProductsCatalog(force = false): Promise<Product[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function clearProductsCatalog() {
|
export function clearProductsCatalog() {
|
||||||
catalogCache = null
|
catalogProducts.value = []
|
||||||
catalogPromise = null
|
catalogPromise = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,20 +34,25 @@
|
|||||||
@click="navigateTo(item.routeName)"
|
@click="navigateTo(item.routeName)"
|
||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<div class="desktop-sidebar__credit">
|
||||||
|
<AppCreditLink variant="sidebar" />
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useConfigStore } from '@/store/configStore'
|
import { useConfigStore } from '@/store/configStore'
|
||||||
import { getProjectNavLogo, isBoursecuProject } from '@/utils/config'
|
import { getProjectSplashLogo, isBoursecuProject } from '@/utils/config'
|
||||||
import { getPrimaryNavItems, SECONDARY_NAV } from '../config/navigation'
|
import { getPrimaryNavItems, SECONDARY_NAV } from '../config/navigation'
|
||||||
import { useAppNavigation } from '../composables/useAppNavigation'
|
import { useAppNavigation } from '../composables/useAppNavigation'
|
||||||
import DesktopNavItem from './DesktopNavItem.vue'
|
import DesktopNavItem from './DesktopNavItem.vue'
|
||||||
|
import AppCreditLink from '@/components/AppCreditLink.vue'
|
||||||
|
|
||||||
const config = useConfigStore()
|
const config = useConfigStore()
|
||||||
const isBoursecu = isBoursecuProject()
|
const isBoursecu = isBoursecuProject()
|
||||||
const navLogo = computed(() => getProjectNavLogo(config.config?.avatar))
|
const navLogo = computed(() => getProjectSplashLogo(config.config?.avatar))
|
||||||
const primaryItems = getPrimaryNavItems()
|
const primaryItems = getPrimaryNavItems()
|
||||||
const secondaryItems = SECONDARY_NAV
|
const secondaryItems = SECONDARY_NAV
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,8 @@
|
|||||||
|
|
||||||
.desktop-sidebar__logo {
|
.desktop-sidebar__logo {
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
max-height: 80px;
|
max-height: 200px;
|
||||||
|
width: min(100%, 200px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +103,13 @@
|
|||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.desktop-sidebar__credit {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 12px 8px 4px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.desktop-sidebar__divider {
|
.desktop-sidebar__divider {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background: rgba(0, 0, 0, 0.08);
|
background: rgba(0, 0, 0, 0.08);
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useConfigStore } from '@/store/configStore'
|
import { useConfigStore } from '@/store/configStore'
|
||||||
import { getProjectNavLogo, isBoursecuProject } from '@/utils/config'
|
import { getProjectMobileNavLogo, isBoursecuProject } from '@/utils/config'
|
||||||
import { SECONDARY_NAV } from '../config/navigation'
|
import { SECONDARY_NAV } from '../config/navigation'
|
||||||
import { useAppNavigation } from '../composables/useAppNavigation'
|
import { useAppNavigation } from '../composables/useAppNavigation'
|
||||||
import MobileBottomNav from './MobileBottomNav.vue'
|
import MobileBottomNav from './MobileBottomNav.vue'
|
||||||
@@ -37,7 +37,7 @@ import MobileOverflowMenu from './MobileOverflowMenu.vue'
|
|||||||
|
|
||||||
const config = useConfigStore()
|
const config = useConfigStore()
|
||||||
const isBoursecu = isBoursecuProject()
|
const isBoursecu = isBoursecuProject()
|
||||||
const navLogo = computed(() => getProjectNavLogo(config.config?.avatar))
|
const navLogo = computed(() => getProjectMobileNavLogo(config.config?.avatar))
|
||||||
|
|
||||||
const {
|
const {
|
||||||
popVisible,
|
popVisible,
|
||||||
|
|||||||
@@ -46,9 +46,7 @@
|
|||||||
--app-credit-footer-offset: 0px;
|
--app-credit-footer-offset: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-credit-footer {
|
/* Mobile fixed bar hidden on desktop — credit shown in sidebar / floating badge */
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Teleport to body escapes parent v-show — hide mobile dock chrome on desktop */
|
/* Teleport to body escapes parent v-show — hide mobile dock chrome on desktop */
|
||||||
.app-dock-add-bar,
|
.app-dock-add-bar,
|
||||||
|
|||||||
+22
-26
@@ -11,6 +11,7 @@ import { useUserStore } from '@/store/userStore'
|
|||||||
import { useWalletStore } from '@/store/walletStore'
|
import { useWalletStore } from '@/store/walletStore'
|
||||||
import { useFundUserStore } from '@/store/fundStore'
|
import { useFundUserStore } from '@/store/fundStore'
|
||||||
import VueLazyLoad from 'vue3-lazyload'
|
import VueLazyLoad from 'vue3-lazyload'
|
||||||
|
import VueApexCharts from 'vue3-apexcharts'
|
||||||
import { getMe } from '@/api/me'
|
import { getMe } from '@/api/me'
|
||||||
import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router'
|
import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router'
|
||||||
import { useAuthStore } from '@/store/useAuthStore'
|
import { useAuthStore } from '@/store/useAuthStore'
|
||||||
@@ -24,6 +25,7 @@ app.use(VueLazyLoad, {
|
|||||||
// options...
|
// options...
|
||||||
})
|
})
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
app.use(VueApexCharts)
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const walletStore = useWalletStore()
|
const walletStore = useWalletStore()
|
||||||
@@ -121,20 +123,16 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
}
|
}
|
||||||
// If not an Apple device and fingerprint exists
|
// If not an Apple device and fingerprint exists
|
||||||
else if (!isApple && finger_print) {
|
else if (!isApple && finger_print) {
|
||||||
// goToLogin(to, next)
|
|
||||||
fingerPosition = await authenticateWithFingerprint()
|
fingerPosition = await authenticateWithFingerprint()
|
||||||
|
|
||||||
if (fingerPosition) {
|
if (fingerPosition) {
|
||||||
// goToHome(to, next) // Redirect to home if fingerprint is authenticated
|
return next()
|
||||||
} else {
|
|
||||||
goToLogin(to, next) // Redirect to login if authentication fails
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
goToLogin(to, next)
|
||||||
} else {
|
} else {
|
||||||
if (from.path === '/') {
|
if (to.name === 'login') {
|
||||||
// goToHome(to, next)
|
return goToHome(to, next)
|
||||||
}
|
|
||||||
if(to.name=="login"){
|
|
||||||
goToHome(to, next)
|
|
||||||
}
|
}
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
@@ -166,26 +164,24 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
// If fingerprint authentication failed or was not done
|
// If fingerprint authentication failed or was not done
|
||||||
else {
|
else {
|
||||||
if (getToken() && !hasFingerPrint) {
|
if (getToken() && !hasFingerPrint) {
|
||||||
next()
|
authStore.setLoading(false)
|
||||||
} else {
|
return next()
|
||||||
if (['login', '404','educate', 'aboutUs'].includes(to.name as string)) {
|
|
||||||
if (to.name === 'login'){
|
|
||||||
goToLogin(to, next)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
return next()
|
|
||||||
}
|
|
||||||
// Allow access to login or 404
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/* if (to.name !== 'login') {
|
|
||||||
return next({ name: 'login' }) // Redirect to login
|
if (['login', '404', 'educate', 'aboutUs'].includes(to.name as string)) {
|
||||||
} else {
|
authStore.setLoading(false)
|
||||||
return next() // If already on the login page, don't redirect
|
if (to.name === 'login' && !getToken()) {
|
||||||
}*/
|
return goToLogin(to, next)
|
||||||
/* return next({ name: 'login' })*/
|
}
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
authStore.setLoading(false)
|
||||||
|
return next({ name: 'login' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authStore.setLoading(false)
|
||||||
})
|
})
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
// if (currentRoute.value !== '/') {
|
// if (currentRoute.value !== '/') {
|
||||||
|
|||||||
+19
-1
@@ -1,4 +1,6 @@
|
|||||||
import boursecuLogo from '@/assets/img/boursemes_logofull.png'
|
import boursecuLogo from '@/assets/img/boursemes_logofull.png'
|
||||||
|
import boursecuSplashLogo from '@/assets/img/boursemes-splash.png'
|
||||||
|
import boursecuLoginLogo from '@/assets/img/boursemes-login.png'
|
||||||
import bazretalaLoginLogo from '@/assets/img/bazretala-login-logo.png'
|
import bazretalaLoginLogo from '@/assets/img/bazretala-login-logo.png'
|
||||||
import bazretalaNavLogo from '@/assets/img/bazretala-nav-logo.png'
|
import bazretalaNavLogo from '@/assets/img/bazretala-nav-logo.png'
|
||||||
import bazretalaFundBg from '@/assets/img/bazretala-fund-bg.png'
|
import bazretalaFundBg from '@/assets/img/bazretala-fund-bg.png'
|
||||||
@@ -46,7 +48,7 @@ export function resolveProjectColor(color?: string | null) {
|
|||||||
|
|
||||||
export function getProjectLoginLogo(avatar?: string | null) {
|
export function getProjectLoginLogo(avatar?: string | null) {
|
||||||
if (isBoursecuProject()) {
|
if (isBoursecuProject()) {
|
||||||
return boursecuLogo
|
return boursecuLoginLogo
|
||||||
}
|
}
|
||||||
if (isBazretalaProject()) {
|
if (isBazretalaProject()) {
|
||||||
return bazretalaLoginLogo
|
return bazretalaLoginLogo
|
||||||
@@ -54,6 +56,14 @@ export function getProjectLoginLogo(avatar?: string | null) {
|
|||||||
return avatar ?? getConfigApp('avatar')
|
return avatar ?? getConfigApp('avatar')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** لوگوی صفحه لودینگ اولیه — جدا از لوگوی لاگین/ناوبری */
|
||||||
|
export function getProjectSplashLogo(avatar?: string | null) {
|
||||||
|
if (isBoursecuProject()) {
|
||||||
|
return boursecuSplashLogo
|
||||||
|
}
|
||||||
|
return getProjectLoginLogo(avatar)
|
||||||
|
}
|
||||||
|
|
||||||
export function getProjectNavLogo(avatar?: string | null) {
|
export function getProjectNavLogo(avatar?: string | null) {
|
||||||
if (isBazretalaProject()) {
|
if (isBazretalaProject()) {
|
||||||
return bazretalaNavLogo
|
return bazretalaNavLogo
|
||||||
@@ -64,6 +74,14 @@ export function getProjectNavLogo(avatar?: string | null) {
|
|||||||
return avatar ?? getConfigApp('avatar')
|
return avatar ?? getConfigApp('avatar')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** لوگوی ناوبری موبایل — boursecu همان لوگوی لاگین (پسزمینه سفید) */
|
||||||
|
export function getProjectMobileNavLogo(avatar?: string | null) {
|
||||||
|
if (isBoursecuProject()) {
|
||||||
|
return boursecuLoginLogo
|
||||||
|
}
|
||||||
|
return getProjectNavLogo(avatar)
|
||||||
|
}
|
||||||
|
|
||||||
export const BAZRETALA_SUPPORT_PHONE = '09121784258'
|
export const BAZRETALA_SUPPORT_PHONE = '09121784258'
|
||||||
|
|
||||||
export function getProjectSupportPhone() {
|
export function getProjectSupportPhone() {
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
const PERSIAN_DIGITS = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'] as const
|
|
||||||
|
|
||||||
export function toPersianDigits(value: string | number | null | undefined): string {
|
|
||||||
if (value === null || value === undefined) return ''
|
|
||||||
return String(value).replace(/\d/g, (digit) => PERSIAN_DIGITS[Number(digit)] ?? digit)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toEnglishDigits(value: string): string {
|
|
||||||
return value.replace(/[۰-۹]/g, (digit) => {
|
|
||||||
const index = PERSIAN_DIGITS.indexOf(digit as (typeof PERSIAN_DIGITS)[number])
|
|
||||||
return index >= 0 ? String(index) : digit
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatFixed(value: string | number | null | undefined, decimals = 2): string {
|
|
||||||
const num = Number(value)
|
|
||||||
if (!Number.isFinite(num)) return toPersianDigits(String(value ?? ''))
|
|
||||||
return toPersianDigits(num.toFixed(decimals))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatPercent(
|
|
||||||
value: number,
|
|
||||||
decimals = 2,
|
|
||||||
options: { showSign?: boolean; suffix?: string } = {},
|
|
||||||
): string {
|
|
||||||
const { showSign = false, suffix = '%' } = options
|
|
||||||
const prefix = showSign && value > 0 ? '+' : value < 0 ? '-' : showSign ? '+' : ''
|
|
||||||
return `${prefix}${formatFixed(Math.abs(value), decimals)}${suffix}`
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { toPersianDigits } from '@/utils/persianDigits'
|
|
||||||
import type { Product } from '@/models/products'
|
import type { Product } from '@/models/products'
|
||||||
|
import { usePriceWithoutType } from '@/utils/usePrice'
|
||||||
import type { UserWalletLine } from '@/models/wallet'
|
import type { UserWalletLine } from '@/models/wallet'
|
||||||
import type { ProductFile } from '@/utils/file'
|
import type { ProductFile } from '@/utils/file'
|
||||||
import type { WalletInventoryCard, WalletMetalTotal } from '@/models/walletAssets'
|
import type { WalletInventoryCard, WalletMetalTotal } from '@/models/walletAssets'
|
||||||
@@ -87,7 +87,7 @@ export function resolveMetalAssetsDisplayLabel(
|
|||||||
case METAL_COPPER:
|
case METAL_COPPER:
|
||||||
return 'دارایی مس'
|
return 'دارایی مس'
|
||||||
case METAL_GOLD:
|
case METAL_GOLD:
|
||||||
return 'دارایی طلایی'
|
return 'دارایی شمش مس'
|
||||||
case METAL_SILVER:
|
case METAL_SILVER:
|
||||||
return 'دارایی نقره'
|
return 'دارایی نقره'
|
||||||
default:
|
default:
|
||||||
@@ -103,16 +103,44 @@ export interface HomeAssetRow {
|
|||||||
value: string
|
value: string
|
||||||
metal?: WalletMetalTotal
|
metal?: WalletMetalTotal
|
||||||
info: HomeAssetRowInfo
|
info: HomeAssetRowInfo
|
||||||
|
/** boursecu — متن مودال توضیحات (info) */
|
||||||
|
modalText?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatInventoryCardProductLabel(card: WalletInventoryCard): string {
|
function formatInventoryCardProductLabel(card: WalletInventoryCard): string {
|
||||||
return `دارایی ${card.title}`
|
return `دارایی ${card.title}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatBoursecuProductModalText(card: WalletInventoryCard): string {
|
||||||
|
return `${formatInventoryCardProductLabel(card)} = موجودی ${card.title}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** boursecu — متن مودال برای ردیفهای metal_totals */
|
||||||
|
export function formatBoursecuMetalModalText(metal: WalletMetalTotal): string {
|
||||||
|
switch (metal.metal_id) {
|
||||||
|
case METAL_COPPER:
|
||||||
|
return 'مجموع دارایی مس = جمع کاتد و مفتول و شمش'
|
||||||
|
case METAL_GOLD:
|
||||||
|
return 'دارایی شمش مس = موجودی شمش مس'
|
||||||
|
case METAL_SILVER:
|
||||||
|
return 'دارایی نقره = موجودی نقره'
|
||||||
|
default:
|
||||||
|
return `${resolveMetalAssetsDisplayLabel(metal, true)} = موجودی ${metal.label}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function findMetalTotalById(metalTotals: WalletMetalTotal[], metalId: number) {
|
function findMetalTotalById(metalTotals: WalletMetalTotal[], metalId: number) {
|
||||||
return metalTotals.find((metal) => metal.metal_id === metalId)
|
return metalTotals.find((metal) => metal.metal_id === metalId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatBoursecuMetalRowValue(metal: WalletMetalTotal): string {
|
||||||
|
const amount = usePriceWithoutType(Number(metal.total_weight ?? 0), false)
|
||||||
|
if (metal.metal_id === METAL_GOLD) {
|
||||||
|
return `${amount} عدد`
|
||||||
|
}
|
||||||
|
return `${amount}${metal.unit ?? ''}`
|
||||||
|
}
|
||||||
|
|
||||||
/** boursecu home — هر محصول / فلز / ریال از customer/wallet/assets جدا نمایش داده میشود */
|
/** boursecu home — هر محصول / فلز / ریال از customer/wallet/assets جدا نمایش داده میشود */
|
||||||
export function buildBoursecuHomeAssetRows(
|
export function buildBoursecuHomeAssetRows(
|
||||||
summary: {
|
summary: {
|
||||||
@@ -142,6 +170,7 @@ export function buildBoursecuHomeAssetRows(
|
|||||||
value: formatInventoryCardValue(card),
|
value: formatInventoryCardValue(card),
|
||||||
metal: findMetalTotalById(metalTotals, card.metal_id),
|
metal: findMetalTotalById(metalTotals, card.metal_id),
|
||||||
info: 'metal',
|
info: 'metal',
|
||||||
|
modalText: formatBoursecuProductModalText(card),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,9 +182,10 @@ export function buildBoursecuHomeAssetRows(
|
|||||||
rows.push({
|
rows.push({
|
||||||
id: `metal-${metal.metal_id}`,
|
id: `metal-${metal.metal_id}`,
|
||||||
label: resolveMetalAssetsDisplayLabel(metal, true),
|
label: resolveMetalAssetsDisplayLabel(metal, true),
|
||||||
value: `${Number(metal.total_weight ?? 0)}${metal.unit ?? ''}`,
|
value: formatBoursecuMetalRowValue(metal),
|
||||||
metal,
|
metal,
|
||||||
info: 'metal',
|
info: 'metal',
|
||||||
|
modalText: formatBoursecuMetalModalText(metal),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +196,7 @@ export function buildBoursecuHomeAssetRows(
|
|||||||
value: formatInventoryCardValue(card),
|
value: formatInventoryCardValue(card),
|
||||||
metal: findMetalTotalById(metalTotals, card.metal_id),
|
metal: findMetalTotalById(metalTotals, card.metal_id),
|
||||||
info: 'metal',
|
info: 'metal',
|
||||||
|
modalText: formatBoursecuProductModalText(card),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +254,7 @@ export function formatMetalAssetsInfoText(metal: WalletMetalTotal): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatRialNumber(value: number): string {
|
function formatRialNumber(value: number): string {
|
||||||
return Math.round(value).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
return usePriceWithoutType(Math.round(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** متن توضیح مودال دارایی ریالی */
|
/** متن توضیح مودال دارایی ریالی */
|
||||||
@@ -276,7 +307,7 @@ export function formatInventoryCardValue(card: {
|
|||||||
}): string {
|
}): string {
|
||||||
const quantity = Number(card.quantity ?? 0)
|
const quantity = Number(card.quantity ?? 0)
|
||||||
const unit = card.unit ?? (card.display_type === 'count' ? 'عدد' : 'گرم')
|
const unit = card.unit ?? (card.display_type === 'count' ? 'عدد' : 'گرم')
|
||||||
return `${quantity} ${unit}`
|
return `${usePriceWithoutType(quantity, false)} ${unit}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findProductInventoryCard(
|
export function findProductInventoryCard(
|
||||||
|
|||||||
+18
-15
@@ -1,24 +1,27 @@
|
|||||||
export function usePrice(price: number, digit = true, unit = 'ریال') {
|
const PERSIAN_DIGITS = '۰۱۲۳۴۵۶۷۸۹'
|
||||||
price= price??0
|
|
||||||
let _price = price.toString()
|
function formatGroupedPrice(price: string | number | null | undefined, digit: boolean) {
|
||||||
|
let value = price === null || price === undefined ? '' : String(price)
|
||||||
if (digit) {
|
if (digit) {
|
||||||
_price = _price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
value = value.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||||
}
|
}
|
||||||
return `${_price} ${unit}`
|
return value.replace(/\d/g, (d) => PERSIAN_DIGITS[Number(d)] ?? d)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePriceWithoutType(price?: number, digit = true, unit = 'ریال') {
|
export function usePrice(price: number, digit = true, unit = 'ریال') {
|
||||||
let _price = price?.toString()
|
price = price ?? 0
|
||||||
if (digit) {
|
const formatted = formatGroupedPrice(price, digit)
|
||||||
_price = _price?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
return `${formatted} ${unit}`
|
||||||
}
|
}
|
||||||
return `${_price}`
|
|
||||||
|
export function usePriceWithoutType(price?: number, digit = true) {
|
||||||
|
return formatGroupedPrice(price ?? '', digit)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTowDecimal(num: string | number): string | number {
|
export function useTowDecimal(num: string | number): string | number {
|
||||||
const temp = num.toString().match(/^-?\d+(?:\.\d{0,2})?/)
|
const temp = num.toString().match(/^-?\d+(?:\.\d{0,2})?/)
|
||||||
if (temp && temp.length > 0) return temp[0]
|
if (temp && temp.length > 0) return formatGroupedPrice(temp[0], false)
|
||||||
return Math.round(+num * 100) / 100
|
return formatGroupedPrice(Math.round(+num * 100) / 100, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useFormat(num: string | number, unit = '') {
|
export function useFormat(num: string | number, unit = '') {
|
||||||
@@ -76,6 +79,6 @@ export function getTransactionGoldAmount(item: TransactionPriceSource): number {
|
|||||||
|
|
||||||
export function formatGoldAmount(amount?: number | null): string {
|
export function formatGoldAmount(amount?: number | null): string {
|
||||||
const num = Number(amount)
|
const num = Number(amount)
|
||||||
if (!Number.isFinite(num)) return '0'
|
if (!Number.isFinite(num)) return '۰'
|
||||||
return String(Number(num.toFixed(3)))
|
return formatGroupedPrice(Number(num.toFixed(3)), false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
>
|
>
|
||||||
<div class="flex justify-between my-2 items-center">
|
<div class="flex justify-between my-2 items-center">
|
||||||
<div v-if="isBoursecuProject()" class="flex items-center gap-2">
|
<div v-if="isBoursecuProject()" class="flex items-center gap-2">
|
||||||
<img :src="projectLogo" alt="" class="h-8 w-auto object-contain" />
|
<img :src="projectLogo" alt="" class="h-10 w-auto max-w-[9rem] object-contain" />
|
||||||
<div class="color-transition font-bold text-primary">{{ appName }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex items-center gap-2" @click.stop="router.push({ name: 'user' })">
|
<div v-else class="flex items-center gap-2" @click.stop="router.push({ name: 'user' })">
|
||||||
<div
|
<div
|
||||||
@@ -46,16 +45,13 @@
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useWindowScroll } from '@vueuse/core'
|
import { useWindowScroll } from '@vueuse/core'
|
||||||
import { useUserStore } from '@/store/userStore'
|
import { useUserStore } from '@/store/userStore'
|
||||||
import { useConfigStore } from '@/store/configStore'
|
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
import Icon from '@/components/BasicPacks/BasicIcon.vue'
|
import Icon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
import { getProjectNavLogo, isBoursecuProject } from '@/utils/config'
|
import { getProjectMobileNavLogo, isBoursecuProject } from '@/utils/config'
|
||||||
|
|
||||||
const { y } = useWindowScroll()
|
const { y } = useWindowScroll()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const config = useConfigStore()
|
const projectLogo = computed(() => getProjectMobileNavLogo())
|
||||||
const projectLogo = computed(() => getProjectNavLogo())
|
|
||||||
const appName = computed(() => config.config?.name_fa || config.config?.name || 'بورس مس')
|
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
logout: []
|
logout: []
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="plain ? 'py-2' : 'bg-primary rounded-lg p-2 py-3 mt-2'">
|
<div :class="rootClass">
|
||||||
<div
|
<div
|
||||||
v-if="!boursecuLayout && meta?.has_count_based_products"
|
v-if="!boursecuLayout && meta?.has_count_based_products"
|
||||||
:class="rowClass"
|
:class="rowClass"
|
||||||
class="flex justify-center w-full z-20"
|
class="flex justify-center w-full z-20"
|
||||||
>
|
>
|
||||||
محصولات:
|
محصولات:
|
||||||
<div class="text-l mr-2 font-bold">{{ countBasedInventory }} عدد</div>
|
<div class="text-l mr-2 font-bold">{{ usePriceWithoutType(countBasedInventory, false) }} عدد</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="boursecuAssetRows.length">
|
<template v-if="boursecuAssetRows.length">
|
||||||
@@ -94,7 +94,7 @@ const props = withDefaults(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
openGoldModal: [metal: WalletMetalTotal]
|
openGoldModal: [metal: WalletMetalTotal, modalText?: string]
|
||||||
openRialModal: []
|
openRialModal: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -103,6 +103,16 @@ const { assetsSummary } = useWalletAssets()
|
|||||||
const rowClass = computed(() => (props.plain ? 'text-primary' : 'text-tertiary'))
|
const rowClass = computed(() => (props.plain ? 'text-primary' : 'text-tertiary'))
|
||||||
const iconColor = computed(() => (props.plain ? 'primary' : 'tertiary'))
|
const iconColor = computed(() => (props.plain ? 'primary' : 'tertiary'))
|
||||||
|
|
||||||
|
const rootClass = computed(() => {
|
||||||
|
if (props.boursecuLayout) {
|
||||||
|
return 'assets-section assets-section--boursecu rounded-lg px-3 py-3 mt-2'
|
||||||
|
}
|
||||||
|
if (props.plain) {
|
||||||
|
return 'py-2'
|
||||||
|
}
|
||||||
|
return 'bg-primary rounded-lg p-2 py-3 mt-2'
|
||||||
|
})
|
||||||
|
|
||||||
const meta = computed(() => assetsSummary.value?.meta)
|
const meta = computed(() => assetsSummary.value?.meta)
|
||||||
const metalTotals = computed(() => assetsSummary.value?.metal_totals ?? [])
|
const metalTotals = computed(() => assetsSummary.value?.metal_totals ?? [])
|
||||||
const primaryMetalTotal = computed(() => metalTotals.value[0])
|
const primaryMetalTotal = computed(() => metalTotals.value[0])
|
||||||
@@ -116,7 +126,7 @@ const countBasedInventory = computed(() =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
function formatMetalTotal(metal: WalletMetalTotal) {
|
function formatMetalTotal(metal: WalletMetalTotal) {
|
||||||
return `${resolveSummaryMetalWeight(metal, inventoryCards.value)}${metal.unit}`
|
return `${usePriceWithoutType(resolveSummaryMetalWeight(metal, inventoryCards.value), false)}${metal.unit}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRowInfo(row: HomeAssetRow) {
|
function handleRowInfo(row: HomeAssetRow) {
|
||||||
@@ -126,7 +136,16 @@ function handleRowInfo(row: HomeAssetRow) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (row.metal) {
|
if (row.metal) {
|
||||||
emit('openGoldModal', row.metal)
|
emit('openGoldModal', row.metal, row.modalText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
@boursecu-surface-border: #d4e3de;
|
||||||
|
|
||||||
|
.assets-section--boursecu {
|
||||||
|
background-color: #eff5f3;
|
||||||
|
border: 2px solid @boursecu-surface-border;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<Vue3Odometer
|
<Vue3Odometer
|
||||||
:value="displayGoldVal"
|
:value="displayGoldVal"
|
||||||
class="o-text !text-xl font-bold"
|
class="o-text !text-xl font-bold"
|
||||||
style="font-family: var(--App-font-family); direction: ltr"
|
style="font-family: iran-sans; direction: ltr"
|
||||||
/>
|
/>
|
||||||
ریال
|
ریال
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div
|
<div
|
||||||
class="w-full flex justify-evenly rounded-lg py-2 home_appear_fade_in"
|
class="w-full flex justify-evenly rounded-lg py-2 home_appear_fade_in"
|
||||||
:class="[
|
:class="[
|
||||||
layout === 'boursecu' ? 'mt-8 pt-2' : 'mt-6',
|
isBoursecuLayout ? 'mt-8 pt-2' : 'mt-6',
|
||||||
variant === 'mobile' ? 'pb-45' : 'mt-0 pb-0',
|
variant === 'mobile' ? 'pb-45' : 'mt-0 pb-0',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
@@ -33,31 +33,28 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { usePriceWithoutType } from '../../../../utils/usePrice'
|
import { usePriceWithoutType } from '../../../../utils/usePrice'
|
||||||
import type { ChartMetalType } from '../composables/usePriceChart'
|
import { price } from '@/utils/price'
|
||||||
import { resolveMetalDayRangePrices } from '../composables/usePriceChart'
|
import { getHomeLayoutId } from '../layouts/registry'
|
||||||
|
import { resolveMetalDayRangePrices, usePriceChart } from '../composables/usePriceChart'
|
||||||
|
|
||||||
const props = withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
price: any
|
|
||||||
layout?: 'default' | 'boursecu'
|
|
||||||
variant?: 'mobile' | 'desktop'
|
variant?: 'mobile' | 'desktop'
|
||||||
selectedMetal?: ChartMetalType
|
|
||||||
maftolRatio?: number
|
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
layout: 'default',
|
|
||||||
variant: 'mobile',
|
variant: 'mobile',
|
||||||
selectedMetal: 'katod',
|
|
||||||
maftolRatio: 0,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const { selectedChartMetal, maftolChartRatio } = usePriceChart()
|
||||||
|
const isBoursecuLayout = computed(() => getHomeLayoutId() === 'boursecu')
|
||||||
|
|
||||||
const rangePrices = computed(() =>
|
const rangePrices = computed(() =>
|
||||||
resolveMetalDayRangePrices({
|
resolveMetalDayRangePrices({
|
||||||
price: props.price,
|
price: price.value,
|
||||||
metal: props.selectedMetal,
|
metal: selectedChartMetal.value,
|
||||||
isBoursecuLayout: props.layout === 'boursecu',
|
isBoursecuLayout: isBoursecuLayout.value,
|
||||||
maftolRatio: props.maftolRatio,
|
maftolRatio: maftolChartRatio.value,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,12 @@ import { useUserStore } from '@/store/userStore'
|
|||||||
import { useWalletStore } from '@/store/walletStore'
|
import { useWalletStore } from '@/store/walletStore'
|
||||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||||
import type { WalletMetalTotal } from '@/models/walletAssets'
|
import type { WalletMetalTotal } from '@/models/walletAssets'
|
||||||
import { formatMetalAssetsInfoText, formatRialAssetsInfoText, resolveAssetsRialValue } from '@/utils/productAssets'
|
import {
|
||||||
|
formatBoursecuMetalModalText,
|
||||||
|
formatMetalAssetsInfoText,
|
||||||
|
formatRialAssetsInfoText,
|
||||||
|
resolveAssetsRialValue,
|
||||||
|
} from '@/utils/productAssets'
|
||||||
import { useConfigStore } from '@/store/configStore'
|
import { useConfigStore } from '@/store/configStore'
|
||||||
import { getTradeBuyPrice, lastPrice, stopLastPricePolling } from '@/utils/price'
|
import { getTradeBuyPrice, lastPrice, stopLastPricePolling } from '@/utils/price'
|
||||||
import { getStoreClosedMessage, isStoreClosed } from '@/utils/storeHours'
|
import { getStoreClosedMessage, isStoreClosed } from '@/utils/storeHours'
|
||||||
@@ -36,7 +41,7 @@ export interface HomePageContext {
|
|||||||
setChartMetal: (metal: ChartMetalType) => void
|
setChartMetal: (metal: ChartMetalType) => void
|
||||||
isBoursecuHomeLayout: ReturnType<typeof computed<boolean>>
|
isBoursecuHomeLayout: ReturnType<typeof computed<boolean>>
|
||||||
handleBannerClick: (banner: { typeClick?: string; modalText?: string; link?: string }) => void
|
handleBannerClick: (banner: { typeClick?: string; modalText?: string; link?: string }) => void
|
||||||
openGoldModal: (metal: WalletMetalTotal) => void
|
openGoldModal: (metal: WalletMetalTotal, modalText?: string) => void
|
||||||
openRialModal: () => void
|
openRialModal: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,9 +147,15 @@ export function useHomePage() {
|
|||||||
window.location.reload()
|
window.location.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
function openGoldModal(metal: WalletMetalTotal) {
|
function openGoldModal(metal: WalletMetalTotal, modalText?: string) {
|
||||||
|
const title =
|
||||||
|
modalText ??
|
||||||
|
(isBoursecuHomeLayout.value
|
||||||
|
? formatBoursecuMetalModalText(metal)
|
||||||
|
: formatMetalAssetsInfoText(metal))
|
||||||
|
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: formatMetalAssetsInfoText(metal),
|
title,
|
||||||
okText: 'متوجه شدم',
|
okText: 'متوجه شدم',
|
||||||
centered: true,
|
centered: true,
|
||||||
cancelButtonProps: { style: { display: 'none' } },
|
cancelButtonProps: { style: { display: 'none' } },
|
||||||
|
|||||||
@@ -21,8 +21,12 @@ export function isHomePageDataInitialized() {
|
|||||||
|
|
||||||
export async function initHomePageData(force = false) {
|
export async function initHomePageData(force = false) {
|
||||||
if (!force && initialized.value) {
|
if (!force && initialized.value) {
|
||||||
// Resume live price updates when returning to home without refetching datasets
|
|
||||||
startLastPricePolling()
|
startLastPricePolling()
|
||||||
|
|
||||||
|
if (getHomeLayoutId() === 'boursecu') {
|
||||||
|
void loadHomeMaftolOffset(false)
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { getAllPrices } from '@/api/home'
|
import { getAllPrices } from '@/api/home'
|
||||||
import type { Product } from '@/models/products'
|
import type { Product } from '@/models/products'
|
||||||
import { getProductsCatalog } from '@/composables/useProductsCatalog'
|
import { getProductsCatalog, getProductsCatalogRef } from '@/composables/useProductsCatalog'
|
||||||
import { getHomeLayoutId } from '../layouts/registry'
|
import { getHomeLayoutId } from '../layouts/registry'
|
||||||
import { getProductBuyPrice, resolvePriceOffsetRatio } from '@/utils/productPrice'
|
import { getProductBuyPrice, resolvePriceOffsetRatio } from '@/utils/productPrice'
|
||||||
import { isKatodMesGroup, isMaftolMesGroup } from '@/utils/productDealMethod'
|
import { isKatodMesGroup, isMaftolMesGroup } from '@/utils/productDealMethod'
|
||||||
@@ -19,7 +19,27 @@ const rawChartPoints = ref<ChartPoint[]>([])
|
|||||||
const chartSelection = ref('DAILY')
|
const chartSelection = ref('DAILY')
|
||||||
const chartLoading = ref(false)
|
const chartLoading = ref(false)
|
||||||
const selectedChartMetal = ref<ChartMetalType>('katod')
|
const selectedChartMetal = ref<ChartMetalType>('katod')
|
||||||
const maftolChartRatio = ref(0)
|
const catalogProducts = getProductsCatalogRef()
|
||||||
|
|
||||||
|
function resolveChartProducts(products: Product[]) {
|
||||||
|
return {
|
||||||
|
katod: findKatodProduct(products),
|
||||||
|
maftol: findMaftolProduct(products),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** نسبت مفتول به کاتد — reactive به قیمت لحظهای و کاتالوگ محصولات */
|
||||||
|
const maftolChartRatio = computed(() => {
|
||||||
|
if (getHomeLayoutId() !== 'boursecu') {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// lastPrice را track کن تا بعد از polling نسبت بهروز شود
|
||||||
|
void lastPrice.value?.price_last
|
||||||
|
|
||||||
|
const { katod, maftol } = resolveChartProducts(catalogProducts.value)
|
||||||
|
return deriveMaftolChartRatio(maftol, katod)
|
||||||
|
})
|
||||||
|
|
||||||
let chartFetchPromise: Promise<void> | null = null
|
let chartFetchPromise: Promise<void> | null = null
|
||||||
let chartInitialized = false
|
let chartInitialized = false
|
||||||
@@ -69,19 +89,25 @@ export function resolveMetalDayRangePrices(options: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deriveMaftolChartRatio(maftolProduct?: Product, katodProduct?: Product) {
|
function deriveMaftolChartRatio(maftolProduct?: Product, katodProduct?: Product) {
|
||||||
const offsetRatio = resolvePriceOffsetRatio(maftolProduct?.price_offset_percent)
|
const maftolOffset = resolvePriceOffsetRatio(maftolProduct?.price_offset_percent)
|
||||||
if (offsetRatio) {
|
if (maftolOffset) {
|
||||||
return offsetRatio
|
return maftolOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!maftolProduct) {
|
if (!maftolProduct) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const basePrice = getTradeBuyPrice(lastPrice.value)
|
const liveBuy = getTradeBuyPrice(lastPrice.value)
|
||||||
const katodPrice = katodProduct ? getProductBuyPrice(katodProduct) : basePrice
|
const katodLive = katodProduct ? getProductBuyPrice(katodProduct) : liveBuy
|
||||||
const maftolPrice = getProductBuyPrice(maftolProduct)
|
const maftolLive = getProductBuyPrice(maftolProduct)
|
||||||
const referencePrice = katodPrice || basePrice
|
|
||||||
|
// وقتی فروشگاه بسته است قیمت لحظهای صفر میشود — از قیمت ثابت کاتالوگ استفاده کن
|
||||||
|
const katodStatic = Number(katodProduct?.price ?? 0) + Number(katodProduct?.price_profit ?? 0)
|
||||||
|
const maftolStatic = Number(maftolProduct.price ?? 0) + Number(maftolProduct.price_profit ?? 0)
|
||||||
|
|
||||||
|
const referencePrice = katodLive || liveBuy || katodStatic
|
||||||
|
const maftolPrice = maftolLive || maftolStatic
|
||||||
|
|
||||||
if (!referencePrice || !maftolPrice || referencePrice === maftolPrice) {
|
if (!referencePrice || !maftolPrice || referencePrice === maftolPrice) {
|
||||||
return 0
|
return 0
|
||||||
@@ -168,12 +194,11 @@ async function fetchPrices(force = false) {
|
|||||||
|
|
||||||
async function loadMaftolOffset(force = false) {
|
async function loadMaftolOffset(force = false) {
|
||||||
if (getHomeLayoutId() !== 'boursecu') {
|
if (getHomeLayoutId() !== 'boursecu') {
|
||||||
maftolChartRatio.value = 0
|
|
||||||
syncChartSeries()
|
syncChartSeries()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!force && maftolOffsetLoaded) {
|
if (!force && maftolOffsetLoaded && catalogProducts.value.length > 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,15 +208,11 @@ async function loadMaftolOffset(force = false) {
|
|||||||
|
|
||||||
maftolOffsetPromise = (async () => {
|
maftolOffsetPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
const products = await getProductsCatalog(force)
|
await getProductsCatalog(force)
|
||||||
const katodProduct = findKatodProduct(products)
|
|
||||||
const maftolProduct = findMaftolProduct(products)
|
|
||||||
maftolChartRatio.value = deriveMaftolChartRatio(maftolProduct, katodProduct)
|
|
||||||
syncChartSeries()
|
syncChartSeries()
|
||||||
maftolOffsetLoaded = true
|
maftolOffsetLoaded = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading maftol chart ratio:', error)
|
console.error('Error loading maftol chart ratio:', error)
|
||||||
maftolChartRatio.value = 0
|
|
||||||
syncChartSeries()
|
syncChartSeries()
|
||||||
} finally {
|
} finally {
|
||||||
maftolOffsetPromise = null
|
maftolOffsetPromise = null
|
||||||
@@ -208,6 +229,10 @@ function updateChartData(timeline: string) {
|
|||||||
|
|
||||||
function setChartMetal(metal: ChartMetalType) {
|
function setChartMetal(metal: ChartMetalType) {
|
||||||
selectedChartMetal.value = metal
|
selectedChartMetal.value = metal
|
||||||
|
|
||||||
|
if (metal === 'maftol' && getHomeLayoutId() === 'boursecu' && !catalogProducts.value.length) {
|
||||||
|
void loadMaftolOffset(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePriceChart() {
|
export function usePriceChart() {
|
||||||
@@ -237,7 +262,6 @@ export function resetHomePriceChart() {
|
|||||||
rawChartPoints.value = []
|
rawChartPoints.value = []
|
||||||
chartSelection.value = 'DAILY'
|
chartSelection.value = 'DAILY'
|
||||||
selectedChartMetal.value = 'katod'
|
selectedChartMetal.value = 'katod'
|
||||||
maftolChartRatio.value = 0
|
|
||||||
chartInitialized = false
|
chartInitialized = false
|
||||||
chartFetchPromise = null
|
chartFetchPromise = null
|
||||||
maftolOffsetLoaded = false
|
maftolOffsetLoaded = false
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { buildBoursecuHomeAssetRows, resolveSummaryMetalWeight } from '@/utils/p
|
|||||||
import { useHomePageContext } from '../../composables/useHomePage'
|
import { useHomePageContext } from '../../composables/useHomePage'
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
openGoldModal: [metal: WalletMetalTotal]
|
openGoldModal: [metal: WalletMetalTotal, modalText?: string]
|
||||||
openRialModal: []
|
openRialModal: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ type MetricItem = {
|
|||||||
label: string
|
label: string
|
||||||
value: string
|
value: string
|
||||||
metal?: WalletMetalTotal
|
metal?: WalletMetalTotal
|
||||||
|
modalText?: string
|
||||||
showInfo?: boolean
|
showInfo?: boolean
|
||||||
onInfo?: 'gold' | 'rial'
|
onInfo?: 'gold' | 'rial'
|
||||||
}
|
}
|
||||||
@@ -40,7 +41,7 @@ const metrics = computed<MetricItem[]>(() => {
|
|||||||
items.push({
|
items.push({
|
||||||
id: 'products',
|
id: 'products',
|
||||||
label: 'محصولات',
|
label: 'محصولات',
|
||||||
value: `${countBasedInventory.value} عدد`,
|
value: `${usePriceWithoutType(countBasedInventory.value, false)} عدد`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ const metrics = computed<MetricItem[]>(() => {
|
|||||||
label: row.label,
|
label: row.label,
|
||||||
value: row.value,
|
value: row.value,
|
||||||
metal: row.metal,
|
metal: row.metal,
|
||||||
|
modalText: row.modalText,
|
||||||
showInfo: row.info !== 'none',
|
showInfo: row.info !== 'none',
|
||||||
onInfo: row.info === 'rial' ? 'rial' : 'gold',
|
onInfo: row.info === 'rial' ? 'rial' : 'gold',
|
||||||
})
|
})
|
||||||
@@ -93,7 +95,7 @@ const metrics = computed<MetricItem[]>(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function formatMetalTotal(metal: WalletMetalTotal) {
|
function formatMetalTotal(metal: WalletMetalTotal) {
|
||||||
return `${resolveSummaryMetalWeight(metal, inventoryCards.value)}${metal.unit}`
|
return `${usePriceWithoutType(resolveSummaryMetalWeight(metal, inventoryCards.value), false)}${metal.unit}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInfo(metric: MetricItem) {
|
function handleInfo(metric: MetricItem) {
|
||||||
@@ -102,7 +104,7 @@ function handleInfo(metric: MetricItem) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (metric.metal) {
|
if (metric.metal) {
|
||||||
emit('openGoldModal', metric.metal)
|
emit('openGoldModal', metric.metal, metric.modalText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<Vue3Odometer
|
<Vue3Odometer
|
||||||
:value="displayGoldVal"
|
:value="displayGoldVal"
|
||||||
class="dhome-hero-price__odometer o-text"
|
class="dhome-hero-price__odometer o-text"
|
||||||
style="font-family: var(--App-font-family)"
|
style="font-family: iran-sans"
|
||||||
/>
|
/>
|
||||||
<span class="dhome-hero-price__unit">ریال</span>
|
<span class="dhome-hero-price__unit">ریال</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, toValue } from 'vue'
|
|
||||||
import { useHomePageContext } from '../../composables/useHomePage'
|
|
||||||
import { price } from '@/utils/price'
|
|
||||||
import CurrentPrice from '../../components/CurrentPrice.vue'
|
import CurrentPrice from '../../components/CurrentPrice.vue'
|
||||||
import PriceStats from '../../components/PriceStats.vue'
|
import PriceStats from '../../components/PriceStats.vue'
|
||||||
import DesktopHomeCard from './DesktopHomeCard.vue'
|
import DesktopHomeCard from './DesktopHomeCard.vue'
|
||||||
|
|
||||||
const ctx = useHomePageContext()
|
|
||||||
const statsLayout = computed(() => (toValue(ctx.isBoursecuHomeLayout) ? 'boursecu' : 'default'))
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -17,7 +11,7 @@ const statsLayout = computed(() => (toValue(ctx.isBoursecuHomeLayout) ? 'boursec
|
|||||||
</DesktopHomeCard>
|
</DesktopHomeCard>
|
||||||
|
|
||||||
<DesktopHomeCard title="بازه قیمت امروز">
|
<DesktopHomeCard title="بازه قیمت امروز">
|
||||||
<PriceStats variant="desktop" :price="price" :layout="statsLayout" />
|
<PriceStats variant="desktop" />
|
||||||
</DesktopHomeCard>
|
</DesktopHomeCard>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -52,5 +52,5 @@ function resolveSectionComponent(sectionId: HomeSectionId) {
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<HomeSkeleton v-if="loading" class="w-full" />
|
<HomeSkeleton v-else class="w-full" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,21 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, inject } from 'vue'
|
|
||||||
import PriceStats from '../../components/PriceStats.vue'
|
import PriceStats from '../../components/PriceStats.vue'
|
||||||
import { price } from '@/utils/price'
|
|
||||||
import { HOME_PAGE_CONTEXT_KEY } from '../../composables/useHomePage'
|
|
||||||
import { usePriceChart } from '../../composables/usePriceChart'
|
|
||||||
|
|
||||||
const ctx = inject(HOME_PAGE_CONTEXT_KEY)!
|
|
||||||
const { maftolChartRatio } = usePriceChart()
|
|
||||||
const layout = computed(() => (ctx.isBoursecuHomeLayout.value ? 'boursecu' : 'default'))
|
|
||||||
const selectedMetal = computed(() => ctx.selectedChartMetal.value)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PriceStats
|
<PriceStats />
|
||||||
:price="price"
|
|
||||||
:layout="layout"
|
|
||||||
:selected-metal="selectedMetal"
|
|
||||||
:maftol-ratio="maftolChartRatio"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ import pkg from '../../../../../package.json'
|
|||||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
import { useConfigStore } from '@/store/configStore'
|
import { useConfigStore } from '@/store/configStore'
|
||||||
import {
|
import {
|
||||||
getProjectLoginLogo,
|
|
||||||
getProjectLoginTaglineLines,
|
getProjectLoginTaglineLines,
|
||||||
|
getProjectSplashLogo,
|
||||||
hasCustomLoginLogoLayout,
|
hasCustomLoginLogoLayout,
|
||||||
isBoursecuProject,
|
isBoursecuProject,
|
||||||
} from '@/utils/config'
|
} from '@/utils/config'
|
||||||
@@ -48,7 +48,7 @@ const isBoursecu = isBoursecuProject()
|
|||||||
const hasCustomLayout = hasCustomLoginLogoLayout()
|
const hasCustomLayout = hasCustomLoginLogoLayout()
|
||||||
const version = pkg.version
|
const version = pkg.version
|
||||||
|
|
||||||
const logo = computed(() => getProjectLoginLogo(config.config?.avatar))
|
const logo = computed(() => getProjectSplashLogo(config.config?.avatar))
|
||||||
|
|
||||||
const taglineLines = computed(() => getProjectLoginTaglineLines())
|
const taglineLines = computed(() => getProjectLoginTaglineLines())
|
||||||
const tagline = computed(() => {
|
const tagline = computed(() => {
|
||||||
|
|||||||
@@ -27,9 +27,9 @@
|
|||||||
|
|
||||||
&--boursecu {
|
&--boursecu {
|
||||||
.dauth-brand__logo {
|
.dauth-brand__logo {
|
||||||
max-width: 300px;
|
max-width: 280px;
|
||||||
max-height: 110px;
|
max-height: 280px;
|
||||||
width: 100%;
|
width: min(100%, 280px);
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user