Split desktop and mobile layouts with shared page data so APIs fetch once per context.
Add layout shells, desktop views, module-level page caches, and UI fixes for auth, home, wallet, transactions, address, and related pages. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Vendored
+2
@@ -62,11 +62,13 @@ declare module 'vue' {
|
|||||||
Size: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/size.vue')['default']
|
Size: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/size.vue')['default']
|
||||||
StartEnd: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/start-end.vue')['default']
|
StartEnd: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/start-end.vue')['default']
|
||||||
Suffix: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/suffix.vue')['default']
|
Suffix: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/suffix.vue')['default']
|
||||||
|
SupportContactBar: typeof import('./src/components/SupportContactBar.vue')['default']
|
||||||
Switchable: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/switchable.vue')['default']
|
Switchable: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/switchable.vue')['default']
|
||||||
Text: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/text.vue')['default']
|
Text: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/text.vue')['default']
|
||||||
Time: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/time.vue')['default']
|
Time: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/time.vue')['default']
|
||||||
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']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+33
@@ -11,6 +11,39 @@
|
|||||||
<meta name="enamad" content="44889195">
|
<meta name="enamad" content="44889195">
|
||||||
<link rel="manifest" href="https://nerkh.gold/get_user/monifist.json"/>
|
<link rel="manifest" href="https://nerkh.gold/get_user/monifist.json"/>
|
||||||
<title></title>
|
<title></title>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
if (window.matchMedia('(min-width: 768px)').matches) {
|
||||||
|
document.documentElement.classList.add('app-layout--desktop')
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
/* Critical: full-viewport splash before Vue/CSS hydrate */
|
||||||
|
html, body, #app {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
html, body, #app {
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.app-splash-screen {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
width: 100vw;
|
||||||
|
min-height: 100vh;
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
z-index: 2147483646;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
+68
-54
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="desktopConfig w-full right-0 left-0 absolute">
|
<div class="desktopConfig w-full right-0 left-0">
|
||||||
<div v-if="popVisible" class="top-0 bottom-0 wrap-fix bg-primary z-100 opacity-60">
|
<div v-if="popVisible" class="top-0 bottom-0 wrap-fix bg-primary z-100 opacity-60">
|
||||||
<div class="absolute right-0 left-0 bottom-10vh flex flex-col items-center">
|
<div class="absolute right-0 left-0 bottom-10vh flex flex-col items-center">
|
||||||
<div>
|
<div>
|
||||||
@@ -10,60 +10,47 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AConfigProvider :theme="antTheme ?? undefined" direction="rtl">
|
<AConfigProvider :theme="antTheme ?? undefined" direction="rtl">
|
||||||
<transition mode="out-in" name="fade">
|
<div v-if="showSplash" class="app-splash-screen flex justify-center" :style="{ backgroundColor: getProjectSplashBackground() }">
|
||||||
|
<template v-if="!isBoursecuProject()">
|
||||||
<div v-if="!showSplash">
|
<div class="app-splash-screen__gradient app-splash-screen__gradient--top h-1/3 flex">
|
||||||
<router-view v-slot="{ Component, route }">
|
<img :src="img_topGradiant" alt="" class="absolute z-10 w-full h-full object-cover"/>
|
||||||
<transition :name="(route.meta.transition as string | undefined) || 'fade'">
|
<img :src="img_pattern" alt="" class="absolute w-full h-full object-none"/>
|
||||||
|
</div>
|
||||||
<div>
|
<div class="app-splash-screen__gradient app-splash-screen__gradient--bottom h-1/3">
|
||||||
<component :is="Component"/>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</router-view>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="flex bottom-0 wrap-fix top-0 z-1000000000000 justify-center"
|
|
||||||
:style="{ backgroundColor: getProjectSplashBackground() }"
|
|
||||||
>
|
|
||||||
<template v-if="!isBoursecuProject()">
|
|
||||||
<div class="top-0 wrap-fix h-1/3 w-screen flex">
|
|
||||||
<img :src="img_topGradiant" alt="" class="absolute z-10 w-screen h-full object-cover"/>
|
|
||||||
<img :src="img_pattern" alt="" class="absolute w-screen h-full object-none"/>
|
|
||||||
</div>
|
|
||||||
<div class="wrap-fix bottom-0 h-1/3 w-screen">
|
|
||||||
<img
|
|
||||||
:src="img_bottomGradiant"
|
|
||||||
alt=""
|
|
||||||
class="absolute z-10 w-screen h-full object-cover"
|
|
||||||
/>
|
|
||||||
<img :src="img_pattern" alt="" class="absolute w-screen h-full object-none"/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="self-center splash_appear_fade_in">
|
|
||||||
<img
|
<img
|
||||||
:src="getProjectLoginLogo(config?.config?.avatar)"
|
:src="img_bottomGradiant"
|
||||||
:class="hasCustomLoginLogoLayout()
|
alt=""
|
||||||
? 'w-[80vw] max-w-[320px] h-auto object-contain'
|
class="absolute z-10 w-full h-full object-cover"
|
||||||
: 'h-[20vh] object-contain'"
|
/>
|
||||||
>
|
<img :src="img_pattern" alt="" class="absolute w-full h-full object-none"/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="fixed w-full bottom-5 home_appear_fade_in_up">
|
</template>
|
||||||
<div
|
<div class="self-center splash_appear_fade_in">
|
||||||
class="text-center text-xs"
|
<img
|
||||||
:class="isBoursecuProject() ? 'text-white' : ''"
|
:src="getProjectLoginLogo(config?.config?.avatar)"
|
||||||
>
|
:class="hasCustomLoginLogoLayout()
|
||||||
نسخه: {{ pkg.version }}
|
? 'w-[80vw] max-w-[320px] h-auto object-contain'
|
||||||
</div>
|
: 'h-[20vh] object-contain'"
|
||||||
</div>
|
>
|
||||||
<div class="absolute bottom-10">
|
</div>
|
||||||
<a-spin :indicator="splashIndicator" size="small"/>
|
<div class="fixed w-full bottom-5 home_appear_fade_in_up">
|
||||||
|
<div
|
||||||
|
class="text-center text-xs"
|
||||||
|
:class="isBoursecuProject() ? 'text-white' : ''"
|
||||||
|
>
|
||||||
|
نسخه: {{ pkg.version }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
<div class="absolute bottom-10">
|
||||||
|
<a-spin :indicator="splashIndicator" size="small"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="app-main">
|
||||||
|
<router-view v-slot="{ Component, route }">
|
||||||
|
<component :is="Component" :key="route.fullPath" />
|
||||||
|
</router-view>
|
||||||
|
</div>
|
||||||
</AConfigProvider>
|
</AConfigProvider>
|
||||||
<van-dialog
|
<van-dialog
|
||||||
v-model:show="versionDialog"
|
v-model:show="versionDialog"
|
||||||
@@ -115,7 +102,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import pkg from '../package.json'
|
import pkg from '../package.json'
|
||||||
import {useRegisterSW} from 'virtual:pwa-register/vue'
|
import {useRegisterSW} from 'virtual:pwa-register/vue'
|
||||||
import {computed, h, onMounted, ref} from 'vue'
|
import {computed, h, onMounted, ref, watch} from 'vue'
|
||||||
import img_topGradiant from '@/views/login/assets/img/gr_top_white.svg'
|
import img_topGradiant from '@/views/login/assets/img/gr_top_white.svg'
|
||||||
import img_pattern from '@/views/login/assets/img/pattern.svg'
|
import img_pattern from '@/views/login/assets/img/pattern.svg'
|
||||||
import {LoadingOutlined} from '@ant-design/icons-vue'
|
import {LoadingOutlined} from '@ant-design/icons-vue'
|
||||||
@@ -123,9 +110,12 @@ 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} from "@/utils/config";
|
import {getProjectFontFamily, getProjectLoginLogo, getProjectSplashBackground, 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'
|
||||||
|
|
||||||
|
useApplyDesktopLayoutClass()
|
||||||
|
|
||||||
const {offlineReady, needRefresh, updateServiceWorker} = useRegisterSW()
|
const {offlineReady, needRefresh, updateServiceWorker} = useRegisterSW()
|
||||||
const config = useConfigStore()
|
const config = useConfigStore()
|
||||||
@@ -171,6 +161,12 @@ onMounted(async () => {
|
|||||||
}, 2000)
|
}, 2000)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(showSplash, (visible) => {
|
||||||
|
if (!visible) {
|
||||||
|
resetAppSurfaceBackground()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const versionDialog = ref<boolean>(false)
|
const versionDialog = ref<boolean>(false)
|
||||||
|
|
||||||
function checkVersion() {
|
function checkVersion() {
|
||||||
@@ -200,6 +196,7 @@ function update() {
|
|||||||
--app-credit-footer-offset: calc(var(--app-credit-bar-inner) + var(--app-safe-bottom));
|
--app-credit-footer-offset: calc(var(--app-credit-bar-inner) + var(--app-safe-bottom));
|
||||||
--app-nav-height: 80px;
|
--app-nav-height: 80px;
|
||||||
--app-bottom-chrome-height: calc(var(--app-nav-height) + var(--app-credit-footer-offset));
|
--app-bottom-chrome-height: calc(var(--app-nav-height) + var(--app-credit-footer-offset));
|
||||||
|
--app-desktop-sidebar-width: 260px;
|
||||||
--app-add-card-height: 88px;
|
--app-add-card-height: 88px;
|
||||||
/* بخشی از کارت کیفپول زیر نوار «افزودن کارت جدید» دیده میشود */
|
/* بخشی از کارت کیفپول زیر نوار «افزودن کارت جدید» دیده میشود */
|
||||||
--wallet-card-dock-overlap: 52px;
|
--wallet-card-dock-overlap: 52px;
|
||||||
@@ -210,6 +207,16 @@ function update() {
|
|||||||
--app-z-dialog: 5300;
|
--app-z-dialog: 5300;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-main {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktopConfig {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
.app-credit-footer {
|
.app-credit-footer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@@ -249,10 +256,17 @@ function update() {
|
|||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 426px) {
|
@media screen and (min-width: 426px) and (max-width: 767.98px) {
|
||||||
.desktopConfig {
|
.desktopConfig {
|
||||||
width: 426px;
|
width: 426px;
|
||||||
position: relative !important;
|
position: relative !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html.app-layout--desktop {
|
||||||
|
#support-button {
|
||||||
|
bottom: 12px;
|
||||||
|
right: calc(var(--app-desktop-sidebar-width, 260px) + 20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
html.project-boursecu {
|
html.project-boursecu {
|
||||||
|
.ant-input-group-addon {
|
||||||
|
background-color: @secondary-color !important;
|
||||||
|
color: @App-primary-color !important;
|
||||||
|
}
|
||||||
|
|
||||||
.text-red,
|
.text-red,
|
||||||
.text-red-600,
|
.text-red-600,
|
||||||
.text-red-700,
|
.text-red-700,
|
||||||
|
|||||||
@@ -2,9 +2,29 @@
|
|||||||
@import (less) './fonts.less';
|
@import (less) './fonts.less';
|
||||||
@import (less) './ant.less';
|
@import (less) './ant.less';
|
||||||
@import (less) './boursecu-theme.less';
|
@import (less) './boursecu-theme.less';
|
||||||
|
@import (less) '../../layouts/styles/desktop-shell.less';
|
||||||
|
@import (less) '../../layouts/styles/mobile-frame.less';
|
||||||
|
@import (less) '../../views/login/desktop/styles/desktop-auth.less';
|
||||||
|
@import (less) '../../views/base/user/desktop/styles/desktop-user.less';
|
||||||
|
@import (less) '../../views/base/products/desktop/styles/desktop-products.less';
|
||||||
|
@import (less) '../../views/base/transactions/desktop/styles/desktop-transactions.less';
|
||||||
|
@import (less) '../../views/base/wallet/desktop/styles/desktop-wallet.less';
|
||||||
|
@import (less) '../../views/base/priceTrigger/desktop/styles/desktop-price-trigger.less';
|
||||||
|
@import (less) '../../views/base/about/desktop/styles/desktop-about.less';
|
||||||
|
@import (less) '../../views/base/rules/desktop/styles/desktop-rules.less';
|
||||||
|
@import (less) '../../views/base/rules/permissions/desktop/styles/desktop-permissions.less';
|
||||||
|
@import (less) '../../views/base/giftCard/desktop/styles/desktop-gift-card.less';
|
||||||
|
@import (less) '../../views/base/address/desktop/styles/desktop-address.less';
|
||||||
|
@import (less) '../../views/base/educate/desktop/styles/desktop-educate.less';
|
||||||
|
@import (less) '../../views/base/aboutUs/desktop/styles/desktop-about-us.less';
|
||||||
|
@import (less) '../../views/base/earnMoney/desktop/styles/desktop-earn-money.less';
|
||||||
|
@import (less) '../../views/base/physicalReceive/desktop/styles/desktop-physical-receive.less';
|
||||||
|
@import (less) '../../views/base/fund/desktop/styles/desktop-fund.less';
|
||||||
|
@import (less) '../../views/base/gold/desktop/styles/desktop-gold.less';
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
|
background-color: @prefixCls-background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -34,7 +54,7 @@ body {
|
|||||||
@apply flex justify-center relative;
|
@apply flex justify-center relative;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@media screen and (min-width: 426px) {
|
@media screen and (min-width: 426px) and (max-width: 767.98px) {
|
||||||
.van-popup--bottom.van-popup--round {
|
.van-popup--bottom.van-popup--round {
|
||||||
width: 426px;
|
width: 426px;
|
||||||
left: calc(50vw - 213px);
|
left: calc(50vw - 213px);
|
||||||
@@ -44,10 +64,31 @@ body {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
right: 0;
|
right: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@media screen and (min-width: 426px) {
|
width: 100%;
|
||||||
|
max-width: 100vw;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
@media screen and (min-width: 426px) and (max-width: 767.98px) {
|
||||||
width: 426px;
|
width: 426px;
|
||||||
left: auto;
|
max-width: 426px;
|
||||||
|
left: 50%;
|
||||||
right: auto;
|
right: auto;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html.app-layout--desktop {
|
||||||
|
@media screen and (min-width: 426px) {
|
||||||
|
.van-popup--bottom.van-popup--round {
|
||||||
|
width: min(480px, calc(100vw - var(--app-desktop-sidebar-width, 260px) - 48px));
|
||||||
|
left: calc(
|
||||||
|
(100vw - var(--app-desktop-sidebar-width, 260px) - min(480px, calc(100vw - var(--app-desktop-sidebar-width, 260px) - 48px))) / 2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#goftino_w {
|
||||||
|
right: calc(var(--app-desktop-sidebar-width, 260px) + 20px) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,16 +33,7 @@ $total-items: 6
|
|||||||
animation: 1s ease slideInFromRight
|
animation: 1s ease slideInFromRight
|
||||||
animation-fill-mode: both
|
animation-fill-mode: both
|
||||||
|
|
||||||
//@if($counter < 13)
|
// Duplicate animation block removed — was leaking styles without a selector
|
||||||
// .animate > div
|
|
||||||
animation: 1s ease 0s 1 slideInFromRight
|
|
||||||
animation-fill-mode: both
|
|
||||||
//transform: translateX(100%)
|
|
||||||
//@for $i from 1 through $total-items
|
|
||||||
// .animate:nth-child(#{$i})
|
|
||||||
// animation-delay: .25s * $i
|
|
||||||
|
|
||||||
//transform: translateX(100%)
|
|
||||||
|
|
||||||
@keyframes slideInFromRight
|
@keyframes slideInFromRight
|
||||||
0%
|
0%
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="variant === 'desktop' ? 'dpage-support' : 'wrap-fix bottom-0 px-10 pb-3rem flex gap-2 bg-white'">
|
||||||
|
<a class="w-full" :href="supportHref">
|
||||||
|
<BasicButton class="w-full" type="primary">تماس با پشتیبانی</BasicButton>
|
||||||
|
</a>
|
||||||
|
<a :href="baleHref" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img src="@/assets/img/bale.png" alt="بله" :class="variant === 'desktop' ? 'dpage-support__bale' : 'h-10 w-10'" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||||
|
import { useConfigStore } from '@/store/configStore'
|
||||||
|
import { getProjectBaleLink, getProjectSupportPhoneLink } from '@/utils/config'
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
variant?: 'mobile' | 'desktop'
|
||||||
|
}>(),
|
||||||
|
{ variant: 'mobile' },
|
||||||
|
)
|
||||||
|
|
||||||
|
const config = useConfigStore()
|
||||||
|
|
||||||
|
const supportHref = computed(() =>
|
||||||
|
config.config.name === 'garoosi'
|
||||||
|
? 'tel:09031265925'
|
||||||
|
: getProjectSupportPhoneLink(),
|
||||||
|
)
|
||||||
|
|
||||||
|
const baleHref = computed(() =>
|
||||||
|
config.config.name === 'garoosi'
|
||||||
|
? 'https://ble.ir/garoosigallery'
|
||||||
|
: getProjectBaleLink(),
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* Deduplicate async work across remounts (e.g. accidental v-if layout switches).
|
||||||
|
* Prefer fixing layout with v-show + parent-level fetch; use this in stores/shared loaders.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const cache = new Map<string, unknown>()
|
||||||
|
const inflight = new Map<string, Promise<unknown>>()
|
||||||
|
|
||||||
|
export async function runOnceAsync<T>(key: string, fn: () => Promise<T>, force = false): Promise<T> {
|
||||||
|
if (!force && cache.has(key)) {
|
||||||
|
return cache.get(key) as T
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!force && inflight.has(key)) {
|
||||||
|
return inflight.get(key) as Promise<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = fn()
|
||||||
|
.then((result) => {
|
||||||
|
cache.set(key, result)
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (inflight.get(key) === promise) {
|
||||||
|
inflight.delete(key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
inflight.set(key, promise)
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetOnceAsync(key: string) {
|
||||||
|
cache.delete(key)
|
||||||
|
inflight.delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetOnceAsyncByPrefix(prefix: string) {
|
||||||
|
for (const key of [...cache.keys(), ...inflight.keys()]) {
|
||||||
|
if (key.startsWith(prefix)) {
|
||||||
|
resetOnceAsync(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { getProducts } from '@/api/products'
|
||||||
|
import type { Product } from '@/models/products'
|
||||||
|
|
||||||
|
/** Shared product catalog — dedupes getProducts() across wallet assets and chart offset. */
|
||||||
|
let catalogCache: Product[] | null = null
|
||||||
|
let catalogPromise: Promise<Product[]> | null = null
|
||||||
|
|
||||||
|
export function getProductsCatalog(force = false): Promise<Product[]> {
|
||||||
|
if (!force && catalogCache) {
|
||||||
|
return Promise.resolve(catalogCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!force && catalogPromise) {
|
||||||
|
return catalogPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
catalogPromise = getProducts()
|
||||||
|
.then((products) => {
|
||||||
|
catalogCache = products ?? []
|
||||||
|
return catalogCache
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
catalogPromise = null
|
||||||
|
})
|
||||||
|
|
||||||
|
return catalogPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearProductsCatalog() {
|
||||||
|
catalogCache = null
|
||||||
|
catalogPromise = null
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { getWalletAssets } from '@/api/wallet'
|
import { getWalletAssets } from '@/api/wallet'
|
||||||
import { getProducts } from '@/api/products'
|
import { getProductsCatalog } from '@/composables/useProductsCatalog'
|
||||||
import type { WalletAssetsSummary } from '@/models/walletAssets'
|
import type { WalletAssetsSummary } from '@/models/walletAssets'
|
||||||
import { enrichInventoryCardsWithCatalog } from '@/utils/productAssets'
|
import { enrichInventoryCardsWithCatalog } from '@/utils/productAssets'
|
||||||
|
|
||||||
@@ -17,8 +17,8 @@ export function useWalletAssets() {
|
|||||||
return assetsPromise
|
return assetsPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
// سرور مرجع داراییهاست (main_wallet_products هم سمت سرور هندل میشود)؛ کلاینت فقط نمایش میدهد
|
// Reuses cached getProductsCatalog() — same catalog as chart offset logic
|
||||||
assetsPromise = Promise.all([getWalletAssets(), getProducts()])
|
assetsPromise = Promise.all([getWalletAssets(), getProductsCatalog(force)])
|
||||||
.then(([data, catalog]) => {
|
.then(([data, catalog]) => {
|
||||||
assetsSummary.value = {
|
assetsSummary.value = {
|
||||||
...data,
|
...data,
|
||||||
|
|||||||
@@ -97,6 +97,16 @@
|
|||||||
window.__APP_CONFIG__ = config
|
window.__APP_CONFIG__ = config
|
||||||
window.__CONFIG_READY__ = true
|
window.__CONFIG_READY__ = true
|
||||||
|
|
||||||
|
// Early splash background — full viewport before Vue mounts
|
||||||
|
const projectName = config?.user?.name
|
||||||
|
if (projectName === 'boursecu') {
|
||||||
|
const splashBg = '#133050'
|
||||||
|
document.documentElement.style.backgroundColor = splashBg
|
||||||
|
document.body.style.backgroundColor = splashBg
|
||||||
|
const metaTheme = document.querySelector('meta[name="theme-color"]')
|
||||||
|
if (metaTheme) metaTheme.setAttribute('content', splashBg)
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve the promise
|
// Resolve the promise
|
||||||
if (configResolve) {
|
if (configResolve) {
|
||||||
configResolve(config)
|
configResolve(config)
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<DesktopAppLayout v-if="isDesktopLayout" />
|
||||||
|
<MobileAppLayout v-else />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useDesktopBreakpoint } from './composables/useAppLayout'
|
||||||
|
import DesktopAppLayout from './desktop/DesktopAppLayout.vue'
|
||||||
|
import MobileAppLayout from './mobile/MobileAppLayout.vue'
|
||||||
|
|
||||||
|
const isDesktopLayout = useDesktopBreakpoint()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<div class="responsive-view-pair">
|
||||||
|
<div
|
||||||
|
v-show="!isDesktop"
|
||||||
|
class="responsive-view-pair__pane"
|
||||||
|
:class="{ 'responsive-pane--hidden': isDesktop }"
|
||||||
|
>
|
||||||
|
<slot name="mobile" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-show="isDesktop"
|
||||||
|
class="responsive-view-pair__pane"
|
||||||
|
:class="{ 'responsive-pane--hidden': !isDesktop }"
|
||||||
|
>
|
||||||
|
<slot name="desktop" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useDesktopBreakpoint } from '../composables/useResponsiveLayout'
|
||||||
|
|
||||||
|
const isDesktop = useDesktopBreakpoint()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.responsive-view-pair {
|
||||||
|
min-height: 100%;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.responsive-view-pair__pane {
|
||||||
|
min-height: 100%;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.responsive-pane--hidden {
|
||||||
|
/* v-show already sets display:none; class reserved for layout overrides */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { useMediaQuery } from '@vueuse/core'
|
||||||
|
import { onUnmounted, watch } from 'vue'
|
||||||
|
import { DESKTOP_LAYOUT_MIN_WIDTH } from '../constants'
|
||||||
|
|
||||||
|
const DESKTOP_LAYOUT_CLASS = 'app-layout--desktop'
|
||||||
|
|
||||||
|
export function useDesktopBreakpoint() {
|
||||||
|
return useMediaQuery(`(min-width: ${DESKTOP_LAYOUT_MIN_WIDTH}px)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Apply html class for global desktop shell styles. Call once from App.vue. */
|
||||||
|
export function useApplyDesktopLayoutClass() {
|
||||||
|
const isDesktopLayout = useDesktopBreakpoint()
|
||||||
|
|
||||||
|
const stop = watch(
|
||||||
|
isDesktopLayout,
|
||||||
|
(desktop) => {
|
||||||
|
document.documentElement.classList.toggle(DESKTOP_LAYOUT_CLASS, desktop)
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stop()
|
||||||
|
document.documentElement.classList.remove(DESKTOP_LAYOUT_CLASS)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated use useDesktopBreakpoint + useApplyDesktopLayoutClass */
|
||||||
|
export function useAppLayout() {
|
||||||
|
const isDesktopLayout = useDesktopBreakpoint()
|
||||||
|
useApplyDesktopLayoutClass()
|
||||||
|
return { isDesktopLayout }
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
export function useAppNavigation() {
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const popVisible = ref(false)
|
||||||
|
const currentRoute = computed(() => route.name)
|
||||||
|
const hideMenu = computed(() => !!route.meta.hideMenu)
|
||||||
|
|
||||||
|
function isNavActive(routeName: string) {
|
||||||
|
return currentRoute.value === routeName
|
||||||
|
}
|
||||||
|
|
||||||
|
function navItemClass(routeName: string) {
|
||||||
|
return isNavActive(routeName) ? 'text-primary' : 'text-gray-400'
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateTo(routeName: string) {
|
||||||
|
popVisible.value = false
|
||||||
|
router.push({ name: routeName })
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleOverflowMenu() {
|
||||||
|
popVisible.value = !popVisible.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeOverflowMenu() {
|
||||||
|
popVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
popVisible,
|
||||||
|
currentRoute,
|
||||||
|
hideMenu,
|
||||||
|
isNavActive,
|
||||||
|
navItemClass,
|
||||||
|
navigateTo,
|
||||||
|
toggleOverflowMenu,
|
||||||
|
closeOverflowMenu,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { useMediaQuery } from '@vueuse/core'
|
||||||
|
import { DESKTOP_LAYOUT_MIN_WIDTH } from '../constants'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reactive desktop breakpoint. Use only for UI switching — never tie API calls to this ref.
|
||||||
|
*/
|
||||||
|
export function useDesktopBreakpoint() {
|
||||||
|
return useMediaQuery(`(min-width: ${DESKTOP_LAYOUT_MIN_WIDTH}px)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guidelines for responsive pages (Vue):
|
||||||
|
*
|
||||||
|
* 1. Call page composables (with onMounted fetch) exactly once in the route `index.vue` shell.
|
||||||
|
* 2. Switch Mobile/Desktop views with `v-show`, not `v-if`, so resize does not destroy children.
|
||||||
|
* 3. Keep `<router-view>` outside responsive chrome wrappers so child routes are not remounted.
|
||||||
|
* 4. Mobile/Desktop views consume shared state via provide/inject — they must not fetch independently.
|
||||||
|
* 5. Prefer store/composable request dedup (in-flight promise + cached result) for shared data.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const RESPONSIVE_HIDDEN_CLASS = 'responsive-pane--hidden'
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { isBoursecuProject } from '@/utils/config'
|
||||||
|
import type { AppNavItem } from '../types'
|
||||||
|
|
||||||
|
const BOURSECU_PRIMARY_NAV: AppNavItem[] = [
|
||||||
|
{ id: 'buyProduct', routeName: 'buyProduct', label: 'سفارشها', icon: 'vuesax-linear:box' },
|
||||||
|
{ id: 'transactions', routeName: 'transactions', label: 'تراکنشها', icon: 'vuesax-linear:receipt-item' },
|
||||||
|
{ id: 'home', routeName: 'home', label: 'صفحه اصلی', icon: 'vuesax-linear:home' },
|
||||||
|
{ id: 'wallet', routeName: 'wallet', label: 'کیف پول', icon: 'vuesax-linear:wallet-1' },
|
||||||
|
{ id: 'user', routeName: 'user', label: 'تنظیمات', icon: 'vuesax-linear:setting-2' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const DEFAULT_PRIMARY_NAV: AppNavItem[] = [
|
||||||
|
{ id: 'home', routeName: 'home', label: 'صفحه اصلی', icon: 'vuesax-linear:home' },
|
||||||
|
{ id: 'transactions', routeName: 'transactions', label: 'گزارش', icon: 'vuesax-linear:receipt-item' },
|
||||||
|
{ id: 'buyProduct', routeName: 'buyProduct', label: 'محصولات', icon: 'vuesax-linear:coin' },
|
||||||
|
{ id: 'wallet', routeName: 'wallet', label: 'کیف پول', icon: 'vuesax-linear:wallet-1' },
|
||||||
|
{ id: 'user', routeName: 'user', label: 'تنظیمات', icon: 'vuesax-linear:setting-2' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const SECONDARY_NAV: AppNavItem[] = [
|
||||||
|
{ id: 'notifyTrigger', routeName: 'notifyTrigger', label: 'هشدار قیمت', icon: 'vuesax-linear:bill' },
|
||||||
|
{ id: 'about', routeName: 'about', label: 'درباره ما', icon: 'vuesax-linear:info-circle' },
|
||||||
|
{ id: 'rules', routeName: 'rules', label: 'قوانین و مقررات', icon: 'vuesax-linear:task' },
|
||||||
|
{ id: 'giftCard', routeName: 'giftCard', label: 'ثبت کارت هدیه', icon: 'vuesax-linear:gift' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function getPrimaryNavItems(): AppNavItem[] {
|
||||||
|
return isBoursecuProject() ? BOURSECU_PRIMARY_NAV : DEFAULT_PRIMARY_NAV
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/** Minimum viewport width for desktop app shell (sidebar layout). */
|
||||||
|
export const DESKTOP_LAYOUT_MIN_WIDTH = 768
|
||||||
|
|
||||||
|
export const DESKTOP_SIDEBAR_WIDTH = 260
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div class="desktop-app-layout">
|
||||||
|
<DesktopSidebar v-if="!hideMenu" />
|
||||||
|
<main
|
||||||
|
class="desktop-app-layout__main"
|
||||||
|
:class="{ 'desktop-app-layout__main--full': hideMenu }"
|
||||||
|
>
|
||||||
|
<router-view v-slot="{ Component, route }">
|
||||||
|
<component
|
||||||
|
:is="Component"
|
||||||
|
:key="route.fullPath"
|
||||||
|
class="desktop-app-layout__content"
|
||||||
|
/>
|
||||||
|
</router-view>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onUnmounted, watch } from 'vue'
|
||||||
|
import { useAppNavigation } from '../composables/useAppNavigation'
|
||||||
|
import DesktopSidebar from './DesktopSidebar.vue'
|
||||||
|
|
||||||
|
const NO_SIDEBAR_CLASS = 'app-layout--desktop-no-sidebar'
|
||||||
|
const { hideMenu } = useAppNavigation()
|
||||||
|
|
||||||
|
watch(
|
||||||
|
hideMenu,
|
||||||
|
(hidden) => {
|
||||||
|
document.documentElement.classList.toggle(NO_SIDEBAR_CLASS, hidden)
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.documentElement.classList.remove(NO_SIDEBAR_CLASS)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" src="./desktop-layout.less"></style>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="desktop-nav-item"
|
||||||
|
:class="{ 'desktop-nav-item--active': active }"
|
||||||
|
@click="$emit('click')"
|
||||||
|
>
|
||||||
|
<BasicIcon :icon="icon" size="20" class="desktop-nav-item__icon" />
|
||||||
|
<span class="desktop-nav-item__label">{{ label }}</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
icon: string
|
||||||
|
label: string
|
||||||
|
active: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
click: []
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<aside
|
||||||
|
class="desktop-sidebar"
|
||||||
|
:class="{ 'desktop-sidebar--boursecu': isBoursecu }"
|
||||||
|
>
|
||||||
|
<div class="desktop-sidebar__brand">
|
||||||
|
<img
|
||||||
|
:src="navLogo"
|
||||||
|
alt=""
|
||||||
|
class="desktop-sidebar__logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="desktop-sidebar__section" aria-label="منوی اصلی">
|
||||||
|
<DesktopNavItem
|
||||||
|
v-for="item in primaryItems"
|
||||||
|
:key="item.id"
|
||||||
|
:icon="item.icon"
|
||||||
|
:label="item.label"
|
||||||
|
:active="isNavActive(item.routeName)"
|
||||||
|
@click="navigateTo(item.routeName)"
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="desktop-sidebar__divider" />
|
||||||
|
|
||||||
|
<nav class="desktop-sidebar__section desktop-sidebar__section--secondary" aria-label="منوی فرعی">
|
||||||
|
<DesktopNavItem
|
||||||
|
v-for="item in secondaryItems"
|
||||||
|
:key="item.id"
|
||||||
|
:icon="item.icon"
|
||||||
|
:label="item.label"
|
||||||
|
:active="isNavActive(item.routeName)"
|
||||||
|
@click="navigateTo(item.routeName)"
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useConfigStore } from '@/store/configStore'
|
||||||
|
import { getProjectNavLogo, isBoursecuProject } from '@/utils/config'
|
||||||
|
import { getPrimaryNavItems, SECONDARY_NAV } from '../config/navigation'
|
||||||
|
import { useAppNavigation } from '../composables/useAppNavigation'
|
||||||
|
import DesktopNavItem from './DesktopNavItem.vue'
|
||||||
|
|
||||||
|
const config = useConfigStore()
|
||||||
|
const isBoursecu = isBoursecuProject()
|
||||||
|
const navLogo = computed(() => getProjectNavLogo(config.config?.avatar))
|
||||||
|
const primaryItems = getPrimaryNavItems()
|
||||||
|
const secondaryItems = SECONDARY_NAV
|
||||||
|
|
||||||
|
const { isNavActive, navigateTo } = useAppNavigation()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
.desktop-app-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100vw;
|
||||||
|
background: @prefixCls-background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-app-layout__main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-app-layout__main--full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-app-layout__content {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
background: @prefixCls-background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-sidebar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: var(--app-desktop-sidebar-width, 260px);
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #fff;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 20px 12px 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-sidebar--boursecu {
|
||||||
|
background: var(--App-brand-nav, #133050);
|
||||||
|
|
||||||
|
.desktop-sidebar__divider {
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-nav-item {
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
background: rgba(255, 255, 255, 0.14);
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
.desktop-nav-item__icon {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-nav-item__icon {
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-sidebar__logo {
|
||||||
|
max-width: 200px;
|
||||||
|
max-height: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-sidebar__brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 8px 12px 20px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-sidebar__logo {
|
||||||
|
max-width: 140px;
|
||||||
|
max-height: 48px;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-sidebar__section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-sidebar__section--secondary {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-sidebar__divider {
|
||||||
|
height: 1px;
|
||||||
|
background: rgba(0, 0, 0, 0.08);
|
||||||
|
margin: 12px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: right;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6b7280;
|
||||||
|
transition: background 0.15s ease, color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.04);
|
||||||
|
color: @App-primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
color: @App-primary-color;
|
||||||
|
|
||||||
|
.desktop-nav-item__icon {
|
||||||
|
color: @App-primary-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-nav-item__icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-nav-item__label {
|
||||||
|
flex: 1;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-sidebar__section--secondary .desktop-nav-item {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/* Shared desktop page primitives */
|
||||||
|
|
||||||
|
.dpage-topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
|
||||||
|
&__back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__eyebrow {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 6px 0 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dpage-surface {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dpage-support {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
> a:first-child {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__bale {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
object-fit: contain;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<MobileOverflowMenu
|
||||||
|
:visible="popVisible"
|
||||||
|
:overflow-items="overflowItems"
|
||||||
|
@close="closeOverflowMenu"
|
||||||
|
@navigate="navigateTo"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div :class="['main-with-bottom-chrome', { 'main-with-bottom-chrome--no-nav': hideMenu }]">
|
||||||
|
<router-view v-slot="{ Component, route }">
|
||||||
|
<transition :name="route.meta.transition || 'fade'">
|
||||||
|
<component :is="Component" class="main-with-bottom-chrome-inner" />
|
||||||
|
</transition>
|
||||||
|
</router-view>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MobileBottomNav
|
||||||
|
v-if="!hideMenu"
|
||||||
|
:is-boursecu="isBoursecu"
|
||||||
|
:nav-logo="navLogo"
|
||||||
|
:nav-item-class="navItemClass"
|
||||||
|
@navigate="navigateTo"
|
||||||
|
@toggle-overflow="toggleOverflowMenu"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useConfigStore } from '@/store/configStore'
|
||||||
|
import { getProjectNavLogo, isBoursecuProject } from '@/utils/config'
|
||||||
|
import { SECONDARY_NAV } from '../config/navigation'
|
||||||
|
import { useAppNavigation } from '../composables/useAppNavigation'
|
||||||
|
import MobileBottomNav from './MobileBottomNav.vue'
|
||||||
|
import MobileOverflowMenu from './MobileOverflowMenu.vue'
|
||||||
|
|
||||||
|
const config = useConfigStore()
|
||||||
|
const isBoursecu = isBoursecuProject()
|
||||||
|
const navLogo = computed(() => getProjectNavLogo(config.config?.avatar))
|
||||||
|
|
||||||
|
const {
|
||||||
|
popVisible,
|
||||||
|
hideMenu,
|
||||||
|
navItemClass,
|
||||||
|
navigateTo,
|
||||||
|
toggleOverflowMenu,
|
||||||
|
closeOverflowMenu,
|
||||||
|
} = useAppNavigation()
|
||||||
|
|
||||||
|
const overflowItems = [
|
||||||
|
{ id: 'user', routeName: 'user', label: 'حساب کاربری', icon: 'vuesax-linear:user' },
|
||||||
|
...SECONDARY_NAV,
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" src="./mobile-layout.less"></style>
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bottom-appbar w-auto mx-auto bg-white">
|
||||||
|
<div class="tabs bottom-0">
|
||||||
|
<div
|
||||||
|
v-if="isBoursecu"
|
||||||
|
:class="navItemClass('buyProduct')"
|
||||||
|
class="tab"
|
||||||
|
@click="$emit('navigate', 'buyProduct')"
|
||||||
|
>
|
||||||
|
<Icon icon="vuesax-linear:box" />
|
||||||
|
<div class="text-xs mt-1 font-bold">سفارش ها</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
:class="navItemClass('home')"
|
||||||
|
class="tab"
|
||||||
|
@click="$emit('navigate', 'home')"
|
||||||
|
>
|
||||||
|
<Icon icon="vuesax-linear:home" />
|
||||||
|
<div class="text-xs mt-1 font-bold">صفحه اصلی</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
:class="navItemClass('transactions')"
|
||||||
|
class="tab"
|
||||||
|
@click="$emit('navigate', 'transactions')"
|
||||||
|
>
|
||||||
|
<Icon icon="vuesax-linear:receipt-item" />
|
||||||
|
<div class="text-xs mt-1 font-bold">{{ isBoursecu ? 'تراکنش ها' : 'گزارش' }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="isBoursecu"
|
||||||
|
:class="navItemClass('home')"
|
||||||
|
class="tab"
|
||||||
|
@click="$emit('navigate', 'home')"
|
||||||
|
>
|
||||||
|
<Icon icon="vuesax-linear:home" />
|
||||||
|
<div class="text-xs mt-1 font-bold">صفحه اصلی</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="main_in" @click="$emit('navigate', 'buyProduct')">
|
||||||
|
<div
|
||||||
|
class="circle_in"
|
||||||
|
:class="isWhiteNavLogo ? 'is-white-nav-logo' : 'text-white'"
|
||||||
|
>
|
||||||
|
<div class="circle_into" @click.stop="$emit('toggle-overflow')">
|
||||||
|
<div
|
||||||
|
v-if="projectName === 'garoosi'"
|
||||||
|
class="p-2 text-tertiary flex flex-col justify-center items-center"
|
||||||
|
>
|
||||||
|
<Icon icon="vuesax-linear:coin" />
|
||||||
|
<div class="text-[9px] font-bold">محصولات</div>
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
class="p-2"
|
||||||
|
:src="navLogo"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
:class="navItemClass('wallet')"
|
||||||
|
class="tab"
|
||||||
|
@click="$emit('navigate', 'wallet')"
|
||||||
|
>
|
||||||
|
<Icon icon="vuesax-linear:wallet-1" />
|
||||||
|
<div class="text-xs mt-1 font-bold">کیف پول</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:class="navItemClass('user')"
|
||||||
|
class="tab"
|
||||||
|
@click="$emit('navigate', 'user')"
|
||||||
|
>
|
||||||
|
<Icon icon="vuesax-linear:setting-2" />
|
||||||
|
<div class="text-xs mt-1 font-bold">تنظیمات</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!isBoursecu" class="main">
|
||||||
|
<div class="circle" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import Icon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
|
import { getConfigApp } from '@/utils/config'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
isBoursecu: boolean
|
||||||
|
navLogo: string
|
||||||
|
navItemClass: (routeName: string) => string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
navigate: [routeName: string]
|
||||||
|
'toggle-overflow': []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const projectName = getConfigApp('name')
|
||||||
|
const whiteNavLogoProjects = ['goldenramin']
|
||||||
|
const isWhiteNavLogo = computed(() => whiteNavLogoProjects.includes(projectName))
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<Transition>
|
||||||
|
<div
|
||||||
|
v-if="visible"
|
||||||
|
class="top-0 bottom-0 bg-primary wrap-fix z-2000 opacity-60"
|
||||||
|
@click="$emit('close')"
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
<Transition name="slide-fade">
|
||||||
|
<div v-if="visible" class="wrap-fix bottom-11vh z-2000">
|
||||||
|
<div class="relative flex flex-col items-center">
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center bg-white rounded-full w-[30px] h-[30px] mb-2"
|
||||||
|
@click="$emit('close')"
|
||||||
|
>
|
||||||
|
<BasicIcon icon="vuesax-linear:close-circle" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center gap-2">
|
||||||
|
<BasicButton
|
||||||
|
v-for="item in overflowItems"
|
||||||
|
:key="item.id"
|
||||||
|
icon-size="16"
|
||||||
|
class="!font-bold w-15rem"
|
||||||
|
@click="$emit('navigate', item.routeName)"
|
||||||
|
>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<BasicIcon :icon="item.icon" size="16" class="absolute right-2" />
|
||||||
|
<div>{{ item.label }}</div>
|
||||||
|
</div>
|
||||||
|
</BasicButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||||
|
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
|
import type { AppNavItem } from '../types'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
overflowItems: AppNavItem[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
close: []
|
||||||
|
navigate: [routeName: string]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
.bottom-appbar {
|
||||||
|
z-index: var(--app-z-nav, 5000);
|
||||||
|
height: var(--app-nav-height, 80px);
|
||||||
|
position: fixed;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
background: #fff;
|
||||||
|
bottom: var(--app-credit-footer-offset, 28px);
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100vw;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
@media screen and (min-width: 426px) {
|
||||||
|
width: 426px;
|
||||||
|
max-width: 426px;
|
||||||
|
left: 50%;
|
||||||
|
right: auto;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background: #fff;
|
||||||
|
justify-content: space-between;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 400;
|
||||||
|
box-shadow: 0 -2px 4px -2px #0000001a;
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
width: 80px;
|
||||||
|
text-align: center;
|
||||||
|
flex-flow: column;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main_in {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
.circle_in {
|
||||||
|
margin-top: -10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
z-index: 20;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
align-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
background-color: @App-primary-color;
|
||||||
|
|
||||||
|
.circle_into {
|
||||||
|
overflow: hidden;
|
||||||
|
@apply bg-primary;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
border: white 1px solid;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle_in.is-white-nav-logo {
|
||||||
|
.circle_into {
|
||||||
|
background: #fff !important;
|
||||||
|
border-color: rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
height: 70px;
|
||||||
|
width: 70px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
border-radius: 50%;
|
||||||
|
height: 70px;
|
||||||
|
width: 70px;
|
||||||
|
z-index: 20;
|
||||||
|
box-shadow: 0 -5px 4px -2px #0000001a;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-enter-active,
|
||||||
|
.v-leave-active {
|
||||||
|
transition: opacity 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-enter-from,
|
||||||
|
.v-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-enter-active {
|
||||||
|
transition: all 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-leave-active {
|
||||||
|
transition: all 0.4s cubic-bezier(1, 0.5, 0.8, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-enter-from,
|
||||||
|
.slide-fade-leave-to {
|
||||||
|
transform: translateX(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-with-bottom-chrome {
|
||||||
|
height: calc(100vh - 72px - var(--app-credit-footer-offset, 28px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-with-bottom-chrome-inner {
|
||||||
|
height: calc(100vh - 72px - var(--app-credit-footer-offset, 28px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-with-bottom-chrome--no-nav,
|
||||||
|
.main-with-bottom-chrome--no-nav .main-with-bottom-chrome-inner {
|
||||||
|
height: calc(100vh - var(--app-credit-footer-offset, 28px));
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
@desktop-layout-min: 768px;
|
||||||
|
@mobile-frame-max: 767.98px;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full-width desktop shell — applies on first paint (no JS class needed).
|
||||||
|
* Sidebar-specific wrap-fix offset stays on html.app-layout--desktop.
|
||||||
|
*/
|
||||||
|
@media screen and (min-width: @desktop-layout-min) {
|
||||||
|
html {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktopConfig {
|
||||||
|
position: static !important;
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: none !important;
|
||||||
|
min-height: 100vh;
|
||||||
|
left: auto !important;
|
||||||
|
right: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrap-fix {
|
||||||
|
width: auto !important;
|
||||||
|
max-width: none !important;
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--app-credit-bar-inner: 0px;
|
||||||
|
--app-credit-footer-offset: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-credit-footer {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Teleport to body escapes parent v-show — hide mobile dock chrome on desktop */
|
||||||
|
.app-dock-add-bar,
|
||||||
|
.wallet-card-slider {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-list,
|
||||||
|
.card-slider-wrap {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: none !important;
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html.app-layout--desktop {
|
||||||
|
@media screen and (min-width: @desktop-layout-min) {
|
||||||
|
.wrap-fix {
|
||||||
|
right: var(--app-desktop-sidebar-width, 260px) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.app-layout--desktop-no-sidebar .wrap-fix {
|
||||||
|
right: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Splash — always full viewport; never sidebar-offset */
|
||||||
|
.app-splash-screen {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
width: 100vw;
|
||||||
|
min-height: 100vh;
|
||||||
|
max-width: none;
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
z-index: 1000000000000;
|
||||||
|
|
||||||
|
&__gradient {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&--top {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--bottom {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html.app-layout--desktop .app-splash-screen {
|
||||||
|
right: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
width: 100vw !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Login — full viewport on desktop */
|
||||||
|
html.app-layout--desktop {
|
||||||
|
.login-page-root,
|
||||||
|
.dauth {
|
||||||
|
width: 100% !important;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dauth__layout {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* Mobile shell width rules.
|
||||||
|
* Below 426px: full viewport (phones).
|
||||||
|
* 426px–767px: centered 426px frame (tablet-ish mobile).
|
||||||
|
*/
|
||||||
|
|
||||||
|
html:not(.app-layout--desktop) {
|
||||||
|
/* Teleport to body — dock bar must span viewport, not shrink to content */
|
||||||
|
.app-dock-add-bar,
|
||||||
|
.wallet-card-slider {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 425.98px) {
|
||||||
|
body {
|
||||||
|
display: block !important;
|
||||||
|
width: 100% !important;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app,
|
||||||
|
.desktopConfig,
|
||||||
|
.app-main,
|
||||||
|
.main-with-bottom-chrome,
|
||||||
|
.main-with-bottom-chrome-inner {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100vw !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrap-fix,
|
||||||
|
.bottom-appbar {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100vw !important;
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-dock-add-bar,
|
||||||
|
.wallet-card-slider {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100vw !important;
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 426px) and (max-width: 767.98px) {
|
||||||
|
body {
|
||||||
|
display: flex !important;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktopConfig {
|
||||||
|
width: 426px !important;
|
||||||
|
max-width: 426px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrap-fix,
|
||||||
|
.bottom-appbar {
|
||||||
|
width: 426px !important;
|
||||||
|
max-width: 426px !important;
|
||||||
|
left: 50% !important;
|
||||||
|
right: auto !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
transform: translateX(-50%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-dock-add-bar,
|
||||||
|
.wallet-card-slider {
|
||||||
|
width: 426px !important;
|
||||||
|
max-width: 426px !important;
|
||||||
|
left: 50% !important;
|
||||||
|
right: auto !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
transform: translateX(-50%) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export interface AppNavItem {
|
||||||
|
id: string
|
||||||
|
routeName: string
|
||||||
|
label: string
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
+14
-10
@@ -3,6 +3,7 @@ import { defineStore } from 'pinia'
|
|||||||
import { getSettings } from '@/api/configuration'
|
import { getSettings } from '@/api/configuration'
|
||||||
import { resolveMeltedGoldLabel, resolveMetalTypeLabel } from '@/utils/metalLabels'
|
import { resolveMeltedGoldLabel, resolveMetalTypeLabel } from '@/utils/metalLabels'
|
||||||
import { resolveTradeWeightUnit } from '@/utils/weightUnit'
|
import { resolveTradeWeightUnit } from '@/utils/weightUnit'
|
||||||
|
import { runOnceAsync } from '@/composables/useOnceAsync'
|
||||||
|
|
||||||
export interface AppSetting {
|
export interface AppSetting {
|
||||||
id?: number
|
id?: number
|
||||||
@@ -45,17 +46,20 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
settings.value = merged
|
settings.value = merged
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchSettings() {
|
async function fetchSettings(force = false) {
|
||||||
try {
|
return runOnceAsync('settings:all', async () => {
|
||||||
const data = await getSettings()
|
try {
|
||||||
if (Array.isArray(data)) {
|
const data = await getSettings()
|
||||||
mergeSettings(data)
|
if (Array.isArray(data)) {
|
||||||
|
mergeSettings(data)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
} finally {
|
||||||
|
loaded.value = true
|
||||||
}
|
}
|
||||||
} catch (e) {
|
return settings.value
|
||||||
console.log(e)
|
}, force)
|
||||||
} finally {
|
|
||||||
loaded.value = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const metalTypeSettingValue = computed(() => getSettingByKey('metal_type'))
|
const metalTypeSettingValue = computed(() => getSettingByKey('metal_type'))
|
||||||
|
|||||||
+14
-1
@@ -114,7 +114,7 @@ export function getProjectLoginTaglineLines() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'بورس مس مرجع هوشمند معاملات',
|
'مرجع هوشمند معاملات',
|
||||||
' و سرمایهگذاری در صنعت مس',
|
' و سرمایهگذاری در صنعت مس',
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -123,6 +123,18 @@ export function getProjectSplashBackground() {
|
|||||||
return isBoursecuProject() ? BOURSECU_BRAND_NAV : '#ffffff'
|
return isBoursecuProject() ? BOURSECU_BRAND_NAV : '#ffffff'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Reset splash-time inline backgrounds so mobile overscroll shows white again. */
|
||||||
|
export function resetAppSurfaceBackground() {
|
||||||
|
const background = '#ffffff'
|
||||||
|
document.documentElement.style.backgroundColor = background
|
||||||
|
document.body.style.backgroundColor = background
|
||||||
|
|
||||||
|
const metaTheme = document.querySelector('meta[name="theme-color"]')
|
||||||
|
if (metaTheme) {
|
||||||
|
metaTheme.setAttribute('content', background)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function getProjectFontFamily() {
|
export function getProjectFontFamily() {
|
||||||
return getConfigApp('name') === 'boursecu' ? "'dana', sans-serif" : "'iran-sans', sans-serif"
|
return getConfigApp('name') === 'boursecu' ? "'dana', sans-serif" : "'iran-sans', sans-serif"
|
||||||
}
|
}
|
||||||
@@ -142,6 +154,7 @@ export function applyProjectTheme() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.documentElement.classList.add('project-boursecu')
|
document.documentElement.classList.add('project-boursecu')
|
||||||
|
document.documentElement.style.setProperty('--App-brand-nav', BOURSECU_BRAND_NAV)
|
||||||
document.documentElement.style.setProperty('--App-brand-red', BOURSECU_BRAND_RED)
|
document.documentElement.style.setProperty('--App-brand-red', BOURSECU_BRAND_RED)
|
||||||
document.documentElement.style.setProperty('--App-brand-red-50', '#f8ecec')
|
document.documentElement.style.setProperty('--App-brand-red-50', '#f8ecec')
|
||||||
document.documentElement.style.setProperty('--App-brand-red-100', '#f0d8d8')
|
document.documentElement.style.setProperty('--App-brand-red-100', '#f0d8d8')
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
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,3 +1,4 @@
|
|||||||
|
import { toPersianDigits } from '@/utils/persianDigits'
|
||||||
import type { Product } from '@/models/products'
|
import type { Product } from '@/models/products'
|
||||||
import type { UserWalletLine } from '@/models/wallet'
|
import type { UserWalletLine } from '@/models/wallet'
|
||||||
import type { ProductFile } from '@/utils/file'
|
import type { ProductFile } from '@/utils/file'
|
||||||
|
|||||||
@@ -2,13 +2,37 @@ import type { Product } from '@/models/products'
|
|||||||
import { getTradeBuyPrice, getTradeSellPrice, lastPrice } from '@/utils/price'
|
import { getTradeBuyPrice, getTradeSellPrice, lastPrice } from '@/utils/price'
|
||||||
import { isMetalCommodityGroup, isMeltedProductUsingAppPrice, isMeltedTradeGroup } from '@/utils/productCategories'
|
import { isMetalCommodityGroup, isMeltedProductUsingAppPrice, isMeltedTradeGroup } from '@/utils/productCategories'
|
||||||
|
|
||||||
function applyPriceOffsetPercent(basePrice: number, offsetPercent?: number | null) {
|
export function resolvePriceOffsetRatio(offsetPercent?: number | null) {
|
||||||
const offset = Number(offsetPercent ?? 0)
|
const offset = Number(offsetPercent ?? 0)
|
||||||
if (!offset) {
|
if (!offset) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5 => 5% ، 0.05 => 5% (اعشاری)
|
||||||
|
if (Math.abs(offset) > 0 && Math.abs(offset) < 1) {
|
||||||
|
return offset
|
||||||
|
}
|
||||||
|
|
||||||
|
return offset / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyPriceOffsetPercent(basePrice: number, offsetPercent?: number | null) {
|
||||||
|
const ratio = resolvePriceOffsetRatio(offsetPercent)
|
||||||
|
if (!ratio) {
|
||||||
return basePrice
|
return basePrice
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.round(basePrice * (1 + offset / 100))
|
return Math.round(basePrice * (1 + ratio))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** boursecu chart: قیمت + (قیمت × offset) */
|
||||||
|
export function applyChartPriceOffset(basePrice: number, offsetPercent?: number | null) {
|
||||||
|
const ratio = resolvePriceOffsetRatio(offsetPercent)
|
||||||
|
if (!ratio) {
|
||||||
|
return basePrice
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.round(basePrice + basePrice * ratio)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** قیمت خرید نمایشی محصول (همان مقداری که در لیست و اکشنشیت نشان داده میشود) */
|
/** قیمت خرید نمایشی محصول (همان مقداری که در لیست و اکشنشیت نشان داده میشود) */
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="variant === 'desktop'
|
||||||
|
? (isBoursecu ? 'dabout-content--boursecu' : 'dabout-content--default')
|
||||||
|
: (isBoursecuProject()
|
||||||
|
? 'mt-5 border-solid border-2 border-primary text-primary rounded-[12px] p-4 text-xs leading-5'
|
||||||
|
: 'bg-primary text-white rounded-[12px] p-4 text-xs leading-5')"
|
||||||
|
class="dabout-content"
|
||||||
|
>
|
||||||
|
<template v-for="(section, sectionIndex) in aboutSections" :key="sectionIndex">
|
||||||
|
<div
|
||||||
|
v-if="sectionIndex > 0"
|
||||||
|
:class="variant === 'desktop' ? 'dabout-content__divider' : 'w-full h-1px my-3 bg-tertiary'"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
v-if="section.title"
|
||||||
|
:class="variant === 'desktop' ? 'dabout-content__title' : 'font-bold text-sm mb-2'"
|
||||||
|
>
|
||||||
|
{{ section.title }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="(paragraph, paragraphIndex) in section.paragraphs ?? []"
|
||||||
|
:key="`paragraph-${sectionIndex}-${paragraphIndex}`"
|
||||||
|
:class="[
|
||||||
|
variant === 'desktop' ? 'dabout-content__paragraph' : '',
|
||||||
|
paragraphIndex > 0 ? 'mt-3' : '',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ paragraph }}
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
v-if="(section.items ?? []).length"
|
||||||
|
:class="variant === 'desktop' ? 'dabout-content__list' : ''"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="(item, itemIndex) in section.items ?? []"
|
||||||
|
:key="`item-${sectionIndex}-${itemIndex}`"
|
||||||
|
:class="variant === 'desktop' ? 'dabout-content__list-item' : 'mt-2'"
|
||||||
|
>
|
||||||
|
<template v-if="variant !== 'desktop'">• {{ item }}</template>
|
||||||
|
<template v-else>{{ item }}</template>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
v-if="variant !== 'desktop'"
|
||||||
|
class="w-full h-1px my-3 bg-tertiary"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useAboutPageContext } from '../composables/useAboutPage'
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
variant?: 'mobile' | 'desktop'
|
||||||
|
}>(),
|
||||||
|
{ variant: 'mobile' },
|
||||||
|
)
|
||||||
|
|
||||||
|
const {
|
||||||
|
aboutSections,
|
||||||
|
isBoursecu,
|
||||||
|
isBoursecuProject,
|
||||||
|
} = useAboutPageContext()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="variant === 'desktop' ? 'dabout-support' : 'wrap-fix bottom-0 px-10 pb-3rem flex gap-2 bg-white'">
|
||||||
|
<a class="w-full" :href="supportHref">
|
||||||
|
<BasicButton class="w-full" type="primary">تماس با پشتیبانی</BasicButton>
|
||||||
|
</a>
|
||||||
|
<a :href="baleHref" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img src="@/assets/img/bale.png" alt="بله" :class="variant === 'desktop' ? 'dabout-support__bale' : 'h-10 w-10'" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||||
|
import { useAboutPageContext } from '../composables/useAboutPage'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
variant?: 'mobile' | 'desktop'
|
||||||
|
}>(),
|
||||||
|
{ variant: 'mobile' },
|
||||||
|
)
|
||||||
|
|
||||||
|
const {
|
||||||
|
config,
|
||||||
|
getProjectSupportPhoneLink,
|
||||||
|
getProjectBaleLink,
|
||||||
|
} = useAboutPageContext()
|
||||||
|
|
||||||
|
const supportHref = computed(() =>
|
||||||
|
config.config.name === 'garoosi'
|
||||||
|
? 'tel:09031265925'
|
||||||
|
: getProjectSupportPhoneLink(),
|
||||||
|
)
|
||||||
|
|
||||||
|
const baleHref = computed(() =>
|
||||||
|
config.config.name === 'garoosi'
|
||||||
|
? 'https://ble.ir/garoosigallery'
|
||||||
|
: getProjectBaleLink(),
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { computed, inject, provide, type InjectionKey, type ComputedRef } from 'vue'
|
||||||
|
import { useConfigStore } from '@/store/configStore'
|
||||||
|
import {
|
||||||
|
getProjectBaleLink,
|
||||||
|
getProjectSupportPhoneLink,
|
||||||
|
isBoursecuProject,
|
||||||
|
} from '@/utils/config'
|
||||||
|
import { getAboutSections, type AboutSection } from '../aboutContent'
|
||||||
|
|
||||||
|
export interface AboutPageContext {
|
||||||
|
config: ReturnType<typeof useConfigStore>
|
||||||
|
aboutSections: ComputedRef<AboutSection[]>
|
||||||
|
isBoursecu: ComputedRef<boolean>
|
||||||
|
isBoursecuProject: typeof isBoursecuProject
|
||||||
|
getProjectSupportPhoneLink: typeof getProjectSupportPhoneLink
|
||||||
|
getProjectBaleLink: typeof getProjectBaleLink
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ABOUT_PAGE_CONTEXT_KEY: InjectionKey<AboutPageContext> = Symbol('aboutPageContext')
|
||||||
|
|
||||||
|
export function useAboutPage() {
|
||||||
|
const config = useConfigStore()
|
||||||
|
const nameProject = config.config.name_fa
|
||||||
|
|
||||||
|
const isBoursecu = computed(() => isBoursecuProject())
|
||||||
|
|
||||||
|
const aboutSections = computed(() => getAboutSections(nameProject, isBoursecu.value))
|
||||||
|
|
||||||
|
const context: AboutPageContext = {
|
||||||
|
config,
|
||||||
|
aboutSections,
|
||||||
|
isBoursecu,
|
||||||
|
isBoursecuProject,
|
||||||
|
getProjectSupportPhoneLink,
|
||||||
|
getProjectBaleLink,
|
||||||
|
}
|
||||||
|
|
||||||
|
provide(ABOUT_PAGE_CONTEXT_KEY, context)
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAboutPageContext() {
|
||||||
|
const context = inject(ABOUT_PAGE_CONTEXT_KEY)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useAboutPageContext must be used within a page that calls useAboutPage()')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dabout">
|
||||||
|
<div class="dabout__container">
|
||||||
|
<header class="dabout-topbar">
|
||||||
|
<button type="button" class="dabout-topbar__back" @click="router.back()">
|
||||||
|
<BasicIcon icon="vuesax-linear:arrow-right-1" size="18" />
|
||||||
|
<span>بازگشت</span>
|
||||||
|
</button>
|
||||||
|
<div class="dabout-topbar__meta">
|
||||||
|
<p class="dabout-topbar__eyebrow">اطلاعات</p>
|
||||||
|
<h1 class="dabout-topbar__title">درباره ما</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="dabout-layout">
|
||||||
|
<section class="dabout-surface dabout-main">
|
||||||
|
<div class="dabout-main__intro">
|
||||||
|
<div class="dabout-main__logo">
|
||||||
|
<BasicIcon icon="vuesax-linear:info-circle" size="28" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="dabout-main__project">{{ config.config.name_fa }}</h2>
|
||||||
|
<p class="dabout-main__tagline">آشنایی با خدمات و چشمانداز ما</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AboutSectionsContent variant="desktop" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside class="dabout-surface dabout-side">
|
||||||
|
<h3 class="dabout-side__title">پشتیبانی و ارتباط</h3>
|
||||||
|
<p class="dabout-side__text">
|
||||||
|
در صورت نیاز به راهنمایی یا پاسخ به سوالات، از راههای زیر با ما در ارتباط باشید.
|
||||||
|
</p>
|
||||||
|
<AboutSupportActions variant="desktop" />
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
|
import router from '@/router'
|
||||||
|
import AboutSectionsContent from '../components/AboutSectionsContent.vue'
|
||||||
|
import AboutSupportActions from '../components/AboutSupportActions.vue'
|
||||||
|
import { useAboutPageContext } from '../composables/useAboutPage'
|
||||||
|
|
||||||
|
const { config } = useAboutPageContext()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
/* Premium desktop about page */
|
||||||
|
|
||||||
|
.dabout {
|
||||||
|
min-height: 100%;
|
||||||
|
background: #f6f7f9;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dabout__container {
|
||||||
|
max-width: 1080px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 28px 36px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dabout-topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
|
||||||
|
&__back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__eyebrow {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dabout-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 300px);
|
||||||
|
gap: 20px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dabout-surface {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dabout-main {
|
||||||
|
padding: 28px;
|
||||||
|
|
||||||
|
&__intro {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #eef0f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(9, 61, 81, 0.08);
|
||||||
|
color: @App-primary-color;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__project {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tagline {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dabout-content {
|
||||||
|
&--default {
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--boursecu {
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid rgba(9, 61, 81, 0.15);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(9, 61, 81, 0.03);
|
||||||
|
color: @App-primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__divider {
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
background: #eef0f3;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: inherit;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__paragraph {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.9;
|
||||||
|
color: inherit;
|
||||||
|
text-align: justify;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list-item {
|
||||||
|
position: relative;
|
||||||
|
padding-right: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 10px;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dabout-side {
|
||||||
|
padding: 24px;
|
||||||
|
position: sticky;
|
||||||
|
top: 24px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0 0 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dabout-support {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
> a:first-child {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__bale {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
object-fit: contain;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1023.98px) {
|
||||||
|
.dabout-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dabout-side {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-164
@@ -1,176 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="about-page-root">
|
||||||
<div class="bg-white wrap-fix z-200001 py-3 px-3 text-primary font-bold drop-shadow flex">
|
<DesktopAboutView v-show="isDesktop" />
|
||||||
<div @click="router.back()" class="cursor-pointer">
|
<MobileAboutView v-show="!isDesktop" />
|
||||||
<BasicIcon icon="vuesax-linear:arrow-right-1" />
|
|
||||||
</div>
|
|
||||||
<div class="mr-2">درباره ما</div>
|
|
||||||
</div>
|
|
||||||
<div class="pt-20">
|
|
||||||
<div>
|
|
||||||
<img :src="img" alt="" class="mt-2 w-full absolute bottom-0" />
|
|
||||||
</div>
|
|
||||||
<div class="text-justify text-base z-200000 relative">
|
|
||||||
<div class="px-10">
|
|
||||||
<!-- <img :src="logoFull" class="mb-20 mt-5" />-->
|
|
||||||
<div class="overflow-y-auto h-65vh pb-20">
|
|
||||||
<div class="bg-primary text-white rounded-[12px] p-4 text-xs leading-5">
|
|
||||||
<template v-for="(section, sectionIndex) in aboutSections" :key="sectionIndex">
|
|
||||||
<div v-if="sectionIndex > 0" class="w-full h-1px my-3 bg-tertiary"></div>
|
|
||||||
<div v-if="section.title" class="font-bold text-sm mb-2">
|
|
||||||
{{ section.title }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-for="(paragraph, paragraphIndex) in section.paragraphs ?? []"
|
|
||||||
:key="`paragraph-${sectionIndex}-${paragraphIndex}`"
|
|
||||||
:class="paragraphIndex > 0 ? 'mt-3' : ''"
|
|
||||||
>
|
|
||||||
{{ paragraph }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-for="(item, itemIndex) in section.items ?? []"
|
|
||||||
:key="`item-${sectionIndex}-${itemIndex}`"
|
|
||||||
class="mt-2"
|
|
||||||
>
|
|
||||||
• {{ item }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="w-full h-1px my-3 bg-tertiary"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="config.config.name === 'garoosi'" class="wrap-fix bottom-0 px-10 pb-3rem flex gap-2 bg-white">
|
|
||||||
<a class="w-full" href="tel:09031265925">
|
|
||||||
<BasicButton class="w-full" type="primary">تماس با پشتیبانی</BasicButton>
|
|
||||||
</a>
|
|
||||||
<a href="https://ble.ir/garoosigallery">
|
|
||||||
<img src="@/assets/img/bale.png" alt="" class="h-10 w-10"/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div v-else class="wrap-fix bottom-0 px-10 pb-3rem flex gap-2 bg-white">
|
|
||||||
<a class="w-full" :href="getProjectSupportPhoneLink()">
|
|
||||||
<BasicButton class="w-full" type="primary">تماس با پشتیبانی</BasicButton>
|
|
||||||
</a>
|
|
||||||
<a :href="getProjectBaleLink()" target="_blank" rel="noopener noreferrer">
|
|
||||||
<img src="@/assets/img/bale.png" alt="بله" class="h-10 w-10"/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- <ActionSheet v-model:show="editPassVisible">salam</ActionSheet>-->
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, reactive, ref, computed } from 'vue'
|
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||||
import img from '@/assets/img/user_ly1.svg'
|
import { useAboutPage } from './composables/useAboutPage'
|
||||||
import gr_bottom from './assets/img/gr_bottom_white.svg'
|
import DesktopAboutView from './desktop/DesktopAboutView.vue'
|
||||||
import { loading } from '@/utils'
|
import MobileAboutView from './mobile/MobileAboutView.vue'
|
||||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
|
||||||
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
|
||||||
import { lengthValidation, required } from '@/utils/inputRules'
|
|
||||||
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
|
||||||
import type { User } from '@/models/login'
|
|
||||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
|
||||||
import router from '@/router'
|
|
||||||
import type { FormInstance } from 'ant-design-vue'
|
|
||||||
import { message } from 'ant-design-vue'
|
|
||||||
import { updateMe } from '@/api/me'
|
|
||||||
import { useUserStore } from '@/store/userStore'
|
|
||||||
import {useConfigStore} from "@/store/configStore";
|
|
||||||
import {getProjectBaleLink, getProjectSupportPhoneLink, getReferralShareText, getReferralShareTitle, isBoursecuProject} from "@/utils/config";
|
|
||||||
import { getAboutSections } from './aboutContent'
|
|
||||||
|
|
||||||
const config=useConfigStore()
|
const isDesktop = useDesktopBreakpoint()
|
||||||
|
|
||||||
const userFormState = ref<User>({
|
useAboutPage()
|
||||||
fname: '',
|
|
||||||
lname: '',
|
|
||||||
mobile: undefined,
|
|
||||||
birth_date: '',
|
|
||||||
phone_number: '',
|
|
||||||
province: '',
|
|
||||||
city: '',
|
|
||||||
n_code: undefined
|
|
||||||
})
|
|
||||||
const formRef = ref<FormInstance>()
|
|
||||||
const userStore = useUserStore()
|
|
||||||
const nameProject= useConfigStore().config.name_fa
|
|
||||||
const aboutSections = computed(() => getAboutSections(nameProject, isBoursecuProject()))
|
|
||||||
|
|
||||||
// const editPassVisible = ref<boolean>(false)
|
|
||||||
const editPassVisible = ref<boolean>(false)
|
|
||||||
|
|
||||||
function setForm() {
|
|
||||||
userFormState.value = userStore.userInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyCode() {
|
|
||||||
// nextTick(() => {
|
|
||||||
const val = userStore.userInfo?.referral_id
|
|
||||||
navigator.clipboard.writeText(val)
|
|
||||||
message.info('کد رونوشت شد')
|
|
||||||
// })
|
|
||||||
}
|
|
||||||
|
|
||||||
function shareCode() {
|
|
||||||
const val = getReferralShareText({
|
|
||||||
userName: userStore.userInfo?.name,
|
|
||||||
projectName: nameProject,
|
|
||||||
referralId: userStore.userInfo?.referral_id,
|
|
||||||
})
|
|
||||||
navigator.share({
|
|
||||||
title: getReferralShareTitle(nameProject),
|
|
||||||
text: val
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetData = () => {
|
|
||||||
formRef.value?.resetFields()
|
|
||||||
userFormState.value = {
|
|
||||||
fname: '',
|
|
||||||
lname: '',
|
|
||||||
mobile: undefined,
|
|
||||||
birth_date: '',
|
|
||||||
phone_number: '',
|
|
||||||
province: '',
|
|
||||||
city: '',
|
|
||||||
n_code: undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
setForm()
|
|
||||||
})
|
|
||||||
|
|
||||||
function ok() {
|
|
||||||
formRef.value?.validateFields().then(async () => {
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
await updateMe(userFormState.value)
|
|
||||||
await userStore.getMyInfo()
|
|
||||||
editPassVisible.value = false
|
|
||||||
message.success('اطلاعات شما با موفقیت به روز رسانی شد')
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/*.fade-enter-active,
|
.about-page-root {
|
||||||
.fade-leave-active {
|
min-height: 100%;
|
||||||
transition: opacity 0.2s ease;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-from,
|
|
||||||
.fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}*/
|
|
||||||
/*.trans{
|
|
||||||
transition: height 1s;
|
|
||||||
}*/
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="bg-white wrap-fix z-200001 py-3 px-3 text-primary font-bold drop-shadow flex">
|
||||||
|
<div class="cursor-pointer" @click="router.back()">
|
||||||
|
<BasicIcon icon="vuesax-linear:arrow-right-1" />
|
||||||
|
</div>
|
||||||
|
<div class="mr-2">درباره ما</div>
|
||||||
|
</div>
|
||||||
|
<div class="pt-20">
|
||||||
|
<div>
|
||||||
|
<img :src="img" alt="" class="mt-2 w-full absolute bottom-0" />
|
||||||
|
</div>
|
||||||
|
<div class="text-justify text-base z-200000 relative">
|
||||||
|
<div class="px-10">
|
||||||
|
<div class="overflow-y-auto h-65vh pb-20">
|
||||||
|
<AboutSectionsContent variant="mobile" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AboutSupportActions variant="mobile" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
|
import img from '@/assets/img/user_ly1.svg'
|
||||||
|
import router from '@/router'
|
||||||
|
import AboutSectionsContent from '../components/AboutSectionsContent.vue'
|
||||||
|
import AboutSupportActions from '../components/AboutSupportActions.vue'
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="variant === 'desktop'
|
||||||
|
? 'daboutus-content'
|
||||||
|
: 'mt-5 text-5 border-solid border-2 border-[#093D51] text-[#093D51] rounded-[12px] p-4'"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="variant !== 'desktop'"
|
||||||
|
:src="config.config?.avatar"
|
||||||
|
class="mb-2 mt-5 w-10rem"
|
||||||
|
style="justify-self: center"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div :class="variant === 'desktop' ? 'daboutus-content__body' : 'text-sm leading-7 space-y-2'">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div :class="variant === 'desktop' ? 'daboutus-content__enamad' : ''" style="justify-self: center">
|
||||||
|
<a
|
||||||
|
referrerpolicy="origin"
|
||||||
|
target="_blank"
|
||||||
|
href="https://trustseal.enamad.ir/?id=710578&Code=DdltBYWJgvc7LJs9eM46SEliXRRU0kSW"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
referrerpolicy="origin"
|
||||||
|
src="https://trustseal.enamad.ir/logo.aspx?id=710578&Code=DdltBYWJgvc7LJs9eM46SEliXRRU0kSW"
|
||||||
|
alt="enamad"
|
||||||
|
style="cursor:pointer"
|
||||||
|
code="DdltBYWJgvc7LJs9eM46SEliXRRU0kSW"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useAboutUsPageContext } from '../composables/useAboutUsPage'
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
variant?: 'mobile' | 'desktop'
|
||||||
|
}>(),
|
||||||
|
{ variant: 'mobile' },
|
||||||
|
)
|
||||||
|
|
||||||
|
const { config } = useAboutUsPageContext()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { inject, provide, type InjectionKey } from 'vue'
|
||||||
|
import { useConfigStore } from '@/store/configStore'
|
||||||
|
|
||||||
|
export interface AboutUsPageContext {
|
||||||
|
config: ReturnType<typeof useConfigStore>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ABOUT_US_PAGE_CONTEXT_KEY: InjectionKey<AboutUsPageContext> = Symbol('aboutUsPageContext')
|
||||||
|
|
||||||
|
export function useAboutUsPage() {
|
||||||
|
const config = useConfigStore()
|
||||||
|
|
||||||
|
const context: AboutUsPageContext = {
|
||||||
|
config,
|
||||||
|
}
|
||||||
|
|
||||||
|
provide(ABOUT_US_PAGE_CONTEXT_KEY, context)
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAboutUsPageContext() {
|
||||||
|
const context = inject(ABOUT_US_PAGE_CONTEXT_KEY)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useAboutUsPageContext must be used within a page that calls useAboutUsPage()')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<template>
|
||||||
|
<div class="daboutus">
|
||||||
|
<div class="daboutus__container">
|
||||||
|
<header class="daboutus-topbar">
|
||||||
|
<button type="button" class="daboutus-topbar__back" @click="router.back()">
|
||||||
|
<BasicIcon icon="vuesax-linear:arrow-right-1" size="18" />
|
||||||
|
<span>بازگشت</span>
|
||||||
|
</button>
|
||||||
|
<div class="daboutus-topbar__meta">
|
||||||
|
<p class="daboutus-topbar__eyebrow">اطلاعات</p>
|
||||||
|
<h1 class="daboutus-topbar__title">درباره ما</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="daboutus-surface daboutus-main">
|
||||||
|
<div class="daboutus-main__intro">
|
||||||
|
<div class="daboutus-main__logo">
|
||||||
|
<img :src="config.config?.avatar" alt="" class="daboutus-main__avatar" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="daboutus-main__project">{{ config.config.name_fa }}</h2>
|
||||||
|
<p class="daboutus-main__tagline">آشنایی با خدمات و چشمانداز ما</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="daboutus-main__scroll">
|
||||||
|
<AboutUsContentPanel variant="desktop" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
|
import router from '@/router'
|
||||||
|
import AboutUsContentPanel from '../components/AboutUsContentPanel.vue'
|
||||||
|
import { useAboutUsPageContext } from '../composables/useAboutUsPage'
|
||||||
|
|
||||||
|
const { config } = useAboutUsPageContext()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
/* Premium desktop about-us page */
|
||||||
|
|
||||||
|
.daboutus {
|
||||||
|
min-height: 100%;
|
||||||
|
background: #f6f7f9;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.daboutus__container {
|
||||||
|
max-width: 860px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 28px 36px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.daboutus-topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
|
||||||
|
&__back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__eyebrow {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.daboutus-surface {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.daboutus-main {
|
||||||
|
padding: 28px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: calc(100vh - 180px);
|
||||||
|
|
||||||
|
&__intro {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #eef0f3;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(9, 61, 81, 0.08);
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__avatar {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__project {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tagline {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-left: 4px;
|
||||||
|
margin-left: -4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.daboutus-content {
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid rgba(9, 61, 81, 0.15);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(9, 61, 81, 0.03);
|
||||||
|
color: @App-primary-color;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: inherit;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__paragraph {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.9;
|
||||||
|
color: inherit;
|
||||||
|
text-align: justify;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__enamad {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid rgba(9, 61, 81, 0.12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1023.98px) {
|
||||||
|
.daboutus-main {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,64 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="about-us-page-root">
|
||||||
<div class="wrap-fix z-1 bg-white py-3 px-3 text-black font-bold drop-shadow flex">
|
<DesktopAboutUsView v-show="isDesktop" />
|
||||||
<div @click="router.back()" class="cursor-pointer">
|
<MobileAboutUsView v-show="!isDesktop" />
|
||||||
<BasicIcon icon="vuesax-linear:arrow-right-1" />
|
|
||||||
</div>
|
|
||||||
<div class="mr-2">درباره ما</div>
|
|
||||||
</div>
|
|
||||||
<div class="pt-20">
|
|
||||||
<div>
|
|
||||||
<img :src="img" alt="" class="mt-2 w-full absolute bottom-0" />
|
|
||||||
</div>
|
|
||||||
<div class="text-justify text-base z-200000 relative">
|
|
||||||
<div class="px-10">
|
|
||||||
<img :src="config.config?.avatar" class="mb-2 mt-5 w-10rem" style="justify-self: center"/>
|
|
||||||
<div class="overflow-y-auto h-65vh pb-20 border-[#093D51]">
|
|
||||||
<div
|
|
||||||
class="mt-5 text-5 border-solid border-2 border-[#093D51] text-[#093D51] rounded-[12px] p-4"
|
|
||||||
>
|
|
||||||
<div class="text-sm leading-7 space-y-2">
|
|
||||||
<h2 class="text-base font-bold mb-3">درباره ما</h2>
|
|
||||||
|
|
||||||
<div class="mt-3"> گروسی گالری بستری امن و آنلاین برای خرید و فروش طلای آبشده فراهم کرده تا کاربران بتوانند سرمایههای خرد خود را به راحتی پسانداز و مدیریت کنند. </div>
|
|
||||||
<div class="mt-3"> هدف ما ایجاد تجربهای سریع، شفاف و کاربرپسند در معاملات طلای آبشده است. </div>
|
|
||||||
|
|
||||||
<!-- <div class="mt-4 border-t pt-3">
|
|
||||||
<h3 class="font-semibold mb-2">اطلاعات تماس</h3>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<div> <span class="font-medium">ایمیل:</span> <a href="mailto:hajiabolfath@gmail.com" class="text-blue-600">hajiabolfath@gmail.com</a> </div>
|
|
||||||
<div> <span class="font-medium">کد پستی:</span> ۳۷۱۳۷۶۴۸۹۰ </div>
|
|
||||||
<div> <span class="font-medium">آدرس:</span> استان قم، شهرستان قم، بخش مرکزی، شهر قم، محله ارم (بازار)، کوچه مرعشی نجفی ۸، خیابان آیتالله مرعشی نجفی (ارم)، تجاری ارم، طبقه ۱، واحد ۱۵۵</div>
|
|
||||||
</div>
|
|
||||||
</div>-->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="justify-self: center">
|
|
||||||
<a referrerpolicy='origin' target='_blank' href='https://trustseal.enamad.ir/?id=710578&Code=DdltBYWJgvc7LJs9eM46SEliXRRU0kSW'>
|
|
||||||
<img referrerpolicy='origin' src='https://trustseal.enamad.ir/logo.aspx?id=710578&Code=DdltBYWJgvc7LJs9eM46SEliXRRU0kSW'
|
|
||||||
alt='enamad'
|
|
||||||
style='cursor:pointer'
|
|
||||||
code='DdltBYWJgvc7LJs9eM46SEliXRRU0kSW'
|
|
||||||
>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {useConfigStore} from "@/store/configStore";
|
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||||
import img from '@/assets/img/user_ly1.svg'
|
import { useAboutUsPage } from './composables/useAboutUsPage'
|
||||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
import DesktopAboutUsView from './desktop/DesktopAboutUsView.vue'
|
||||||
import router from '@/router'
|
import MobileAboutUsView from './mobile/MobileAboutUsView.vue'
|
||||||
|
|
||||||
const config = useConfigStore()
|
const isDesktop = useDesktopBreakpoint()
|
||||||
|
|
||||||
|
useAboutUsPage()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.about-us-page-root {
|
||||||
|
min-height: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="wrap-fix z-1 bg-white py-3 px-3 text-black font-bold drop-shadow flex">
|
||||||
|
<div class="cursor-pointer" @click="router.back()">
|
||||||
|
<BasicIcon icon="vuesax-linear:arrow-right-1" />
|
||||||
|
</div>
|
||||||
|
<div class="mr-2">درباره ما</div>
|
||||||
|
</div>
|
||||||
|
<div class="pt-20">
|
||||||
|
<div>
|
||||||
|
<img :src="img" alt="" class="mt-2 w-full absolute bottom-0" />
|
||||||
|
</div>
|
||||||
|
<div class="text-justify text-base z-200000 relative">
|
||||||
|
<div class="px-10">
|
||||||
|
<div class="overflow-y-auto h-65vh pb-20 border-[#093D51]">
|
||||||
|
<AboutUsContentPanel variant="mobile" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
|
import img from '@/assets/img/user_ly1.svg'
|
||||||
|
import router from '@/router'
|
||||||
|
import AboutUsContentPanel from '../components/AboutUsContentPanel.vue'
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<template>
|
||||||
|
<a-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="address"
|
||||||
|
:class="variant === 'desktop' ? 'daddr-form' : '!mt-5'"
|
||||||
|
name="address-form"
|
||||||
|
>
|
||||||
|
<div :class="variant === 'desktop' ? 'daddr-form__fields' : 'flex flex-col items-center'">
|
||||||
|
<div
|
||||||
|
v-if="variant !== 'desktop'"
|
||||||
|
class="font-bold text-lg mb-2"
|
||||||
|
>
|
||||||
|
آدرس
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:class="variant === 'desktop'
|
||||||
|
? 'daddr-form__grid'
|
||||||
|
: 'flex flex-col gap-4 pb-20 w-full h-60vh overflow-y-scroll'"
|
||||||
|
>
|
||||||
|
<a-form-item label="استان" name="province" :rules="[required()]">
|
||||||
|
<BasicSelect
|
||||||
|
v-model:value="address.province"
|
||||||
|
:items="provinceList"
|
||||||
|
value-name="title"
|
||||||
|
key-name="title"
|
||||||
|
:size="variant === 'desktop' ? 'large' : undefined"
|
||||||
|
@change="changeProvince"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="شهر" name="city" :rules="[required()]">
|
||||||
|
<BasicSelect
|
||||||
|
v-model:value="address.city"
|
||||||
|
:disabled="!cityList"
|
||||||
|
value-name="title"
|
||||||
|
:items="cityList"
|
||||||
|
key-name="title"
|
||||||
|
:size="variant === 'desktop' ? 'large' : undefined"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="کد پستی" name="zip" :rules="[required()]">
|
||||||
|
<BasicInput
|
||||||
|
v-model:value="address.zip"
|
||||||
|
placeholder="کد پستی را وارد نمایید"
|
||||||
|
:size="variant === 'desktop' ? 'large' : undefined"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="تلفن تماس" name="phone" :rules="[required()]">
|
||||||
|
<BasicInput
|
||||||
|
v-model:value="address.phone"
|
||||||
|
placeholder="تلفن تماس را وارد نمایید"
|
||||||
|
:size="variant === 'desktop' ? 'large' : undefined"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item
|
||||||
|
label="آدرس کامل"
|
||||||
|
name="address"
|
||||||
|
:rules="[required()]"
|
||||||
|
:class="variant === 'desktop' ? 'daddr-form__address-item' : ''"
|
||||||
|
>
|
||||||
|
<BasicTextArea
|
||||||
|
v-model:value="address.address"
|
||||||
|
placeholder="آدرس کامل را وارد نمایید"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<BasicButton
|
||||||
|
:class="variant === 'desktop' ? 'daddr-form__submit' : '!text-white px-3 pb-4rem'"
|
||||||
|
type="primary"
|
||||||
|
@click="handleOk"
|
||||||
|
>
|
||||||
|
{{ editId ? 'ویرایش آدرس' : 'ثبت آدرس' }}
|
||||||
|
</BasicButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||||
|
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
||||||
|
import BasicSelect from '@/components/BasicPacks/BasicSelect.vue'
|
||||||
|
import BasicTextArea from '@/components/BasicPacks/BasicTextArea.vue'
|
||||||
|
import { required } from '@/utils/inputRules'
|
||||||
|
import { useAddressPageContext } from '../composables/useAddressPage'
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
variant?: 'mobile' | 'desktop'
|
||||||
|
}>(),
|
||||||
|
{ variant: 'mobile' },
|
||||||
|
)
|
||||||
|
|
||||||
|
const {
|
||||||
|
address,
|
||||||
|
formRef,
|
||||||
|
editId,
|
||||||
|
provinceList,
|
||||||
|
cityList,
|
||||||
|
changeProvince,
|
||||||
|
handleOk,
|
||||||
|
} = useAddressPageContext()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<template>
|
||||||
|
<div class="address-mobile-item shadow mb-2 p-2 rounded-lg w-full">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="font-semibold text-base mb-2">{{ item.address }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2 mt-2">
|
||||||
|
<BasicInfoItem is-line title="استان" :value="item.province" />
|
||||||
|
<BasicInfoItem is-line title="شهر" :value="item.city" />
|
||||||
|
<BasicInfoItem is-line title="کد پستی" :value="item.zip" />
|
||||||
|
<BasicInfoItem is-line title="شماره تلفن" :value="item.phone" />
|
||||||
|
</div>
|
||||||
|
<a-divider class="!my-3" />
|
||||||
|
|
||||||
|
<div class="flex justify-between mt-1">
|
||||||
|
<div class="mb-2">{{ formatAddressDate(item.created_at) }}</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<BasicIcon
|
||||||
|
color="red"
|
||||||
|
icon="vuesax-linear:trash"
|
||||||
|
class="p-1"
|
||||||
|
@click.stop="deleteConfirmVisible = true"
|
||||||
|
/>
|
||||||
|
<BasicIcon
|
||||||
|
color="green"
|
||||||
|
icon="vuesax-linear:edit"
|
||||||
|
class="p-1"
|
||||||
|
@click.stop="emits('edit', item)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogVant
|
||||||
|
v-model:show="deleteConfirmVisible"
|
||||||
|
teleport="body"
|
||||||
|
:close-on-click-overlay="false"
|
||||||
|
:time-out="-1"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<div class="p-5 text-sm">آیا از حذف آدرس اطمینان دارید؟</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #button>
|
||||||
|
<div class="text-center text-red flex items-center justify-center" @click="deleteItem">
|
||||||
|
<a-spin :spinning="deleteLoading" size="small">تایید</a-spin>
|
||||||
|
</div>
|
||||||
|
<div class="text-center" @click="deleteConfirmVisible = false">انصراف</div>
|
||||||
|
</template>
|
||||||
|
</DialogVant>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { toJalali } from '@/utils/useDateTime'
|
||||||
|
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
|
import BasicInfoItem from '@/components/BasicPacks/BasicInfoItem.vue'
|
||||||
|
import DialogVant from '@/components/DialogVant.vue'
|
||||||
|
import type { Address } from '@/models/address'
|
||||||
|
import { deleteAddress } from '@/api/address'
|
||||||
|
|
||||||
|
const deleteLoading = ref(false)
|
||||||
|
const deleteConfirmVisible = ref(false)
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
item: Address
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
edit: [item: Address]
|
||||||
|
refresh: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function formatAddressDate(dateTime?: string) {
|
||||||
|
if (!dateTime) return ''
|
||||||
|
const formatted = toJalali(dateTime)
|
||||||
|
return formatted.includes('Invalid') ? '' : formatted
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteItem() {
|
||||||
|
try {
|
||||||
|
deleteLoading.value = true
|
||||||
|
await deleteAddress(props.item.id!)
|
||||||
|
deleteConfirmVisible.value = false
|
||||||
|
emits('refresh')
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
} finally {
|
||||||
|
deleteLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,112 +1,155 @@
|
|||||||
<template>
|
<template>
|
||||||
<div @click="infoVisible = true" class="shadow mb-2 p-2 rounded-lg">
|
<div v-if="variant === 'desktop'" class="daddr-card">
|
||||||
|
<p class="daddr-card__address">{{ item.address }}</p>
|
||||||
|
|
||||||
|
<div class="daddr-card__meta">
|
||||||
|
<div class="daddr-card__row">
|
||||||
|
<span class="daddr-card__label">استان:</span>
|
||||||
|
<span class="daddr-card__value">{{ item.province }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="daddr-card__row">
|
||||||
|
<span class="daddr-card__label">شهر:</span>
|
||||||
|
<span class="daddr-card__value">{{ item.city }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="daddr-card__row">
|
||||||
|
<span class="daddr-card__label">کد پستی:</span>
|
||||||
|
<span class="daddr-card__value">{{ item.zip }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="daddr-card__row">
|
||||||
|
<span class="daddr-card__label">شماره تلفن:</span>
|
||||||
|
<span class="daddr-card__value">{{ item.phone }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="daddr-card__actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="daddr-card__icon-btn daddr-card__icon-btn--edit"
|
||||||
|
@click.stop="emits('edit', item)"
|
||||||
|
>
|
||||||
|
<BasicIcon color="green" icon="vuesax-linear:edit" size="18" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="daddr-card__icon-btn daddr-card__icon-btn--danger"
|
||||||
|
@click.stop="deleteConfirmVisible = true"
|
||||||
|
>
|
||||||
|
<BasicIcon color="red" icon="vuesax-linear:trash" size="18" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="daddr-card__footer">
|
||||||
|
<span class="daddr-card__date">{{ formatAddressDate(item.created_at) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogVant
|
||||||
|
v-model:show="deleteConfirmVisible"
|
||||||
|
:close-on-click-overlay="false"
|
||||||
|
:time-out="-1"
|
||||||
|
teleport="body"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<div class="p-5 text-sm">آیا از حذف آدرس اطمینان دارید؟</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #button>
|
||||||
|
<div class="text-center text-red flex items-center justify-center" @click="deleteItem">
|
||||||
|
<a-spin :spinning="deleteLoading" size="small">تایید</a-spin>
|
||||||
|
</div>
|
||||||
|
<div class="text-center" @click="deleteConfirmVisible = false">انصراف</div>
|
||||||
|
</template>
|
||||||
|
</DialogVant>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="shadow mb-2 p-2 rounded-lg">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div class="font-semibold text-base mb-2">{{item.address }}</div>
|
<div class="font-semibold text-base mb-2">{{ item.address }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2 mt-2">
|
<div class="flex flex-col gap-2 mt-2">
|
||||||
<BasicInfoItem is-line title="استان" :value="item.province"/>
|
<BasicInfoItem is-line title="استان" :value="item.province" />
|
||||||
<BasicInfoItem is-line title="شهر" :value="item.city"/>
|
<BasicInfoItem is-line title="شهر" :value="item.city" />
|
||||||
<BasicInfoItem is-line title="کد پستی" :value="item.zip"/>
|
<BasicInfoItem is-line title="کد پستی" :value="item.zip" />
|
||||||
<BasicInfoItem is-line title="شماره تلفن" :value="item.phone"/>
|
<BasicInfoItem is-line title="شماره تلفن" :value="item.phone" />
|
||||||
</div>
|
</div>
|
||||||
<a-divider class="!my-3" />
|
<a-divider class="!my-3" />
|
||||||
|
|
||||||
<div class="flex justify-between mt-1">
|
<div class="flex justify-between mt-1">
|
||||||
<div class="mb-2">{{toJalali(item.created_at)}}</div>
|
<div class="mb-2">{{ formatAddressDate(item.created_at) }}</div>
|
||||||
<div class="flex gap-2 ">
|
<div class="flex gap-2">
|
||||||
<BasicIcon color="red" icon="vuesax-linear:trash" @click.stop="deleteConfirmVisible = true" class="p-1"/>
|
<BasicIcon
|
||||||
<BasicIcon color="green" icon="vuesax-linear:edit" @click.stop="emits('edit',item)" class="p-1"/>
|
color="red"
|
||||||
|
icon="vuesax-linear:trash"
|
||||||
|
class="p-1"
|
||||||
|
@click.stop="deleteConfirmVisible = true"
|
||||||
|
/>
|
||||||
|
<BasicIcon
|
||||||
|
color="green"
|
||||||
|
icon="vuesax-linear:edit"
|
||||||
|
class="p-1"
|
||||||
|
@click.stop="emits('edit', item)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
<DialogVant
|
||||||
|
v-model:show="deleteConfirmVisible"
|
||||||
<!-- <teleport to="app">-->
|
teleport="body"
|
||||||
<DialogVant v-model:show="deleteConfirmVisible" teleport="body" :closeOnClickOverlay="false" :timeOut="-1">
|
:close-on-click-overlay="false"
|
||||||
<!-- <template v-slot:img>
|
:time-out="-1"
|
||||||
<img :src="registerDelayImage" alt="" class="opacity-80">
|
>
|
||||||
</template>-->
|
<template #title>
|
||||||
|
|
||||||
<template v-slot:title>
|
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<div class="p-5 text-sm">
|
<div class="p-5 text-sm">آیا از حذف آدرس اطمینان دارید؟</div>
|
||||||
آیا از حذف آدرس اطمینان دارید؟
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<template #button>
|
||||||
<template v-slot:button>
|
|
||||||
<div class="text-center text-red flex items-center justify-center" @click="deleteItem">
|
<div class="text-center text-red flex items-center justify-center" @click="deleteItem">
|
||||||
<a-spin :spinning="deleteLoading" size="small">
|
<a-spin :spinning="deleteLoading" size="small">تایید</a-spin>
|
||||||
تایید
|
|
||||||
</a-spin>
|
|
||||||
</div>
|
|
||||||
<div class="text-center" @click="deleteConfirmVisible = false">
|
|
||||||
انصراف
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="text-center" @click="deleteConfirmVisible = false">انصراف</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
</DialogVant>
|
</DialogVant>
|
||||||
<!-- </teleport>-->
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { toJalali } from '@/utils/useDateTime'
|
||||||
import { toJalali } from '../../../../utils/useDateTime'
|
|
||||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
import BasicInfoItem from '@/components/BasicPacks/BasicInfoItem.vue'
|
import BasicInfoItem from '@/components/BasicPacks/BasicInfoItem.vue'
|
||||||
import DialogVant from '@/components/DialogVant.vue'
|
import DialogVant from '@/components/DialogVant.vue'
|
||||||
import { updatePriceTrigger } from '@/api/triggerPrice'
|
|
||||||
import { loading } from '@/utils'
|
|
||||||
import type { Address } from '@/models/address'
|
import type { Address } from '@/models/address'
|
||||||
import { deleteAddress } from '@/api/address'
|
import { deleteAddress } from '@/api/address'
|
||||||
|
|
||||||
const infoVisible = ref<boolean>(false)
|
|
||||||
|
|
||||||
const isActive = ref<boolean>(false)
|
|
||||||
|
|
||||||
const deleteLoading = ref(false)
|
const deleteLoading = ref(false)
|
||||||
const deleteConfirmVisible = ref(false)
|
const deleteConfirmVisible = ref(false)
|
||||||
|
|
||||||
interface Prop {
|
const props = withDefaults(
|
||||||
item: Address
|
defineProps<{
|
||||||
}
|
item: Address
|
||||||
function changeActive(val: boolean) {
|
variant?: 'mobile' | 'desktop'
|
||||||
console.log(val)
|
}>(),
|
||||||
try {
|
{ variant: 'mobile' },
|
||||||
loading.value = true
|
)
|
||||||
updatePriceTrigger(props?.item.id, {is_active: val})
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
isActive.value = !isActive.value
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function returnPriceCondition(val: string) {
|
const emits = defineEmits<{
|
||||||
switch (val) {
|
edit: [item: Address]
|
||||||
case '==': return 'مساوی'
|
refresh: []
|
||||||
case '<': return 'کوچکتر'
|
}>()
|
||||||
// case '==': return 'مساوی'
|
|
||||||
}
|
function formatAddressDate(dateTime?: string) {
|
||||||
/* if (val === '=='){
|
if (!dateTime) return ''
|
||||||
return 'مساوی'
|
const formatted = toJalali(dateTime)
|
||||||
} else if (val === '<'){
|
return formatted.includes('Invalid') ? '' : formatted
|
||||||
return 'کوچکتر'
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteItem() {
|
async function deleteItem() {
|
||||||
try {
|
try {
|
||||||
deleteLoading.value = true
|
deleteLoading.value = true
|
||||||
await deleteAddress(props.item.id)
|
await deleteAddress(props.item.id!)
|
||||||
deleteConfirmVisible.value = false
|
deleteConfirmVisible.value = false
|
||||||
emits('refresh')
|
emits('refresh')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -115,42 +158,4 @@ async function deleteItem() {
|
|||||||
deleteLoading.value = false
|
deleteLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function returnAction(val: string) {
|
|
||||||
switch (val) {
|
|
||||||
case 'sms': return 'پیامک'
|
|
||||||
case 'email': return 'ایمیل'
|
|
||||||
case 'notify': return 'نوتیفیکیشن'
|
|
||||||
// case '==': return 'مساوی'
|
|
||||||
}
|
|
||||||
/* if (val === '=='){
|
|
||||||
return 'مساوی'
|
|
||||||
} else if (val === '<'){
|
|
||||||
return 'کوچکتر'
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Prop>(), {})
|
|
||||||
const emits = defineEmits(['edit','refresh'])
|
|
||||||
|
|
||||||
/*const title = computed(() => {
|
|
||||||
if (isUp.value) {
|
|
||||||
if (isGold.value) {
|
|
||||||
return 'واریز ریالی'
|
|
||||||
} else {
|
|
||||||
return 'خرید طلا'
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isGold.value) {
|
|
||||||
return 'برداشت ریالی'
|
|
||||||
} else {
|
|
||||||
return 'فروش طلا'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})*/
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import { inject, onMounted, provide, ref, type InjectionKey, type Ref } from 'vue'
|
||||||
|
import { cloneDeep } from 'lodash'
|
||||||
|
import { type FormInstance, message } from 'ant-design-vue'
|
||||||
|
import { createAddress, getCity, updateAddress } from '@/api/address'
|
||||||
|
import type { Address } from '@/models/address'
|
||||||
|
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||||
|
import { loading, loadingMore } from '@/utils'
|
||||||
|
import { isBazretalaProject } from '@/utils/config'
|
||||||
|
import {
|
||||||
|
addressItems,
|
||||||
|
addressProvinceList,
|
||||||
|
initAddressPageData,
|
||||||
|
refreshAddressItems,
|
||||||
|
} from './useAddressPageData'
|
||||||
|
|
||||||
|
export interface AddressPageContext {
|
||||||
|
provinceList: typeof addressProvinceList
|
||||||
|
cityList: Ref<any[] | undefined>
|
||||||
|
items: typeof addressItems
|
||||||
|
addAddressVisible: Ref<boolean>
|
||||||
|
editId: Ref<number | undefined>
|
||||||
|
address: Ref<Address>
|
||||||
|
formRef: Ref<FormInstance | undefined>
|
||||||
|
loading: typeof loading
|
||||||
|
loadingMore: typeof loadingMore
|
||||||
|
isBazretalaProject: typeof isBazretalaProject
|
||||||
|
resetFields: () => void
|
||||||
|
openAdd: () => void
|
||||||
|
openEdit: (val: Address) => void
|
||||||
|
handleOk: (e?: MouseEvent) => void
|
||||||
|
initItems: () => Promise<void>
|
||||||
|
changeProvince: (obj: { id: number }) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ADDRESS_PAGE_CONTEXT_KEY: InjectionKey<AddressPageContext> =
|
||||||
|
Symbol('addressPageContext')
|
||||||
|
|
||||||
|
const emptyAddress = (): Address => ({
|
||||||
|
city: '',
|
||||||
|
province: '',
|
||||||
|
phone: '',
|
||||||
|
address: '',
|
||||||
|
zip: '',
|
||||||
|
default_address: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
export function useAddressPage() {
|
||||||
|
const isDesktop = useDesktopBreakpoint()
|
||||||
|
const cityList = ref<any[]>()
|
||||||
|
const addAddressVisible = ref(false)
|
||||||
|
const editId = ref<number | undefined>(undefined)
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const address = ref<Address>(emptyAddress())
|
||||||
|
|
||||||
|
function resetFields() {
|
||||||
|
editId.value = undefined
|
||||||
|
address.value = emptyAddress()
|
||||||
|
cityList.value = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAdd() {
|
||||||
|
resetFields()
|
||||||
|
editId.value = undefined
|
||||||
|
addAddressVisible.value = !isDesktop.value
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openEdit(val: Address) {
|
||||||
|
address.value = cloneDeep(val)
|
||||||
|
editId.value = address.value.id
|
||||||
|
if (address.value.province) {
|
||||||
|
const province = addressProvinceList.value.find((item) => item.title === address.value.province)
|
||||||
|
if (province?.id) {
|
||||||
|
cityList.value = await getCity(province.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addAddressVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOk = (_e?: MouseEvent) => {
|
||||||
|
formRef.value?.validate().then(async () => {
|
||||||
|
if (editId.value) {
|
||||||
|
await updateAddress(editId.value, address.value)
|
||||||
|
} else {
|
||||||
|
await createAddress(address.value)
|
||||||
|
}
|
||||||
|
await initItems()
|
||||||
|
addAddressVisible.value = false
|
||||||
|
message.success('آدرس با موفقیت ثبت شد')
|
||||||
|
resetFields()
|
||||||
|
}).catch((error) => {
|
||||||
|
console.log('error', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initItems() {
|
||||||
|
await refreshAddressItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeProvince = async (obj: { id: number }) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
cityList.value = undefined
|
||||||
|
address.value.city = undefined
|
||||||
|
cityList.value = await getCity(obj.id)
|
||||||
|
} catch (e) {
|
||||||
|
message.error('خطا در دریافت شهرها')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await initAddressPageData()
|
||||||
|
})
|
||||||
|
|
||||||
|
const context: AddressPageContext = {
|
||||||
|
provinceList: addressProvinceList,
|
||||||
|
cityList,
|
||||||
|
items: addressItems,
|
||||||
|
addAddressVisible,
|
||||||
|
editId,
|
||||||
|
address,
|
||||||
|
formRef,
|
||||||
|
loading,
|
||||||
|
loadingMore,
|
||||||
|
isBazretalaProject,
|
||||||
|
resetFields,
|
||||||
|
openAdd,
|
||||||
|
openEdit,
|
||||||
|
handleOk,
|
||||||
|
initItems,
|
||||||
|
changeProvince,
|
||||||
|
}
|
||||||
|
|
||||||
|
provide(ADDRESS_PAGE_CONTEXT_KEY, context)
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAddressPageContext() {
|
||||||
|
const context = inject(ADDRESS_PAGE_CONTEXT_KEY)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useAddressPageContext must be used within a page that calls useAddressPage()')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
import { getProvince, tableAddress } from '@/api/address'
|
||||||
|
import type { Address } from '@/models/address'
|
||||||
|
import { loading, loadingMore } from '@/utils'
|
||||||
|
import { runOnceAsync, resetOnceAsyncByPrefix } from '@/composables/useOnceAsync'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared address page datasets — mobile and desktop views read this module state only.
|
||||||
|
*/
|
||||||
|
let initPromise: Promise<void> | null = null
|
||||||
|
const initialized = ref(false)
|
||||||
|
|
||||||
|
export const addressItems = ref<Address[]>([])
|
||||||
|
export const addressProvinceList = ref<any[]>([])
|
||||||
|
|
||||||
|
export function isAddressPageDataInitialized() {
|
||||||
|
return initialized.value
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function refreshAddressItems() {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const data = await tableAddress({ page: 0, limit: 150 })
|
||||||
|
addressItems.value = data.rows
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Address list refresh error:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
loadingMore.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAddressItems() {
|
||||||
|
const data = await tableAddress({ page: 0, limit: 150 })
|
||||||
|
addressItems.value = data.rows
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadProvinces() {
|
||||||
|
addressProvinceList.value = await getProvince()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initAddressPageData(force = false) {
|
||||||
|
if (!force && initialized.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!force && initPromise) {
|
||||||
|
return initPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
initPromise = (async () => {
|
||||||
|
try {
|
||||||
|
if (force) {
|
||||||
|
resetAddressPageData()
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
runOnceAsync('address:items', () => loadAddressItems(), force),
|
||||||
|
runOnceAsync('address:provinces', () => loadProvinces(), force),
|
||||||
|
])
|
||||||
|
|
||||||
|
initialized.value = true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Address page initialization error:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
initPromise = null
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
return initPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetAddressPageData() {
|
||||||
|
initialized.value = false
|
||||||
|
initPromise = null
|
||||||
|
resetOnceAsyncByPrefix('address:')
|
||||||
|
addressItems.value = []
|
||||||
|
addressProvinceList.value = []
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<div class="daddr">
|
||||||
|
<div class="daddr__container">
|
||||||
|
<header class="daddr-topbar">
|
||||||
|
<p class="daddr-topbar__eyebrow">حساب کاربری</p>
|
||||||
|
<h1 class="daddr-topbar__title">آدرسها</h1>
|
||||||
|
<p class="daddr-topbar__subtitle">آدرسهای تحویل سفارش خود را مدیریت کنید</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="isBazretalaProject()"
|
||||||
|
class="daddr-notice"
|
||||||
|
>
|
||||||
|
جهت تحویل فیزیکی محصول در تهران و قم ازطریق معرف اقدام فرمائید یا با شماره امورمالی و حسابداری 09925202099 درارتباط باشید.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="daddr-grid">
|
||||||
|
<section class="daddr-surface daddr-form">
|
||||||
|
<div class="daddr-form__header">
|
||||||
|
<h2 class="daddr-form__title">{{ editId ? 'ویرایش آدرس' : 'ثبت آدرس جدید' }}</h2>
|
||||||
|
<button
|
||||||
|
v-if="editId"
|
||||||
|
type="button"
|
||||||
|
class="daddr-form__reset"
|
||||||
|
@click="openAdd()"
|
||||||
|
>
|
||||||
|
آدرس جدید
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<AddressForm variant="desktop" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="daddr-surface daddr-list">
|
||||||
|
<div class="daddr-list__header">
|
||||||
|
<h3 class="daddr-list__title">آدرسهای ثبتشده</h3>
|
||||||
|
<span class="daddr-list__count">{{ items.length }} مورد</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="daddr-list__skeletons">
|
||||||
|
<TransactionSkeleton v-for="i in 6" :key="i" class="mt-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="items.length" class="daddr-list__items">
|
||||||
|
<list-anim>
|
||||||
|
<Item
|
||||||
|
v-for="item in items"
|
||||||
|
:key="`desktop-${item.id}`"
|
||||||
|
:item="item"
|
||||||
|
variant="desktop"
|
||||||
|
@edit="openEdit"
|
||||||
|
@refresh="initItems"
|
||||||
|
/>
|
||||||
|
</list-anim>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="daddr-empty">
|
||||||
|
<BasicIcon icon="vuesax-linear:location" size="36" />
|
||||||
|
<p>هنوز آدرسی ثبت نشده است.</p>
|
||||||
|
<button type="button" class="daddr-empty__btn" @click="openAdd()">
|
||||||
|
افزودن اولین آدرس
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
|
import ListAnim from '@/components/ListAnim.vue'
|
||||||
|
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
|
||||||
|
import AddressForm from '../components/AddressForm.vue'
|
||||||
|
import Item from '../components/Item.vue'
|
||||||
|
import { useAddressPageContext } from '../composables/useAddressPage'
|
||||||
|
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
loading,
|
||||||
|
editId,
|
||||||
|
isBazretalaProject,
|
||||||
|
openAdd,
|
||||||
|
openEdit,
|
||||||
|
initItems,
|
||||||
|
} = useAddressPageContext()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
/* Premium desktop address page — scoped to desktop shell only */
|
||||||
|
|
||||||
|
html.app-layout--desktop {
|
||||||
|
.daddr {
|
||||||
|
min-height: 100%;
|
||||||
|
background: #f6f7f9;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.daddr__container {
|
||||||
|
max-width: 1180px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 28px 36px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.daddr-topbar {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
|
||||||
|
&__eyebrow {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0 0 6px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 520px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.daddr-notice {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.15);
|
||||||
|
background: rgba(59, 130, 246, 0.06);
|
||||||
|
color: @App-primary-color;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.8;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
.daddr-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 380px) minmax(0, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.daddr-surface {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.daddr-form {
|
||||||
|
padding: 24px;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__reset {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: inherit;
|
||||||
|
padding: 6px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s ease, color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: @App-primary-color;
|
||||||
|
color: @App-primary-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__fields {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__address-item {
|
||||||
|
margin-bottom: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__submit {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.daddr-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 420px;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 20px 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__count {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6b7280;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__items {
|
||||||
|
padding: 16px 24px 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__skeletons {
|
||||||
|
padding: 16px 24px 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.daddr-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 12px 16px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
border: 1px solid #eef0f3;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fafbfc;
|
||||||
|
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #dbe0e6;
|
||||||
|
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__address {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
color: #9ca3af;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__value {
|
||||||
|
color: #374151;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s ease, background 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--danger:hover {
|
||||||
|
border-color: #fecaca;
|
||||||
|
background: #fef2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--edit:hover {
|
||||||
|
border-color: #bbf7d0;
|
||||||
|
background: #f0fdf4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid #eef0f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__date {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.daddr-empty {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 48px 24px;
|
||||||
|
color: #6b7280;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__btn {
|
||||||
|
margin-top: 4px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: @App-primary-color;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: inherit;
|
||||||
|
padding: 10px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.92;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1023.98px) {
|
||||||
|
.daddr-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,257 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div v-if="!isDesktop">
|
||||||
<div class="bg-white py-3 px-3 text-primary font-bold drop-shadow wrap-fix flex z-10">
|
<MobileAddressView />
|
||||||
<div @click="router.back()">
|
|
||||||
<BasicIcon icon="vuesax-linear:arrow-right-1" />
|
|
||||||
</div>
|
|
||||||
<div class="mr-2">آدرس ها</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<img :src="img" alt="" class="mt-2 w-full absolute bottom-0" />
|
|
||||||
</div>
|
|
||||||
<!-- <a-spin :spinning="loading">-->
|
|
||||||
<div ref="el" :class="loading? 'h-screen': ''" class="px-3 pt-16 pb-2 overflow-y-scroll"
|
|
||||||
style="max-height: calc(100vh - 50px)">
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="isBazretalaProject()"
|
|
||||||
class="text-justify text-sm text-primary mb-4 px-1 leading-7"
|
|
||||||
>
|
|
||||||
جهت تحویل فیزیکی محصول در تهران و قم ازطریق معرف اقدام فرمائید یا با شماره امورمالی و حسابداری 09925202099 درارتباط باشید.
|
|
||||||
</div>
|
|
||||||
<list-anim v-if="!loading" class="pb-[calc(var(--app-bottom-chrome-height)+var(--app-add-card-height)+1rem)]">
|
|
||||||
<Item v-for="item in items" :key="item.id" :item="item" @edit="openEdit" @refresh="initItems" />
|
|
||||||
<!-- <TransactionSkeleton v-if="loadingMore" class="mt-1 mb-8" />-->
|
|
||||||
</list-anim>
|
|
||||||
<div v-if="loading">
|
|
||||||
<TransactionSkeleton v-for="i in 13" :key="i" class="mt-1" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<!-- </a-spin>-->
|
|
||||||
<BasicActionSheet v-model:show="addAddressVisible" class="h-80vh" >
|
|
||||||
<a-form ref="formRef" :model="address" class="!mt-5" name="basic"
|
|
||||||
@finish="ok()">
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<div class="font-bold text-lg mb-2">آدرس</div>
|
|
||||||
<div class="flex flex-col gap-4 pb-20 w-full h-60vh overflow-y-scroll">
|
|
||||||
<a-form-item label="استان" name="province" :rules="[required()]">
|
|
||||||
<BasicSelect
|
|
||||||
v-model:value="address.province"
|
|
||||||
:items="provinceList"
|
|
||||||
@change="changeProvince"
|
|
||||||
valueName="title"
|
|
||||||
keyName="title"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="شهر" name="city" :rules="[required()]">
|
|
||||||
<BasicSelect
|
|
||||||
:disabled="!cityList"
|
|
||||||
v-model:value="address.city"
|
|
||||||
valueName="title"
|
|
||||||
:items="cityList"
|
|
||||||
keyName="title"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
|
|
||||||
<a-form-item
|
|
||||||
label="کد پستی" name="zip" :rules="[required()]">
|
|
||||||
<BasicInput placeholder="کد پستی را وارد نمایید" v-model:value="address.zip" />
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item
|
|
||||||
label="تلفن تماس" name="phone" :rules="[required()]"
|
|
||||||
>
|
|
||||||
<BasicInput placeholder="تلفن تماس را وارد نمایید" v-model:value="address.phone" />
|
|
||||||
</a-form-item>
|
|
||||||
<!-- <FormItem label="کد پستی" name="">
|
|
||||||
<Input placeholder="کد پستی را وارد نمایید" />
|
|
||||||
</FormItem>-->
|
|
||||||
<a-form-item
|
|
||||||
label="آدرس کامل"
|
|
||||||
name="address"
|
|
||||||
:rules="[required()]"
|
|
||||||
>
|
|
||||||
<BasicTextArea placeholder="آدرس کامل را وارد نمایید" v-model:value="address.address" />
|
|
||||||
</a-form-item>
|
|
||||||
<!-- <div class=" w-full pb-5 !text-white flex justify-center items-center">-->
|
|
||||||
<BasicButton class=" !text-white px-3 pb-4rem" type="primary" @click="handleOk">ثبت آدرس
|
|
||||||
</BasicButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- </div>-->
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</a-form>
|
|
||||||
|
|
||||||
|
<BasicActionSheet v-model:show="addAddressVisible" class="h-80vh">
|
||||||
|
<AddressForm />
|
||||||
</BasicActionSheet>
|
</BasicActionSheet>
|
||||||
<!-- <div class="bg-primary w-3rem h-3rem flex items-center justify-center rounded-lg fixed bottom-12vh left-8vw"
|
|
||||||
@click="openAdd()">
|
|
||||||
<BasicIcon icon="vuesax-linear:add" color="white" />
|
|
||||||
</div>-->
|
|
||||||
<Teleport to="body">
|
|
||||||
<div
|
|
||||||
v-if="!addAddressVisible"
|
|
||||||
class="add-address app-dock-add-bar"
|
|
||||||
@click="openAdd()"
|
|
||||||
>
|
|
||||||
<div>افزودن آدرس جدید</div>
|
|
||||||
<BasicIcon icon="vuesax-linear:add" />
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<DesktopAddressView />
|
||||||
|
|
||||||
|
<BasicModal
|
||||||
|
v-model:visible="addAddressVisible"
|
||||||
|
:title="editId ? 'ویرایش آدرس' : 'ثبت آدرس جدید'"
|
||||||
|
width="520px"
|
||||||
|
destroy-on-close
|
||||||
|
custom-footer
|
||||||
|
>
|
||||||
|
<AddressForm variant="desktop" />
|
||||||
|
<template #footer>
|
||||||
|
<div></div>
|
||||||
|
</template>
|
||||||
|
</BasicModal>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { createPriceTrigger, getPriceTriggers, updatePriceTrigger } from '@/api/triggerPrice'
|
|
||||||
|
|
||||||
const radioStyle = reactive({
|
|
||||||
display: '',
|
|
||||||
height: '30px',
|
|
||||||
lineHeight: '30px'
|
|
||||||
})
|
|
||||||
import type { SelectItem } from '@/models/SelectItem'
|
|
||||||
|
|
||||||
// import Item from "./components/Item.vue";
|
|
||||||
/*import {lengthValidation, required} from "@/utils/inputRules";
|
|
||||||
import {loading} from "@/utils";
|
|
||||||
import BasicButton from "@/components/BasicPacks/BasicButton.vue";
|
|
||||||
import BasicActionSheet from "@/components/BasicPacks/BasicActionSheet.vue";
|
|
||||||
import BasicInput from "@/components/BasicPacks/BasicInput.vue";*/
|
|
||||||
import { onMounted, reactive, ref } from 'vue'
|
|
||||||
import type { TableEngineInput, TableOutput } from '@/models/table'
|
|
||||||
import { loading, loadingMore } from '@/utils'
|
|
||||||
// import {getOrders} from "@/api/order";
|
|
||||||
import { getTransactionTable } from '@/api/transaction'
|
|
||||||
import type { Transaction } from '@/models/transaction'
|
|
||||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
|
||||||
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
||||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
import BasicModal from '@/components/BasicPacks/BasicModal.vue'
|
||||||
import BaseDatePicker from '@/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue'
|
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||||
import { useInfiniteScroll, useScroll } from '@vueuse/core'
|
import { useAddressPage } from './composables/useAddressPage'
|
||||||
import ListAnim from '@/components/ListAnim.vue'
|
import AddressForm from './components/AddressForm.vue'
|
||||||
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
|
import DesktopAddressView from './desktop/DesktopAddressView.vue'
|
||||||
import { DotLottieVue } from '@lottiefiles/dotlottie-vue'
|
import MobileAddressView from './mobile/MobileAddressView.vue'
|
||||||
import { mobileValidation, required } from '@/utils/inputRules'
|
|
||||||
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
|
||||||
import type { User } from '@/models/login'
|
|
||||||
import { type FormInstance, message } from 'ant-design-vue'
|
|
||||||
import type { PriceTrigger } from '@/models/PriceTrigger'
|
|
||||||
import BasicSelect from '@/components/BasicPacks/BasicSelect.vue'
|
|
||||||
import Item from './components/Item.vue'
|
|
||||||
import { cloneDeep } from 'lodash'
|
|
||||||
import router from '@/router'
|
|
||||||
import type { Address } from '@/models/address'
|
|
||||||
import { createAddress, getCity, getProvince, tableAddress, updateAddress } from '@/api/address'
|
|
||||||
import BasicTextArea from '@/components/BasicPacks/BasicTextArea.vue'
|
|
||||||
import img from "@/assets/img/user_ly1.svg";
|
|
||||||
import { isBazretalaProject } from '@/utils/config'
|
|
||||||
|
|
||||||
const provinceList = ref<any[]>([])
|
const isDesktop = useDesktopBreakpoint()
|
||||||
const cityList = ref<any[]>()
|
|
||||||
const items = ref<PriceTrigger[]>([])
|
|
||||||
const addAddressVisible = ref<boolean>(false)
|
|
||||||
const editId = ref<number | undefined>(undefined)
|
|
||||||
const el = ref()
|
|
||||||
|
|
||||||
const address = ref<Address>({
|
|
||||||
city: '',
|
|
||||||
province: '',
|
|
||||||
phone: '',
|
|
||||||
address: '',
|
|
||||||
zip: '',
|
|
||||||
default_address: false
|
|
||||||
})
|
|
||||||
const formRef = ref<FormInstance>()
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await initItems()
|
|
||||||
provinceList.value = await getProvince()
|
|
||||||
})
|
|
||||||
|
|
||||||
function resetFields() {
|
|
||||||
editId.value = undefined
|
|
||||||
address.value = {
|
|
||||||
city: '',
|
|
||||||
province: '',
|
|
||||||
phone: '',
|
|
||||||
address: '',
|
|
||||||
zip: '',
|
|
||||||
default_address: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openAdd() {
|
|
||||||
resetFields()
|
|
||||||
editId.value = undefined
|
|
||||||
addAddressVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleOk = (e: MouseEvent) => {
|
|
||||||
formRef.value?.validate().then(async () => {
|
|
||||||
editId.value ? await updateAddress(editId.value, address.value) : await createAddress(address.value)
|
|
||||||
await initItems()
|
|
||||||
addAddressVisible.value = false
|
|
||||||
message.success('آدرس با موفقیت ثبت شد')
|
|
||||||
resetFields()
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.log('error', error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initItems() {
|
|
||||||
try {
|
|
||||||
items.value = []
|
|
||||||
const data = await tableAddress({ page: 0, limit: 150 })
|
|
||||||
items.value = data.rows
|
|
||||||
|
|
||||||
// console.log(items.value)
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
loadingMore.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const changeProvince = async (obj: any) => {
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
cityList.value = undefined
|
|
||||||
address.value.city = undefined
|
|
||||||
cityList.value = await getCity(obj.id)
|
|
||||||
} catch (e) {
|
|
||||||
message.error('خطا در دریافت شهرها')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openEdit(val: PriceTrigger) {
|
|
||||||
address.value = cloneDeep(val)
|
|
||||||
editId.value = address.value.id
|
|
||||||
addAddressVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ok() {
|
|
||||||
formRef.value?.validateFields().then(async () => {
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
editId.value ? await updatePriceTrigger(editId.value, address.value) : await createPriceTrigger(address.value)
|
|
||||||
message.success('آدرس با موفقیت ایجاد شد')
|
|
||||||
await initItems()
|
|
||||||
addAddressVisible.value = false
|
|
||||||
//
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const { addAddressVisible, editId } = useAddressPage()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="bg-white py-3 px-3 text-primary font-bold drop-shadow wrap-fix flex z-10">
|
||||||
|
<div @click="router.back()">
|
||||||
|
<BasicIcon icon="vuesax-linear:arrow-right-1" />
|
||||||
|
</div>
|
||||||
|
<div class="mr-2">آدرس ها</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<img :src="img" alt="" class="mt-2 w-full absolute bottom-0" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
:class="loading ? 'h-screen' : ''"
|
||||||
|
class="px-3 pt-16 pb-2 overflow-y-scroll"
|
||||||
|
style="max-height: calc(100vh - 50px)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="isBazretalaProject()"
|
||||||
|
class="text-justify text-sm text-primary mb-4 px-1 leading-7"
|
||||||
|
>
|
||||||
|
جهت تحویل فیزیکی محصول در تهران و قم ازطریق معرف اقدام فرمائید یا با شماره امورمالی و حسابداری 09925202099 درارتباط باشید.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!loading"
|
||||||
|
class="pb-[calc(var(--app-bottom-chrome-height)+var(--app-add-card-height)+1rem)]"
|
||||||
|
>
|
||||||
|
<Item
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.id"
|
||||||
|
:item="item"
|
||||||
|
@edit="openEdit"
|
||||||
|
@refresh="initItems"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="loading">
|
||||||
|
<TransactionSkeleton v-for="i in 13" :key="i" class="mt-1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Teleport to="body">
|
||||||
|
<div
|
||||||
|
v-if="!addAddressVisible"
|
||||||
|
class="add-address app-dock-add-bar"
|
||||||
|
@click="openAdd()"
|
||||||
|
>
|
||||||
|
<div>افزودن آدرس جدید</div>
|
||||||
|
<BasicIcon icon="vuesax-linear:add" />
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
|
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
|
||||||
|
import img from '@/assets/img/user_ly1.svg'
|
||||||
|
import router from '@/router'
|
||||||
|
import Item from '../components/Item.vue'
|
||||||
|
import { useAddressPageContext } from '../composables/useAddressPage'
|
||||||
|
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
loading,
|
||||||
|
addAddressVisible,
|
||||||
|
isBazretalaProject,
|
||||||
|
openAdd,
|
||||||
|
openEdit,
|
||||||
|
initItems,
|
||||||
|
} = useAddressPageContext()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { inject, onMounted, provide, ref, type InjectionKey, type Ref } from 'vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import { getEarnMoney } from '@/api/earnMoney'
|
||||||
|
import type { EarnMoney } from '@/models/earnMoney'
|
||||||
|
import { useUserStore } from '@/store/userStore'
|
||||||
|
import { useConfigStore } from '@/store/configStore'
|
||||||
|
import { getReferralShareText, getReferralShareTitle } from '@/utils/config'
|
||||||
|
import { loading } from '@/utils'
|
||||||
|
|
||||||
|
export interface EarnMoneyPageContext {
|
||||||
|
data: Ref<EarnMoney | undefined>
|
||||||
|
userStore: ReturnType<typeof useUserStore>
|
||||||
|
nameProject: string
|
||||||
|
isTransaction: Ref<boolean>
|
||||||
|
loading: typeof loading
|
||||||
|
setIsTransaction: (value: boolean) => void
|
||||||
|
copyCode: () => void
|
||||||
|
shareCode: () => void
|
||||||
|
loadEarnMoney: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EARN_MONEY_PAGE_CONTEXT_KEY: InjectionKey<EarnMoneyPageContext> =
|
||||||
|
Symbol('earnMoneyPageContext')
|
||||||
|
|
||||||
|
export function useEarnMoneyPage() {
|
||||||
|
const data = ref<EarnMoney>()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const nameProject = useConfigStore().config.name_fa
|
||||||
|
const isTransaction = ref<boolean>(false)
|
||||||
|
|
||||||
|
function setIsTransaction(value: boolean) {
|
||||||
|
isTransaction.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyCode() {
|
||||||
|
const val = userStore.userInfo?.referral_id
|
||||||
|
navigator.clipboard.writeText(val)
|
||||||
|
message.info('کد رونوشت شد')
|
||||||
|
}
|
||||||
|
|
||||||
|
function shareCode() {
|
||||||
|
const val = getReferralShareText({
|
||||||
|
userName: userStore.userInfo?.name,
|
||||||
|
projectName: nameProject,
|
||||||
|
referralId: userStore.userInfo?.referral_id,
|
||||||
|
})
|
||||||
|
navigator.share({
|
||||||
|
title: getReferralShareTitle(nameProject),
|
||||||
|
text: val,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadEarnMoney() {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
data.value = await getEarnMoney()
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadEarnMoney()
|
||||||
|
})
|
||||||
|
|
||||||
|
const context: EarnMoneyPageContext = {
|
||||||
|
data,
|
||||||
|
userStore,
|
||||||
|
nameProject,
|
||||||
|
isTransaction,
|
||||||
|
loading,
|
||||||
|
setIsTransaction,
|
||||||
|
copyCode,
|
||||||
|
shareCode,
|
||||||
|
loadEarnMoney,
|
||||||
|
}
|
||||||
|
|
||||||
|
provide(EARN_MONEY_PAGE_CONTEXT_KEY, context)
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEarnMoneyPageContext() {
|
||||||
|
const context = inject(EARN_MONEY_PAGE_CONTEXT_KEY)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useEarnMoneyPageContext must be used within a page that calls useEarnMoneyPage()')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dearn">
|
||||||
|
<div class="dearn__container">
|
||||||
|
<header class="dearn-topbar">
|
||||||
|
<p class="dearn-topbar__eyebrow">بازاریابی</p>
|
||||||
|
<h1 class="dearn-topbar__title">کسب درآمد</h1>
|
||||||
|
<p class="dearn-topbar__subtitle">با دعوت دوستان، پاداش دریافت کنید</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="dearn-surface dearn-referral">
|
||||||
|
<div class="dearn-referral__header">
|
||||||
|
<div class="dearn-referral__icon">
|
||||||
|
<img :src="coin" alt="" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="dearn-referral__title">دعوت دوستان</h2>
|
||||||
|
<p class="dearn-referral__desc">هر دعوت موفق 10 سوت طلا هدیه</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dearn-referral__code">
|
||||||
|
<span class="dearn-referral__code-label">کد بازاریابی</span>
|
||||||
|
<span class="dearn-referral__code-value">{{ userStore.userInfo.referral_id }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dearn-referral__actions">
|
||||||
|
<BasicButton
|
||||||
|
icon="vuesax-linear:share"
|
||||||
|
type="primary"
|
||||||
|
class="dearn-referral__btn"
|
||||||
|
icon-size="18"
|
||||||
|
@click.stop="shareCode()"
|
||||||
|
>
|
||||||
|
اشتراک گذاری
|
||||||
|
</BasicButton>
|
||||||
|
<BasicButton
|
||||||
|
icon="vuesax-linear:copy"
|
||||||
|
class="dearn-referral__btn"
|
||||||
|
icon-size="18"
|
||||||
|
@click.stop="copyCode()"
|
||||||
|
>
|
||||||
|
رونوشت
|
||||||
|
</BasicButton>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="dearn-stats">
|
||||||
|
<div class="dearn-stats__card dearn-stats__card--users">
|
||||||
|
<span class="dearn-stats__value">{{ data?.usersSum ?? 0 }} عدد</span>
|
||||||
|
<span class="dearn-stats__label">تعداد زیر مجموعه</span>
|
||||||
|
</div>
|
||||||
|
<div class="dearn-stats__card dearn-stats__card--gifts">
|
||||||
|
<span class="dearn-stats__value">{{ data?.giftsSum ?? 0 }} سوت</span>
|
||||||
|
<span class="dearn-stats__label">میزان کل پاداش ها</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dearn-tabs">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="dearn-tabs__btn"
|
||||||
|
:class="{ 'dearn-tabs__btn--active': isTransaction }"
|
||||||
|
@click="setIsTransaction(true)"
|
||||||
|
>
|
||||||
|
پاداش ها
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="dearn-tabs__btn"
|
||||||
|
:class="{ 'dearn-tabs__btn--active': !isTransaction }"
|
||||||
|
@click="setIsTransaction(false)"
|
||||||
|
>
|
||||||
|
فهرست زیر مجموعه
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="dearn-surface dearn-list">
|
||||||
|
<div v-if="loading" class="dearn-list__loading">
|
||||||
|
<TransactionSkeleton v-for="i in 6" :key="i" class="mt-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div v-if="isTransaction" class="dearn-list__items">
|
||||||
|
<GiftCard v-for="(val, i) in data?.gifts" :key="i" :item="val" />
|
||||||
|
<div v-if="!data?.gifts?.length" class="dearn-empty">
|
||||||
|
<BasicIcon icon="vuesax-linear:gift" size="36" />
|
||||||
|
<p>هنوز پاداشی ثبت نشده است.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="dearn-list__items">
|
||||||
|
<EarnCard v-for="(val, i) in data?.users" :key="i" :item="val" />
|
||||||
|
<div v-if="!data?.users?.length" class="dearn-empty">
|
||||||
|
<BasicIcon icon="vuesax-linear:people" size="36" />
|
||||||
|
<p>هنوز زیرمجموعهای ثبت نشده است.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import coin from '@/assets/img/coin.png'
|
||||||
|
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||||
|
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
|
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
|
||||||
|
import EarnCard from '../components/EarnCard.vue'
|
||||||
|
import GiftCard from '../components/GiftCard.vue'
|
||||||
|
import { useEarnMoneyPageContext } from '../composables/useEarnMoneyPage'
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
userStore,
|
||||||
|
isTransaction,
|
||||||
|
loading,
|
||||||
|
setIsTransaction,
|
||||||
|
copyCode,
|
||||||
|
shareCode,
|
||||||
|
} = useEarnMoneyPageContext()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
/* Premium desktop earn money page */
|
||||||
|
|
||||||
|
.dearn {
|
||||||
|
min-height: 100%;
|
||||||
|
background: #f6f7f9;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dearn__container {
|
||||||
|
max-width: 960px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 28px 36px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dearn-topbar {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
|
||||||
|
&__eyebrow {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0 0 6px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dearn-surface {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dearn-referral {
|
||||||
|
padding: 24px 28px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #eef0f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #dbeafe;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__desc {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__code {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #f9fafb;
|
||||||
|
border: 1px dashed #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__code-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__code-value {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: @App-primary-color;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__btn {
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dearn-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
&__card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 20px 16px;
|
||||||
|
border-radius: 14px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&--users {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--gifts {
|
||||||
|
background: #fef9c3;
|
||||||
|
color: #ca8a04;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__value {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dearn-tabs {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
&__btn {
|
||||||
|
min-width: 140px;
|
||||||
|
padding: 10px 18px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6b7280;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
background: #fff;
|
||||||
|
color: @App-primary-color;
|
||||||
|
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dearn-list {
|
||||||
|
padding: 20px 24px 24px;
|
||||||
|
|
||||||
|
&__loading,
|
||||||
|
&__items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dearn-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 48px 24px;
|
||||||
|
color: #9ca3af;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg,
|
||||||
|
.basic-icon {
|
||||||
|
color: #d1d5db;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1023.98px) {
|
||||||
|
.dearn-stats {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dearn-referral__actions {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,159 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="earn-money-page-root">
|
||||||
<div class="bg-white w-full py-3 px-3 text-primary font-bold drop-shadow flex">
|
<DesktopEarnMoneyView v-show="isDesktop" />
|
||||||
<div @click="router.back()">
|
<MobileEarnMoneyView v-show="!isDesktop" />
|
||||||
<BasicIcon icon="vuesax-linear:arrow-right-1" />
|
|
||||||
</div>
|
|
||||||
<div class="mr-2">کسب درآمد</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="p-4">
|
|
||||||
<div class="text-center p-3 border-solid border-1 rounded-[8px] border-primary ">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class=" bg-[#DBEAFE] h-10 w-10 rounded-100% flex justify-center items-center">
|
|
||||||
<img :src="coin" />
|
|
||||||
</div>
|
|
||||||
<div class="mr-4">
|
|
||||||
<div class="flex ">
|
|
||||||
<div class="font-bold">دعوت دوستان</div>
|
|
||||||
<!-- <div class="text-xs ">راهنمای کسب درآمد</div>-->
|
|
||||||
</div>
|
|
||||||
<div class="text-center mt-2 text-xs">هر دعوت موفق 10 سوت طلا هدیه</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-center mt-4">
|
|
||||||
<div class="text-[10px] content-center ">کد بازاریابی</div>
|
|
||||||
<div class="mr-2 font-bold">{{userStore.userInfo.referral_id}}</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex mt-4 gap-2">
|
|
||||||
<BasicButton icon="vuesax-linear:share" size="small" class="flex-1" icon-size="18" @click.stop="shareCode()">
|
|
||||||
اشتراک گذاری
|
|
||||||
</BasicButton>
|
|
||||||
<BasicButton icon="vuesax-linear:copy" size="small" class="flex-1" icon-size="18" @click.stop="copyCode()">
|
|
||||||
رونوشت
|
|
||||||
</BasicButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 gap-4 mt-4 ">
|
|
||||||
<div class="text-center bg-[#DBEAFE] p-2 rounded-[8px]">
|
|
||||||
<div class="font-bold text-[#2563EB] text-md">
|
|
||||||
{{data?.usersSum}} عدد
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-[#2563EB] mt-2">تعداد زیر مجموعه</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-center bg-[#FEF9C3] p-2 rounded-[8px]">
|
|
||||||
<div class="font-bold text-[#CA8A04] text-md">
|
|
||||||
{{data?.giftsSum}} سوت
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-[#CA8A04] mt-2">میزان کل پاداش ها</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-evenly w-full px-2 h-11 items-center bg-gray-50 relative rounded-lg mt-6 ">
|
|
||||||
<div :class="isTransaction?'text-primary bg-white h-8 w-1/2 rounded-lg':'text-gray-400'" class="w-full text-center z-2 flex items-center justify-center"
|
|
||||||
@click.stop="setIsTransaction(true) ">
|
|
||||||
پاداش ها
|
|
||||||
</div>
|
|
||||||
<div :class="!isTransaction?'text-primary bg-white h-8 w-1/2 rounded-lg':'text-gray-400'" class="w-full text-center z-2 flex items-center justify-center"
|
|
||||||
@click.stop="setIsTransaction(false) ">
|
|
||||||
فهرست زیر مجموعه
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- //**************************************-->
|
|
||||||
<!-- <div class="flex mt-6">-->
|
|
||||||
<!-- <div class="bg-[#0A1B37] p-2 rounded-[8px] w-full">-->
|
|
||||||
<!-- <div class="text-white text-center">فهرست زیرمجموعه</div>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- <div class="bg-[#0A1B37] p-2 rounded-[8px] w-full mr-2">-->
|
|
||||||
<!-- <div class="text-white text-center">پاداش ها</div>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<div class="border-solid border-1 border-[#E5E7EB] mt-6 "></div>
|
|
||||||
<div v-if="isTransaction">
|
|
||||||
<GiftCard v-for="(val,i) in data?.gifts" :key="i" :item="val"/>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<EarnCard v-for="(val,i) in data?.users" :key="i" :item="val"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||||
|
import { useEarnMoneyPage } from './composables/useEarnMoneyPage'
|
||||||
|
import DesktopEarnMoneyView from './desktop/DesktopEarnMoneyView.vue'
|
||||||
|
import MobileEarnMoneyView from './mobile/MobileEarnMoneyView.vue'
|
||||||
|
|
||||||
import router from '@/router'
|
const isDesktop = useDesktopBreakpoint()
|
||||||
import coin from '@/assets/img/coin.png'
|
|
||||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
|
||||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
|
||||||
import { useUserStore } from '@/store/userStore'
|
|
||||||
import { type FormInstance, message } from 'ant-design-vue'
|
|
||||||
import { onMounted, reactive, ref } from 'vue'
|
|
||||||
import type { EarnMoney } from '@/models/earnMoney'
|
|
||||||
import { loading } from '@/utils'
|
|
||||||
import { getEarnMoney } from '@/api/earnMoney'
|
|
||||||
import EarnCard from '@/views/base/earnMoney/components/EarnCard.vue'
|
|
||||||
import TransactionCard from '@/views/base/transactions/components/Item.vue'
|
|
||||||
import GiftCard from '@/views/base/earnMoney/components/GiftCard.vue'
|
|
||||||
import {useConfigStore} from "@/store/configStore";
|
|
||||||
import {getReferralShareText, getReferralShareTitle} from "@/utils/config";
|
|
||||||
|
|
||||||
const data = ref<EarnMoney>()
|
|
||||||
|
|
||||||
const userStore = useUserStore()
|
|
||||||
const nameProject= useConfigStore().config.name_fa
|
|
||||||
|
|
||||||
const isTransaction = ref<boolean>(false)
|
|
||||||
|
|
||||||
function setIsTransaction(value: boolean) {
|
|
||||||
isTransaction.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyCode() {
|
|
||||||
// nextTick(() => {
|
|
||||||
const val = userStore.userInfo?.referral_id
|
|
||||||
navigator.clipboard.writeText(val)
|
|
||||||
message.info('کد رونوشت شد')
|
|
||||||
// })
|
|
||||||
}
|
|
||||||
|
|
||||||
function shareCode() {
|
|
||||||
const val = getReferralShareText({
|
|
||||||
userName: userStore.userInfo?.name,
|
|
||||||
projectName: nameProject,
|
|
||||||
referralId: userStore.userInfo?.referral_id,
|
|
||||||
})
|
|
||||||
navigator.share({
|
|
||||||
title: getReferralShareTitle(nameProject),
|
|
||||||
text: val
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function earnMoney() {
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
data.value = await getEarnMoney()
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function subsetList(){
|
|
||||||
|
|
||||||
}
|
|
||||||
onMounted(async () => {
|
|
||||||
earnMoney()
|
|
||||||
})
|
|
||||||
|
|
||||||
function useStyle() {
|
|
||||||
if (data.value?.User) return 'right: 2%;'
|
|
||||||
// if (isbuy.value) return 'animation-direction: normal'
|
|
||||||
// else return 'animation-direction: reverse'
|
|
||||||
else return ' right: 98%; transform: translate(100%, 0);'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
useEarnMoneyPage()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.earn-money-page-root {
|
||||||
</style>
|
min-height: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="bg-white w-full py-3 px-3 text-primary font-bold drop-shadow flex">
|
||||||
|
<div @click="router.back()">
|
||||||
|
<BasicIcon icon="vuesax-linear:arrow-right-1" />
|
||||||
|
</div>
|
||||||
|
<div class="mr-2">کسب درآمد</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="text-center p-3 border-solid border-1 rounded-[8px] border-primary">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="bg-[#DBEAFE] h-10 w-10 rounded-100% flex justify-center items-center">
|
||||||
|
<img :src="coin" alt="" />
|
||||||
|
</div>
|
||||||
|
<div class="mr-4">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="font-bold">دعوت دوستان</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center mt-2 text-xs">هر دعوت موفق 10 سوت طلا هدیه</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center mt-4">
|
||||||
|
<div class="text-[10px] content-center">کد بازاریابی</div>
|
||||||
|
<div class="mr-2 font-bold">{{ userStore.userInfo.referral_id }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex mt-4 gap-2">
|
||||||
|
<BasicButton
|
||||||
|
icon="vuesax-linear:share"
|
||||||
|
size="small"
|
||||||
|
class="flex-1"
|
||||||
|
icon-size="18"
|
||||||
|
@click.stop="shareCode()"
|
||||||
|
>
|
||||||
|
اشتراک گذاری
|
||||||
|
</BasicButton>
|
||||||
|
<BasicButton
|
||||||
|
icon="vuesax-linear:copy"
|
||||||
|
size="small"
|
||||||
|
class="flex-1"
|
||||||
|
icon-size="18"
|
||||||
|
@click.stop="copyCode()"
|
||||||
|
>
|
||||||
|
رونوشت
|
||||||
|
</BasicButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4 mt-4">
|
||||||
|
<div class="text-center bg-[#DBEAFE] p-2 rounded-[8px]">
|
||||||
|
<div class="font-bold text-[#2563EB] text-md">
|
||||||
|
{{ data?.usersSum }} عدد
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-[#2563EB] mt-2">تعداد زیر مجموعه</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center bg-[#FEF9C3] p-2 rounded-[8px]">
|
||||||
|
<div class="font-bold text-[#CA8A04] text-md">
|
||||||
|
{{ data?.giftsSum }} سوت
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-[#CA8A04] mt-2">میزان کل پاداش ها</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex justify-evenly w-full px-2 h-11 items-center bg-gray-50 relative rounded-lg mt-6"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:class="isTransaction ? 'text-primary bg-white h-8 w-1/2 rounded-lg' : 'text-gray-400'"
|
||||||
|
class="w-full text-center z-2 flex items-center justify-center"
|
||||||
|
@click.stop="setIsTransaction(true)"
|
||||||
|
>
|
||||||
|
پاداش ها
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:class="!isTransaction ? 'text-primary bg-white h-8 w-1/2 rounded-lg' : 'text-gray-400'"
|
||||||
|
class="w-full text-center z-2 flex items-center justify-center"
|
||||||
|
@click.stop="setIsTransaction(false)"
|
||||||
|
>
|
||||||
|
فهرست زیر مجموعه
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="border-solid border-1 border-[#E5E7EB] mt-6"></div>
|
||||||
|
<div v-if="isTransaction">
|
||||||
|
<GiftCard v-for="(val, i) in data?.gifts" :key="i" :item="val" />
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<EarnCard v-for="(val, i) in data?.users" :key="i" :item="val" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import coin from '@/assets/img/coin.png'
|
||||||
|
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
|
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||||
|
import router from '@/router'
|
||||||
|
import EarnCard from '../components/EarnCard.vue'
|
||||||
|
import GiftCard from '../components/GiftCard.vue'
|
||||||
|
import { useEarnMoneyPageContext } from '../composables/useEarnMoneyPage'
|
||||||
|
|
||||||
|
const { data, userStore, isTransaction, setIsTransaction, copyCode, shareCode } =
|
||||||
|
useEarnMoneyPageContext()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="variant === 'desktop'
|
||||||
|
? 'deducate-content'
|
||||||
|
: 'mt-5 text-5 border-solid border-2 border-[#093D51] text-[#093D51] rounded-[12px] p-4'"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="variant === 'desktop'"
|
||||||
|
class="deducate-content__avatar-wrap"
|
||||||
|
>
|
||||||
|
<img :src="config.config.avatar" alt="" class="deducate-content__avatar" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="h-20vh flex justify-center"
|
||||||
|
>
|
||||||
|
<img :src="config.config.avatar" alt="" class="mb-20 h-15vh mt-5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :class="variant === 'desktop' ? 'deducate-content__body' : ''">
|
||||||
|
<div
|
||||||
|
:class="variant === 'desktop' ? 'deducate-content__section-title' : 'font-bold text-sm text-[#093D51]'"
|
||||||
|
>
|
||||||
|
شیوهٔ ورود و ثبت نام در گروسی گالری
|
||||||
|
</div>
|
||||||
|
<div :class="variant === 'desktop' ? 'deducate-content__text' : 'mt-5 text-xs leading-5'">
|
||||||
|
• برای ثبتنام در گروسی گالری، اگر قبلاً حساب کاربری دارید میتوانید از طریق پیامک وارد شوید.<br />
|
||||||
|
• اگر حساب کاربری ندارید، "از این قسمت" وارد صفحه ثبت نام شده و با وارد کردن نام و شماره تلفنی که با کد ملی شما مطابقت دارد، حساب کاربری بسازید.<br />
|
||||||
|
• الزامی به وارد کردن کد معرف نیست.
|
||||||
|
</div>
|
||||||
|
<img :src="img1" :class="variant === 'desktop' ? 'deducate-content__image' : ''" />
|
||||||
|
|
||||||
|
<div :class="variant === 'desktop' ? 'deducate-content__divider' : 'w-full h-1px my-3 bg-secondary'"></div>
|
||||||
|
<div :class="variant === 'desktop' ? 'deducate-content__section-title' : 'font-bold mt-4 text-sm text-[#093D51]'">
|
||||||
|
شارژ کیف پول
|
||||||
|
</div>
|
||||||
|
<div :class="variant === 'desktop' ? 'deducate-content__text' : 'mt-5 text-xs leading-5'">
|
||||||
|
• ابتدا کیف پول را در منو انتخاب کرده، مبلغ مورد نظر را وارد نموده و دکمه واریز را انتخاب کنید تا کیف پول شما شارژ شود.<br />
|
||||||
|
</div>
|
||||||
|
<img :src="img2" :class="variant === 'desktop' ? 'deducate-content__image' : ''" />
|
||||||
|
|
||||||
|
<div :class="variant === 'desktop' ? 'deducate-content__divider' : 'w-full h-1px my-3 bg-secondary'"></div>
|
||||||
|
<div :class="variant === 'desktop' ? 'deducate-content__section-title' : 'font-bold mt-4 text-sm text-[#093D51]'">
|
||||||
|
شیوهٔ خرید از گروسی گالری
|
||||||
|
</div>
|
||||||
|
<div :class="variant === 'desktop' ? 'deducate-content__text' : 'mt-5 text-xs leading-5'">
|
||||||
|
• سپس از بخش «خرید و فروش» انتخاب کنید که طلای آبشده میخواهید یا محصولات دیگر (شامل گوی، شمش یا مکعب).<br />
|
||||||
|
• توجه داشته باشید کارمزد طلای آبشده حدود ۲٪ است و کارمزد ریالی محصولات دیگر در پایین صفحه قیمت درج شده است.
|
||||||
|
</div>
|
||||||
|
<img :src="img4" :class="variant === 'desktop' ? 'deducate-content__image' : ''" />
|
||||||
|
|
||||||
|
<div :class="variant === 'desktop' ? 'deducate-content__divider' : 'w-full h-1px my-3 bg-secondary'"></div>
|
||||||
|
<div :class="variant === 'desktop' ? 'deducate-content__section-title' : 'font-bold mt-4 text-sm text-[#093D51]'">
|
||||||
|
شیوهٔ فروش در گروسی گالری
|
||||||
|
</div>
|
||||||
|
<div :class="variant === 'desktop' ? 'deducate-content__text' : 'mt-5 text-xs leading-5'">
|
||||||
|
• برای فروش محصولاتی که خریداری کردهاید، به بخش «خرید و فروش» مراجعه کرده، محصول موردنظر را انتخاب و گزینه فروش را بزنید تا مبلغ قابل دریافت نمایش داده شود.<br />
|
||||||
|
• مبلغ به کیف پول شما واریز میشود و از طریق پیام پشتیبانی میتوانید درخواست برداشت دهید.<br />
|
||||||
|
• اگر موجودی کیف پول بیش از ۱۰ میلیون تومان باشد، مبلغ تا پایان روز کاری بعد به حساب اعلامشده شما واریز خواهد شد.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :class="variant === 'desktop' ? 'deducate-content__divider' : 'w-full h-1px my-3 bg-secondary'"></div>
|
||||||
|
<div :class="variant === 'desktop' ? 'deducate-content__section-title' : 'font-bold mt-4 text-sm text-[#093D51]'">
|
||||||
|
شیوهٔ دریافت فیزیکی طلا (ارسال)
|
||||||
|
</div>
|
||||||
|
<div :class="variant === 'desktop' ? 'deducate-content__text' : 'mt-5 text-xs leading-5'">
|
||||||
|
• ابتدا موجودی محصول موردنظر را با پشتیبانی در واتساپ هماهنگ کنید.<br />
|
||||||
|
• سپس روی لوگو کلیک کرده، گزینه «برداشت فیزیکی» و بعد «درخواست جدید» را انتخاب کنید.<br />
|
||||||
|
• محصولات خریداریشده و تعداد آنها را مشخص کرده و آدرس خود را ثبت نمایید.<br />
|
||||||
|
• پشتیبانی عکس و وزن محصول را برای شما ارسال میکند.<br />
|
||||||
|
• ارسال بهصورت پسکرایه و بیمهشده انجام میشود.<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :class="variant === 'desktop' ? 'deducate-content__divider' : 'w-full h-1px my-3 bg-secondary'"></div>
|
||||||
|
<div :class="variant === 'desktop' ? 'deducate-content__section-title' : 'font-bold mt-4 text-sm text-[#093D51]'">
|
||||||
|
پاداش معرفی
|
||||||
|
</div>
|
||||||
|
<div :class="variant === 'desktop' ? 'deducate-content__text' : 'mt-5 text-xs leading-5'">
|
||||||
|
• در صفحه اصلی روی "تنظیمات" کلیک کرده و وارد حساب کاربری خود شوید.
|
||||||
|
</div>
|
||||||
|
<div :class="variant === 'desktop' ? 'deducate-content__text' : 'mt-5 text-xs leading-5'">
|
||||||
|
• سپس گزینه «اشتراکگذاری» یا «کپی لینک» را انتخاب کرده و لینک دعوت را برای دوستانتان ارسال کنید.<br />
|
||||||
|
• در صورتی که دوستان شما از طریق لینک شما ثبتنام کنند، بهصورت خودکار ۱ سوت طلا بهعنوان پاداش دریافت خواهید کرد.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import img1 from '@/assets/img/1.png'
|
||||||
|
import img2 from '@/assets/img/2.png'
|
||||||
|
import img4 from '@/assets/img/4.png'
|
||||||
|
import { useEducatePageContext } from '../composables/useEducatePage'
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
variant?: 'mobile' | 'desktop'
|
||||||
|
}>(),
|
||||||
|
{ variant: 'mobile' },
|
||||||
|
)
|
||||||
|
|
||||||
|
const { config } = useEducatePageContext()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="config.config.name === 'garoosi'"
|
||||||
|
:class="variant === 'desktop' ? 'deducate-support' : 'wrap-fix bottom-0 px-10 py-6 flex gap-2 bg-white'"
|
||||||
|
>
|
||||||
|
<a class="w-full" href="tel:09031265925">
|
||||||
|
<BasicButton class="w-full" type="primary">تماس با پشتیبانی</BasicButton>
|
||||||
|
</a>
|
||||||
|
<a href="https://ble.ir/garoosigallery">
|
||||||
|
<img
|
||||||
|
src="@/assets/img/bale.png"
|
||||||
|
alt=""
|
||||||
|
:class="variant === 'desktop' ? 'deducate-support__bale' : 'h-10 w-10'"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
:class="variant === 'desktop' ? 'deducate-support' : 'wrap-fix bottom-0 px-10 py-5 flex gap-2 bg-white'"
|
||||||
|
>
|
||||||
|
<a class="w-full" href="tel:09306612518">
|
||||||
|
<BasicButton class="w-full" type="primary">تماس با پشتیبانی</BasicButton>
|
||||||
|
</a>
|
||||||
|
<a href="">
|
||||||
|
<BasicButton icon="vuesax-linear:whatsapp" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||||
|
import { useEducatePageContext } from '../composables/useEducatePage'
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
variant?: 'mobile' | 'desktop'
|
||||||
|
}>(),
|
||||||
|
{ variant: 'mobile' },
|
||||||
|
)
|
||||||
|
|
||||||
|
const { config } = useEducatePageContext()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { inject, onMounted, provide, ref, type InjectionKey } from 'vue'
|
||||||
|
import type { FormInstance } from 'ant-design-vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import { updateMe } from '@/api/me'
|
||||||
|
import type { User } from '@/models/login'
|
||||||
|
import { useConfigStore } from '@/store/configStore'
|
||||||
|
import { useUserStore } from '@/store/userStore'
|
||||||
|
import { loading } from '@/utils'
|
||||||
|
|
||||||
|
export interface EducatePageContext {
|
||||||
|
config: ReturnType<typeof useConfigStore>
|
||||||
|
userFormState: ReturnType<typeof ref<User>>
|
||||||
|
formRef: ReturnType<typeof ref<FormInstance | undefined>>
|
||||||
|
editPassVisible: ReturnType<typeof ref<boolean>>
|
||||||
|
setForm: () => void
|
||||||
|
copyCode: () => void
|
||||||
|
resetData: () => void
|
||||||
|
ok: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EDUCATE_PAGE_CONTEXT_KEY: InjectionKey<EducatePageContext> = Symbol('educatePageContext')
|
||||||
|
|
||||||
|
export function useEducatePage() {
|
||||||
|
const config = useConfigStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const userFormState = ref<User>({
|
||||||
|
fname: '',
|
||||||
|
lname: '',
|
||||||
|
mobile: undefined,
|
||||||
|
birth_date: '',
|
||||||
|
phone_number: '',
|
||||||
|
province: '',
|
||||||
|
city: '',
|
||||||
|
n_code: undefined,
|
||||||
|
})
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const editPassVisible = ref<boolean>(false)
|
||||||
|
|
||||||
|
function setForm() {
|
||||||
|
userFormState.value = userStore.userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyCode() {
|
||||||
|
const val = userStore.userInfo?.referral_id
|
||||||
|
navigator.clipboard.writeText(val)
|
||||||
|
message.info('کد رونوشت شد')
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetData = () => {
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
userFormState.value = {
|
||||||
|
fname: '',
|
||||||
|
lname: '',
|
||||||
|
mobile: undefined,
|
||||||
|
birth_date: '',
|
||||||
|
phone_number: '',
|
||||||
|
province: '',
|
||||||
|
city: '',
|
||||||
|
n_code: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ok() {
|
||||||
|
formRef.value?.validateFields().then(async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
await updateMe(userFormState.value)
|
||||||
|
await userStore.getMyInfo()
|
||||||
|
editPassVisible.value = false
|
||||||
|
message.success('اطلاعات شما با موفقیت به روز رسانی شد')
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setForm()
|
||||||
|
})
|
||||||
|
|
||||||
|
const context: EducatePageContext = {
|
||||||
|
config,
|
||||||
|
userFormState,
|
||||||
|
formRef,
|
||||||
|
editPassVisible,
|
||||||
|
setForm,
|
||||||
|
copyCode,
|
||||||
|
resetData,
|
||||||
|
ok,
|
||||||
|
}
|
||||||
|
|
||||||
|
provide(EDUCATE_PAGE_CONTEXT_KEY, context)
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEducatePageContext() {
|
||||||
|
const context = inject(EDUCATE_PAGE_CONTEXT_KEY)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useEducatePageContext must be used within a page that calls useEducatePage()')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<div class="deducate">
|
||||||
|
<div class="deducate__container">
|
||||||
|
<header class="deducate-topbar">
|
||||||
|
<button type="button" class="deducate-topbar__back" @click="router.back()">
|
||||||
|
<BasicIcon icon="vuesax-linear:arrow-right-1" size="18" />
|
||||||
|
<span>بازگشت</span>
|
||||||
|
</button>
|
||||||
|
<div class="deducate-topbar__meta">
|
||||||
|
<p class="deducate-topbar__eyebrow">راهنما</p>
|
||||||
|
<h1 class="deducate-topbar__title">آموزش نرم افزار</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="deducate-layout">
|
||||||
|
<section class="deducate-surface deducate-main">
|
||||||
|
<div class="deducate-main__intro">
|
||||||
|
<div class="deducate-main__logo">
|
||||||
|
<BasicIcon icon="vuesax-linear:book" size="28" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="deducate-main__project">{{ config.config.name_fa }}</h2>
|
||||||
|
<p class="deducate-main__tagline">آموزش گامبهگام استفاده از خدمات</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="deducate-main__scroll">
|
||||||
|
<EducateContentPanel variant="desktop" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside class="deducate-surface deducate-side">
|
||||||
|
<h3 class="deducate-side__title">پشتیبانی و ارتباط</h3>
|
||||||
|
<p class="deducate-side__text">
|
||||||
|
در صورت نیاز به راهنمایی بیشتر، با پشتیبانی تماس بگیرید.
|
||||||
|
</p>
|
||||||
|
<EducateSupportActions variant="desktop" />
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
|
import router from '@/router'
|
||||||
|
import EducateContentPanel from '../components/EducateContentPanel.vue'
|
||||||
|
import EducateSupportActions from '../components/EducateSupportActions.vue'
|
||||||
|
import { useEducatePageContext } from '../composables/useEducatePage'
|
||||||
|
|
||||||
|
const { config } = useEducatePageContext()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
/* Premium desktop educate page */
|
||||||
|
|
||||||
|
.deducate {
|
||||||
|
min-height: 100%;
|
||||||
|
background: #f6f7f9;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deducate__container {
|
||||||
|
max-width: 1080px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 28px 36px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deducate-topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
|
||||||
|
&__back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__eyebrow {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.deducate-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 300px);
|
||||||
|
gap: 20px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deducate-surface {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deducate-main {
|
||||||
|
padding: 28px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: calc(100vh - 180px);
|
||||||
|
|
||||||
|
&__intro {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #eef0f3;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(9, 61, 81, 0.08);
|
||||||
|
color: @App-primary-color;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__project {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tagline {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-left: 4px;
|
||||||
|
margin-left: -4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.deducate-content {
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid rgba(9, 61, 81, 0.15);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(9, 61, 81, 0.03);
|
||||||
|
color: @App-primary-color;
|
||||||
|
|
||||||
|
&__avatar-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__avatar {
|
||||||
|
max-height: 120px;
|
||||||
|
width: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__section-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: inherit;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: inherit;
|
||||||
|
text-align: justify;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__image {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
margin: 12px 0 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__divider {
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
background: rgba(9, 61, 81, 0.12);
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.deducate-side {
|
||||||
|
padding: 24px;
|
||||||
|
position: sticky;
|
||||||
|
top: 24px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0 0 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.deducate-support {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
> a:first-child {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__bale {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
object-fit: contain;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1023.98px) {
|
||||||
|
.deducate-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deducate-side {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deducate-main {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,208 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="educate-page-root">
|
||||||
<div class="wrap-fix z-1 bg-white py-3 px-3 text-black font-bold drop-shadow flex">
|
<DesktopEducateView v-show="isDesktop" />
|
||||||
<div @click="router.back()" class="cursor-pointer">
|
<MobileEducateView v-show="!isDesktop" />
|
||||||
<BasicIcon icon="vuesax-linear:arrow-right-1" />
|
|
||||||
</div>
|
|
||||||
<div class="mr-2">آموزش نرم افزار</div>
|
|
||||||
</div>
|
|
||||||
<div class="pt-10">
|
|
||||||
<div>
|
|
||||||
<!-- <img :src="img" alt="" class="mt-2 w-full absolute bottom-0" />-->
|
|
||||||
<!-- <img :src="config.config.avatar" alt="" class="mt-2 w-full absolute bottom-0">-->
|
|
||||||
</div>
|
|
||||||
<div class="text-justify text-base z-200000 relative">
|
|
||||||
<div class="px-10">
|
|
||||||
<div class="h-20vh flex justify-center">
|
|
||||||
<img :src="config.config.avatar" alt="" class="mb-20 h-15vh mt-5">
|
|
||||||
</div>
|
|
||||||
<div class="overflow-y-auto h-60vh pb-10 border-[#093D51]">
|
|
||||||
<div
|
|
||||||
class="mt-5 text-5 border-solid border-2 border-[#093D51] text-[#093D51] rounded-[12px] p-4"
|
|
||||||
>
|
|
||||||
<div class="font-bold text-sm text-[#093D51]">
|
|
||||||
شیوهٔ ورود و ثبت نام در گروسی گالری
|
|
||||||
</div>
|
|
||||||
<div class="mt-5 text-xs leading-5">
|
|
||||||
• برای ثبتنام در گروسی گالری، اگر قبلاً حساب کاربری دارید میتوانید از طریق پیامک وارد شوید.<br />
|
|
||||||
• اگر حساب کاربری ندارید، "از این قسمت" وارد صفحه ثبت نام شده و با وارد کردن نام و شماره تلفنی که با کد ملی شما مطابقت دارد، حساب کاربری بسازید.<br />
|
|
||||||
• الزامی به وارد کردن کد معرف نیست.
|
|
||||||
</div>
|
|
||||||
<img :src="img1" />
|
|
||||||
|
|
||||||
<div class="w-full h-1px my-3 bg-secondary"></div>
|
|
||||||
<div class="font-bold mt-4 text-sm text-[#093D51]">
|
|
||||||
شارژ کیف پول
|
|
||||||
</div>
|
|
||||||
<div class="mt-5 text-xs leading-5">
|
|
||||||
• ابتدا کیف پول را در منو انتخاب کرده، مبلغ مورد نظر را وارد نموده و دکمه واریز را انتخاب کنید تا کیف پول شما شارژ شود.<br />
|
|
||||||
</div>
|
|
||||||
<img :src="img2" />
|
|
||||||
|
|
||||||
<div class="w-full h-1px my-3 bg-secondary"></div>
|
|
||||||
<div class="font-bold mt-4 text-sm text-[#093D51]">
|
|
||||||
شیوهٔ خرید از گروسی گالری
|
|
||||||
</div>
|
|
||||||
<div class="mt-5 text-xs leading-5">
|
|
||||||
• سپس از بخش «خرید و فروش» انتخاب کنید که طلای آبشده میخواهید یا محصولات دیگر (شامل گوی، شمش یا مکعب).<br />
|
|
||||||
• توجه داشته باشید کارمزد طلای آبشده حدود ۲٪ است و کارمزد ریالی محصولات دیگر در پایین صفحه قیمت درج شده است.
|
|
||||||
</div>
|
|
||||||
<img :src="img4" />
|
|
||||||
|
|
||||||
<div class="w-full h-1px my-3 bg-secondary"></div>
|
|
||||||
<div class="font-bold mt-4 text-sm text-[#093D51]">
|
|
||||||
شیوهٔ فروش در گروسی گالری
|
|
||||||
</div>
|
|
||||||
<div class="mt-5 text-xs leading-5">
|
|
||||||
• برای فروش محصولاتی که خریداری کردهاید، به بخش «خرید و فروش» مراجعه کرده، محصول موردنظر را انتخاب و گزینه فروش را بزنید تا مبلغ قابل دریافت نمایش داده شود.<br />
|
|
||||||
• مبلغ به کیف پول شما واریز میشود و از طریق پیام پشتیبانی میتوانید درخواست برداشت دهید.<br />
|
|
||||||
• اگر موجودی کیف پول بیش از ۱۰ میلیون تومان باشد، مبلغ تا پایان روز کاری بعد به حساب اعلامشده شما واریز خواهد شد.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-full h-1px my-3 bg-secondary"></div>
|
|
||||||
<div class="font-bold mt-4 text-sm text-[#093D51]">
|
|
||||||
شیوهٔ دریافت فیزیکی طلا (ارسال)
|
|
||||||
</div>
|
|
||||||
<div class="mt-5 text-xs leading-5">
|
|
||||||
• ابتدا موجودی محصول موردنظر را با پشتیبانی در واتساپ هماهنگ کنید.<br />
|
|
||||||
• سپس روی لوگو کلیک کرده، گزینه «برداشت فیزیکی» و بعد «درخواست جدید» را انتخاب کنید.<br />
|
|
||||||
• محصولات خریداریشده و تعداد آنها را مشخص کرده و آدرس خود را ثبت نمایید.<br />
|
|
||||||
• پشتیبانی عکس و وزن محصول را برای شما ارسال میکند.<br />
|
|
||||||
• ارسال بهصورت پسکرایه و بیمهشده انجام میشود.<br />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-full h-1px my-3 bg-secondary"></div>
|
|
||||||
<div class="font-bold mt-4 text-sm text-[#093D51]">
|
|
||||||
پاداش معرفی
|
|
||||||
</div>
|
|
||||||
<div class="mt-5 text-xs leading-5">
|
|
||||||
• در صفحه اصلی روی "تنظیمات" کلیک کرده و وارد حساب کاربری خود شوید.
|
|
||||||
</div>
|
|
||||||
<!-- <img :src="img2" />-->
|
|
||||||
<div class="mt-5 text-xs leading-5">
|
|
||||||
• سپس گزینه «اشتراکگذاری» یا «کپی لینک» را انتخاب کرده و لینک دعوت را برای دوستانتان ارسال کنید.<br />
|
|
||||||
• در صورتی که دوستان شما از طریق لینک شما ثبتنام کنند، بهصورت خودکار ۱ سوت طلا بهعنوان پاداش دریافت خواهید کرد.
|
|
||||||
</div>
|
|
||||||
<!-- <img :src="img3" />-->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="config.config.name === 'garoosi'" class="wrap-fix bottom-0 px-10 py-6 flex gap-2 bg-white">
|
|
||||||
<a class="w-full" href="tel:09031265925">
|
|
||||||
<BasicButton class="w-full" type="primary">تماس با پشتیبانی</BasicButton>
|
|
||||||
</a>
|
|
||||||
<a href="https://ble.ir/garoosigallery">
|
|
||||||
<img src="@/assets/img/bale.png" alt="" class="h-10 w-10"/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="wrap-fix bottom-0 px-10 py-5 flex gap-2 bg-white">
|
|
||||||
<a class="w-full" href="tel:09306612518">
|
|
||||||
<BasicButton class="w-full" type="primary">تماس با پشتیبانی</BasicButton>
|
|
||||||
</a>
|
|
||||||
<a href="">
|
|
||||||
<BasicButton icon="vuesax-linear:whatsapp" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- <ActionSheet v-model:show="editPassVisible">salam</ActionSheet>-->
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, reactive, ref } from 'vue'
|
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||||
import img from '@/assets/img/user_ly1.svg'
|
import { useEducatePage } from './composables/useEducatePage'
|
||||||
import img1 from '@/assets/img/1.png'
|
import DesktopEducateView from './desktop/DesktopEducateView.vue'
|
||||||
import img2 from '@/assets/img/2.png'
|
import MobileEducateView from './mobile/MobileEducateView.vue'
|
||||||
import img4 from '@/assets/img/4.png'
|
|
||||||
import { loading } from '@/utils'
|
|
||||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
|
||||||
import type { User } from '@/models/login'
|
|
||||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
|
||||||
import router from '@/router'
|
|
||||||
import type { FormInstance } from 'ant-design-vue'
|
|
||||||
import { message } from 'ant-design-vue'
|
|
||||||
import { updateMe } from '@/api/me'
|
|
||||||
import { useUserStore } from '@/store/userStore'
|
|
||||||
import {useConfigStore} from "@/store/configStore";
|
|
||||||
const config=useConfigStore()
|
|
||||||
const userFormState = ref<User>({
|
|
||||||
fname: '',
|
|
||||||
lname: '',
|
|
||||||
mobile: undefined,
|
|
||||||
birth_date: '',
|
|
||||||
phone_number: '',
|
|
||||||
province: '',
|
|
||||||
city: '',
|
|
||||||
n_code: undefined
|
|
||||||
})
|
|
||||||
const formRef = ref<FormInstance>()
|
|
||||||
const userStore = useUserStore()
|
|
||||||
|
|
||||||
// const editPassVisible = ref<boolean>(false)
|
const isDesktop = useDesktopBreakpoint()
|
||||||
const editPassVisible = ref<boolean>(false)
|
|
||||||
|
|
||||||
function setForm() {
|
useEducatePage()
|
||||||
userFormState.value = userStore.userInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyCode() {
|
|
||||||
// nextTick(() => {
|
|
||||||
const val = userStore.userInfo?.referral_id
|
|
||||||
navigator.clipboard.writeText(val)
|
|
||||||
message.info('کد رونوشت شد')
|
|
||||||
// })
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetData = () => {
|
|
||||||
formRef.value?.resetFields()
|
|
||||||
userFormState.value = {
|
|
||||||
fname: '',
|
|
||||||
lname: '',
|
|
||||||
mobile: undefined,
|
|
||||||
birth_date: '',
|
|
||||||
phone_number: '',
|
|
||||||
province: '',
|
|
||||||
city: '',
|
|
||||||
n_code: undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
setForm()
|
|
||||||
})
|
|
||||||
|
|
||||||
function ok() {
|
|
||||||
formRef.value?.validateFields().then(async () => {
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
await updateMe(userFormState.value)
|
|
||||||
await userStore.getMyInfo()
|
|
||||||
editPassVisible.value = false
|
|
||||||
message.success('اطلاعات شما با موفقیت به روز رسانی شد')
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/*.fade-enter-active,
|
.educate-page-root {
|
||||||
.fade-leave-active {
|
min-height: 100%;
|
||||||
transition: opacity 0.2s ease;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-from,
|
|
||||||
.fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}*/
|
|
||||||
/*.trans{
|
|
||||||
transition: height 1s;
|
|
||||||
}*/
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="wrap-fix z-1 bg-white py-3 px-3 text-black font-bold drop-shadow flex">
|
||||||
|
<div class="cursor-pointer" @click="router.back()">
|
||||||
|
<BasicIcon icon="vuesax-linear:arrow-right-1" />
|
||||||
|
</div>
|
||||||
|
<div class="mr-2">آموزش نرم افزار</div>
|
||||||
|
</div>
|
||||||
|
<div class="pt-10">
|
||||||
|
<div class="text-justify text-base z-200000 relative">
|
||||||
|
<div class="px-10">
|
||||||
|
<div class="overflow-y-auto h-60vh pb-10 border-[#093D51]">
|
||||||
|
<EducateContentPanel variant="mobile" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<EducateSupportActions variant="mobile" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
|
import router from '@/router'
|
||||||
|
import EducateContentPanel from '../components/EducateContentPanel.vue'
|
||||||
|
import EducateSupportActions from '../components/EducateSupportActions.vue'
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="item" class="bg-white flex flex-col items-center p-3 rounded-xl">
|
<div
|
||||||
|
v-if="item"
|
||||||
|
class="bg-white flex flex-col items-center p-3 rounded-xl"
|
||||||
|
:class="{ 'dfund-card': variant === 'desktop' }"
|
||||||
|
>
|
||||||
<img :src="displayImage" class="w-[87px] h-[87px] object-contain" alt="" @error="onImageError">
|
<img :src="displayImage" class="w-[87px] h-[87px] object-contain" alt="" @error="onImageError">
|
||||||
<div class="mt-3">{{ item.title ?? '-' }}</div>
|
<div class="mt-3">{{ item.title ?? '-' }}</div>
|
||||||
<div class="mt-1 flex flex-col items-center gap-0.5 text-center">
|
<div class="mt-1 flex flex-col items-center gap-0.5 text-center">
|
||||||
@@ -21,9 +25,12 @@ import { getProductDisplayImage } from '@/utils/productAssets'
|
|||||||
|
|
||||||
interface Prop {
|
interface Prop {
|
||||||
item: WalletInventoryCard
|
item: WalletInventoryCard
|
||||||
|
variant?: 'mobile' | 'desktop'
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Prop>()
|
const props = withDefaults(defineProps<Prop>(), {
|
||||||
|
variant: 'mobile',
|
||||||
|
})
|
||||||
const imageSrc = ref('')
|
const imageSrc = ref('')
|
||||||
|
|
||||||
const displayImage = computed(() => imageSrc.value || resolveImage())
|
const displayImage = computed(() => imageSrc.value || resolveImage())
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { computed, inject, onMounted, provide, type InjectionKey, type Ref } from 'vue'
|
||||||
|
import router from '@/router'
|
||||||
|
import { loading } from '@/utils'
|
||||||
|
import { usePriceWithoutType } from '@/utils/usePrice'
|
||||||
|
import { useConfigStore } from '@/store/configStore'
|
||||||
|
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||||
|
import type { WalletMetalTotal } from '@/models/walletAssets'
|
||||||
|
import { resolveSummaryMetalWeight, fundPageInventoryCards } from '@/utils/productAssets'
|
||||||
|
import { useWalletStore } from '@/store/walletStore'
|
||||||
|
import {
|
||||||
|
getProjectFundEmptyImage,
|
||||||
|
getProjectFundEmptyText,
|
||||||
|
getProjectFundHeaderBackground,
|
||||||
|
isBazretalaProject,
|
||||||
|
} from '@/utils/config'
|
||||||
|
import img from '@/assets/img/user_ly1.svg'
|
||||||
|
import empty from '@/assets/img/cash_empty.svg'
|
||||||
|
|
||||||
|
export interface FundPageContext {
|
||||||
|
isDesktop: Ref<boolean>
|
||||||
|
nameProject: string
|
||||||
|
meta: ReturnType<typeof computed>
|
||||||
|
inventoryCards: ReturnType<typeof computed>
|
||||||
|
metalTotals: ReturnType<typeof computed>
|
||||||
|
primaryMetalTotal: ReturnType<typeof computed>
|
||||||
|
cashWallet: ReturnType<typeof computed>
|
||||||
|
totalRialValue: ReturnType<typeof computed>
|
||||||
|
countBasedInventory: ReturnType<typeof computed>
|
||||||
|
fundHeaderBackground: string
|
||||||
|
fundEmptyImage: string
|
||||||
|
fundEmptyText: string
|
||||||
|
isBazretalaProject: typeof isBazretalaProject
|
||||||
|
formatMetalTotal: (metal: WalletMetalTotal) => string
|
||||||
|
openFundInfo: (productId: number) => void
|
||||||
|
goBack: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FUND_PAGE_KEY: InjectionKey<FundPageContext> = Symbol('fundPage')
|
||||||
|
|
||||||
|
export function useFundPage(isDesktop: Ref<boolean>) {
|
||||||
|
const nameProject = useConfigStore().config.name
|
||||||
|
const { assetsSummary, loadWalletAssets } = useWalletAssets()
|
||||||
|
const walletStore = useWalletStore()
|
||||||
|
const fundHeaderBackground = getProjectFundHeaderBackground(img)
|
||||||
|
const fundEmptyImage = getProjectFundEmptyImage(empty)
|
||||||
|
const fundEmptyText = getProjectFundEmptyText()
|
||||||
|
|
||||||
|
const meta = computed(() => assetsSummary.value?.meta)
|
||||||
|
const inventoryCards = computed(() =>
|
||||||
|
fundPageInventoryCards(assetsSummary.value?.inventory_cards ?? [])
|
||||||
|
)
|
||||||
|
const metalTotals = computed(() => assetsSummary.value?.metal_totals ?? [])
|
||||||
|
const primaryMetalTotal = computed(() => metalTotals.value[0])
|
||||||
|
const cashWallet = computed(() => assetsSummary.value?.cash_wallet ?? 0)
|
||||||
|
const totalRialValue = computed(() => assetsSummary.value?.total_rial_value ?? 0)
|
||||||
|
const countBasedInventory = computed(() =>
|
||||||
|
(assetsSummary.value?.count_products ?? []).reduce(
|
||||||
|
(sum, item) => sum + Number(item.quantity ?? 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
function formatMetalTotal(metal: WalletMetalTotal) {
|
||||||
|
return `${resolveSummaryMetalWeight(metal, inventoryCards.value)}${metal.unit}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function openFundInfo(productId: number) {
|
||||||
|
router.push({ name: 'fund_info', params: { id: productId } })
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const tasks = [loadWalletAssets(true)]
|
||||||
|
if (isBazretalaProject()) {
|
||||||
|
tasks.push(walletStore.getMyWallet())
|
||||||
|
}
|
||||||
|
await Promise.all(tasks)
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const context: FundPageContext = {
|
||||||
|
isDesktop,
|
||||||
|
nameProject,
|
||||||
|
meta,
|
||||||
|
inventoryCards,
|
||||||
|
metalTotals,
|
||||||
|
primaryMetalTotal,
|
||||||
|
cashWallet,
|
||||||
|
totalRialValue,
|
||||||
|
countBasedInventory,
|
||||||
|
fundHeaderBackground,
|
||||||
|
fundEmptyImage,
|
||||||
|
fundEmptyText,
|
||||||
|
isBazretalaProject,
|
||||||
|
formatMetalTotal,
|
||||||
|
openFundInfo,
|
||||||
|
goBack,
|
||||||
|
}
|
||||||
|
|
||||||
|
provide(FUND_PAGE_KEY, context)
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFundPageContext() {
|
||||||
|
const context = inject(FUND_PAGE_KEY)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useFundPageContext must be used within useFundPage')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dfund">
|
||||||
|
<div class="dfund__container">
|
||||||
|
<header class="dfund-topbar">
|
||||||
|
<button type="button" class="dfund-topbar__back" @click="goBack()">
|
||||||
|
<BasicIcon icon="vuesax-linear:arrow-right-1" size="18" />
|
||||||
|
بازگشت
|
||||||
|
</button>
|
||||||
|
<div class="dfund-topbar__meta">
|
||||||
|
<p class="dfund-topbar__eyebrow">دارایی</p>
|
||||||
|
<h1 class="dfund-topbar__title">داراییها</h1>
|
||||||
|
<p class="dfund-topbar__subtitle">نمای کلی موجودی محصولات و کیف پول</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="dfund-summary"
|
||||||
|
:class="{ 'dfund-summary--bazretala': isBazretalaProject() }"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="fundHeaderBackground"
|
||||||
|
alt=""
|
||||||
|
class="dfund-summary__bg"
|
||||||
|
:class="{ 'dfund-summary__bg--cover': isBazretalaProject() }"
|
||||||
|
/>
|
||||||
|
<div class="dfund-summary__content">
|
||||||
|
<h2 class="dfund-summary__title">موجودی داراییها</h2>
|
||||||
|
|
||||||
|
<div v-if="meta?.has_count_based_products" class="dfund-summary__row">
|
||||||
|
<span>محصولات</span>
|
||||||
|
<strong>{{ countBasedInventory }} عدد</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="isBazretalaProject()">
|
||||||
|
<div class="dfund-summary__row">
|
||||||
|
<span>دارایی نقره</span>
|
||||||
|
<strong>{{ walletStore.walletInfo?.count_silver ?? 0 }} گرم</strong>
|
||||||
|
</div>
|
||||||
|
<div class="dfund-summary__row">
|
||||||
|
<span>دارایی طلایی</span>
|
||||||
|
<strong>{{ walletStore.walletInfo?.count_gold_wallet ?? 0 }} گرم</strong>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="meta?.single_metal && primaryMetalTotal">
|
||||||
|
<div class="dfund-summary__row">
|
||||||
|
<span>{{ primaryMetalTotal.label }}</span>
|
||||||
|
<strong>{{ formatMetalTotal(primaryMetalTotal) }}</strong>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
v-for="metal in metalTotals"
|
||||||
|
:key="`metal-${metal.metal_id}`"
|
||||||
|
class="dfund-summary__row"
|
||||||
|
>
|
||||||
|
<span>{{ metal.label }}</span>
|
||||||
|
<strong>{{ formatMetalTotal(metal) }}</strong>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="meta?.has_count_based_products && !isBazretalaProject()"
|
||||||
|
class="dfund-summary__row"
|
||||||
|
>
|
||||||
|
<span>مجموع موجودی</span>
|
||||||
|
<strong>{{ countBasedInventory }} عدد</strong>
|
||||||
|
</div>
|
||||||
|
<div class="dfund-summary__row">
|
||||||
|
<span>دارایی ریالی</span>
|
||||||
|
<strong class="number-ltr">{{ usePriceWithoutType(cashWallet) }} ریال</strong>
|
||||||
|
</div>
|
||||||
|
<div class="dfund-summary__row dfund-summary__row--highlight">
|
||||||
|
<span>معادل ریالی مجموع</span>
|
||||||
|
<strong class="number-ltr">{{ usePriceWithoutType(totalRialValue) }} ریال</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="dfund-inventory">
|
||||||
|
<div class="dfund-inventory__header">
|
||||||
|
<h3 class="dfund-inventory__title">لیست داراییها</h3>
|
||||||
|
<span class="dfund-inventory__count">{{ inventoryCards.length }} مورد</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="inventoryCards.length === 0 && !loading"
|
||||||
|
class="dfund-inventory__empty"
|
||||||
|
>
|
||||||
|
<img :src="fundEmptyImage" alt="" class="dfund-inventory__empty-img" />
|
||||||
|
<p v-if="nameProject !== 'abaeigold'">{{ fundEmptyText }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<div v-if="inventoryCards.length" class="dfund-inventory__grid">
|
||||||
|
<button
|
||||||
|
v-for="item in inventoryCards"
|
||||||
|
:key="item.product_id"
|
||||||
|
type="button"
|
||||||
|
class="dfund-inventory__card"
|
||||||
|
@click="openFundInfo(item.product_id)"
|
||||||
|
>
|
||||||
|
<Item :item="item" variant="desktop" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
|
import { loading } from '@/utils'
|
||||||
|
import { usePriceWithoutType } from '@/utils/usePrice'
|
||||||
|
import { useWalletStore } from '@/store/walletStore'
|
||||||
|
import Item from '../components/Item.vue'
|
||||||
|
import { useFundPageContext } from '../composables/useFundPage'
|
||||||
|
|
||||||
|
const walletStore = useWalletStore()
|
||||||
|
|
||||||
|
const {
|
||||||
|
nameProject,
|
||||||
|
meta,
|
||||||
|
inventoryCards,
|
||||||
|
metalTotals,
|
||||||
|
primaryMetalTotal,
|
||||||
|
cashWallet,
|
||||||
|
totalRialValue,
|
||||||
|
countBasedInventory,
|
||||||
|
fundHeaderBackground,
|
||||||
|
fundEmptyImage,
|
||||||
|
fundEmptyText,
|
||||||
|
isBazretalaProject,
|
||||||
|
formatMetalTotal,
|
||||||
|
openFundInfo,
|
||||||
|
goBack,
|
||||||
|
} = useFundPageContext()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,495 @@
|
|||||||
|
/* Premium desktop fund page */
|
||||||
|
|
||||||
|
.dfund {
|
||||||
|
min-height: 100%;
|
||||||
|
background: #f6f7f9;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund__container {
|
||||||
|
max-width: 1180px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 28px 36px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
|
||||||
|
&__back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__eyebrow {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 6px 0 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-summary {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
background: @App-primary-color;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&--bazretala {
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__bg {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
opacity: 0.08;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
&--cover {
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
opacity: 0.35;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 24px 28px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 16px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--highlight {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-surface {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-inventory {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__count {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__card {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
text-align: inherit;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:hover .dfund-card {
|
||||||
|
border-color: rgba(15, 23, 42, 0.12);
|
||||||
|
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 48px 24px;
|
||||||
|
text-align: center;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__empty-img {
|
||||||
|
width: 160px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-card {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.08) !important;
|
||||||
|
border-radius: 14px !important;
|
||||||
|
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.dfund-summary__content {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-inventory__grid {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.dfund-inventory__grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fund info desktop */
|
||||||
|
|
||||||
|
.dfund-info {
|
||||||
|
min-height: 100%;
|
||||||
|
background: #f6f7f9;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-info__container {
|
||||||
|
max-width: 1180px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 28px 36px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-info-topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
|
||||||
|
&__back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__eyebrow {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-info__layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 280px minmax(0, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-info__hero {
|
||||||
|
padding: 28px 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-info__image {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: #e0f2fe;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 80%;
|
||||||
|
max-height: 80%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-info__balance {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-info__balance-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: @App-primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-info__balance-unit {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-info__balance-label {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-info__transactions {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-info__transactions-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-info__transactions-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-info__transactions-count {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-info__transactions-list {
|
||||||
|
max-height: 520px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fund new desktop */
|
||||||
|
|
||||||
|
.dfund-new {
|
||||||
|
min-height: 100%;
|
||||||
|
background: #f6f7f9;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-new__container {
|
||||||
|
max-width: 640px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 28px 36px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-new-topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
|
||||||
|
&__back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__eyebrow {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-new__card {
|
||||||
|
padding: 32px 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-new__preview {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-new__preview-img {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: #e0f2fe;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-new__label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-new__tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 20px 0 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-new__tag {
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.1);
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #4b5563;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
background: @App-primary-color;
|
||||||
|
border-color: @App-primary-color;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dfund-new__submit {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
+12
-159
@@ -1,170 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="fund-page-root">
|
||||||
<div class="bg-white w-full py-3 px-3 text-primary font-bold drop-shadow fixed md:absolute top-0 flex h-12 z-20">
|
<DesktopFundView v-show="isDesktop" />
|
||||||
<div @click="router.back()">
|
<MobileFundView v-show="!isDesktop" />
|
||||||
<BasicIcon icon="vuesax-linear:arrow-right-1"/>
|
|
||||||
</div>
|
|
||||||
<div class="mr-2">دارایی ها</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="w-screen md:w-full h-[70vh] fixed md:absolute overflow-hidden"
|
|
||||||
:class="isBazretalaProject() ? 'bazretala-fund-header' : 'text-tertiary bg-primary'"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
:src="fundHeaderBackground"
|
|
||||||
alt=""
|
|
||||||
:class="isBazretalaProject()
|
|
||||||
? 'pointer-events-none z-0 absolute inset-0 w-full object-cover object-center'
|
|
||||||
: 'pointer-events-none z-0 w-full absolute bottom-0 left-0 opacity-5'"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="pt-15 flex flex-col relative z-10"
|
|
||||||
:class="isBazretalaProject() ? 'text-white' : 'text-tertiary'"
|
|
||||||
>
|
|
||||||
<div class="font-bold mr-5 z-20">موجودی دارایی ها</div>
|
|
||||||
<div v-if="meta?.has_count_based_products" class="flex mt-4 justify-center mt-2 w-full z-20">
|
|
||||||
محصولات:
|
|
||||||
<div class="text-l mr-4 font-bold">{{ countBasedInventory }} عدد</div>
|
|
||||||
</div>
|
|
||||||
<template v-if="isBazretalaProject()">
|
|
||||||
<div class="flex justify-center mt-2 w-full z-20">
|
|
||||||
دارایی نقره:
|
|
||||||
<div class="text-l mr-4 font-bold">{{ walletStore.walletInfo?.count_silver ?? 0 }}گرم</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-center mt-2 w-full z-20">
|
|
||||||
دارایی طلایی:
|
|
||||||
<div class="text-l mr-4 font-bold">{{ walletStore.walletInfo?.count_gold_wallet ?? 0 }}گرم</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="meta?.single_metal && primaryMetalTotal">
|
|
||||||
<div class="flex justify-center mt-2 w-full z-20">
|
|
||||||
{{ primaryMetalTotal.label }}:
|
|
||||||
<div class="text-l mr-4 font-bold">{{ formatMetalTotal(primaryMetalTotal) }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div
|
|
||||||
v-for="metal in metalTotals"
|
|
||||||
:key="`metal-${metal.metal_id}`"
|
|
||||||
class="flex justify-center mt-2 w-full z-20"
|
|
||||||
>
|
|
||||||
{{ metal.label }}:
|
|
||||||
<div class="text-l mr-4 font-bold">{{ formatMetalTotal(metal) }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div v-if="meta?.has_count_based_products && !isBazretalaProject()" class="flex justify-center mt-2 w-full z-20">
|
|
||||||
مجموع موجودی:
|
|
||||||
<div class="text-l mr-4 font-bold">{{ countBasedInventory }} عدد</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-center mt-2 w-full z-20">
|
|
||||||
دارایی ریالی:
|
|
||||||
<div class="text-l mr-4 font-bold">{{ usePriceWithoutType(cashWallet) }}ریال</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-center mt-2 w-full z-20">
|
|
||||||
معادل ریالی مجموع دارایی ها:
|
|
||||||
<div class="text-l mr-4 font-bold">{{ usePriceWithoutType(totalRialValue) }}ریال</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pt-[20rem] ">
|
|
||||||
<div class="bg-gray-100 w-screen md:w-full z-20 relative rounded-t-3xl h-[calc(100vh-20rem-70px)] overflow-y-auto pb-15"
|
|
||||||
style="box-shadow: 0px -2px 4px -2px #0000001A;">
|
|
||||||
|
|
||||||
<div v-if="inventoryCards.length === 0 && !loading" class="w-full h-full flex justify-center items-center">
|
|
||||||
<div>
|
|
||||||
<img
|
|
||||||
:src="fundEmptyImage"
|
|
||||||
alt=""
|
|
||||||
:class="isBazretalaProject() ? 'w-48 max-w-[11rem]' : 'w-70'"
|
|
||||||
>
|
|
||||||
<div v-if="nameProject !== 'abaeigold'" class="text-center text-lg">{{ fundEmptyText }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a-spin :spinning="loading">
|
|
||||||
<div :class="loading ? 'h-[40vh]' : ''" class="grid grid-cols-2 gap-3 p-5">
|
|
||||||
<Item
|
|
||||||
v-for="item in inventoryCards"
|
|
||||||
:key="item.product_id"
|
|
||||||
:item="item"
|
|
||||||
@click="router.push({ name: 'fund_info', params: { id: item.product_id } })"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</a-spin>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import router from '@/router'
|
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
import { useFundPage } from './composables/useFundPage'
|
||||||
import img from '@/assets/img/user_ly1.svg'
|
import DesktopFundView from './desktop/DesktopFundView.vue'
|
||||||
import empty from '@/assets/img/cash_empty.svg'
|
import MobileFundView from './mobile/MobileFundView.vue'
|
||||||
import Item from './components/Item.vue'
|
|
||||||
import { onMounted, computed } from 'vue'
|
|
||||||
import { loading } from '@/utils'
|
|
||||||
import { usePriceWithoutType } from '@/utils/usePrice'
|
|
||||||
import { useConfigStore } from '@/store/configStore'
|
|
||||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
|
||||||
import type { WalletMetalTotal } from '@/models/walletAssets'
|
|
||||||
import { resolveSummaryMetalWeight, fundPageInventoryCards } from '@/utils/productAssets'
|
|
||||||
import { useWalletStore } from '@/store/walletStore'
|
|
||||||
import {
|
|
||||||
getProjectFundEmptyImage,
|
|
||||||
getProjectFundEmptyText,
|
|
||||||
getProjectFundHeaderBackground,
|
|
||||||
isBazretalaProject,
|
|
||||||
} from '@/utils/config'
|
|
||||||
|
|
||||||
const nameProject = useConfigStore().config.name
|
const isDesktop = useDesktopBreakpoint()
|
||||||
const { assetsSummary, loadWalletAssets } = useWalletAssets()
|
useFundPage(isDesktop)
|
||||||
const walletStore = useWalletStore()
|
|
||||||
const fundHeaderBackground = getProjectFundHeaderBackground(img)
|
|
||||||
const fundEmptyImage = getProjectFundEmptyImage(empty)
|
|
||||||
const fundEmptyText = getProjectFundEmptyText()
|
|
||||||
|
|
||||||
const meta = computed(() => assetsSummary.value?.meta)
|
|
||||||
const inventoryCards = computed(() => fundPageInventoryCards(assetsSummary.value?.inventory_cards ?? []))
|
|
||||||
const metalTotals = computed(() => assetsSummary.value?.metal_totals ?? [])
|
|
||||||
const primaryMetalTotal = computed(() => metalTotals.value[0])
|
|
||||||
const cashWallet = computed(() => assetsSummary.value?.cash_wallet ?? 0)
|
|
||||||
const totalRialValue = computed(() => assetsSummary.value?.total_rial_value ?? 0)
|
|
||||||
const countBasedInventory = computed(() =>
|
|
||||||
(assetsSummary.value?.count_products ?? []).reduce((sum, item) => sum + Number(item.quantity ?? 0), 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
const tasks = [loadWalletAssets(true)]
|
|
||||||
if (isBazretalaProject()) {
|
|
||||||
tasks.push(walletStore.getMyWallet())
|
|
||||||
}
|
|
||||||
await Promise.all(tasks)
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function formatMetalTotal(metal: WalletMetalTotal) {
|
|
||||||
return `${resolveSummaryMetalWeight(metal, inventoryCards.value)}${metal.unit}`
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.bazretala-fund-header {
|
.fund-page-root {
|
||||||
background-color: #000;
|
min-height: 100%;
|
||||||
}
|
height: 100%;
|
||||||
|
|
||||||
.bazretala-fund-header::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 1;
|
|
||||||
background: rgba(0, 0, 0, 0.45);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="variant === 'desktop' ? 'dfund-info-edit' : ''">
|
||||||
|
<div v-if="variant !== 'desktop'" class="font-bold text-lg">گنج {{ info?.title }}</div>
|
||||||
|
|
||||||
|
<BasicButton
|
||||||
|
:class="variant === 'desktop' ? 'mt-4 w-full' : 'mt-12 w-full'"
|
||||||
|
icon="vuesax-linear:edit"
|
||||||
|
icon-size="16"
|
||||||
|
@click="$router.push({ name: 'fund_add', query: { id: infoId } })"
|
||||||
|
>
|
||||||
|
<div class="flex w-full justify-right">ویرایش</div>
|
||||||
|
</BasicButton>
|
||||||
|
<BasicButton
|
||||||
|
:loading="loading"
|
||||||
|
:class="variant === 'desktop' ? 'mt-2 w-full !text-red-600' : 'mb-10 mt-2 w-full !text-red-600'"
|
||||||
|
icon="vuesax-linear:trash"
|
||||||
|
icon-size="16"
|
||||||
|
@click="deleteFundConfirm"
|
||||||
|
>
|
||||||
|
<div class="flex w-full justify-right">حذف</div>
|
||||||
|
</BasicButton>
|
||||||
|
|
||||||
|
<BasicButton
|
||||||
|
v-if="variant !== 'desktop'"
|
||||||
|
class="mb-20 mt-2 w-full"
|
||||||
|
type="primary"
|
||||||
|
@click="transactionShow = false"
|
||||||
|
>
|
||||||
|
بازگشت
|
||||||
|
</BasicButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||||
|
import { loading } from '@/utils'
|
||||||
|
import { useFundInfoPageContext } from '../composables/useFundInfoPage'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
variant?: 'mobile' | 'desktop'
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
variant: 'mobile',
|
||||||
|
})
|
||||||
|
|
||||||
|
const { info, infoId, transactionShow, deleteFundConfirm } = useFundInfoPageContext()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="variant === 'desktop' ? 'dfund-info-form' : ''">
|
||||||
|
<div v-if="variant !== 'desktop'" class="font-bold text-lg">
|
||||||
|
{{ isDeposit ? 'واریز' : 'برداشت' }}
|
||||||
|
</div>
|
||||||
|
<a-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="userFormState"
|
||||||
|
:class="variant === 'desktop' ? '!mt-0' : '!mt-5'"
|
||||||
|
name="basic"
|
||||||
|
@finish="transaction()"
|
||||||
|
>
|
||||||
|
<a-form-item :rules="[required()]" label="مقدار گرم طلا" name="value" class="!block">
|
||||||
|
<BasicInput
|
||||||
|
v-model:value="userFormState.value"
|
||||||
|
addon-before="گرم"
|
||||||
|
place-holder="گرم"
|
||||||
|
type="text"
|
||||||
|
inputmode="decimal"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
<div v-if="isDeposit" class="mt-2 text-xs font-bold leading-5">
|
||||||
|
<div>{{ formatGoldWalletBalanceText(walletStore?.walletInfo?.gold ?? 0) }}</div>
|
||||||
|
<div v-if="info?.wallet?.gold !== 0">
|
||||||
|
{{ `موجودی قابل برداشت گنج: ${info?.wallet?.gold ?? 0} گرم` }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="mt-2 text-xs font-bold">
|
||||||
|
موجودی گنج {{ info?.title }}: {{ info?.wallet?.gold }} گرم
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<BasicButton
|
||||||
|
:disabled="transButtonDisable"
|
||||||
|
class="w-full py-2 mt-2"
|
||||||
|
html-type="submit"
|
||||||
|
type="primary"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<a-spin v-if="loading" size="small" />
|
||||||
|
<div v-else>{{ isDeposit ? 'واریز' : 'برداشت' }}</div>
|
||||||
|
</div>
|
||||||
|
</BasicButton>
|
||||||
|
<BasicButton
|
||||||
|
v-if="variant !== 'desktop'"
|
||||||
|
class="mb-20 mt-2 w-full"
|
||||||
|
@click="transactionShow = false"
|
||||||
|
>
|
||||||
|
بازگشت
|
||||||
|
</BasicButton>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||||
|
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
||||||
|
import { loading } from '@/utils'
|
||||||
|
import { required } from '@/utils/inputRules'
|
||||||
|
import { formatGoldWalletBalanceText } from '@/utils/productDealMethod'
|
||||||
|
import { useFundInfoPageContext } from '../composables/useFundInfoPage'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
variant?: 'mobile' | 'desktop'
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
variant: 'mobile',
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
info,
|
||||||
|
isDeposit,
|
||||||
|
formRef,
|
||||||
|
userFormState,
|
||||||
|
transButtonDisable,
|
||||||
|
walletStore,
|
||||||
|
transactionShow,
|
||||||
|
transaction,
|
||||||
|
} = useFundInfoPageContext()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import { computed, inject, onMounted, provide, reactive, ref, watch, type InjectionKey } from 'vue'
|
||||||
|
import router from '@/router'
|
||||||
|
import { loading } from '@/utils'
|
||||||
|
import { deleteFund, fundDeposit, fundWithdraw, getFundById, getFundTransactionTable } from '@/api/fund'
|
||||||
|
import type { FundTransaction } from '@/models/fund'
|
||||||
|
import type { TableInput, TableOutput } from '@/models/table'
|
||||||
|
import type { Transaction } from '@/models/transaction'
|
||||||
|
import type { FormInstance } from 'ant-design-vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import { useWalletStore } from '@/store/walletStore'
|
||||||
|
import { useFundUserStore } from '@/store/fundStore'
|
||||||
|
import { getOneProductWallet } from '@/api/products'
|
||||||
|
import type { Product } from '@/models/products'
|
||||||
|
import homeImg from '@/assets/img/cash_home.svg'
|
||||||
|
import giftImg from '@/assets/img/cash_gift.svg'
|
||||||
|
import circulationImg from '@/assets/img/cash_circulation.svg'
|
||||||
|
import loveImg from '@/assets/img/cash_love.svg'
|
||||||
|
import repairsImg from '@/assets/img/cash_repairs.svg'
|
||||||
|
import savingImg from '@/assets/img/cash_saving.svg'
|
||||||
|
|
||||||
|
export const FUND_INFO_PAGE_KEY: InjectionKey<ReturnType<typeof useFundInfoPage>> =
|
||||||
|
Symbol('fundInfoPage')
|
||||||
|
|
||||||
|
export function useFundInfoPage() {
|
||||||
|
const info = ref<Product>()
|
||||||
|
const items = ref<Transaction[]>([])
|
||||||
|
const tableOutput = ref<TableOutput<Transaction> | null>(null)
|
||||||
|
const pagination = ref<TableInput>({})
|
||||||
|
const transactionShow = ref<boolean>()
|
||||||
|
const editShow = ref<boolean>()
|
||||||
|
const showDeleteConfirm = ref<boolean>()
|
||||||
|
const isDeposit = ref<boolean>()
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const fundStore = useFundUserStore()
|
||||||
|
const walletStore = useWalletStore()
|
||||||
|
|
||||||
|
const infoId: number = parseInt(String(router.currentRoute.value.params.id))
|
||||||
|
const userFormState = reactive<FundTransaction>({
|
||||||
|
value: undefined,
|
||||||
|
fund_id: infoId,
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(transactionShow, (n) => (!n ? (userFormState.value = undefined) : ''))
|
||||||
|
|
||||||
|
const transButtonDisable = computed(() => {
|
||||||
|
if (!loading.value) {
|
||||||
|
if (userFormState.value !== undefined) {
|
||||||
|
if (isDeposit.value) {
|
||||||
|
return userFormState.value > walletStore?.walletInfo?.gold
|
||||||
|
}
|
||||||
|
return userFormState.value > (info.value?.wallet?.gold ?? 0)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
const itemImg = computed(() => {
|
||||||
|
switch (info.value?.title) {
|
||||||
|
case 'خرید لوازم منزل':
|
||||||
|
return homeImg
|
||||||
|
case 'هدیه':
|
||||||
|
return giftImg
|
||||||
|
case 'گردش':
|
||||||
|
return circulationImg
|
||||||
|
case 'ازدواج':
|
||||||
|
return loveImg
|
||||||
|
case 'تعمیرات':
|
||||||
|
return repairsImg
|
||||||
|
case 'پس انداز':
|
||||||
|
return savingImg
|
||||||
|
default:
|
||||||
|
return savingImg
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function deleteFundConfirm() {
|
||||||
|
if (info.value?.wallet === undefined || info.value?.wallet?.gold === 0) {
|
||||||
|
showDeleteConfirm.value = true
|
||||||
|
} else {
|
||||||
|
message.error('ابتدا گنج خود را خالی کنید')
|
||||||
|
editShow.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteFundFunction() {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
showDeleteConfirm.value = false
|
||||||
|
await deleteFund(infoId)
|
||||||
|
await fundStore.getMyFund()
|
||||||
|
router.back()
|
||||||
|
message.success('گنج با موفقیت حذف شد')
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initInfo() {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
info.value = await getOneProductWallet(infoId)
|
||||||
|
await initTable()
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initTable() {
|
||||||
|
if (info.value?.wallet) {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
items.value = []
|
||||||
|
Object.assign(pagination.value, { wallet_id: info.value?.wallet?.id })
|
||||||
|
tableOutput.value = await getFundTransactionTable(pagination.value)
|
||||||
|
items.value.push(...tableOutput.value.rows)
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function transaction() {
|
||||||
|
formRef.value?.validateFields().then(async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
userFormState.fund_id = infoId
|
||||||
|
isDeposit.value ? await fundDeposit(userFormState) : await fundWithdraw(userFormState)
|
||||||
|
await initInfo()
|
||||||
|
await fundStore.getMyFund()
|
||||||
|
await walletStore.getMyWallet()
|
||||||
|
message.success(`با موفقیت${isDeposit.value ? ' واریز' : ' برداشت'} شد`)
|
||||||
|
transactionShow.value = false
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initInfo()
|
||||||
|
})
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
info,
|
||||||
|
items,
|
||||||
|
transactionShow,
|
||||||
|
editShow,
|
||||||
|
showDeleteConfirm,
|
||||||
|
isDeposit,
|
||||||
|
formRef,
|
||||||
|
userFormState,
|
||||||
|
transButtonDisable,
|
||||||
|
itemImg,
|
||||||
|
walletStore,
|
||||||
|
deleteFundConfirm,
|
||||||
|
deleteFundFunction,
|
||||||
|
transaction,
|
||||||
|
goBack,
|
||||||
|
infoId,
|
||||||
|
}
|
||||||
|
|
||||||
|
provide(FUND_INFO_PAGE_KEY, context)
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFundInfoPageContext() {
|
||||||
|
const context = inject(FUND_INFO_PAGE_KEY)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useFundInfoPageContext must be used within useFundInfoPage')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dfund-info">
|
||||||
|
<div class="dfund-info__container">
|
||||||
|
<header class="dfund-info-topbar">
|
||||||
|
<button type="button" class="dfund-info-topbar__back" @click="goBack()">
|
||||||
|
<BasicIcon icon="vuesax-linear:arrow-right-1" size="18" />
|
||||||
|
بازگشت
|
||||||
|
</button>
|
||||||
|
<div class="dfund-info-topbar__meta">
|
||||||
|
<p class="dfund-info-topbar__eyebrow">جزئیات دارایی</p>
|
||||||
|
<h1 class="dfund-info-topbar__title">{{ info?.title ?? '—' }}</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="dfund-info__layout">
|
||||||
|
<section class="dfund-info__hero dfund-surface">
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<div class="dfund-info__image">
|
||||||
|
<img v-if="info?.group_type == 'گوی'" :src="goyImg" alt="" />
|
||||||
|
<img v-if="info?.group_type == 'مکعب'" :src="mokaabImg" alt="" />
|
||||||
|
<img v-if="info?.group_type == 'سکه'" :src="sekkeImg" alt="" />
|
||||||
|
<img v-if="info?.group_type == 'مس'" :src="mesImg" alt="" />
|
||||||
|
<img v-if="info?.group_type == 'نقره'" :src="noghrehImg" alt="" />
|
||||||
|
<img
|
||||||
|
v-if="info?.group_type == 'طلا' || info?.group_type == 'آبشده' || info?.group_type == null"
|
||||||
|
:src="defaultImg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
<div class="dfund-info__balance">
|
||||||
|
<span class="dfund-info__balance-value">{{ info?.wallet?.gold ?? 0 }}</span>
|
||||||
|
<span class="dfund-info__balance-unit">{{ info?.unit }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="dfund-info__balance-label">موجودی گنج {{ info?.title }}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="dfund-info__transactions dfund-surface">
|
||||||
|
<div class="dfund-info__transactions-header">
|
||||||
|
<h2 class="dfund-info__transactions-title">تراکنشها</h2>
|
||||||
|
<span class="dfund-info__transactions-count">{{ items.length }} مورد</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dfund-info__transactions-list">
|
||||||
|
<list-anim v-if="!loading">
|
||||||
|
<Item
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.id"
|
||||||
|
:item="item"
|
||||||
|
:title="info?.title"
|
||||||
|
/>
|
||||||
|
</list-anim>
|
||||||
|
<TransactionSkeleton v-for="i in 6" v-else :key="i" class="mt-1" />
|
||||||
|
<empty v-if="items.length === 0 && !loading" text="هیچ تراکنشی یافت نشد" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
|
import { loading } from '@/utils'
|
||||||
|
import ListAnim from '@/components/ListAnim.vue'
|
||||||
|
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
|
||||||
|
import Empty from '@/components/empty.vue'
|
||||||
|
import Item from '../components/Item.vue'
|
||||||
|
import goyImg from '@/assets/img/goyImg.svg'
|
||||||
|
import mokaabImg from '@/assets/img/mokaabImg.svg'
|
||||||
|
import sekkeImg from '@/assets/img/sekkeImg.svg'
|
||||||
|
import mesImg from '@/assets/img/mesImg.jpg'
|
||||||
|
import noghrehImg from '@/assets/img/noghrehImg.webp'
|
||||||
|
import defaultImg from '@/assets/img/defaultAbshodeh.svg'
|
||||||
|
import { useFundInfoPageContext } from '../composables/useFundInfoPage'
|
||||||
|
|
||||||
|
const { info, items, goBack } = useFundInfoPageContext()
|
||||||
|
</script>
|
||||||
@@ -1,305 +1,79 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="fund-info-page-root">
|
||||||
<div class="bg-white wrap-fix py-3 px-3 text-primary font-bold drop-shadow flex h-12 z-20 justify-between">
|
<DesktopFundInfoView v-show="isDesktop" />
|
||||||
<div class="flex ">
|
<MobileFundInfoView v-show="!isDesktop" />
|
||||||
<div @click="router.back()" class="cursor-pointer">
|
|
||||||
<BasicIcon icon="vuesax-linear:arrow-right-1" />
|
<BasicActionSheet v-if="!isDesktop" v-model:show="transactionShow">
|
||||||
</div>
|
<FundTransactionForm />
|
||||||
<div class="mr-2">{{ info?.title }}</div>
|
|
||||||
</div>
|
|
||||||
<!-- <div @click="editShow = true">
|
|
||||||
<BasicIcon icon="vuesax-linear:setting-4" />
|
|
||||||
</div>-->
|
|
||||||
</div>
|
|
||||||
<div class="pt-20 flex flex-col justify-center items-center">
|
|
||||||
<a-spin :spinning="loading">
|
|
||||||
<div class="bg-sky-100 w-[120px] aspect-square flex justify-center rounded-xl">
|
|
||||||
<!-- <img v-if="!loading" :src="itemImg" alt="">-->
|
|
||||||
<img v-if="info?.group_type=='گوی'" :src="goyImg">
|
|
||||||
<img v-if="info?.group_type=='مکعب'" :src="mokaabImg">
|
|
||||||
<img v-if="info?.group_type=='سکه'" :src="sekkeImg">
|
|
||||||
<img v-if="info?.group_type=='مس'" :src="mesImg">
|
|
||||||
<img v-if="info?.group_type=='نقره'" :src="noghrehImg">
|
|
||||||
<img v-if="info?.group_type=='طلا' || info?.group_type=='آبشده' || info?.group_type== null" :src="defaultImg">
|
|
||||||
</div>
|
|
||||||
</a-spin>
|
|
||||||
<div class="mt-2 font-bold flex text-xs items-center text-gray-500">
|
|
||||||
<div class="ml-1 text-base text-primary">{{ info?.wallet?.gold?? 0 }}</div>
|
|
||||||
{{info?.unit}}
|
|
||||||
</div>
|
|
||||||
<div class="text-gray-400">موجودی گنج {{ info?.title }}</div>
|
|
||||||
<div class="w-full p-5">
|
|
||||||
<!-- <div class="w-full flex gap-3">
|
|
||||||
<BasicButton class="w-full !bg-[#F3F0E9] !border-0 !rounded" icon="vuesax-linear:add-square" icon-size="18"
|
|
||||||
@click="()=> {transactionShow = true; isDeposit= false}">برداشت
|
|
||||||
</BasicButton>
|
|
||||||
<BasicButton class="w-full !bg-[#F3F0E9] !border-0 !rounded" icon="vuesax-linear:arrow-up" icon-size="18"
|
|
||||||
@click="()=> {transactionShow = true; isDeposit= true}">واریز
|
|
||||||
</BasicButton>
|
|
||||||
</div>-->
|
|
||||||
<div class=" font-bold mt-5">تراکنش ها</div>
|
|
||||||
<!-- <a-spin :spinning="loading">-->
|
|
||||||
<div class="mt-5 pb-17 overflow-y-scroll h-[54vh]">
|
|
||||||
<list-anim v-if="!loading">
|
|
||||||
<Item v-for="item in items" :key="item.id" :item="item" :title="info?.title"/>
|
|
||||||
</list-anim>
|
|
||||||
<TransactionSkeleton v-for="i in 8" v-else :key="i" class="mt-1" />
|
|
||||||
<empty v-if="items.length === 0 && !loading" text="هیچ تراکنشی یافت نشد" />
|
|
||||||
</div>
|
|
||||||
<!-- </a-spin>-->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<BasicActionSheet v-model:show="transactionShow">
|
|
||||||
<div class="font-bold text-lg">{{ isDeposit ? 'واریز' : 'برداشت' }}</div>
|
|
||||||
<a-form ref="formRef" :model="userFormState" class="!mt-5" name="basic" @finish="transaction()">
|
|
||||||
<a-form-item :rules="[required()]" label="مقدار گرم طلا" name="value" class="!block">
|
|
||||||
<BasicInput v-model:value="userFormState.value"
|
|
||||||
addon-before="گرم" place-holder="گرم"
|
|
||||||
type="text" inputmode="decimal"
|
|
||||||
size="large" />
|
|
||||||
<div v-if="isDeposit" class="mt-2 text-xs font-bold leading-5">
|
|
||||||
<div>{{ formatGoldWalletBalanceText(walletStore?.walletInfo?.gold ?? 0) }}</div>
|
|
||||||
<div v-if="info?.wallet?.gold !== 0">
|
|
||||||
{{ `موجودی قابل برداشت گنج: ${info?.wallet?.gold ?? 0} گرم` }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="mt-2 text-xs font-bold">موجودی گنج {{ info.title }}: {{ info?.wallet?.gold }} گرم
|
|
||||||
</div>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item>
|
|
||||||
<BasicButton
|
|
||||||
:disabled="transButtonDisable"
|
|
||||||
class=" w-full py-2 mt-2"
|
|
||||||
html-type="submit"
|
|
||||||
type="primary"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<a-spin v-if="loading" size="small" />
|
|
||||||
<div v-else>{{ isDeposit ? 'واریز' : 'برداشت' }}</div>
|
|
||||||
</div>
|
|
||||||
</BasicButton>
|
|
||||||
<BasicButton class="mb-20 mt-2 w-full" @click="transactionShow = false">بازگشت</BasicButton>
|
|
||||||
</a-form-item>
|
|
||||||
</a-form>
|
|
||||||
</BasicActionSheet>
|
</BasicActionSheet>
|
||||||
<BasicActionSheet v-model:show="editShow">
|
<BasicModal
|
||||||
<div class="font-bold text-lg">گنج {{ info?.title }}</div>
|
v-else
|
||||||
|
v-model:visible="transactionShow"
|
||||||
|
:title="isDeposit ? 'واریز' : 'برداشت'"
|
||||||
|
width="480px"
|
||||||
|
destroy-on-close
|
||||||
|
custom-footer
|
||||||
|
>
|
||||||
|
<FundTransactionForm variant="desktop" />
|
||||||
|
<template #footer>
|
||||||
|
<div></div>
|
||||||
|
</template>
|
||||||
|
</BasicModal>
|
||||||
|
|
||||||
<BasicButton class="mt-12 w-full" icon="vuesax-linear:edit"
|
<BasicActionSheet v-if="!isDesktop" v-model:show="editShow">
|
||||||
icon-size="16" @click="$router.push({name: 'fund_add', query:{id:infoId}})">
|
<FundEditActions />
|
||||||
<div class="flex w-full justify-right">ویرایش</div>
|
|
||||||
</BasicButton>
|
|
||||||
<BasicButton :loading="loading" class="mb-10 mt-2 w-full !text-red-600"
|
|
||||||
icon="vuesax-linear:trash"
|
|
||||||
icon-size="16"
|
|
||||||
@click="deleteFundConfirm">
|
|
||||||
<div class="flex w-full justify-right">حذف</div>
|
|
||||||
</BasicButton>
|
|
||||||
|
|
||||||
<BasicButton class="mb-20 mt-2 w-full" type="primary" @click="transactionShow = false">بازگشت</BasicButton>
|
|
||||||
</BasicActionSheet>
|
</BasicActionSheet>
|
||||||
<DialogVant v-model:show="showDeleteConfirm" :closeOnClickOverlay="true" :timeOut="-1">
|
<BasicModal
|
||||||
<!-- <template v-slot:img>
|
v-else
|
||||||
<img :src="registerDelayImage" alt="" class="opacity-80">
|
v-model:visible="editShow"
|
||||||
</template>-->
|
:title="`گنج ${info?.title ?? ''}`"
|
||||||
|
width="420px"
|
||||||
<template v-slot:title>
|
destroy-on-close
|
||||||
<div class="p-5 text-sm">
|
custom-footer
|
||||||
آیا از حذف گنج خود اطمینان دارید؟
|
>
|
||||||
</div>
|
<FundEditActions variant="desktop" />
|
||||||
|
<template #footer>
|
||||||
|
<div></div>
|
||||||
</template>
|
</template>
|
||||||
|
</BasicModal>
|
||||||
|
|
||||||
<template v-slot:button>
|
<DialogVant v-model:show="showDeleteConfirm" :close-on-click-overlay="true" :time-out="-1">
|
||||||
<div class="text-center text-red" @click="deleteFundFunction">
|
<template #title>
|
||||||
حذف
|
<div class="p-5 text-sm">آیا از حذف گنج خود اطمینان دارید؟</div>
|
||||||
</div>
|
</template>
|
||||||
<div class="text-center" @click="showDeleteConfirm = false">
|
<template #button>
|
||||||
انصراف
|
<div class="text-center text-red" @click="deleteFundFunction">حذف</div>
|
||||||
</div>
|
<div class="text-center" @click="showDeleteConfirm = false">انصراف</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
</DialogVant>
|
</DialogVant>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|
||||||
import router from '@/router'
|
|
||||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
|
||||||
import Item from './components/Item.vue'
|
|
||||||
import homeImg from '@/assets/img/cash_home.svg'
|
|
||||||
import giftImg from '@/assets/img/cash_gift.svg'
|
|
||||||
import circulationImg from '@/assets/img/cash_circulation.svg'
|
|
||||||
import loveImg from '@/assets/img/cash_love.svg'
|
|
||||||
import repairsImg from '@/assets/img/cash_repairs.svg'
|
|
||||||
import savingImg from '@/assets/img/cash_saving.svg'
|
|
||||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
|
||||||
import { loading } from '@/utils'
|
|
||||||
import { deleteFund, fundDeposit, fundWithdraw, getFundById, getFundTransactionTable } from '@/api/fund'
|
|
||||||
import type { Fund, FundTransaction } from '@/models/fund'
|
|
||||||
import type { TableInput, TableOutput } from '@/models/table'
|
|
||||||
import type { Transaction } from '@/models/transaction'
|
|
||||||
import Empty from '@/components/empty.vue'
|
|
||||||
import { required } from '@/utils/inputRules'
|
|
||||||
import type { FormInstance } from 'ant-design-vue'
|
|
||||||
import { message } from 'ant-design-vue'
|
|
||||||
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
||||||
import { formatGoldWalletBalanceText } from '@/utils/productDealMethod'
|
import BasicModal from '@/components/BasicPacks/BasicModal.vue'
|
||||||
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
|
||||||
import { useWalletStore } from '@/store/walletStore'
|
|
||||||
import ListAnim from '@/components/ListAnim.vue'
|
|
||||||
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
|
|
||||||
import DialogVant from '@/components/DialogVant.vue'
|
import DialogVant from '@/components/DialogVant.vue'
|
||||||
import { useFundUserStore } from '@/store/fundStore'
|
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||||
import { getOneProductWallet } from '@/api/products'
|
import { useFundInfoPage } from './composables/useFundInfoPage'
|
||||||
import goyImg from '@/assets/img/goyImg.svg'
|
import DesktopFundInfoView from './desktop/DesktopFundInfoView.vue'
|
||||||
import mokaabImg from '@/assets/img/mokaabImg.svg'
|
import MobileFundInfoView from './mobile/MobileFundInfoView.vue'
|
||||||
import sekkeImg from "@/assets/img/sekkeImg.svg"
|
import FundTransactionForm from './components/FundTransactionForm.vue'
|
||||||
import mesImg from "@/assets/img/mesImg.jpg"
|
import FundEditActions from './components/FundEditActions.vue'
|
||||||
import noghrehImg from "@/assets/img/noghrehImg.webp"
|
|
||||||
import defaultImg from '@/assets/img/defaultAbshodeh.svg'
|
|
||||||
import type { Product } from '@/models/products'
|
|
||||||
|
|
||||||
const info = ref<Product>()
|
|
||||||
const items = ref<Transaction[]>([])
|
|
||||||
const tableOutput = ref<TableOutput<Transaction> | null>(null)
|
|
||||||
const pagination = ref<TableInput>({/*limit: 100, page: 0*/ })
|
|
||||||
const transactionShow = ref<boolean>()
|
|
||||||
const editShow = ref<boolean>()
|
|
||||||
const showDeleteConfirm = ref<boolean>()
|
|
||||||
const isDeposit = ref<boolean>()
|
|
||||||
const formRef = ref<FormInstance>()
|
|
||||||
let fundStore = useFundUserStore()
|
|
||||||
let walletStore = useWalletStore()
|
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
initInfo()
|
|
||||||
// initTable()
|
|
||||||
})
|
|
||||||
|
|
||||||
function deleteFundConfirm() {
|
|
||||||
if (info.value?.wallet === undefined || info.value?.wallet?.gold === 0) {
|
|
||||||
showDeleteConfirm.value = true
|
|
||||||
} else {
|
|
||||||
message.error('ابتدا گنج خود را خالی کنید')
|
|
||||||
editShow.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteFundFunction() {
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
showDeleteConfirm.value = false
|
|
||||||
await deleteFund(infoId)
|
|
||||||
await fundStore.getMyFund()
|
|
||||||
router.back()
|
|
||||||
message.success('گنج با موفقیت حذف شد')
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const infoId: number = parseInt(<string>router.currentRoute.value.params.id)
|
|
||||||
const userFormState = reactive<FundTransaction>({
|
|
||||||
value: undefined,
|
|
||||||
fund_id: infoId
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(transactionShow, (n) => !n ? userFormState.value = undefined : '')
|
|
||||||
|
|
||||||
const transButtonDisable = computed(() => {
|
|
||||||
if (!loading.value) {
|
|
||||||
if (userFormState.value !== undefined) {
|
|
||||||
if (isDeposit.value) {
|
|
||||||
return userFormState.value > walletStore?.walletInfo?.gold
|
|
||||||
} else {
|
|
||||||
return userFormState.value > info.value?.wallet?.gold
|
|
||||||
}
|
|
||||||
// return isDeposit.value ? userFormState.value > walletStore?.walletInfo?.gold : userFormState.value > info.value?.wallet?.gold
|
|
||||||
} else return true
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
async function initInfo() {
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
info.value = await getOneProductWallet(infoId)
|
|
||||||
//pagination.value.page=0
|
|
||||||
await initTable()
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initTable() {
|
|
||||||
if (info.value?.wallet)
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
items.value = []
|
|
||||||
Object.assign(pagination.value, { wallet_id: info.value?.wallet?.id })
|
|
||||||
// pagination.value.fund_id = infoId
|
|
||||||
tableOutput.value = await getFundTransactionTable(pagination.value)
|
|
||||||
items.value.push(...tableOutput.value.rows)
|
|
||||||
// console.log(tableOutput.value)
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function transaction() {
|
|
||||||
formRef.value?.validateFields().then(async () => {
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
userFormState.fund_id = infoId
|
|
||||||
isDeposit.value ? await fundDeposit(userFormState) : await fundWithdraw(userFormState)
|
|
||||||
await initInfo()
|
|
||||||
// await initTable()
|
|
||||||
await fundStore.getMyFund()
|
|
||||||
await walletStore.getMyWallet()
|
|
||||||
fundStore = useWalletStore()
|
|
||||||
walletStore = useWalletStore()
|
|
||||||
message.success(` با موفقیت${isDeposit.value ? ' واریز' : ' برداشت'} شد`)
|
|
||||||
transactionShow.value = false
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemImg = computed(() => {
|
|
||||||
switch (info.value?.title) {
|
|
||||||
case 'خرید لوازم منزل':
|
|
||||||
return homeImg
|
|
||||||
case 'هدیه':
|
|
||||||
return giftImg
|
|
||||||
case 'گردش':
|
|
||||||
return circulationImg
|
|
||||||
case 'ازدواج':
|
|
||||||
return loveImg
|
|
||||||
case 'تعمیرات':
|
|
||||||
return repairsImg
|
|
||||||
case 'پس انداز':
|
|
||||||
return savingImg
|
|
||||||
default:
|
|
||||||
return savingImg
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
const isDesktop = useDesktopBreakpoint()
|
||||||
|
const {
|
||||||
|
info,
|
||||||
|
transactionShow,
|
||||||
|
editShow,
|
||||||
|
showDeleteConfirm,
|
||||||
|
isDeposit,
|
||||||
|
deleteFundFunction,
|
||||||
|
} = useFundInfoPage()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.fund-info-page-root {
|
||||||
|
min-height: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="bg-white wrap-fix py-3 px-3 text-primary font-bold drop-shadow flex h-12 z-20 justify-between"
|
||||||
|
>
|
||||||
|
<div class="flex">
|
||||||
|
<div class="cursor-pointer" @click="goBack()">
|
||||||
|
<BasicIcon icon="vuesax-linear:arrow-right-1" />
|
||||||
|
</div>
|
||||||
|
<div class="mr-2">{{ info?.title }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pt-20 flex flex-col justify-center items-center">
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<div class="bg-sky-100 w-[120px] aspect-square flex justify-center rounded-xl">
|
||||||
|
<img v-if="info?.group_type == 'گوی'" :src="goyImg" alt="" />
|
||||||
|
<img v-if="info?.group_type == 'مکعب'" :src="mokaabImg" alt="" />
|
||||||
|
<img v-if="info?.group_type == 'سکه'" :src="sekkeImg" alt="" />
|
||||||
|
<img v-if="info?.group_type == 'مس'" :src="mesImg" alt="" />
|
||||||
|
<img v-if="info?.group_type == 'نقره'" :src="noghrehImg" alt="" />
|
||||||
|
<img
|
||||||
|
v-if="info?.group_type == 'طلا' || info?.group_type == 'آبشده' || info?.group_type == null"
|
||||||
|
:src="defaultImg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
<div class="mt-2 font-bold flex text-xs items-center text-gray-500">
|
||||||
|
<div class="ml-1 text-base text-primary">{{ info?.wallet?.gold ?? 0 }}</div>
|
||||||
|
{{ info?.unit }}
|
||||||
|
</div>
|
||||||
|
<div class="text-gray-400">موجودی گنج {{ info?.title }}</div>
|
||||||
|
<div class="w-full p-5">
|
||||||
|
<div class="font-bold mt-5">تراکنش ها</div>
|
||||||
|
<div class="mt-5 pb-17 overflow-y-scroll h-[54vh]">
|
||||||
|
<list-anim v-if="!loading">
|
||||||
|
<Item v-for="item in items" :key="item.id" :item="item" :title="info?.title" />
|
||||||
|
</list-anim>
|
||||||
|
<TransactionSkeleton v-for="i in 8" v-else :key="i" class="mt-1" />
|
||||||
|
<empty v-if="items.length === 0 && !loading" text="هیچ تراکنشی یافت نشد" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
|
import { loading } from '@/utils'
|
||||||
|
import ListAnim from '@/components/ListAnim.vue'
|
||||||
|
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
|
||||||
|
import Empty from '@/components/empty.vue'
|
||||||
|
import Item from '../components/Item.vue'
|
||||||
|
import goyImg from '@/assets/img/goyImg.svg'
|
||||||
|
import mokaabImg from '@/assets/img/mokaabImg.svg'
|
||||||
|
import sekkeImg from '@/assets/img/sekkeImg.svg'
|
||||||
|
import mesImg from '@/assets/img/mesImg.jpg'
|
||||||
|
import noghrehImg from '@/assets/img/noghrehImg.webp'
|
||||||
|
import defaultImg from '@/assets/img/defaultAbshodeh.svg'
|
||||||
|
import { useFundInfoPageContext } from '../composables/useFundInfoPage'
|
||||||
|
|
||||||
|
const { info, items, goBack } = useFundInfoPageContext()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="bg-white w-full py-3 px-3 text-primary font-bold drop-shadow fixed md:absolute top-0 flex h-12 z-20"
|
||||||
|
>
|
||||||
|
<div @click="goBack()">
|
||||||
|
<BasicIcon icon="vuesax-linear:arrow-right-1" />
|
||||||
|
</div>
|
||||||
|
<div class="mr-2">دارایی ها</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="w-screen md:w-full h-[70vh] fixed md:absolute overflow-hidden"
|
||||||
|
:class="isBazretalaProject() ? 'bazretala-fund-header' : 'text-tertiary bg-primary'"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="fundHeaderBackground"
|
||||||
|
alt=""
|
||||||
|
:class="
|
||||||
|
isBazretalaProject()
|
||||||
|
? 'pointer-events-none z-0 absolute inset-0 w-full object-cover object-center'
|
||||||
|
: 'pointer-events-none z-0 w-full absolute bottom-0 left-0 opacity-5'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="pt-15 flex flex-col relative z-10"
|
||||||
|
:class="isBazretalaProject() ? 'text-white' : 'text-tertiary'"
|
||||||
|
>
|
||||||
|
<div class="font-bold mr-5 z-20">موجودی دارایی ها</div>
|
||||||
|
<div
|
||||||
|
v-if="meta?.has_count_based_products"
|
||||||
|
class="flex mt-4 justify-center mt-2 w-full z-20"
|
||||||
|
>
|
||||||
|
محصولات:
|
||||||
|
<div class="text-l mr-4 font-bold">{{ countBasedInventory }} عدد</div>
|
||||||
|
</div>
|
||||||
|
<template v-if="isBazretalaProject()">
|
||||||
|
<div class="flex justify-center mt-2 w-full z-20">
|
||||||
|
دارایی نقره:
|
||||||
|
<div class="text-l mr-4 font-bold">
|
||||||
|
{{ walletStore.walletInfo?.count_silver ?? 0 }}گرم
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center mt-2 w-full z-20">
|
||||||
|
دارایی طلایی:
|
||||||
|
<div class="text-l mr-4 font-bold">
|
||||||
|
{{ walletStore.walletInfo?.count_gold_wallet ?? 0 }}گرم
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="meta?.single_metal && primaryMetalTotal">
|
||||||
|
<div class="flex justify-center mt-2 w-full z-20">
|
||||||
|
{{ primaryMetalTotal.label }}:
|
||||||
|
<div class="text-l mr-4 font-bold">{{ formatMetalTotal(primaryMetalTotal) }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
v-for="metal in metalTotals"
|
||||||
|
:key="`metal-${metal.metal_id}`"
|
||||||
|
class="flex justify-center mt-2 w-full z-20"
|
||||||
|
>
|
||||||
|
{{ metal.label }}:
|
||||||
|
<div class="text-l mr-4 font-bold">{{ formatMetalTotal(metal) }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
v-if="meta?.has_count_based_products && !isBazretalaProject()"
|
||||||
|
class="flex justify-center mt-2 w-full z-20"
|
||||||
|
>
|
||||||
|
مجموع موجودی:
|
||||||
|
<div class="text-l mr-4 font-bold">{{ countBasedInventory }} عدد</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center mt-2 w-full z-20">
|
||||||
|
دارایی ریالی:
|
||||||
|
<div class="text-l mr-4 font-bold">{{ usePriceWithoutType(cashWallet) }}ریال</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center mt-2 w-full z-20">
|
||||||
|
معادل ریالی مجموع دارایی ها:
|
||||||
|
<div class="text-l mr-4 font-bold">{{ usePriceWithoutType(totalRialValue) }}ریال</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pt-[20rem]">
|
||||||
|
<div
|
||||||
|
class="bg-gray-100 w-screen md:w-full z-20 relative rounded-t-3xl h-[calc(100vh-20rem-70px)] overflow-y-auto pb-15"
|
||||||
|
style="box-shadow: 0px -2px 4px -2px #0000001a"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="inventoryCards.length === 0 && !loading"
|
||||||
|
class="w-full h-full flex justify-center items-center"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
:src="fundEmptyImage"
|
||||||
|
alt=""
|
||||||
|
:class="isBazretalaProject() ? 'w-48 max-w-[11rem]' : 'w-70'"
|
||||||
|
/>
|
||||||
|
<div v-if="nameProject !== 'abaeigold'" class="text-center text-lg">
|
||||||
|
{{ fundEmptyText }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<div :class="loading ? 'h-[40vh]' : ''" class="grid grid-cols-2 gap-3 p-5">
|
||||||
|
<Item
|
||||||
|
v-for="item in inventoryCards"
|
||||||
|
:key="item.product_id"
|
||||||
|
:item="item"
|
||||||
|
@click="openFundInfo(item.product_id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
|
import { loading } from '@/utils'
|
||||||
|
import { usePriceWithoutType } from '@/utils/usePrice'
|
||||||
|
import { useWalletStore } from '@/store/walletStore'
|
||||||
|
import Item from '../components/Item.vue'
|
||||||
|
import { useFundPageContext } from '../composables/useFundPage'
|
||||||
|
|
||||||
|
const walletStore = useWalletStore()
|
||||||
|
|
||||||
|
const {
|
||||||
|
nameProject,
|
||||||
|
meta,
|
||||||
|
inventoryCards,
|
||||||
|
metalTotals,
|
||||||
|
primaryMetalTotal,
|
||||||
|
cashWallet,
|
||||||
|
totalRialValue,
|
||||||
|
countBasedInventory,
|
||||||
|
fundHeaderBackground,
|
||||||
|
fundEmptyImage,
|
||||||
|
fundEmptyText,
|
||||||
|
isBazretalaProject,
|
||||||
|
formatMetalTotal,
|
||||||
|
openFundInfo,
|
||||||
|
goBack,
|
||||||
|
} = useFundPageContext()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.bazretala-fund-header {
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bazretala-fund-header::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { computed, inject, onMounted, provide, ref, type InjectionKey } from 'vue'
|
||||||
|
import router from '@/router'
|
||||||
|
import { loading } from '@/utils'
|
||||||
|
import { usePrefix } from '@/utils/usePrefix'
|
||||||
|
import { createFund, editFund, getFundById } from '@/api/fund'
|
||||||
|
import { message } from 'ant-design-vue/es'
|
||||||
|
import { useFundUserStore } from '@/store/fundStore'
|
||||||
|
import type { Fund } from '@/models/fund'
|
||||||
|
import homeImg from '@/assets/img/cash_home.svg'
|
||||||
|
import giftImg from '@/assets/img/cash_gift.svg'
|
||||||
|
import circulationImg from '@/assets/img/cash_circulation.svg'
|
||||||
|
import loveImg from '@/assets/img/cash_love.svg'
|
||||||
|
import repairsImg from '@/assets/img/cash_repairs.svg'
|
||||||
|
import savingImg from '@/assets/img/cash_saving.svg'
|
||||||
|
|
||||||
|
export const FUND_NEW_PAGE_KEY: InjectionKey<ReturnType<typeof useFundNewPage>> =
|
||||||
|
Symbol('fundNewPage')
|
||||||
|
|
||||||
|
const types = [
|
||||||
|
{ name: 'خرید لوازم منزل' },
|
||||||
|
{ name: 'پس انداز' },
|
||||||
|
{ name: 'گردش' },
|
||||||
|
{ name: 'ازدواج' },
|
||||||
|
{ name: 'تعمیرات' },
|
||||||
|
{ name: 'هدیه' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function useFundNewPage() {
|
||||||
|
const fundStore = useFundUserStore()
|
||||||
|
const edit_id = computed(() => parseInt(router.currentRoute.value.query.id as string))
|
||||||
|
const name = ref<string>('پس انداز')
|
||||||
|
const { prefixCls } = usePrefix('input_name')
|
||||||
|
const info = ref<Fund>()
|
||||||
|
|
||||||
|
const itemImg = computed(() => {
|
||||||
|
switch (name.value) {
|
||||||
|
case 'خرید لوازم منزل':
|
||||||
|
return homeImg
|
||||||
|
case 'هدیه':
|
||||||
|
return giftImg
|
||||||
|
case 'گردش':
|
||||||
|
return circulationImg
|
||||||
|
case 'ازدواج':
|
||||||
|
return loveImg
|
||||||
|
case 'تعمیرات':
|
||||||
|
return repairsImg
|
||||||
|
case 'پس انداز':
|
||||||
|
return savingImg
|
||||||
|
default:
|
||||||
|
return savingImg
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function getEditInfo() {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
info.value = await getFundById(edit_id.value)
|
||||||
|
name.value = info.value?.title ?? ''
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ok() {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
if (!name.value) {
|
||||||
|
message.error('ابتدا نام گنج را وارد کنید')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
edit_id.value
|
||||||
|
? await editFund(edit_id.value, { title: name.value })
|
||||||
|
: await createFund({ title: name.value })
|
||||||
|
await fundStore.getMyFund()
|
||||||
|
message.success(`گنج با موفقیت ${edit_id.value ? 'ویرایش' : 'ساخته'} شد`)
|
||||||
|
router.back()
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (edit_id.value) {
|
||||||
|
getEditInfo()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
types,
|
||||||
|
edit_id,
|
||||||
|
name,
|
||||||
|
prefixCls,
|
||||||
|
info,
|
||||||
|
itemImg,
|
||||||
|
ok,
|
||||||
|
goBack,
|
||||||
|
}
|
||||||
|
|
||||||
|
provide(FUND_NEW_PAGE_KEY, context)
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFundNewPageContext() {
|
||||||
|
const context = inject(FUND_NEW_PAGE_KEY)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useFundNewPageContext must be used within useFundNewPage')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dfund-new">
|
||||||
|
<div class="dfund-new__container">
|
||||||
|
<header class="dfund-new-topbar">
|
||||||
|
<button type="button" class="dfund-new-topbar__back" @click="goBack()">
|
||||||
|
<BasicIcon icon="vuesax-linear:arrow-right-1" size="18" />
|
||||||
|
بازگشت
|
||||||
|
</button>
|
||||||
|
<div class="dfund-new-topbar__meta">
|
||||||
|
<p class="dfund-new-topbar__eyebrow">گنج</p>
|
||||||
|
<h1 class="dfund-new-topbar__title">
|
||||||
|
{{ edit_id ? `ویرایش گنج ${name}` : 'صندوق جدید' }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<section class="dfund-new__card dfund-surface">
|
||||||
|
<div class="dfund-new__preview">
|
||||||
|
<img :src="itemImg" alt="" class="dfund-new__preview-img" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dfund-new__field" :class="[prefixCls]">
|
||||||
|
<label class="dfund-new__label">عنوان گنج</label>
|
||||||
|
<van-field
|
||||||
|
v-model="name"
|
||||||
|
label=""
|
||||||
|
placeholder="عنوان را وارد کنید"
|
||||||
|
input-align="center"
|
||||||
|
:show-word-limit="true"
|
||||||
|
maxlength="25"
|
||||||
|
rows="1"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dfund-new__tags">
|
||||||
|
<button
|
||||||
|
v-for="item in types"
|
||||||
|
:key="item.name"
|
||||||
|
type="button"
|
||||||
|
class="dfund-new__tag"
|
||||||
|
:class="{ 'dfund-new__tag--active': item.name === name }"
|
||||||
|
@click="name = item.name"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BasicButton class="dfund-new__submit" type="primary" :disabled="loading" @click="ok()">
|
||||||
|
<a-spin v-if="loading" size="small" />
|
||||||
|
<span v-else>{{ edit_id ? 'ویرایش گنج' : 'ساخت گنج' }}</span>
|
||||||
|
</BasicButton>
|
||||||
|
</section>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
|
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||||
|
import { loading } from '@/utils'
|
||||||
|
import { useFundNewPageContext } from '../composables/useFundNewPage'
|
||||||
|
|
||||||
|
const { types, edit_id, name, prefixCls, itemImg, ok, goBack } = useFundNewPageContext()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
@prefix: ~'@{prefixCls}-input_name';
|
||||||
|
|
||||||
|
.@{prefix} {
|
||||||
|
.van-field__value {
|
||||||
|
@apply flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.van-field__body {
|
||||||
|
@apply w-full mr-5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,194 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="fund-new-page-root">
|
||||||
<div class="bg-white wrap-fix py-3 px-3 text-primary font-bold drop-shadow flex h-12 z-20 justify-between">
|
<DesktopFundNewView v-show="isDesktop" />
|
||||||
<div class="flex ">
|
<MobileFundNewView v-show="!isDesktop" />
|
||||||
<div @click="router.back()" class="cursor-pointer">
|
|
||||||
<BasicIcon icon="vuesax-linear:arrow-right-1"/>
|
|
||||||
</div>
|
|
||||||
<div class="mr-2">{{edit_id? `ویرایش گنج ${name}`: 'صندوق جدید'}}</div>
|
|
||||||
</div>
|
|
||||||
<div @click="router.back()">
|
|
||||||
<!-- <BasicIcon icon="vuesax-linear:setting-4"/>-->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a-spin :spinning="loading">
|
|
||||||
<div class="pt-20 flex flex-col justify-center items-center">
|
|
||||||
<div class="bg-sky-100 w-[120px] aspect-square flex justify-center rounded-xl"><img :src="itemImg" alt=""></div>
|
|
||||||
<!-- <div class="mt-1 text-xs">تغییر عکس</div>-->
|
|
||||||
<div class="flex mt-3 w-full relative px-5" :class="[prefixCls]">
|
|
||||||
<!-- <div class="text-primary font-bold mx-auto items-center">پس انداز</div>
|
|
||||||
<div class="absolute left-5 text-gray-400 text-xs">7/25</div>-->
|
|
||||||
<van-field
|
|
||||||
v-model="name"
|
|
||||||
label=""
|
|
||||||
class="font-bold"
|
|
||||||
placeholder="عنوان را وارد کنید"
|
|
||||||
input-align="center"
|
|
||||||
:show-word-limit="true"
|
|
||||||
maxlength="25"
|
|
||||||
rows="1"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex overflow-x-scroll gap-2 whitespace-nowrap max-w-[320px] mt-2 px-3">
|
|
||||||
<!-- <BasicTags
|
|
||||||
:items="types"
|
|
||||||
color="green"
|
|
||||||
:has-add="false"
|
|
||||||
/>-->
|
|
||||||
<!-- <BasicButton v-for="item in types" size="small">{{item.name}}</BasicButton>-->
|
|
||||||
<div v-for="item in types"
|
|
||||||
class="border-1 px-3 py-2 flex-nowrap rounded text-xs color-transition background-color-transition"
|
|
||||||
:key="item.name"
|
|
||||||
:class="item.name === name? 'bg-primary text-white':'bg-gray-200 text-gray-600'"
|
|
||||||
@click="name = item.name">
|
|
||||||
{{ item.name }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- <div class="mt-2 font-bold flex text-xs items-center text-gray-500">
|
|
||||||
<div class="ml-1 text-base text-primary">{{ router.currentRoute.value.query.amount }}</div>
|
|
||||||
گرم
|
|
||||||
</div>
|
|
||||||
<div class="text-gray-400">موجودی صندوق {{itemName}}</div>
|
|
||||||
<div class="w-full p-5">
|
|
||||||
<div class="w-full flex gap-3">
|
|
||||||
<BasicButton class="w-full !bg-light" icon="vuesax-linear:add-square" icon-size="18">برداشت</BasicButton>
|
|
||||||
<BasicButton class="w-full !bg-light" icon="vuesax-linear:arrow-up" icon-size="18">واریز</BasicButton>
|
|
||||||
</div>
|
|
||||||
<div class="text-lg font-bold mt-5">تراکنش ها</div>
|
|
||||||
<div class="mt-5 pb-17">
|
|
||||||
<Item v-for="item in items" :is-up="item.isUp" :has-transition="item.hasTransition" :key="item"/>
|
|
||||||
</div>
|
|
||||||
</div>-->
|
|
||||||
<div class="wrap-fix flex justify-center bottom-23 px-5 z-200" @click="ok()">
|
|
||||||
<BasicButton class="rounded w-full px-5 py-2 text-center" type="primary" :disabled="loading">
|
|
||||||
<a-spin v-if="loading" size="small"/>
|
|
||||||
<div v-else>{{edit_id? 'ویرایش گنج':'ساخت گنج'}}</div>
|
|
||||||
</BasicButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a-spin>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||||
|
import { useFundNewPage } from './composables/useFundNewPage'
|
||||||
|
import DesktopFundNewView from './desktop/DesktopFundNewView.vue'
|
||||||
|
import MobileFundNewView from './mobile/MobileFundNewView.vue'
|
||||||
|
|
||||||
import router from "@/router";
|
const isDesktop = useDesktopBreakpoint()
|
||||||
import {computed, onMounted, ref} from "vue";
|
useFundNewPage()
|
||||||
import homeImg from "@/assets/img/cash_home.svg";
|
|
||||||
import giftImg from "@/assets/img/cash_gift.svg";
|
|
||||||
import circulationImg from "@/assets/img/cash_circulation.svg";
|
|
||||||
import loveImg from "@/assets/img/cash_love.svg";
|
|
||||||
import repairsImg from "@/assets/img/cash_repairs.svg";
|
|
||||||
import savingImg from "@/assets/img/cash_saving.svg";
|
|
||||||
import {usePrefix} from "@/utils/usePrefix";
|
|
||||||
import {loading} from "@/utils";
|
|
||||||
import {createFund, editFund, getFundById} from "@/api/fund";
|
|
||||||
import {message} from "ant-design-vue/es";
|
|
||||||
import {useFundUserStore} from "@/store/fundStore";
|
|
||||||
import BasicButton from "@/components/BasicPacks/BasicButton.vue";
|
|
||||||
import type {Fund} from "@/models/fund";
|
|
||||||
|
|
||||||
const fundStore = useFundUserStore()
|
|
||||||
|
|
||||||
|
|
||||||
const types = [
|
|
||||||
{
|
|
||||||
name: 'خرید لوازم منزل'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'پس انداز'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'گردش'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ازدواج'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'تعمیرات'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'هدیه'
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const edit_id = computed(() => {
|
|
||||||
return parseInt(router.currentRoute.value.query.id as string)
|
|
||||||
})
|
|
||||||
const name = ref<string>('پس انداز')
|
|
||||||
const {prefixCls} = usePrefix('input_name')
|
|
||||||
const info = ref<Fund>()
|
|
||||||
|
|
||||||
async function getEditInfo() {
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
info.value = await getFundById(edit_id.value)
|
|
||||||
name.value = info.value?.title?? ''
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (edit_id.value) {
|
|
||||||
getEditInfo()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const itemImg = computed(() => {
|
|
||||||
switch (name.value) {
|
|
||||||
case 'خرید لوازم منزل':
|
|
||||||
return homeImg
|
|
||||||
case 'هدیه':
|
|
||||||
return giftImg
|
|
||||||
case 'گردش':
|
|
||||||
return circulationImg
|
|
||||||
case 'ازدواج':
|
|
||||||
return loveImg
|
|
||||||
case 'تعمیرات':
|
|
||||||
return repairsImg
|
|
||||||
case 'پس انداز':
|
|
||||||
return savingImg
|
|
||||||
default:
|
|
||||||
return savingImg
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
async function ok() {
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
// console.log(name.value)
|
|
||||||
name.value ? edit_id.value? await editFund(edit_id.value,{title: name.value}) :await createFund({title: name.value}) : () => {
|
|
||||||
message.error('ابتدا نام گنج را وارد کنید')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await fundStore.getMyFund()
|
|
||||||
message.success(`گنج با موفقیت ${edit_id.value? 'ویرایش':'ساخته'} شد`)
|
|
||||||
router.back()
|
|
||||||
// name.value? message.warn('ss'): message.error('ابتدا نام صندوق را وارد کنید')
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style scoped>
|
||||||
@prefix: ~'@{prefixCls}-input_name';
|
.fund-new-page-root {
|
||||||
|
min-height: 100%;
|
||||||
.@{prefix} {
|
height: 100%;
|
||||||
.van-field__value {
|
|
||||||
@apply flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.van-field__body {
|
|
||||||
@apply w-full mr-5;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="bg-white wrap-fix py-3 px-3 text-primary font-bold drop-shadow flex h-12 z-20 justify-between"
|
||||||
|
>
|
||||||
|
<div class="flex">
|
||||||
|
<div class="cursor-pointer" @click="goBack()">
|
||||||
|
<BasicIcon icon="vuesax-linear:arrow-right-1" />
|
||||||
|
</div>
|
||||||
|
<div class="mr-2">{{ edit_id ? `ویرایش گنج ${name}` : 'صندوق جدید' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<div class="pt-20 flex flex-col justify-center items-center">
|
||||||
|
<div class="bg-sky-100 w-[120px] aspect-square flex justify-center rounded-xl">
|
||||||
|
<img :src="itemImg" alt="" />
|
||||||
|
</div>
|
||||||
|
<div class="flex mt-3 w-full relative px-5" :class="[prefixCls]">
|
||||||
|
<van-field
|
||||||
|
v-model="name"
|
||||||
|
label=""
|
||||||
|
class="font-bold"
|
||||||
|
placeholder="عنوان را وارد کنید"
|
||||||
|
input-align="center"
|
||||||
|
:show-word-limit="true"
|
||||||
|
maxlength="25"
|
||||||
|
rows="1"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex overflow-x-scroll gap-2 whitespace-nowrap max-w-[320px] mt-2 px-3">
|
||||||
|
<div
|
||||||
|
v-for="item in types"
|
||||||
|
:key="item.name"
|
||||||
|
class="border-1 px-3 py-2 flex-nowrap rounded text-xs color-transition background-color-transition"
|
||||||
|
:class="item.name === name ? 'bg-primary text-white' : 'bg-gray-200 text-gray-600'"
|
||||||
|
@click="name = item.name"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wrap-fix flex justify-center bottom-23 px-5 z-200" @click="ok()">
|
||||||
|
<BasicButton class="rounded w-full px-5 py-2 text-center" type="primary" :disabled="loading">
|
||||||
|
<a-spin v-if="loading" size="small" />
|
||||||
|
<div v-else>{{ edit_id ? 'ویرایش گنج' : 'ساخت گنج' }}</div>
|
||||||
|
</BasicButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
|
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||||
|
import { loading } from '@/utils'
|
||||||
|
import { useFundNewPageContext } from '../composables/useFundNewPage'
|
||||||
|
|
||||||
|
const { types, edit_id, name, prefixCls, itemImg, ok, goBack } = useFundNewPageContext()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
@prefix: ~'@{prefixCls}-input_name';
|
||||||
|
|
||||||
|
.@{prefix} {
|
||||||
|
.van-field__value {
|
||||||
|
@apply flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.van-field__body {
|
||||||
|
@apply w-full mr-5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<a-form
|
||||||
|
:model="form"
|
||||||
|
class="!mt-0"
|
||||||
|
name="gift-card-form"
|
||||||
|
@finish="registerGiftCard"
|
||||||
|
>
|
||||||
|
<a-form-item :rules="[required()]" label="کد کارت هدیه" name="code">
|
||||||
|
<BasicInput
|
||||||
|
v-model:value="form.code"
|
||||||
|
place-holder="کد کارت هدیه را وارد کنید"
|
||||||
|
size="large"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<p :class="variant === 'desktop' ? 'dgift-form__hint' : 'font-bold text-xs mt-3'">
|
||||||
|
کد کارت هدیه خود را در فیلد بالا وارد کنید و سپس دکمه ثبت را بزنید.
|
||||||
|
</p>
|
||||||
|
<a-form-item>
|
||||||
|
<BasicButton
|
||||||
|
:disabled="disableButton"
|
||||||
|
:class="variant === 'desktop' ? 'dgift-form__submit' : 'w-full py-2 mt-5'"
|
||||||
|
html-type="submit"
|
||||||
|
type="primary"
|
||||||
|
>
|
||||||
|
<a-spin v-if="loading" size="small" />
|
||||||
|
<span v-else>ثبت کد</span>
|
||||||
|
</BasicButton>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||||
|
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
||||||
|
import { required } from '@/utils/inputRules'
|
||||||
|
import { useGiftCardPageContext } from '../composables/useGiftCardPage'
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
variant?: 'mobile' | 'desktop'
|
||||||
|
}>(),
|
||||||
|
{ variant: 'mobile' },
|
||||||
|
)
|
||||||
|
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
disableButton,
|
||||||
|
loading,
|
||||||
|
registerGiftCard,
|
||||||
|
} = useGiftCardPageContext()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { computed, inject, provide, ref, type ComputedRef, type InjectionKey, type Ref } from 'vue'
|
||||||
|
import { loading } from '@/utils'
|
||||||
|
import { registerCodeGiftCard } from '@/api/giftCard'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
|
export interface GiftCardPageContext {
|
||||||
|
form: Ref<{ code: string }>
|
||||||
|
disableButton: ComputedRef<boolean>
|
||||||
|
loading: typeof loading
|
||||||
|
registerGiftCard: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GIFT_CARD_PAGE_CONTEXT_KEY: InjectionKey<GiftCardPageContext> =
|
||||||
|
Symbol('giftCardPageContext')
|
||||||
|
|
||||||
|
export function useGiftCardPage() {
|
||||||
|
const form = ref<{ code: string }>({
|
||||||
|
code: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const disableButton = computed(() => {
|
||||||
|
if (!loading.value) {
|
||||||
|
return !form.value.code || form.value.code.trim().length === 0
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
async function registerGiftCard() {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
await registerCodeGiftCard(form.value.code)
|
||||||
|
message.success('کد کارت هدیه با موفقیت ثبت شد')
|
||||||
|
form.value.code = ''
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.response?.data?.message || 'خطا در ثبت کد کارت هدیه')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const context: GiftCardPageContext = {
|
||||||
|
form,
|
||||||
|
disableButton,
|
||||||
|
loading,
|
||||||
|
registerGiftCard,
|
||||||
|
}
|
||||||
|
|
||||||
|
provide(GIFT_CARD_PAGE_CONTEXT_KEY, context)
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGiftCardPageContext() {
|
||||||
|
const context = inject(GIFT_CARD_PAGE_CONTEXT_KEY)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useGiftCardPageContext must be used within a page that calls useGiftCardPage()')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dgift">
|
||||||
|
<div class="dgift__container">
|
||||||
|
<header class="dgift-topbar">
|
||||||
|
<button type="button" class="dgift-topbar__back" @click="router.back()">
|
||||||
|
<BasicIcon icon="vuesax-linear:arrow-right-1" size="18" />
|
||||||
|
<span>بازگشت</span>
|
||||||
|
</button>
|
||||||
|
<div class="dgift-topbar__meta">
|
||||||
|
<p class="dgift-topbar__eyebrow">هدایا</p>
|
||||||
|
<h1 class="dgift-topbar__title">ثبت کارت هدیه</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="dgift-layout">
|
||||||
|
<section class="dgift-surface dgift-form">
|
||||||
|
<div class="dgift-form__header">
|
||||||
|
<div class="dgift-form__icon">
|
||||||
|
<BasicIcon icon="vuesax-linear:gift" size="28" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="dgift-form__title">ثبت کد کارت هدیه</h2>
|
||||||
|
<p class="dgift-form__subtitle">کد دریافتی خود را وارد کنید تا اعتبار به حساب شما اضافه شود</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<GiftCardForm variant="desktop" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside class="dgift-surface dgift-guide">
|
||||||
|
<h3 class="dgift-guide__title">راهنمای استفاده</h3>
|
||||||
|
<ol class="dgift-guide__steps">
|
||||||
|
<li>کد کارت هدیه را از پیامک یا کارت فیزیکی دریافت کنید.</li>
|
||||||
|
<li>کد را بدون فاصله اضافی در فیلد مربوطه وارد کنید.</li>
|
||||||
|
<li>روی «ثبت کد» بزنید و پیام تأیید را بررسی کنید.</li>
|
||||||
|
</ol>
|
||||||
|
<div class="dgift-guide__note">
|
||||||
|
<BasicIcon icon="vuesax-linear:info-circle" size="18" />
|
||||||
|
<p>هر کد فقط یکبار قابل استفاده است و پس از ثبت موفق، موجودی کیف پول بهروزرسانی میشود.</p>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
|
import router from '@/router'
|
||||||
|
import GiftCardForm from '../components/GiftCardForm.vue'
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
/* Premium desktop gift card page */
|
||||||
|
|
||||||
|
.dgift {
|
||||||
|
min-height: 100%;
|
||||||
|
background: #f6f7f9;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dgift__container {
|
||||||
|
max-width: 960px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 28px 36px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dgift-topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
|
||||||
|
&__back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__eyebrow {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dgift-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 320px);
|
||||||
|
gap: 20px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dgift-surface {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dgift-form {
|
||||||
|
padding: 28px;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #eef0f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(9, 61, 81, 0.08);
|
||||||
|
color: @App-primary-color;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__hint {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__submit {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 44px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dgift-guide {
|
||||||
|
padding: 24px;
|
||||||
|
position: sticky;
|
||||||
|
top: 24px;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__steps {
|
||||||
|
margin: 0 0 20px;
|
||||||
|
padding: 0 20px 0 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__note {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #f9fafb;
|
||||||
|
border: 1px solid #eef0f3;
|
||||||
|
color: #6b7280;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg,
|
||||||
|
.basic-icon {
|
||||||
|
color: @App-primary-color;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1023.98px) {
|
||||||
|
.dgift-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dgift-guide {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,78 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="gift-card-page-root">
|
||||||
<div class="bg-white py-3 px-3 text-black font-bold drop-shadow z-[1999] wrap-fix">کارت هدیه</div>
|
<DesktopGiftCardView v-show="isDesktop" />
|
||||||
<div class="px-5 pt-26 pb-2">
|
<MobileGiftCardView v-show="!isDesktop" />
|
||||||
<a-form :model="form" class="!mt-5" name="basic" @finish="registerGiftCard">
|
|
||||||
<div class="border-gray-200 border-1 rounded-xl p-5 trans h-40 relative">
|
|
||||||
<bg_main />
|
|
||||||
<div class="z-100 relative">
|
|
||||||
<a-form-item :rules="[required()]" label="کد کارت هدیه" name="code">
|
|
||||||
<BasicInput
|
|
||||||
v-model:value="form.code"
|
|
||||||
place-holder="کد کارت هدیه را وارد کنید"
|
|
||||||
size="large"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
<div class="font-bold text-xs mt-3">
|
|
||||||
کد کارت هدیه خود را در فیلد بالا وارد کنید و سپس دکمه ثبت را بزنید.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a-form-item>
|
|
||||||
<BasicButton
|
|
||||||
:disabled="disableButton"
|
|
||||||
class="w-full py-2 mt-5"
|
|
||||||
html-type="submit"
|
|
||||||
type="primary"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<a-spin v-if="loading" size="small" />
|
|
||||||
<div v-else>ثبت کد</div>
|
|
||||||
</div>
|
|
||||||
</BasicButton>
|
|
||||||
</a-form-item>
|
|
||||||
</a-form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from 'vue'
|
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||||
import { loading } from '@/utils'
|
import { useGiftCardPage } from './composables/useGiftCardPage'
|
||||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
import DesktopGiftCardView from './desktop/DesktopGiftCardView.vue'
|
||||||
import Bg_main from '@/components/bg_main.vue'
|
import MobileGiftCardView from './mobile/MobileGiftCardView.vue'
|
||||||
import { registerCodeGiftCard } from '@/api/giftCard'
|
|
||||||
import { message } from 'ant-design-vue'
|
|
||||||
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
|
||||||
import { required } from '@/utils/inputRules'
|
|
||||||
|
|
||||||
const form = ref<{ code: string }>({
|
const isDesktop = useDesktopBreakpoint()
|
||||||
code: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const disableButton = computed(() => {
|
useGiftCardPage()
|
||||||
if (!loading.value) {
|
|
||||||
return !form.value.code || form.value.code.trim().length === 0
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
async function registerGiftCard() {
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
await registerCodeGiftCard(form.value.code)
|
|
||||||
message.success('کد کارت هدیه با موفقیت ثبت شد')
|
|
||||||
form.value.code = ''
|
|
||||||
} catch (e: any) {
|
|
||||||
message.error(e?.response?.data?.message || 'خطا در ثبت کد کارت هدیه')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.gift-card-page-root {
|
||||||
|
min-height: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="bg-white py-3 px-3 text-black font-bold drop-shadow z-[1999] wrap-fix">کارت هدیه</div>
|
||||||
|
<div class="px-5 pt-26 pb-2">
|
||||||
|
<div class="border-gray-200 border-1 rounded-xl p-5 trans h-40 relative">
|
||||||
|
<bg_main />
|
||||||
|
<div class="z-100 relative">
|
||||||
|
<GiftCardForm variant="mobile" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import Bg_main from '@/components/bg_main.vue'
|
||||||
|
import GiftCardForm from '../components/GiftCardForm.vue'
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/* Desktop standalone gold trade page */
|
||||||
|
|
||||||
|
.dgold {
|
||||||
|
min-height: 100%;
|
||||||
|
background: #f6f7f9;
|
||||||
|
color: #111827;
|
||||||
|
padding: 28px 36px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dgold-topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto 24px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
|
||||||
|
&__back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__eyebrow {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dgold__panel {
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: calc(100vh - 180px);
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dgold__panel .mb-25vh {
|
||||||
|
margin-bottom: 2rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dgold__panel .bottom-18 {
|
||||||
|
position: static !important;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
@@ -1,5 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-35rem overflow-y-auto">
|
<div :class="isStandaloneDesktop ? 'dgold' : 'h-35rem overflow-y-auto'">
|
||||||
|
<header v-if="isStandaloneDesktop" class="dgold-topbar">
|
||||||
|
<button type="button" class="dgold-topbar__back" @click="router.back()">
|
||||||
|
<BasicIcon icon="vuesax-linear:arrow-right-1" size="18" />
|
||||||
|
بازگشت
|
||||||
|
</button>
|
||||||
|
<div class="dgold-topbar__meta">
|
||||||
|
<p class="dgold-topbar__eyebrow">معامله</p>
|
||||||
|
<h1 class="dgold-topbar__title">خرید و فروش {{ tradeProductLabel }}</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div :class="isStandaloneDesktop ? 'dgold__panel' : ''">
|
||||||
<div class=" !overflow-y-scroll bg-white py-3 text-primary font-bold flex justify-evenly">
|
<div class=" !overflow-y-scroll bg-white py-3 text-primary font-bold flex justify-evenly">
|
||||||
<!-- <div @click="isBuy = false" class="w-full text-center" :class="!isBuy?'text-primary':'text-gray-400'">خرید طلا</div>
|
<!-- <div @click="isBuy = false" class="w-full text-center" :class="!isBuy?'text-primary':'text-gray-400'">خرید طلا</div>
|
||||||
<div @click="isBuy = true" class="w-full text-center" :class="isBuy?'text-primary':'text-gray-400'">فروش طلا</div>-->
|
<div @click="isBuy = true" class="w-full text-center" :class="isBuy?'text-primary':'text-gray-400'">فروش طلا</div>-->
|
||||||
@@ -150,7 +161,7 @@
|
|||||||
</div>-->
|
</div>-->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BasicActionSheet v-model:show="confirmAction">
|
<BasicActionSheet v-if="!isStandaloneDesktop" v-model:show="confirmAction">
|
||||||
<div class="font-bold text-lg"></div>
|
<div class="font-bold text-lg"></div>
|
||||||
<div class="w-full rounded-lg bg-gray-50 p-3">
|
<div class="w-full rounded-lg bg-gray-50 p-3">
|
||||||
<div class="flex justify-between ">
|
<div class="flex justify-between ">
|
||||||
@@ -185,6 +196,52 @@
|
|||||||
</div>
|
</div>
|
||||||
<BasicButton class="mb-20 mt-5 w-full" type="primary" @click="ok(); confirmAction= false">ثبت نهایی</BasicButton>
|
<BasicButton class="mb-20 mt-5 w-full" type="primary" @click="ok(); confirmAction= false">ثبت نهایی</BasicButton>
|
||||||
</BasicActionSheet>
|
</BasicActionSheet>
|
||||||
|
<BasicModal
|
||||||
|
v-else
|
||||||
|
v-model:visible="confirmAction"
|
||||||
|
title="تأیید سفارش"
|
||||||
|
width="480px"
|
||||||
|
destroy-on-close
|
||||||
|
custom-footer
|
||||||
|
>
|
||||||
|
<div class="w-full rounded-lg bg-gray-50 p-3">
|
||||||
|
<div class="flex justify-between ">
|
||||||
|
<div>وزن:</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="font-bold">{{ form.goldValue }}</div>
|
||||||
|
<div class="text-xs mr-1">{{ weightUnit }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between mt-3">
|
||||||
|
<div>مظنه:</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="font-bold">{{ usePriceWithoutType(Math.round(goldPriceValue)) }}</div>
|
||||||
|
<div class="text-xs mr-1">ریال</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between mt-3">
|
||||||
|
<div>کارمزد:</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="font-bold text-xl">{{ feeInGrams }}</div>
|
||||||
|
<div class="text-xs mr-1">{{ weightUnit }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>(معادل {{ equivalentPrice }} ریال)</div>
|
||||||
|
<div class="flex justify-between mt-3">
|
||||||
|
<div>{{ isBuy ? 'قیمت قابل پرداخت:' : 'مبلغ دریافتی:' }}</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="font-bold">{{ usePriceWithoutType(payablePrice.toFixed(0)) }}</div>
|
||||||
|
<div class="text-xs mr-1">ریال</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<BasicButton class="mt-5 w-full" type="primary" @click="ok(); confirmAction = false">
|
||||||
|
ثبت نهایی
|
||||||
|
</BasicButton>
|
||||||
|
<template #footer>
|
||||||
|
<div></div>
|
||||||
|
</template>
|
||||||
|
</BasicModal>
|
||||||
<van-dialog v-model:show="noCompleteVisible" class="completeButtonStyle" confirm-button-text="تکمیل اطلاعات"
|
<van-dialog v-model:show="noCompleteVisible" class="completeButtonStyle" confirm-button-text="تکمیل اطلاعات"
|
||||||
show-confirm-button @confirm="router.push({name: 'user',query:{showEdit: true}})">
|
show-confirm-button @confirm="router.push({name: 'user',query:{showEdit: true}})">
|
||||||
<div class="flex flex-col items-center p-2">
|
<div class="flex flex-col items-center p-2">
|
||||||
@@ -197,6 +254,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</van-dialog>
|
</van-dialog>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -218,6 +276,8 @@ import { getLastPrice } from '@/api/home'
|
|||||||
import Icon from '@/components/BasicPacks/BasicIcon.vue'
|
import Icon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
import { useUserStore } from '@/store/userStore'
|
import { useUserStore } from '@/store/userStore'
|
||||||
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
||||||
|
import BasicModal from '@/components/BasicPacks/BasicModal.vue'
|
||||||
|
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||||
import { justNumber } from '@/utils/inputRules'
|
import { justNumber } from '@/utils/inputRules'
|
||||||
import GoldSkeleton from '@/views/base/gold/components/GoldSkeleton.vue'
|
import GoldSkeleton from '@/views/base/gold/components/GoldSkeleton.vue'
|
||||||
import GoldLoading from '@/components/GoldLoading.vue'
|
import GoldLoading from '@/components/GoldLoading.vue'
|
||||||
@@ -270,6 +330,8 @@ interface Prop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Prop>(), {})
|
const props = withDefaults(defineProps<Prop>(), {})
|
||||||
|
const isDesktop = useDesktopBreakpoint()
|
||||||
|
const isStandaloneDesktop = computed(() => isDesktop.value && !props.ProductItem)
|
||||||
const isBuy = ref<boolean>(true)
|
const isBuy = ref<boolean>(true)
|
||||||
|
|
||||||
const tradeProductLabel = computed(() => {
|
const tradeProductLabel = computed(() => {
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:class="y > 50 ? 'bg-white shadow opacity-95' : ''"
|
:class="y > 50 ? 'bg-white shadow opacity-95' : ''"
|
||||||
class="px-4 py-2 top-0 z-5 wrap-fix background-color-transition"
|
class="px-4 py-2 top-0 z-5 wrap-fix background-color-transition"
|
||||||
>
|
>
|
||||||
<div class="flex justify-between my-2">
|
<div class="flex justify-between my-2 items-center">
|
||||||
<div class="flex items-center gap-2" @click.stop="router.push({ name: 'user' })">
|
<div v-if="isBoursecuProject()" class="flex items-center gap-2">
|
||||||
|
<img :src="projectLogo" alt="" class="h-8 w-auto object-contain" />
|
||||||
|
<div class="color-transition font-bold text-primary">{{ appName }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex items-center gap-2" @click.stop="router.push({ name: 'user' })">
|
||||||
<div
|
<div
|
||||||
class="z-100000 rounded-full border-1"
|
class="z-100000 rounded-full border-1"
|
||||||
:class="y > 50 ? 'text-primary border-primary' : 'text-primary border-primary'"
|
:class="y > 50 ? 'text-primary border-primary' : 'text-primary border-primary'"
|
||||||
>
|
>
|
||||||
<Icon icon="vuesax-linear:user" size="20" />
|
<Icon icon="vuesax-linear:user" size="20" />
|
||||||
</div>
|
</div>
|
||||||
@@ -15,26 +19,69 @@
|
|||||||
{{ userStore.userInfo?.name }}
|
{{ userStore.userInfo?.name }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Icon
|
|
||||||
:color="y > 50 ? '#0A1B37' : '#0A1B37'"
|
<div class="flex items-center gap-2">
|
||||||
class="color-transition"
|
<button
|
||||||
icon="vuesax-linear:logout"
|
v-if="isBoursecuProject()"
|
||||||
|
type="button"
|
||||||
|
class="mhome-header-btn"
|
||||||
|
@click="router.push({ name: 'user' })"
|
||||||
|
>
|
||||||
|
<Icon icon="vuesax-linear:user" size="18" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="mhome-header-btn mhome-header-btn--logout"
|
||||||
@click="$emit('logout')"
|
@click="$emit('logout')"
|
||||||
/>
|
>
|
||||||
|
<Icon icon="vuesax-linear:logout" size="18" />
|
||||||
|
<span>خروج</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
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'
|
||||||
|
|
||||||
const { y } = useWindowScroll()
|
const { y } = useWindowScroll()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
const config = useConfigStore()
|
||||||
|
const projectLogo = computed(() => getProjectNavLogo())
|
||||||
|
const appName = computed(() => config.config?.name_fa || config.config?.name || 'بورس مس')
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
logout: []
|
logout: []
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mhome-header-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
color: #0a1b37;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mhome-header-btn--logout {
|
||||||
|
color: #dc2626;
|
||||||
|
border-color: #fecaca;
|
||||||
|
background: #fef2f2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,26 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-primary rounded-lg p-2 py-3 mt-2">
|
<div :class="plain ? 'py-2' : 'bg-primary rounded-lg p-2 py-3 mt-2'">
|
||||||
<div v-if="meta?.has_count_based_products" class="flex justify-center text-tertiary w-full z-20">
|
<div
|
||||||
|
v-if="meta?.has_count_based_products"
|
||||||
|
:class="rowClass"
|
||||||
|
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">{{ countBasedInventory }} عدد</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="meta?.single_metal && primaryMetalTotal">
|
<template v-if="meta?.single_metal && primaryMetalTotal">
|
||||||
<div class="flex mt-2 justify-center text-tertiary w-full z-20">
|
<div :class="[rowClass, meta?.has_count_based_products ? 'mt-2' : '']" class="flex justify-center w-full z-20">
|
||||||
{{ primaryMetalTotal.label }}:
|
{{ primaryMetalTotal.label }}:
|
||||||
<div class="text-l mr-2 font-bold">{{ formatMetalTotal(primaryMetalTotal) }}</div>
|
<div class="text-l mr-2 font-bold">{{ formatMetalTotal(primaryMetalTotal) }}</div>
|
||||||
<BasicIcon
|
<BasicIcon
|
||||||
icon="vuesax-linear:info-circle"
|
icon="vuesax-linear:info-circle"
|
||||||
size="18"
|
size="18"
|
||||||
color="tertiary"
|
:color="iconColor"
|
||||||
@click="$emit('openGoldModal', primaryMetalTotal)"
|
@click="$emit('openGoldModal', primaryMetalTotal)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div
|
<div
|
||||||
v-for="metal in metalTotals"
|
v-for="(metal, index) in metalTotals"
|
||||||
:key="metal.metal_id"
|
:key="metal.metal_id"
|
||||||
class="flex mt-2 justify-center text-tertiary w-full z-20"
|
:class="[rowClass, index > 0 || meta?.has_count_based_products ? 'mt-2' : '']"
|
||||||
|
class="flex justify-center w-full z-20"
|
||||||
>
|
>
|
||||||
{{ metal.label }}:
|
{{ metal.label }}:
|
||||||
<div class="text-l mr-2 font-bold">{{ formatMetalTotal(metal) }}</div>
|
<div class="text-l mr-2 font-bold">{{ formatMetalTotal(metal) }}</div>
|
||||||
@@ -28,34 +33,50 @@
|
|||||||
v-if="metal.metal_id === metalTotals[0]?.metal_id"
|
v-if="metal.metal_id === metalTotals[0]?.metal_id"
|
||||||
icon="vuesax-linear:info-circle"
|
icon="vuesax-linear:info-circle"
|
||||||
size="18"
|
size="18"
|
||||||
color="tertiary"
|
:color="iconColor"
|
||||||
@click="$emit('openGoldModal', metal)"
|
@click="$emit('openGoldModal', metal)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="flex mt-2 justify-center text-tertiary w-full z-20">
|
<div :class="[rowClass, 'mt-2']" class="flex justify-center w-full z-20">
|
||||||
دارایی ریالی:
|
دارایی ریالی:
|
||||||
<div class="text-l mr-2 font-bold">{{ usePriceWithoutType(totalRialValue) }} ریال</div>
|
<div class="text-l mr-2 font-bold">{{ usePriceWithoutType(totalRialValue) }} ریال</div>
|
||||||
<BasicIcon @click="$emit('openRialModal')" icon="vuesax-linear:info-circle" size="18" color="tertiary" />
|
<BasicIcon
|
||||||
|
icon="vuesax-linear:info-circle"
|
||||||
|
size="18"
|
||||||
|
:color="iconColor"
|
||||||
|
@click="$emit('openRialModal')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted } from 'vue'
|
import { computed } from 'vue'
|
||||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
import { usePriceWithoutType } from '@/utils/usePrice'
|
import { usePriceWithoutType } from '@/utils/usePrice'
|
||||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||||
import type { WalletMetalTotal } from '@/models/walletAssets'
|
import type { WalletMetalTotal } from '@/models/walletAssets'
|
||||||
import { resolveSummaryMetalWeight } from '@/utils/productAssets'
|
import { resolveSummaryMetalWeight } from '@/utils/productAssets'
|
||||||
|
|
||||||
const { assetsSummary, loadWalletAssets } = useWalletAssets()
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
plain?: boolean
|
||||||
|
}>(),
|
||||||
|
{ plain: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Display-only — wallet assets are loaded once in useHomePageData.initHomePageData() */
|
||||||
|
const { assetsSummary } = useWalletAssets()
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
openGoldModal: [metal: WalletMetalTotal]
|
openGoldModal: [metal: WalletMetalTotal]
|
||||||
openRialModal: []
|
openRialModal: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const rowClass = computed(() => (props.plain ? 'text-primary' : 'text-tertiary'))
|
||||||
|
const iconColor = computed(() => (props.plain ? 'primary' : 'tertiary'))
|
||||||
|
|
||||||
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])
|
||||||
@@ -65,10 +86,6 @@ const countBasedInventory = computed(() =>
|
|||||||
(assetsSummary.value?.count_products ?? []).reduce((sum, item) => sum + Number(item.quantity ?? 0), 0)
|
(assetsSummary.value?.count_products ?? []).reduce((sum, item) => sum + Number(item.quantity ?? 0), 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
void loadWalletAssets()
|
|
||||||
})
|
|
||||||
|
|
||||||
function formatMetalTotal(metal: WalletMetalTotal) {
|
function formatMetalTotal(metal: WalletMetalTotal) {
|
||||||
return `${resolveSummaryMetalWeight(metal, inventoryCards.value)}${metal.unit}`
|
return `${resolveSummaryMetalWeight(metal, inventoryCards.value)}${metal.unit}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, watch } from 'vue'
|
import { computed, watch } from 'vue'
|
||||||
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
||||||
import BasicInfoItem from '@/components/BasicPacks/BasicInfoItem.vue'
|
import BasicInfoItem from '@/components/BasicPacks/BasicInfoItem.vue'
|
||||||
import { usePrice } from '@/utils/usePrice'
|
import { usePrice } from '@/utils/usePrice'
|
||||||
@@ -81,10 +81,6 @@ const cashWallet = computed(() => assetsSummary.value?.cash_wallet ?? 0)
|
|||||||
const assetsRialValue = computed(() => assetsSummary.value?.assets_rial_value ?? 0)
|
const assetsRialValue = computed(() => assetsSummary.value?.assets_rial_value ?? 0)
|
||||||
const totalRialValue = computed(() => assetsSummary.value?.total_rial_value ?? 0)
|
const totalRialValue = computed(() => assetsSummary.value?.total_rial_value ?? 0)
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
void loadWalletAssets()
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.show,
|
() => props.show,
|
||||||
(open) => {
|
(open) => {
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Icon from '@/components/BasicPacks/BasicIcon.vue'
|
||||||
|
import { isBoursecuProject } from '@/utils/config'
|
||||||
|
import type { HomeQuickActionItem } from '../layouts/types'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
action: HomeQuickActionItem
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
click: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isBoursecu = isBoursecuProject()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="home-quick-action-box flex flex-col items-center justify-center gap-2 rounded-xl border-2 p-3 min-h-[96px] w-full"
|
||||||
|
:class="{ 'home-quick-action-box--boursecu': isBoursecu }"
|
||||||
|
@click="$emit('click')"
|
||||||
|
>
|
||||||
|
<div class="flex h-11 w-11 shrink-0 items-center justify-center">
|
||||||
|
<Icon class="text-primary" :icon="action.icon" size="28" />
|
||||||
|
</div>
|
||||||
|
<span class="text-primary text-xs font-bold text-center leading-5">{{ action.label }}</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.home-quick-action-box {
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-quick-action-box--boursecu {
|
||||||
|
background-color: #eff5f3;
|
||||||
|
border-color: #d4e3de;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,63 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="skeletonRatio" class="h-fit">
|
<div ref="skeletonRatio" class="h-fit w-full min-h-[620px]">
|
||||||
<content-loader :width="skeletonSize.width" class="bg-white" height="100vh">
|
<content-loader
|
||||||
<rect :width="skeletonSize.width" height="5" rx="3" ry="3" x="0" y="0"/>
|
:width="skeletonWidth"
|
||||||
<rect :width="skeletonSize.width" height="5" rx="3" ry="3" x="0" y="84"/>
|
class="bg-white"
|
||||||
<rect :width="5" height="85" rx="3" ry="3" x="0" y="0"/>
|
:height="skeletonHeight"
|
||||||
<rect :width="5" :x="skeletonSize.width-5" height="85" rx="3" ry="3" y="0"/>
|
>
|
||||||
<rect :width="2" :x="skeletonSize.width/2" height="60" rx="3" ry="3" y="15"/>
|
<rect :width="skeletonWidth" height="5" rx="3" ry="3" x="0" y="0" />
|
||||||
<rect :width="skeletonSize.width/2-20" :x="10" height="20" rx="10" ry="10" y="15"/>
|
<rect :width="skeletonWidth" height="5" rx="3" ry="3" x="0" y="84" />
|
||||||
<rect :width="skeletonSize.width/2-20" :x="skeletonSize.width/2 + 10" height="20" rx="10" ry="10" y="15"/>
|
<rect width="5" height="85" rx="3" ry="3" x="0" y="0" />
|
||||||
<rect :width="skeletonSize.width/2-40" :x="20" height="10" rx="5" ry="5" y="40"/>
|
<rect width="5" :x="skeletonWidth - 5" height="85" rx="3" ry="3" y="0" />
|
||||||
<rect :width="skeletonSize.width/2-40" :x="skeletonSize.width/2 + 20" height="10" rx="5" ry="5" y="40"/>
|
<rect width="2" :x="skeletonWidth / 2" height="60" rx="3" ry="3" y="15" />
|
||||||
<rect :width="skeletonSize.width/2-30" :x="15" height="18" rx="10" ry="10" y="60"/>
|
<rect :width="halfWidth(20)" x="10" height="20" rx="10" ry="10" y="15" />
|
||||||
<rect :width="skeletonSize.width/2-30" :x="skeletonSize.width/2 + 15" height="18" rx="10" ry="10" y="60"/>
|
<rect :width="halfWidth(20)" :x="skeletonWidth / 2 + 10" height="20" rx="10" ry="10" y="15" />
|
||||||
<circle :cx="skeletonSize.width/4-skeletonSize.width/8" cy="145" r="20"/>
|
<rect :width="halfWidth(40)" x="20" height="10" rx="5" ry="5" y="40" />
|
||||||
<circle :cx="skeletonSize.width/4+skeletonSize.width/8" cy="145" r="20"/>
|
<rect :width="halfWidth(40)" :x="skeletonWidth / 2 + 20" height="10" rx="5" ry="5" y="40" />
|
||||||
<circle :cx="skeletonSize.width/2+ skeletonSize.width/8" cy="145" r="20"/>
|
<rect :width="halfWidth(30)" x="15" height="18" rx="10" ry="10" y="60" />
|
||||||
<circle :cx="skeletonSize.width - skeletonSize.width/8" cy="145" r="20"/>
|
<rect :width="halfWidth(30)" :x="skeletonWidth / 2 + 15" height="18" rx="10" ry="10" y="60" />
|
||||||
<rect :width="50" :x="skeletonSize.width/4-skeletonSize.width/8 - 25" height="15" rx="3" ry="3" y="170"/>
|
<circle :cx="skeletonWidth / 4 - skeletonWidth / 8" cy="145" r="20" />
|
||||||
<rect :width="50" :x="skeletonSize.width/4+skeletonSize.width/8 - 25" height="15" rx="3" ry="3" y="170"/>
|
<circle :cx="skeletonWidth / 4 + skeletonWidth / 8" cy="145" r="20" />
|
||||||
<rect :width="50" :x="skeletonSize.width/2+ skeletonSize.width/8 - 25" height="15" rx="3" ry="3" y="170"/>
|
<circle :cx="skeletonWidth / 2 + skeletonWidth / 8" cy="145" r="20" />
|
||||||
<rect :width="50" :x="skeletonSize.width - skeletonSize.width/8 - 25" height="15" rx="3" ry="3" y="170"/>
|
<circle :cx="skeletonWidth - skeletonWidth / 8" cy="145" r="20" />
|
||||||
<rect :width="skeletonSize.width - 45" :x="45" height="20" rx="3" ry="3" y="230"/>
|
<rect width="50" :x="skeletonWidth / 4 - skeletonWidth / 8 - 25" height="15" rx="3" ry="3" y="170" />
|
||||||
<rect :width="skeletonSize.width" :x="0" height="80" rx="3" ry="3" y="260"/>
|
<rect width="50" :x="skeletonWidth / 4 + skeletonWidth / 8 - 25" height="15" rx="3" ry="3" y="170" />
|
||||||
<rect :width="skeletonSize.width/2 - 5" :x="0" height="50" rx="3" ry="3" y="360"/>
|
<rect width="50" :x="skeletonWidth / 2 + skeletonWidth / 8 - 25" height="15" rx="3" ry="3" y="170" />
|
||||||
<rect :width="skeletonSize.width/2 - 5" :x="skeletonSize.width/2 + 5" height="50" rx="3" ry="3" y="360"/>
|
<rect width="50" :x="skeletonWidth - skeletonWidth / 8 - 25" height="15" rx="3" ry="3" y="170" />
|
||||||
<rect :width="skeletonSize.width" :x="0" height="170" rx="3" ry="3" y="440"/>
|
<rect :width="fullWidth(45)" x="45" height="20" rx="3" ry="3" y="230" />
|
||||||
<!-- <rect height="20" rx="3" ry="3" width="100" x="0" y="0"/>
|
<rect :width="skeletonWidth" x="0" height="80" rx="3" ry="3" y="260" />
|
||||||
<rect :width="skeletonSize.width/4 + 40" :x="skeletonSize.width/2" height="20" rx="3" ry="3" y="0"/>
|
<rect :width="halfWidth(10)" x="0" height="50" rx="3" ry="3" y="360" />
|
||||||
<rect height="20" rx="3" ry="3" width="80" x="0" y="30"/>
|
<rect :width="halfWidth(10)" :x="skeletonWidth / 2 + 5" height="50" rx="3" ry="3" y="360" />
|
||||||
<rect :x="skeletonSize.width - 40" height="40" rx="12" ry="12" width="40" y="0"/>
|
<rect :width="skeletonWidth" x="0" height="170" rx="3" ry="3" y="440" />
|
||||||
<rect :width="skeletonSize.width" :x="0" :y="skeletonSize.height-5" height="2" rx="12" ry="12"/>-->
|
|
||||||
<!-- <circle :cx="skeletonSize.width - 20" :cy="skeletonSize.height/2 - 10" r="20" />-->
|
|
||||||
<!-- <rect :x="10" y="20" rx="3" ry="3" :width="skeletonSize.width - 20" height="150"/>
|
|
||||||
<circle :cx="skeletonSize.width/2" cy="170" r="15" />
|
|
||||||
<rect :x="skeletonSize.width/2 - skeletonSize.width/3 -10" y="190" rx="3" ry="3" :width="skeletonSize.width/3 + skeletonSize.width/3 + 20" height="20"/>
|
|
||||||
<rect :x="skeletonSize.width/2 - skeletonSize.width/3 +20" y="220" rx="3" ry="3" :width="skeletonSize.width/3 + skeletonSize.width/3 - 40" height="20"/>-->
|
|
||||||
<!-- <rect x="90" y="0" rx="3" ry="3" width="80" height="20" />
|
|
||||||
<rect x="180" y="0" rx="3" ry="3" width="80" height="20" />
|
|
||||||
<rect x="270" y="0" rx="3" ry="3" width="80" height="20" ♥/>-->
|
|
||||||
</content-loader>
|
</content-loader>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {reactive, ref, watch} from "vue";
|
import { computed, ref } from 'vue'
|
||||||
import {useElementSize} from "@vueuse/core";
|
import { useElementSize } from '@vueuse/core'
|
||||||
import {ContentLoader} from "vue-content-loader";
|
import { ContentLoader } from 'vue-content-loader'
|
||||||
|
|
||||||
const skeletonRatio = ref(null)
|
const skeletonHeight = 620
|
||||||
const skeletonSize = reactive(
|
const skeletonRatio = ref<HTMLElement | null>(null)
|
||||||
useElementSize(skeletonRatio, {width: 0, height: 0}, {box: 'border-box'})
|
const { width } = useElementSize(skeletonRatio, { width: 0, height: 0 }, { box: 'border-box' })
|
||||||
)
|
|
||||||
/*watch(skeletonSize, () => {
|
const skeletonWidth = computed(() => Math.max(width.value, 320))
|
||||||
console.log(skeletonSize)
|
|
||||||
})*/
|
function halfWidth(padding: number) {
|
||||||
|
return Math.max(skeletonWidth.value / 2 - padding, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function fullWidth(padding: number) {
|
||||||
|
return Math.max(skeletonWidth.value - padding, 0)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -1,17 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-gray-50 w-full flex justify-evenly rounded-lg py-4 mt-4 h-[15rem]">
|
<div
|
||||||
|
class="bg-gray-50 w-full rounded-lg py-4"
|
||||||
|
:class="[
|
||||||
|
chartContainerClass,
|
||||||
|
variant === 'desktop' ? 'mt-0' : 'mt-4',
|
||||||
|
layout === 'boursecu' && variant !== 'desktop' ? 'price-chart-box--boursecu' : '',
|
||||||
|
]"
|
||||||
|
>
|
||||||
<div class="w-full text-center pt-2">
|
<div class="w-full text-center pt-2">
|
||||||
<a-radio-group v-model:value="selectedValue" button-style="solid">
|
<div v-if="showMetalSelector" class="chart-metal-segment mb-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="chart-metal-segment__item"
|
||||||
|
:class="{ 'chart-metal-segment__item--active': selectedMetal === 'katod' }"
|
||||||
|
@click="emit('update:selectedMetal', 'katod')"
|
||||||
|
>
|
||||||
|
کاتد مس
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="chart-metal-segment__item"
|
||||||
|
:class="{ 'chart-metal-segment__item--active': selectedMetal === 'maftol' }"
|
||||||
|
@click="emit('update:selectedMetal', 'maftol')"
|
||||||
|
>
|
||||||
|
مفتول مس
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<a-radio-group v-model:value="selectedValue" button-style="solid" class="chart-timeline-group">
|
||||||
<a-radio-button value="DAILY" @click="handleUpdate('DAILY')">24 ساعت اخیر</a-radio-button>
|
<a-radio-button value="DAILY" @click="handleUpdate('DAILY')">24 ساعت اخیر</a-radio-button>
|
||||||
<a-radio-button value="WEEKLY" @click="handleUpdate('WEEKLY')">هفته گذشته</a-radio-button>
|
<a-radio-button value="WEEKLY" @click="handleUpdate('WEEKLY')">هفته گذشته</a-radio-button>
|
||||||
<a-radio-button value="MONTHLY" @click="handleUpdate('MONTHLY')">ماه گذشته</a-radio-button>
|
<a-radio-button value="MONTHLY" @click="handleUpdate('MONTHLY')">ماه گذشته</a-radio-button>
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
<div id="chart-timeline">
|
<div ref="chartContainerRef" id="chart-timeline" class="w-full" :class="chartInnerClass">
|
||||||
<apexchart
|
<apexchart
|
||||||
type="area"
|
v-if="canRenderChart"
|
||||||
height="200"
|
:key="`${layout}-${variant}-${selectedMetal}-${chartSelection}-${seriesLength}`"
|
||||||
:options="chartOptions"
|
type="area"
|
||||||
:series="props.priceChartSeries"
|
width="100%"
|
||||||
|
:height="chartHeight"
|
||||||
|
:options="chartOptions"
|
||||||
|
:series="props.priceChartSeries"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -19,21 +47,71 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||||
import { useConfigStore } from '@/store/configStore'
|
import { useConfigStore } from '@/store/configStore'
|
||||||
import {usePrice} from "@/utils/usePrice";
|
import { usePrice } from '@/utils/usePrice'
|
||||||
import {getProjectFontFamily} from "@/utils/config";
|
import { getProjectFontFamily } from '@/utils/config'
|
||||||
|
import { useElementSize } from '@vueuse/core'
|
||||||
|
|
||||||
const props = defineProps<{
|
import type { ChartMetalType } from '../composables/usePriceChart'
|
||||||
priceChartSeries: any[]
|
|
||||||
}>()
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
priceChartSeries: any[]
|
||||||
|
layout?: 'default' | 'boursecu'
|
||||||
|
variant?: 'mobile' | 'desktop'
|
||||||
|
chartSelection?: string
|
||||||
|
selectedMetal?: ChartMetalType
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
layout: 'default',
|
||||||
|
variant: 'mobile',
|
||||||
|
chartSelection: 'DAILY',
|
||||||
|
selectedMetal: 'katod',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const chartHeight = computed(() => (props.variant === 'desktop' ? 320 : 200))
|
||||||
|
const chartContainerClass = computed(() => {
|
||||||
|
if (props.variant === 'desktop') {
|
||||||
|
return 'min-h-[22rem]'
|
||||||
|
}
|
||||||
|
return props.layout === 'default' ? 'min-h-[15rem] h-[15rem]' : 'min-h-[15rem]'
|
||||||
|
})
|
||||||
|
const chartInnerClass = computed(() =>
|
||||||
|
props.variant === 'desktop' ? 'min-h-[320px]' : 'min-h-[200px]'
|
||||||
|
)
|
||||||
|
|
||||||
|
const showMetalSelector = computed(() => props.layout === 'boursecu')
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
updateData: [timeline: string]
|
updateData: [timeline: string]
|
||||||
|
'update:selectedMetal': [metal: ChartMetalType]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const config = useConfigStore()
|
const config = useConfigStore()
|
||||||
const selectedValue = ref('DAILY')
|
const chartContainerRef = ref<HTMLElement | null>(null)
|
||||||
|
const chartMounted = ref(false)
|
||||||
|
const selectedValue = ref(props.chartSelection)
|
||||||
|
|
||||||
|
const { width: chartWidth } = useElementSize(chartContainerRef)
|
||||||
|
|
||||||
|
const seriesLength = computed(() => props.priceChartSeries?.[0]?.data?.length ?? 0)
|
||||||
|
const canRenderChart = computed(
|
||||||
|
() => chartMounted.value && chartWidth.value > 0 && seriesLength.value > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.chartSelection,
|
||||||
|
(timeline) => {
|
||||||
|
selectedValue.value = timeline
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick()
|
||||||
|
chartMounted.value = true
|
||||||
|
})
|
||||||
|
|
||||||
const toPersianNumber = (num: number) => {
|
const toPersianNumber = (num: number) => {
|
||||||
return num.toString().replace(/\d/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[d])
|
return num.toString().replace(/\d/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[d])
|
||||||
@@ -43,67 +121,108 @@ const chartOptions = computed(() => {
|
|||||||
const fontFamily = getProjectFontFamily()
|
const fontFamily = getProjectFontFamily()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chart: {
|
chart: {
|
||||||
id: 'نوسانات قیمت',
|
id: 'نوسانات قیمت',
|
||||||
type: 'area',
|
type: 'area',
|
||||||
height: 200,
|
height: 200,
|
||||||
toolbar: { show: false },
|
toolbar: { show: false },
|
||||||
zoom: { autoScaleYaxis: true }
|
zoom: { autoScaleYaxis: true },
|
||||||
},
|
redrawOnParentResize: true,
|
||||||
grid: {
|
},
|
||||||
padding: { top: 0, right: 0, bottom: 0, left: 0 },
|
grid: {
|
||||||
yaxis: { lines: { show: false } }
|
padding: { top: 0, right: 0, bottom: 0, left: 0 },
|
||||||
},
|
yaxis: { lines: { show: false } },
|
||||||
stroke: { width: 2 },
|
},
|
||||||
dataLabels: {
|
stroke: { width: 2 },
|
||||||
enabled: false,
|
dataLabels: {
|
||||||
// formatter: (val) => toPersianNumber(val),
|
enabled: false,
|
||||||
style: {
|
style: {
|
||||||
fontFamily
|
fontFamily,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
yaxis: {
|
yaxis: {
|
||||||
tickAmount: 8,
|
tickAmount: 8,
|
||||||
show: false,
|
show: false,
|
||||||
labels: {
|
labels: {
|
||||||
|
formatter: (val: number) => toPersianNumber(usePrice(val, true, 'ه.ریال')),
|
||||||
|
style: { fontSize: '13px', fontFamily },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
show: false,
|
||||||
|
type: 'datetime',
|
||||||
|
axisTicks: { show: false },
|
||||||
|
tooltip: { enabled: false },
|
||||||
|
labels: { show: false },
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
x: {
|
||||||
|
formatter: (value: number) => {
|
||||||
|
const date = new Date(value)
|
||||||
|
return `${date.toLocaleDateString('fa-IR')} - ${date.toLocaleTimeString('fa-IR', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})}`
|
||||||
|
},
|
||||||
|
},
|
||||||
formatter: (val: number) => toPersianNumber(usePrice(val, true, 'ه.ریال')),
|
formatter: (val: number) => toPersianNumber(usePrice(val, true, 'ه.ریال')),
|
||||||
style: { fontSize: '13px', fontFamily }
|
style: { fontFamily },
|
||||||
}
|
},
|
||||||
},
|
colors: [config.config.color],
|
||||||
xaxis: {
|
fill: {
|
||||||
show: false,
|
type: 'gradient',
|
||||||
type: 'datetime',
|
gradient: {
|
||||||
axisTicks: { show: false },
|
shadeIntensity: 1,
|
||||||
tooltip: { enabled: false },
|
opacityFrom: 0.7,
|
||||||
labels: { show: false }
|
opacityTo: 0.9,
|
||||||
},
|
stops: [0, 100],
|
||||||
tooltip: {
|
},
|
||||||
x: {
|
|
||||||
formatter: (value: number) => {
|
|
||||||
const date = new Date(value)
|
|
||||||
return `${date.toLocaleDateString('fa-IR')} - ${date.toLocaleTimeString('fa-IR', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})}`
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
formatter: (val: number) => toPersianNumber(usePrice(val, true, 'ه.ریال')),
|
|
||||||
style: { fontFamily }
|
|
||||||
},
|
|
||||||
colors: [config.config.color],
|
|
||||||
fill: {
|
|
||||||
type: 'gradient',
|
|
||||||
gradient: {
|
|
||||||
shadeIntensity: 1,
|
|
||||||
opacityFrom: 0.7,
|
|
||||||
opacityTo: 0.9,
|
|
||||||
stops: [0, 100]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleUpdate = (timeline: string) => {
|
const handleUpdate = (timeline: string) => {
|
||||||
emit('updateData', timeline)
|
emit('updateData', timeline)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
@boursecu-surface-border: #d4e3de;
|
||||||
|
|
||||||
|
.price-chart-box--boursecu {
|
||||||
|
border: 2px solid @boursecu-surface-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-metal-segment {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 3px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #dfe8e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-metal-segment__item {
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 6.5rem;
|
||||||
|
padding: 0.4rem 0.75rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: @App-primary-color;
|
||||||
|
opacity: 0.65;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.25;
|
||||||
|
transition: background-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-metal-segment__item--active {
|
||||||
|
background: #fff;
|
||||||
|
color: @App-primary-color;
|
||||||
|
opacity: 1;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 1px 4px rgba(10, 27, 55, 0.12);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full flex justify-evenly rounded-lg py-2 home_appear_fade_in mt-6 pb-45">
|
<div
|
||||||
|
class="w-full flex justify-evenly rounded-lg py-2 home_appear_fade_in"
|
||||||
|
:class="[
|
||||||
|
layout === 'boursecu' ? 'mt-8 pt-2' : 'mt-6',
|
||||||
|
variant === 'mobile' ? 'pb-45' : 'mt-0 pb-0',
|
||||||
|
]"
|
||||||
|
>
|
||||||
<div class="text-center w-full">
|
<div class="text-center w-full">
|
||||||
<div>بالاترین قیمت امروز</div>
|
<div>بالاترین قیمت امروز</div>
|
||||||
<div v-if="price" class="flex flex-col items-center mt-3">
|
<div v-if="price" class="flex flex-col items-center mt-3">
|
||||||
@@ -29,9 +35,17 @@ import { computed } from 'vue'
|
|||||||
import { getTradeBuyPrice, getTradeSellPrice } from '@/utils/price'
|
import { getTradeBuyPrice, getTradeSellPrice } from '@/utils/price'
|
||||||
import { usePriceWithoutType } from '../../../../utils/usePrice'
|
import { usePriceWithoutType } from '../../../../utils/usePrice'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
price: any
|
defineProps<{
|
||||||
}>()
|
price: any
|
||||||
|
layout?: 'default' | 'boursecu'
|
||||||
|
variant?: 'mobile' | 'desktop'
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
layout: 'default',
|
||||||
|
variant: 'mobile',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const highPrice = computed(() => props.price?.up?.buy ?? getTradeBuyPrice(props.price))
|
const highPrice = computed(() => props.price?.up?.buy ?? getTradeBuyPrice(props.price))
|
||||||
const lowPrice = computed(() => props.price?.up?.sell ?? getTradeSellPrice(props.price))
|
const lowPrice = computed(() => props.price?.up?.sell ?? getTradeSellPrice(props.price))
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user