Merge feature/responsive-layout-refactor into modular for desktop layouts.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Vendored
+1
-1
@@ -25,7 +25,6 @@ declare module 'vue' {
|
||||
ATag: typeof import('ant-design-vue/es')['Tag']
|
||||
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||
AUpload: typeof import('ant-design-vue/es')['Upload']
|
||||
BaseDatePicker: typeof import('./src/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue')['default']
|
||||
Basic: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/basic.vue')['default']
|
||||
BasicActionSheet: typeof import('./src/components/BasicPacks/BasicActionSheet.vue')['default']
|
||||
@@ -63,6 +62,7 @@ declare module 'vue' {
|
||||
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']
|
||||
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']
|
||||
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']
|
||||
|
||||
+33
@@ -10,6 +10,39 @@
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<meta name="enamad" content="44889195">
|
||||
<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>
|
||||
<body>
|
||||
|
||||
+68
-54
@@ -1,5 +1,5 @@
|
||||
<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 class="absolute right-0 left-0 bottom-10vh flex flex-col items-center">
|
||||
<div>
|
||||
@@ -10,60 +10,47 @@
|
||||
</div>
|
||||
</div>
|
||||
<AConfigProvider :theme="antTheme ?? undefined" direction="rtl">
|
||||
<transition mode="out-in" name="fade">
|
||||
|
||||
<div v-if="!showSplash">
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition :name="(route.meta.transition as string | undefined) || 'fade'">
|
||||
|
||||
<div>
|
||||
<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">
|
||||
<div v-if="showSplash" class="app-splash-screen flex justify-center" :style="{ backgroundColor: getProjectSplashBackground() }">
|
||||
<template v-if="!isBoursecuProject()">
|
||||
<div class="app-splash-screen__gradient app-splash-screen__gradient--top h-1/3 flex">
|
||||
<img :src="img_topGradiant" alt="" class="absolute z-10 w-full h-full object-cover"/>
|
||||
<img :src="img_pattern" alt="" class="absolute w-full h-full object-none"/>
|
||||
</div>
|
||||
<div class="app-splash-screen__gradient app-splash-screen__gradient--bottom h-1/3">
|
||||
<img
|
||||
:src="getProjectLoginLogo(config?.config?.avatar)"
|
||||
:class="hasCustomLoginLogoLayout()
|
||||
? 'w-[80vw] max-w-[320px] h-auto object-contain'
|
||||
: 'h-[20vh] object-contain'"
|
||||
>
|
||||
|
||||
:src="img_bottomGradiant"
|
||||
alt=""
|
||||
class="absolute z-10 w-full h-full object-cover"
|
||||
/>
|
||||
<img :src="img_pattern" alt="" class="absolute w-full h-full object-none"/>
|
||||
</div>
|
||||
<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 class="absolute bottom-10">
|
||||
<a-spin :indicator="splashIndicator" size="small"/>
|
||||
</template>
|
||||
<div class="self-center splash_appear_fade_in">
|
||||
<img
|
||||
:src="getProjectLoginLogo(config?.config?.avatar)"
|
||||
:class="hasCustomLoginLogoLayout()
|
||||
? 'w-[80vw] max-w-[320px] h-auto object-contain'
|
||||
: 'h-[20vh] object-contain'"
|
||||
>
|
||||
</div>
|
||||
<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>
|
||||
</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>
|
||||
<van-dialog
|
||||
v-model:show="versionDialog"
|
||||
@@ -123,7 +110,7 @@
|
||||
<script lang="ts" setup>
|
||||
import pkg from '../package.json'
|
||||
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_pattern from '@/views/login/assets/img/pattern.svg'
|
||||
import {LoadingOutlined} from '@ant-design/icons-vue'
|
||||
@@ -131,9 +118,10 @@ import img_bottomGradiant from '@/views/login/assets/img/gr_bottom_white.svg'
|
||||
import {DotLottieVue} from '@lottiefiles/dotlottie-vue'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
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 BasicIcon from "@/components/BasicPacks/BasicIcon.vue";
|
||||
import { useApplyDesktopLayoutClass } from '@/layouts/composables/useAppLayout'
|
||||
import {
|
||||
buildSupportMessengerUrl,
|
||||
loadSupportMessengerFromSettings,
|
||||
@@ -141,6 +129,8 @@ import {
|
||||
} from '@/utils/supportMessenger'
|
||||
import {useDraggableFloatingButton} from '@/utils/useDraggableFloatingButton'
|
||||
|
||||
useApplyDesktopLayoutClass()
|
||||
|
||||
const {offlineReady, needRefresh, updateServiceWorker} = useRegisterSW()
|
||||
const config = useConfigStore()
|
||||
const splashIndicator = computed(() => h(LoadingOutlined, {
|
||||
@@ -259,6 +249,12 @@ onMounted(async () => {
|
||||
}, 2000)
|
||||
})
|
||||
|
||||
watch(showSplash, (visible) => {
|
||||
if (!visible) {
|
||||
resetAppSurfaceBackground()
|
||||
}
|
||||
})
|
||||
|
||||
const versionDialog = ref<boolean>(false)
|
||||
|
||||
function checkVersion() {
|
||||
@@ -288,6 +284,7 @@ function update() {
|
||||
--app-credit-footer-offset: calc(var(--app-credit-bar-inner) + var(--app-safe-bottom));
|
||||
--app-nav-height: 80px;
|
||||
--app-bottom-chrome-height: calc(var(--app-nav-height) + var(--app-credit-footer-offset));
|
||||
--app-desktop-sidebar-width: 260px;
|
||||
--app-add-card-height: 88px;
|
||||
/* بخشی از کارت کیفپول زیر نوار «افزودن کارت جدید» دیده میشود */
|
||||
--wallet-card-dock-overlap: 52px;
|
||||
@@ -298,6 +295,16 @@ function update() {
|
||||
--app-z-dialog: 5300;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.desktopConfig {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.app-credit-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
@@ -368,10 +375,17 @@ function update() {
|
||||
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.22);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 426px) {
|
||||
@media screen and (min-width: 426px) and (max-width: 767.98px) {
|
||||
.desktopConfig {
|
||||
width: 426px;
|
||||
position: relative !important;
|
||||
}
|
||||
}
|
||||
|
||||
html.app-layout--desktop {
|
||||
#support-button {
|
||||
bottom: 12px;
|
||||
right: calc(var(--app-desktop-sidebar-width, 260px) + 20px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
html.project-boursecu {
|
||||
.ant-input-group-addon {
|
||||
background-color: @secondary-color !important;
|
||||
color: @App-primary-color !important;
|
||||
}
|
||||
|
||||
.text-red,
|
||||
.text-red-600,
|
||||
.text-red-700,
|
||||
|
||||
@@ -2,9 +2,29 @@
|
||||
@import (less) './fonts.less';
|
||||
@import (less) './ant.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 {
|
||||
scroll-behavior: smooth;
|
||||
background-color: @prefixCls-background-color;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -34,7 +54,7 @@ body {
|
||||
@apply flex justify-center 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 {
|
||||
width: 426px;
|
||||
left: calc(50vw - 213px);
|
||||
@@ -44,10 +64,31 @@ body {
|
||||
position: fixed;
|
||||
right: 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;
|
||||
left: auto;
|
||||
max-width: 426px;
|
||||
left: 50%;
|
||||
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-fill-mode: both
|
||||
|
||||
//@if($counter < 13)
|
||||
// .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%)
|
||||
// Duplicate animation block removed — was leaking styles without a selector
|
||||
|
||||
@keyframes slideInFromRight
|
||||
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 { getWalletAssets } from '@/api/wallet'
|
||||
import { getProducts } from '@/api/products'
|
||||
import { getProductsCatalog } from '@/composables/useProductsCatalog'
|
||||
import type { WalletAssetsSummary } from '@/models/walletAssets'
|
||||
import { enrichInventoryCardsWithCatalog } from '@/utils/productAssets'
|
||||
|
||||
@@ -17,8 +17,8 @@ export function useWalletAssets() {
|
||||
return assetsPromise
|
||||
}
|
||||
|
||||
// سرور مرجع داراییهاست (main_wallet_products هم سمت سرور هندل میشود)؛ کلاینت فقط نمایش میدهد
|
||||
assetsPromise = Promise.all([getWalletAssets(), getProducts()])
|
||||
// Reuses cached getProductsCatalog() — same catalog as chart offset logic
|
||||
assetsPromise = Promise.all([getWalletAssets(), getProductsCatalog(force)])
|
||||
.then(([data, catalog]) => {
|
||||
assetsSummary.value = {
|
||||
...data,
|
||||
|
||||
@@ -97,6 +97,16 @@
|
||||
window.__APP_CONFIG__ = config
|
||||
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)
|
||||
}
|
||||
|
||||
if (config?.user?.name_fa) {
|
||||
document.title = config.user.name_fa
|
||||
}
|
||||
|
||||
@@ -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 { resolveMeltedGoldLabel, resolveMetalTypeLabel } from '@/utils/metalLabels'
|
||||
import { resolveTradeWeightUnit } from '@/utils/weightUnit'
|
||||
import { runOnceAsync } from '@/composables/useOnceAsync'
|
||||
|
||||
export interface AppSetting {
|
||||
id?: number
|
||||
@@ -45,17 +46,20 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
settings.value = merged
|
||||
}
|
||||
|
||||
async function fetchSettings() {
|
||||
try {
|
||||
const data = await getSettings()
|
||||
if (Array.isArray(data)) {
|
||||
mergeSettings(data)
|
||||
async function fetchSettings(force = false) {
|
||||
return runOnceAsync('settings:all', async () => {
|
||||
try {
|
||||
const data = await getSettings()
|
||||
if (Array.isArray(data)) {
|
||||
mergeSettings(data)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loaded.value = true
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loaded.value = true
|
||||
}
|
||||
return settings.value
|
||||
}, force)
|
||||
}
|
||||
|
||||
const metalTypeSettingValue = computed(() => getSettingByKey('metal_type'))
|
||||
|
||||
+14
-1
@@ -114,7 +114,7 @@ export function getProjectLoginTaglineLines() {
|
||||
}
|
||||
|
||||
return [
|
||||
'بورس مس مرجع هوشمند معاملات',
|
||||
'مرجع هوشمند معاملات',
|
||||
' و سرمایهگذاری در صنعت مس',
|
||||
]
|
||||
}
|
||||
@@ -123,6 +123,18 @@ export function getProjectSplashBackground() {
|
||||
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() {
|
||||
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.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-50', '#f8ecec')
|
||||
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 { UserWalletLine } from '@/models/wallet'
|
||||
import type { ProductFile } from '@/utils/file'
|
||||
@@ -46,6 +47,138 @@ export function getTotalMetalLabel(metalId: number): string {
|
||||
return `کل دارایی ${getMetalLabel(metalId)}`
|
||||
}
|
||||
|
||||
/** boursecu home — ترتیب نمایش: مس، طلا، سایر فلزها */
|
||||
export function resolveBoursecuAssetMetalTotals(
|
||||
metalTotals: WalletMetalTotal[]
|
||||
): WalletMetalTotal[] {
|
||||
if (!metalTotals.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
const ordered: WalletMetalTotal[] = []
|
||||
const copper = metalTotals.find((metal) => metal.metal_id === METAL_COPPER)
|
||||
const gold = metalTotals.find((metal) => metal.metal_id === METAL_GOLD)
|
||||
|
||||
if (copper) {
|
||||
ordered.push(copper)
|
||||
}
|
||||
if (gold) {
|
||||
ordered.push(gold)
|
||||
}
|
||||
|
||||
for (const metal of metalTotals) {
|
||||
if (!ordered.some((item) => item.metal_id === metal.metal_id)) {
|
||||
ordered.push(metal)
|
||||
}
|
||||
}
|
||||
|
||||
return ordered
|
||||
}
|
||||
|
||||
export function resolveMetalAssetsDisplayLabel(
|
||||
metal: WalletMetalTotal,
|
||||
isBoursecuLayout: boolean
|
||||
): string {
|
||||
if (!isBoursecuLayout) {
|
||||
return metal.label
|
||||
}
|
||||
|
||||
switch (metal.metal_id) {
|
||||
case METAL_COPPER:
|
||||
return 'دارایی مس'
|
||||
case METAL_GOLD:
|
||||
return 'دارایی طلایی'
|
||||
case METAL_SILVER:
|
||||
return 'دارایی نقره'
|
||||
default:
|
||||
return metal.label
|
||||
}
|
||||
}
|
||||
|
||||
export type HomeAssetRowInfo = 'metal' | 'rial' | 'none'
|
||||
|
||||
export interface HomeAssetRow {
|
||||
id: string
|
||||
label: string
|
||||
value: string
|
||||
metal?: WalletMetalTotal
|
||||
info: HomeAssetRowInfo
|
||||
}
|
||||
|
||||
function formatInventoryCardProductLabel(card: WalletInventoryCard): string {
|
||||
return `دارایی ${card.title}`
|
||||
}
|
||||
|
||||
function findMetalTotalById(metalTotals: WalletMetalTotal[], metalId: number) {
|
||||
return metalTotals.find((metal) => metal.metal_id === metalId)
|
||||
}
|
||||
|
||||
/** boursecu home — هر محصول / فلز / ریال از customer/wallet/assets جدا نمایش داده میشود */
|
||||
export function buildBoursecuHomeAssetRows(
|
||||
summary: {
|
||||
inventory_cards?: WalletInventoryCard[]
|
||||
count_products?: WalletInventoryCard[]
|
||||
metal_totals?: WalletMetalTotal[]
|
||||
total_rial_value?: number
|
||||
} | null | undefined
|
||||
): HomeAssetRow[] {
|
||||
if (!summary) {
|
||||
return []
|
||||
}
|
||||
|
||||
const rows: HomeAssetRow[] = []
|
||||
const metalTotals = summary.metal_totals ?? []
|
||||
const inventoryCards = summary.inventory_cards ?? []
|
||||
const countProducts = summary.count_products ?? []
|
||||
|
||||
for (const card of inventoryCards) {
|
||||
if (card.display_type === 'count') {
|
||||
continue
|
||||
}
|
||||
|
||||
rows.push({
|
||||
id: `inventory-${card.product_id}`,
|
||||
label: formatInventoryCardProductLabel(card),
|
||||
value: formatInventoryCardValue(card),
|
||||
metal: findMetalTotalById(metalTotals, card.metal_id),
|
||||
info: 'metal',
|
||||
})
|
||||
}
|
||||
|
||||
for (const metal of metalTotals) {
|
||||
if (metal.metal_id === METAL_COPPER) {
|
||||
continue
|
||||
}
|
||||
|
||||
rows.push({
|
||||
id: `metal-${metal.metal_id}`,
|
||||
label: resolveMetalAssetsDisplayLabel(metal, true),
|
||||
value: `${Number(metal.total_weight ?? 0)}${metal.unit ?? ''}`,
|
||||
metal,
|
||||
info: 'metal',
|
||||
})
|
||||
}
|
||||
|
||||
for (const card of countProducts) {
|
||||
rows.push({
|
||||
id: `count-${card.product_id}`,
|
||||
label: formatInventoryCardProductLabel(card),
|
||||
value: formatInventoryCardValue(card),
|
||||
metal: findMetalTotalById(metalTotals, card.metal_id),
|
||||
info: 'metal',
|
||||
})
|
||||
}
|
||||
|
||||
rows.push({
|
||||
id: 'rial-total',
|
||||
label: 'دارایی ریالی',
|
||||
value: `${formatRialNumber(summary.total_rial_value ?? 0)} ریال`,
|
||||
info: 'rial',
|
||||
})
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
export function sumInventoryCardsWeight(
|
||||
cards: Array<{ metal_id?: number; quantity?: number }>,
|
||||
metalId: number
|
||||
|
||||
@@ -6,13 +6,37 @@ function productUsesLastPriceIfZero(product: Product): boolean {
|
||||
return Boolean(product.use_last_price_if_zero)
|
||||
}
|
||||
|
||||
function applyPriceOffsetPercent(basePrice: number, offsetPercent?: number | null) {
|
||||
export function resolvePriceOffsetRatio(offsetPercent?: number | null) {
|
||||
const offset = Number(offsetPercent ?? 0)
|
||||
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 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)
|
||||
}
|
||||
|
||||
/** قیمت پایه بدون کارمزد — اگر صفر باشد نباید فقط cash_fee بهعنوان قیمت نمایش داده شود */
|
||||
|
||||
@@ -17,6 +17,9 @@ export interface SupportMessengerSettings {
|
||||
|
||||
export const supportMessengerSettings = ref<SupportMessengerSettings>({})
|
||||
|
||||
export const DEFAULT_SUPPORT_MESSENGER_TEXT =
|
||||
'جهت شارژ حساب کاربری خود، مبلغ موردنظر را واریز نموده و از طریق پیامرسان به پشتیبانی اطلاع دهید.'
|
||||
|
||||
function getSupportSettingValue(
|
||||
settings: Array<{ key?: string; is_active?: boolean | number; value?: unknown }>,
|
||||
key: string
|
||||
|
||||
@@ -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>
|
||||
<div>
|
||||
<div class="bg-white wrap-fix z-200001 py-3 px-3 text-primary font-bold drop-shadow flex">
|
||||
<div @click="router.back()" class="cursor-pointer">
|
||||
<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 class="about-page-root">
|
||||
<DesktopAboutView v-show="isDesktop" />
|
||||
<MobileAboutView v-show="!isDesktop" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, ref, computed } from 'vue'
|
||||
import img from '@/assets/img/user_ly1.svg'
|
||||
import gr_bottom from './assets/img/gr_bottom_white.svg'
|
||||
import { loading } from '@/utils'
|
||||
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'
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import { useAboutPage } from './composables/useAboutPage'
|
||||
import DesktopAboutView from './desktop/DesktopAboutView.vue'
|
||||
import MobileAboutView from './mobile/MobileAboutView.vue'
|
||||
|
||||
const config=useConfigStore()
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
|
||||
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 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
|
||||
}
|
||||
})
|
||||
}
|
||||
useAboutPage()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/*.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
.about-page-root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}*/
|
||||
/*.trans{
|
||||
transition: height 1s;
|
||||
}*/
|
||||
</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>
|
||||
<div>
|
||||
<div class="wrap-fix z-1 bg-white py-3 px-3 text-black font-bold drop-shadow flex">
|
||||
<div @click="router.back()" class="cursor-pointer">
|
||||
<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 class="about-us-page-root">
|
||||
<DesktopAboutUsView v-show="isDesktop" />
|
||||
<MobileAboutUsView v-show="!isDesktop" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {useConfigStore} from "@/store/configStore";
|
||||
import img from '@/assets/img/user_ly1.svg'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import router from '@/router'
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import { useAboutUsPage } from './composables/useAboutUsPage'
|
||||
import DesktopAboutUsView from './desktop/DesktopAboutUsView.vue'
|
||||
import MobileAboutUsView from './mobile/MobileAboutUsView.vue'
|
||||
|
||||
const config = useConfigStore()
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
|
||||
useAboutUsPage()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.about-us-page-root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</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>
|
||||
<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>
|
||||
<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 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"/>
|
||||
<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">{{toJalali(item.created_at)}}</div>
|
||||
<div class="flex gap-2 ">
|
||||
<BasicIcon color="red" icon="vuesax-linear:trash" @click.stop="deleteConfirmVisible = true" class="p-1"/>
|
||||
<BasicIcon color="green" icon="vuesax-linear:edit" @click.stop="emits('edit',item)" class="p-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>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- <teleport to="app">-->
|
||||
<DialogVant v-model:show="deleteConfirmVisible" teleport="body" :closeOnClickOverlay="false" :timeOut="-1">
|
||||
<!-- <template v-slot:img>
|
||||
<img :src="registerDelayImage" alt="" class="opacity-80">
|
||||
</template>-->
|
||||
|
||||
<template v-slot:title>
|
||||
<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 class="p-5 text-sm">آیا از حذف آدرس اطمینان دارید؟</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:button>
|
||||
<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">
|
||||
انصراف
|
||||
<a-spin :spinning="deleteLoading" size="small">تایید</a-spin>
|
||||
</div>
|
||||
<div class="text-center" @click="deleteConfirmVisible = false">انصراف</div>
|
||||
</template>
|
||||
|
||||
</DialogVant>
|
||||
<!-- </teleport>-->
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { toJalali } from '../../../../utils/useDateTime'
|
||||
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 { updatePriceTrigger } from '@/api/triggerPrice'
|
||||
import { loading } from '@/utils'
|
||||
import type { Address } from '@/models/address'
|
||||
import { deleteAddress } from '@/api/address'
|
||||
|
||||
const infoVisible = ref<boolean>(false)
|
||||
|
||||
const isActive = ref<boolean>(false)
|
||||
|
||||
const deleteLoading = ref(false)
|
||||
const deleteConfirmVisible = ref(false)
|
||||
|
||||
interface Prop {
|
||||
item: Address
|
||||
}
|
||||
function changeActive(val: boolean) {
|
||||
console.log(val)
|
||||
try {
|
||||
loading.value = true
|
||||
updatePriceTrigger(props?.item.id, {is_active: val})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
isActive.value = !isActive.value
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
item: Address
|
||||
variant?: 'mobile' | 'desktop'
|
||||
}>(),
|
||||
{ variant: 'mobile' },
|
||||
)
|
||||
|
||||
function returnPriceCondition(val: string) {
|
||||
switch (val) {
|
||||
case '==': return 'مساوی'
|
||||
case '<': return 'کوچکتر'
|
||||
// case '==': return 'مساوی'
|
||||
}
|
||||
/* if (val === '=='){
|
||||
return 'مساوی'
|
||||
} else if (val === '<'){
|
||||
return 'کوچکتر'
|
||||
}*/
|
||||
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)
|
||||
await deleteAddress(props.item.id!)
|
||||
deleteConfirmVisible.value = false
|
||||
emits('refresh')
|
||||
} catch (e) {
|
||||
@@ -115,42 +158,4 @@ async function deleteItem() {
|
||||
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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<!-- <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>
|
||||
<div v-if="!isDesktop">
|
||||
<MobileAddressView />
|
||||
|
||||
<BasicActionSheet v-model:show="addAddressVisible" class="h-80vh">
|
||||
<AddressForm />
|
||||
</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>
|
||||
|
||||
<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>
|
||||
|
||||
<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 BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import BaseDatePicker from '@/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue'
|
||||
import { useInfiniteScroll, useScroll } from '@vueuse/core'
|
||||
import ListAnim from '@/components/ListAnim.vue'
|
||||
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
|
||||
import { DotLottieVue } from '@lottiefiles/dotlottie-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'
|
||||
import BasicModal from '@/components/BasicPacks/BasicModal.vue'
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import { useAddressPage } from './composables/useAddressPage'
|
||||
import AddressForm from './components/AddressForm.vue'
|
||||
import DesktopAddressView from './desktop/DesktopAddressView.vue'
|
||||
import MobileAddressView from './mobile/MobileAddressView.vue'
|
||||
|
||||
const provinceList = ref<any[]>([])
|
||||
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 isDesktop = useDesktopBreakpoint()
|
||||
|
||||
const { addAddressVisible, editId } = useAddressPage()
|
||||
</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>
|
||||
<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 class="earn-money-page-root">
|
||||
<DesktopEarnMoneyView v-show="isDesktop" />
|
||||
<MobileEarnMoneyView v-show="!isDesktop" />
|
||||
</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>
|
||||
|
||||
<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'
|
||||
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);'
|
||||
}
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
|
||||
useEarnMoneyPage()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
.earn-money-page-root {
|
||||
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>
|
||||
<div>
|
||||
<div class="wrap-fix z-1 bg-white py-3 px-3 text-black font-bold drop-shadow flex">
|
||||
<div @click="router.back()" class="cursor-pointer">
|
||||
<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 class="educate-page-root">
|
||||
<DesktopEducateView v-show="isDesktop" />
|
||||
<MobileEducateView v-show="!isDesktop" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import img from '@/assets/img/user_ly1.svg'
|
||||
import img1 from '@/assets/img/1.png'
|
||||
import img2 from '@/assets/img/2.png'
|
||||
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()
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import { useEducatePage } from './composables/useEducatePage'
|
||||
import DesktopEducateView from './desktop/DesktopEducateView.vue'
|
||||
import MobileEducateView from './mobile/MobileEducateView.vue'
|
||||
|
||||
// const editPassVisible = ref<boolean>(false)
|
||||
const editPassVisible = ref<boolean>(false)
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
|
||||
function setForm() {
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
useEducatePage()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/*.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
.educate-page-root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}*/
|
||||
/*.trans{
|
||||
transition: height 1s;
|
||||
}*/
|
||||
</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>
|
||||
<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">
|
||||
<div class="mt-3">{{ item.title ?? '-' }}</div>
|
||||
<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 {
|
||||
item: WalletInventoryCard
|
||||
variant?: 'mobile' | 'desktop'
|
||||
}
|
||||
|
||||
const props = defineProps<Prop>()
|
||||
const props = withDefaults(defineProps<Prop>(), {
|
||||
variant: 'mobile',
|
||||
})
|
||||
const imageSrc = ref('')
|
||||
|
||||
const displayImage = computed(() => imageSrc.value || resolveImage())
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
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 { getLastPrices } from '@/utils/price'
|
||||
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), getLastPrices()]
|
||||
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
-162
@@ -1,173 +1,23 @@
|
||||
<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="router.back()">
|
||||
<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(cashWallet + metalRialValue) }}ریال</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 class="fund-page-root">
|
||||
<DesktopFundView v-show="isDesktop" />
|
||||
<MobileFundView v-show="!isDesktop" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import router from '@/router'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import img from '@/assets/img/user_ly1.svg'
|
||||
import empty from '@/assets/img/cash_empty.svg'
|
||||
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, resolveAssetsRialValue } from '@/utils/productAssets'
|
||||
import { useWalletStore } from '@/store/walletStore'
|
||||
import { getTradeBuyPrice, lastPrice } from '@/utils/price'
|
||||
import {
|
||||
getProjectFundEmptyImage,
|
||||
getProjectFundEmptyText,
|
||||
getProjectFundHeaderBackground,
|
||||
isBazretalaProject,
|
||||
} from '@/utils/config'
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import { useFundPage } from './composables/useFundPage'
|
||||
import DesktopFundView from './desktop/DesktopFundView.vue'
|
||||
import MobileFundView from './mobile/MobileFundView.vue'
|
||||
|
||||
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 metalRialValue = computed(() =>
|
||||
resolveAssetsRialValue(assetsSummary.value, getTradeBuyPrice(lastPrice.value))
|
||||
)
|
||||
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), getLastPrices()]
|
||||
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}`
|
||||
}
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
useFundPage(isDesktop)
|
||||
</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;
|
||||
.fund-page-root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</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>
|
||||
<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 @click="router.back()" class="cursor-pointer">
|
||||
<BasicIcon icon="vuesax-linear:arrow-right-1" />
|
||||
</div>
|
||||
<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>
|
||||
<div class="fund-info-page-root">
|
||||
<DesktopFundInfoView v-show="isDesktop" />
|
||||
<MobileFundInfoView v-show="!isDesktop" />
|
||||
|
||||
<BasicActionSheet v-if="!isDesktop" v-model:show="transactionShow">
|
||||
<FundTransactionForm />
|
||||
</BasicActionSheet>
|
||||
<BasicActionSheet v-model:show="editShow">
|
||||
<div class="font-bold text-lg">گنج {{ info?.title }}</div>
|
||||
<BasicModal
|
||||
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"
|
||||
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="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 v-if="!isDesktop" v-model:show="editShow">
|
||||
<FundEditActions />
|
||||
</BasicActionSheet>
|
||||
<DialogVant v-model:show="showDeleteConfirm" :closeOnClickOverlay="true" :timeOut="-1">
|
||||
<!-- <template v-slot:img>
|
||||
<img :src="registerDelayImage" alt="" class="opacity-80">
|
||||
</template>-->
|
||||
|
||||
<template v-slot:title>
|
||||
<div class="p-5 text-sm">
|
||||
آیا از حذف گنج خود اطمینان دارید؟
|
||||
</div>
|
||||
<BasicModal
|
||||
v-else
|
||||
v-model:visible="editShow"
|
||||
:title="`گنج ${info?.title ?? ''}`"
|
||||
width="420px"
|
||||
destroy-on-close
|
||||
custom-footer
|
||||
>
|
||||
<FundEditActions variant="desktop" />
|
||||
<template #footer>
|
||||
<div></div>
|
||||
</template>
|
||||
</BasicModal>
|
||||
|
||||
<template v-slot:button>
|
||||
<div class="text-center text-red" @click="deleteFundFunction">
|
||||
حذف
|
||||
</div>
|
||||
<div class="text-center" @click="showDeleteConfirm = false">
|
||||
انصراف
|
||||
</div>
|
||||
<DialogVant v-model:show="showDeleteConfirm" :close-on-click-overlay="true" :time-out="-1">
|
||||
<template #title>
|
||||
<div class="p-5 text-sm">آیا از حذف گنج خود اطمینان دارید؟</div>
|
||||
</template>
|
||||
<template #button>
|
||||
<div class="text-center text-red" @click="deleteFundFunction">حذف</div>
|
||||
<div class="text-center" @click="showDeleteConfirm = false">انصراف</div>
|
||||
</template>
|
||||
|
||||
</DialogVant>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 { formatGoldWalletBalanceText } from '@/utils/productDealMethod'
|
||||
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 BasicModal from '@/components/BasicPacks/BasicModal.vue'
|
||||
import DialogVant from '@/components/DialogVant.vue'
|
||||
import { useFundUserStore } from '@/store/fundStore'
|
||||
import { getOneProductWallet } from '@/api/products'
|
||||
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 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
|
||||
}
|
||||
})
|
||||
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import { useFundInfoPage } from './composables/useFundInfoPage'
|
||||
import DesktopFundInfoView from './desktop/DesktopFundInfoView.vue'
|
||||
import MobileFundInfoView from './mobile/MobileFundInfoView.vue'
|
||||
import FundTransactionForm from './components/FundTransactionForm.vue'
|
||||
import FundEditActions from './components/FundEditActions.vue'
|
||||
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
const {
|
||||
info,
|
||||
transactionShow,
|
||||
editShow,
|
||||
showDeleteConfirm,
|
||||
isDeposit,
|
||||
deleteFundFunction,
|
||||
} = useFundInfoPage()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.fund-info-page-root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</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>
|
||||
<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 @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 class="fund-new-page-root">
|
||||
<DesktopFundNewView v-show="isDesktop" />
|
||||
<MobileFundNewView v-show="!isDesktop" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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";
|
||||
import {computed, onMounted, ref} from "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 {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
|
||||
}
|
||||
}
|
||||
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
useFundNewPage()
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@prefix: ~'@{prefixCls}-input_name';
|
||||
|
||||
.@{prefix} {
|
||||
.van-field__value {
|
||||
@apply flex;
|
||||
}
|
||||
|
||||
.van-field__body {
|
||||
@apply w-full mr-5;
|
||||
}
|
||||
<style scoped>
|
||||
.fund-new-page-root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</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>
|
||||
<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">
|
||||
<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 class="gift-card-page-root">
|
||||
<DesktopGiftCardView v-show="isDesktop" />
|
||||
<MobileGiftCardView v-show="!isDesktop" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { loading } from '@/utils'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import Bg_main from '@/components/bg_main.vue'
|
||||
import { registerCodeGiftCard } from '@/api/giftCard'
|
||||
import { message } from 'ant-design-vue'
|
||||
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
||||
import { required } from '@/utils/inputRules'
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import { useGiftCardPage } from './composables/useGiftCardPage'
|
||||
import DesktopGiftCardView from './desktop/DesktopGiftCardView.vue'
|
||||
import MobileGiftCardView from './mobile/MobileGiftCardView.vue'
|
||||
|
||||
const form = ref<{ code: string }>({
|
||||
code: ''
|
||||
})
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
|
||||
const disableButton = computed(() => {
|
||||
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
|
||||
}
|
||||
}
|
||||
useGiftCardPage()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.gift-card-page-root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</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>
|
||||
<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 @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>-->
|
||||
@@ -150,7 +161,7 @@
|
||||
</div>-->
|
||||
</div>
|
||||
|
||||
<BasicActionSheet v-model:show="confirmAction">
|
||||
<BasicActionSheet v-if="!isStandaloneDesktop" v-model:show="confirmAction">
|
||||
<div class="font-bold text-lg"></div>
|
||||
<div class="w-full rounded-lg bg-gray-50 p-3">
|
||||
<div class="flex justify-between ">
|
||||
@@ -185,6 +196,52 @@
|
||||
</div>
|
||||
<BasicButton class="mb-20 mt-5 w-full" type="primary" @click="ok(); confirmAction= false">ثبت نهایی</BasicButton>
|
||||
</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="تکمیل اطلاعات"
|
||||
show-confirm-button @confirm="router.push({name: 'user',query:{showEdit: true}})">
|
||||
<div class="flex flex-col items-center p-2">
|
||||
@@ -197,6 +254,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</van-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -218,6 +276,8 @@ import { getLastPrice } from '@/api/home'
|
||||
import Icon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
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 GoldSkeleton from '@/views/base/gold/components/GoldSkeleton.vue'
|
||||
import GoldLoading from '@/components/GoldLoading.vue'
|
||||
@@ -274,6 +334,8 @@ interface Prop {
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Prop>(), {})
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
const isStandaloneDesktop = computed(() => isDesktop.value && !props.ProductItem)
|
||||
const isBuy = ref<boolean>(true)
|
||||
|
||||
const tradeProductLabel = computed(() => {
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
<template>
|
||||
<div
|
||||
:class="y > 50 ? 'bg-white shadow opacity-95' : ''"
|
||||
class="px-4 py-2 top-0 z-5 wrap-fix background-color-transition"
|
||||
:class="y > 50 ? 'bg-white shadow opacity-95' : ''"
|
||||
class="px-4 py-2 top-0 z-5 wrap-fix background-color-transition"
|
||||
>
|
||||
<div class="flex justify-between my-2">
|
||||
<div class="flex items-center gap-2" @click.stop="router.push({ name: 'user' })">
|
||||
<div class="flex justify-between my-2 items-center">
|
||||
<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
|
||||
class="z-100000 rounded-full border-1"
|
||||
:class="y > 50 ? 'text-primary border-primary' : 'text-primary border-primary'"
|
||||
class="z-100000 rounded-full border-1"
|
||||
:class="y > 50 ? 'text-primary border-primary' : 'text-primary border-primary'"
|
||||
>
|
||||
<Icon icon="vuesax-linear:user" size="20" />
|
||||
</div>
|
||||
@@ -15,26 +19,69 @@
|
||||
{{ userStore.userInfo?.name }}
|
||||
</div>
|
||||
</div>
|
||||
<Icon
|
||||
:color="y > 50 ? '#0A1B37' : '#0A1B37'"
|
||||
class="color-transition"
|
||||
icon="vuesax-linear:logout"
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
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')"
|
||||
/>
|
||||
>
|
||||
<Icon icon="vuesax-linear:logout" size="18" />
|
||||
<span>خروج</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useWindowScroll } from '@vueuse/core'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { useConfigStore } from '@/store/configStore'
|
||||
import router from '@/router'
|
||||
import Icon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { getProjectNavLogo, isBoursecuProject } from '@/utils/config'
|
||||
|
||||
const { y } = useWindowScroll()
|
||||
const userStore = useUserStore()
|
||||
const config = useConfigStore()
|
||||
const projectLogo = computed(() => getProjectNavLogo())
|
||||
const appName = computed(() => config.config?.name_fa || config.config?.name || 'بورس مس')
|
||||
|
||||
defineEmits<{
|
||||
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,52 @@
|
||||
<template>
|
||||
<div class="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 :class="plain ? 'py-2' : 'bg-primary rounded-lg p-2 py-3 mt-2'">
|
||||
<div
|
||||
v-if="!boursecuLayout && 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>
|
||||
<template v-if="meta?.single_metal && primaryMetalTotal">
|
||||
<div class="flex mt-2 justify-center text-tertiary w-full z-20">
|
||||
|
||||
<template v-if="boursecuAssetRows.length">
|
||||
<div
|
||||
v-for="(row, index) in boursecuAssetRows"
|
||||
:key="row.id"
|
||||
:class="[rowClass, index > 0 ? 'mt-2' : '']"
|
||||
class="flex justify-center w-full z-20"
|
||||
>
|
||||
{{ row.label }}:
|
||||
<div class="text-l mr-2 font-bold">{{ row.value }}</div>
|
||||
<BasicIcon
|
||||
v-if="row.info !== 'none'"
|
||||
icon="vuesax-linear:info-circle"
|
||||
size="18"
|
||||
:color="iconColor"
|
||||
@click="handleRowInfo(row)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="meta?.single_metal && primaryMetalTotal">
|
||||
<div :class="[rowClass, meta?.has_count_based_products ? 'mt-2' : '']" class="flex justify-center w-full z-20">
|
||||
{{ primaryMetalTotal.label }}:
|
||||
<div class="text-l mr-2 font-bold">{{ formatMetalTotal(primaryMetalTotal) }}</div>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:info-circle"
|
||||
size="18"
|
||||
color="tertiary"
|
||||
:color="iconColor"
|
||||
@click="$emit('openGoldModal', primaryMetalTotal)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
|
||||
<template v-else-if="!boursecuLayout">
|
||||
<div
|
||||
v-for="metal in metalTotals"
|
||||
v-for="(metal, index) in metalTotals"
|
||||
: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 }}:
|
||||
<div class="text-l mr-2 font-bold">{{ formatMetalTotal(metal) }}</div>
|
||||
@@ -28,48 +54,79 @@
|
||||
v-if="metal.metal_id === metalTotals[0]?.metal_id"
|
||||
icon="vuesax-linear:info-circle"
|
||||
size="18"
|
||||
color="tertiary"
|
||||
:color="iconColor"
|
||||
@click="$emit('openGoldModal', metal)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div :class="[rowClass, 'mt-2']" class="flex justify-center w-full z-20">
|
||||
دارایی ریالی:
|
||||
<div class="text-l mr-2 font-bold">{{ usePriceWithoutType(totalRialValue) }} ریال</div>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:info-circle"
|
||||
size="18"
|
||||
:color="iconColor"
|
||||
@click="$emit('openRialModal')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex mt-2 justify-center text-tertiary w-full z-20">
|
||||
دارایی ریالی:
|
||||
<div class="text-l mr-2 font-bold">{{ usePriceWithoutType(cashWallet) }} ریال</div>
|
||||
<BasicIcon @click="$emit('openRialModal')" icon="vuesax-linear:info-circle" size="18" color="tertiary" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { usePriceWithoutType } from '@/utils/usePrice'
|
||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
import type { WalletMetalTotal } from '@/models/walletAssets'
|
||||
import { resolveSummaryMetalWeight } from '@/utils/productAssets'
|
||||
import {
|
||||
buildBoursecuHomeAssetRows,
|
||||
resolveSummaryMetalWeight,
|
||||
type HomeAssetRow,
|
||||
} from '@/utils/productAssets'
|
||||
|
||||
const { assetsSummary, loadWalletAssets } = useWalletAssets()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
plain?: boolean
|
||||
boursecuLayout?: boolean
|
||||
}>(),
|
||||
{ plain: false, boursecuLayout: false }
|
||||
)
|
||||
|
||||
defineEmits<{
|
||||
const emit = defineEmits<{
|
||||
openGoldModal: [metal: WalletMetalTotal]
|
||||
openRialModal: []
|
||||
}>()
|
||||
|
||||
const { assetsSummary } = useWalletAssets()
|
||||
|
||||
const rowClass = computed(() => (props.plain ? 'text-primary' : 'text-tertiary'))
|
||||
const iconColor = computed(() => (props.plain ? 'primary' : 'tertiary'))
|
||||
|
||||
const meta = computed(() => assetsSummary.value?.meta)
|
||||
const metalTotals = computed(() => assetsSummary.value?.metal_totals ?? [])
|
||||
const primaryMetalTotal = computed(() => metalTotals.value[0])
|
||||
const cashWallet = computed(() => assetsSummary.value?.cash_wallet ?? 0)
|
||||
const boursecuAssetRows = computed(() =>
|
||||
props.boursecuLayout ? buildBoursecuHomeAssetRows(assetsSummary.value) : []
|
||||
)
|
||||
const totalRialValue = computed(() => assetsSummary.value?.total_rial_value ?? 0)
|
||||
const inventoryCards = computed(() => assetsSummary.value?.inventory_cards ?? [])
|
||||
const countBasedInventory = computed(() =>
|
||||
(assetsSummary.value?.count_products ?? []).reduce((sum, item) => sum + Number(item.quantity ?? 0), 0)
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
void loadWalletAssets()
|
||||
})
|
||||
|
||||
function formatMetalTotal(metal: WalletMetalTotal) {
|
||||
return `${resolveSummaryMetalWeight(metal, inventoryCards.value)}${metal.unit}`
|
||||
}
|
||||
|
||||
function handleRowInfo(row: HomeAssetRow) {
|
||||
if (row.info === 'rial') {
|
||||
emit('openRialModal')
|
||||
return
|
||||
}
|
||||
|
||||
if (row.metal) {
|
||||
emit('openGoldModal', row.metal)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
import { computed, watch } from 'vue'
|
||||
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
||||
import BasicInfoItem from '@/components/BasicPacks/BasicInfoItem.vue'
|
||||
import { usePrice } from '@/utils/usePrice'
|
||||
@@ -84,10 +84,6 @@ const metalRialValue = computed(() =>
|
||||
)
|
||||
const totalRialValue = computed(() => cashWallet.value + metalRialValue.value)
|
||||
|
||||
onMounted(() => {
|
||||
void loadWalletAssets()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(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>
|
||||
<div ref="skeletonRatio" class="h-fit">
|
||||
<content-loader :width="skeletonSize.width" class="bg-white" height="100vh">
|
||||
<rect :width="skeletonSize.width" height="5" rx="3" ry="3" x="0" y="0"/>
|
||||
<rect :width="skeletonSize.width" height="5" rx="3" ry="3" x="0" y="84"/>
|
||||
<rect :width="5" height="85" rx="3" ry="3" x="0" y="0"/>
|
||||
<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="skeletonSize.width/2-20" :x="10" height="20" rx="10" ry="10" y="15"/>
|
||||
<rect :width="skeletonSize.width/2-20" :x="skeletonSize.width/2 + 10" height="20" rx="10" ry="10" y="15"/>
|
||||
<rect :width="skeletonSize.width/2-40" :x="20" height="10" rx="5" ry="5" y="40"/>
|
||||
<rect :width="skeletonSize.width/2-40" :x="skeletonSize.width/2 + 20" height="10" rx="5" ry="5" y="40"/>
|
||||
<rect :width="skeletonSize.width/2-30" :x="15" height="18" rx="10" ry="10" y="60"/>
|
||||
<rect :width="skeletonSize.width/2-30" :x="skeletonSize.width/2 + 15" height="18" rx="10" ry="10" y="60"/>
|
||||
<circle :cx="skeletonSize.width/4-skeletonSize.width/8" cy="145" r="20"/>
|
||||
<circle :cx="skeletonSize.width/4+skeletonSize.width/8" cy="145" r="20"/>
|
||||
<circle :cx="skeletonSize.width/2+ skeletonSize.width/8" cy="145" r="20"/>
|
||||
<circle :cx="skeletonSize.width - skeletonSize.width/8" cy="145" r="20"/>
|
||||
<rect :width="50" :x="skeletonSize.width/4-skeletonSize.width/8 - 25" height="15" rx="3" ry="3" y="170"/>
|
||||
<rect :width="50" :x="skeletonSize.width/4+skeletonSize.width/8 - 25" height="15" rx="3" ry="3" y="170"/>
|
||||
<rect :width="50" :x="skeletonSize.width/2+ skeletonSize.width/8 - 25" height="15" rx="3" ry="3" y="170"/>
|
||||
<rect :width="50" :x="skeletonSize.width - skeletonSize.width/8 - 25" height="15" rx="3" ry="3" y="170"/>
|
||||
<rect :width="skeletonSize.width - 45" :x="45" height="20" rx="3" ry="3" y="230"/>
|
||||
<rect :width="skeletonSize.width" :x="0" height="80" rx="3" ry="3" y="260"/>
|
||||
<rect :width="skeletonSize.width/2 - 5" :x="0" height="50" rx="3" ry="3" y="360"/>
|
||||
<rect :width="skeletonSize.width/2 - 5" :x="skeletonSize.width/2 + 5" height="50" rx="3" ry="3" y="360"/>
|
||||
<rect :width="skeletonSize.width" :x="0" height="170" rx="3" ry="3" y="440"/>
|
||||
<!-- <rect height="20" rx="3" ry="3" width="100" x="0" y="0"/>
|
||||
<rect :width="skeletonSize.width/4 + 40" :x="skeletonSize.width/2" height="20" rx="3" ry="3" y="0"/>
|
||||
<rect height="20" rx="3" ry="3" width="80" x="0" y="30"/>
|
||||
<rect :x="skeletonSize.width - 40" height="40" rx="12" ry="12" width="40" y="0"/>
|
||||
<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" ♥/>-->
|
||||
<div ref="skeletonRatio" class="h-fit w-full min-h-[620px]">
|
||||
<content-loader
|
||||
:width="skeletonWidth"
|
||||
class="bg-white"
|
||||
:height="skeletonHeight"
|
||||
>
|
||||
<rect :width="skeletonWidth" height="5" rx="3" ry="3" x="0" y="0" />
|
||||
<rect :width="skeletonWidth" height="5" rx="3" ry="3" x="0" y="84" />
|
||||
<rect width="5" height="85" rx="3" ry="3" x="0" y="0" />
|
||||
<rect width="5" :x="skeletonWidth - 5" height="85" rx="3" ry="3" y="0" />
|
||||
<rect width="2" :x="skeletonWidth / 2" height="60" rx="3" ry="3" y="15" />
|
||||
<rect :width="halfWidth(20)" x="10" height="20" rx="10" ry="10" y="15" />
|
||||
<rect :width="halfWidth(20)" :x="skeletonWidth / 2 + 10" height="20" rx="10" ry="10" y="15" />
|
||||
<rect :width="halfWidth(40)" x="20" height="10" rx="5" ry="5" y="40" />
|
||||
<rect :width="halfWidth(40)" :x="skeletonWidth / 2 + 20" height="10" rx="5" ry="5" y="40" />
|
||||
<rect :width="halfWidth(30)" x="15" height="18" rx="10" ry="10" y="60" />
|
||||
<rect :width="halfWidth(30)" :x="skeletonWidth / 2 + 15" height="18" rx="10" ry="10" y="60" />
|
||||
<circle :cx="skeletonWidth / 4 - skeletonWidth / 8" cy="145" r="20" />
|
||||
<circle :cx="skeletonWidth / 4 + skeletonWidth / 8" cy="145" r="20" />
|
||||
<circle :cx="skeletonWidth / 2 + skeletonWidth / 8" cy="145" r="20" />
|
||||
<circle :cx="skeletonWidth - skeletonWidth / 8" cy="145" r="20" />
|
||||
<rect width="50" :x="skeletonWidth / 4 - skeletonWidth / 8 - 25" height="15" rx="3" ry="3" y="170" />
|
||||
<rect width="50" :x="skeletonWidth / 4 + skeletonWidth / 8 - 25" height="15" rx="3" ry="3" y="170" />
|
||||
<rect width="50" :x="skeletonWidth / 2 + skeletonWidth / 8 - 25" height="15" rx="3" ry="3" y="170" />
|
||||
<rect width="50" :x="skeletonWidth - skeletonWidth / 8 - 25" height="15" rx="3" ry="3" y="170" />
|
||||
<rect :width="fullWidth(45)" x="45" height="20" rx="3" ry="3" y="230" />
|
||||
<rect :width="skeletonWidth" x="0" height="80" rx="3" ry="3" y="260" />
|
||||
<rect :width="halfWidth(10)" x="0" height="50" rx="3" ry="3" y="360" />
|
||||
<rect :width="halfWidth(10)" :x="skeletonWidth / 2 + 5" height="50" rx="3" ry="3" y="360" />
|
||||
<rect :width="skeletonWidth" x="0" height="170" rx="3" ry="3" y="440" />
|
||||
</content-loader>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {reactive, ref, watch} from "vue";
|
||||
import {useElementSize} from "@vueuse/core";
|
||||
import {ContentLoader} from "vue-content-loader";
|
||||
import { computed, ref } from 'vue'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
import { ContentLoader } from 'vue-content-loader'
|
||||
|
||||
const skeletonRatio = ref(null)
|
||||
const skeletonSize = reactive(
|
||||
useElementSize(skeletonRatio, {width: 0, height: 0}, {box: 'border-box'})
|
||||
)
|
||||
/*watch(skeletonSize, () => {
|
||||
console.log(skeletonSize)
|
||||
})*/
|
||||
const skeletonHeight = 620
|
||||
const skeletonRatio = ref<HTMLElement | null>(null)
|
||||
const { width } = useElementSize(skeletonRatio, { width: 0, height: 0 }, { box: 'border-box' })
|
||||
|
||||
const skeletonWidth = computed(() => Math.max(width.value, 320))
|
||||
|
||||
function halfWidth(padding: number) {
|
||||
return Math.max(skeletonWidth.value / 2 - padding, 0)
|
||||
}
|
||||
|
||||
function fullWidth(padding: number) {
|
||||
return Math.max(skeletonWidth.value - padding, 0)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,201 +1,228 @@
|
||||
<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">
|
||||
<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="WEEKLY" @click="handleUpdate('WEEKLY')">هفته گذشته</a-radio-button>
|
||||
<a-radio-button value="MONTHLY" @click="handleUpdate('MONTHLY')">ماه گذشته</a-radio-button>
|
||||
</a-radio-group>
|
||||
<div class="min-h-[200px] relative">
|
||||
<div v-if="hasChartData" ref="chartEl" class="h-[200px] w-full" />
|
||||
<div
|
||||
v-else
|
||||
class="h-[200px] flex items-center justify-center text-gray-400 text-sm"
|
||||
>
|
||||
داده نمودار در دسترس نیست
|
||||
</div>
|
||||
<div ref="chartContainerRef" id="chart-timeline" class="w-full" :class="chartInnerClass">
|
||||
<apexchart
|
||||
v-if="canRenderChart"
|
||||
:key="`${layout}-${variant}-${selectedMetal}-${chartSelection}-${seriesLength}`"
|
||||
type="area"
|
||||
width="100%"
|
||||
:height="chartHeight"
|
||||
:options="chartOptions"
|
||||
:series="props.priceChartSeries"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as echarts from 'echarts'
|
||||
import type { EChartsOption } from 'echarts'
|
||||
import { ref, computed, watch, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { useConfigStore } from '@/store/configStore'
|
||||
import { usePrice } from '@/utils/usePrice'
|
||||
import { getProjectFontFamily } from '@/utils/config'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
|
||||
type ChartPoint = [number, number]
|
||||
import type { ChartMetalType } from '../composables/usePriceChart'
|
||||
|
||||
const props = defineProps<{
|
||||
priceChartSeries?: Array<{ name?: string; data?: unknown[] }> | null
|
||||
}>()
|
||||
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<{
|
||||
updateData: [timeline: string]
|
||||
'update:selectedMetal': [metal: ChartMetalType]
|
||||
}>()
|
||||
|
||||
const configStore = useConfigStore()
|
||||
const selectedValue = ref('DAILY')
|
||||
const chartEl = ref<HTMLElement | null>(null)
|
||||
let chart: echarts.ECharts | null = null
|
||||
let renderToken = 0
|
||||
const config = useConfigStore()
|
||||
const chartContainerRef = ref<HTMLElement | null>(null)
|
||||
const chartMounted = ref(false)
|
||||
const selectedValue = ref(props.chartSelection)
|
||||
|
||||
function extractPoints(): ChartPoint[] {
|
||||
const source = props.priceChartSeries
|
||||
if (!Array.isArray(source) || source.length === 0) {
|
||||
return []
|
||||
}
|
||||
const { width: chartWidth } = useElementSize(chartContainerRef)
|
||||
|
||||
const raw = source[0]?.data
|
||||
if (!Array.isArray(raw)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return raw
|
||||
.map((item) => {
|
||||
if (!Array.isArray(item) || item.length < 2) {
|
||||
return null
|
||||
}
|
||||
|
||||
let x = Number(item[0])
|
||||
let y = Number(item[1])
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y) || y <= 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (x > 0 && x < 1e11) {
|
||||
x *= 1000
|
||||
}
|
||||
|
||||
return [x, y] as ChartPoint
|
||||
})
|
||||
.filter((point): point is ChartPoint => point !== null)
|
||||
.sort((a, b) => a[0] - b[0])
|
||||
}
|
||||
|
||||
const chartPoints = computed(() => extractPoints())
|
||||
const hasChartData = computed(() => chartPoints.value.length >= 2)
|
||||
|
||||
const toPersianNumber = (num: number) =>
|
||||
num.toString().replace(/\d/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[d])
|
||||
|
||||
function buildOption(points: ChartPoint[]): EChartsOption {
|
||||
const color = configStore.config?.color || '#CEA34C'
|
||||
|
||||
return {
|
||||
animation: true,
|
||||
grid: {
|
||||
left: 4,
|
||||
right: 4,
|
||||
top: 8,
|
||||
bottom: 4,
|
||||
containLabel: false
|
||||
},
|
||||
xAxis: {
|
||||
type: 'time',
|
||||
show: false,
|
||||
boundaryGap: false
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
show: false,
|
||||
scale: true
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
confine: true,
|
||||
formatter: (params) => {
|
||||
const row = Array.isArray(params) ? params[0] : params
|
||||
if (!row || !Array.isArray(row.value)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const [timestamp, value] = row.value as [number, number]
|
||||
const date = new Date(timestamp)
|
||||
const dateText = `${date.toLocaleDateString('fa-IR')} - ${date.toLocaleTimeString('fa-IR', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}`
|
||||
|
||||
return `${dateText}<br/>${toPersianNumber(usePrice(value, true, 'ه.ریال'))}`
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'قیمت',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
data: points,
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color
|
||||
},
|
||||
areaStyle: {
|
||||
color,
|
||||
opacity: 0.25
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function destroyChart() {
|
||||
if (!chart) {
|
||||
return
|
||||
}
|
||||
|
||||
chart.dispose()
|
||||
chart = null
|
||||
}
|
||||
|
||||
async function renderChart() {
|
||||
const token = ++renderToken
|
||||
|
||||
if (!hasChartData.value) {
|
||||
destroyChart()
|
||||
return
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
if (token !== renderToken || !chartEl.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const points = chartPoints.value
|
||||
|
||||
try {
|
||||
if (!chart || chart.isDisposed()) {
|
||||
chart = echarts.init(chartEl.value, undefined, { renderer: 'canvas' })
|
||||
}
|
||||
|
||||
chart.setOption(buildOption(points), true)
|
||||
chart.resize()
|
||||
} catch (error) {
|
||||
console.error('Price chart render failed:', error)
|
||||
destroyChart()
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [chartPoints.value.length, selectedValue.value, configStore.config?.color] as const,
|
||||
() => {
|
||||
void renderChart()
|
||||
},
|
||||
{ immediate: true, flush: 'post' }
|
||||
const seriesLength = computed(() => props.priceChartSeries?.[0]?.data?.length ?? 0)
|
||||
const canRenderChart = computed(
|
||||
() => chartMounted.value && chartWidth.value > 0 && seriesLength.value > 0
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
renderToken++
|
||||
destroyChart()
|
||||
watch(
|
||||
() => props.chartSelection,
|
||||
(timeline) => {
|
||||
selectedValue.value = timeline
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
chartMounted.value = true
|
||||
})
|
||||
|
||||
const toPersianNumber = (num: number) => {
|
||||
return num.toString().replace(/\d/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[d])
|
||||
}
|
||||
|
||||
const chartOptions = computed(() => {
|
||||
const fontFamily = getProjectFontFamily()
|
||||
|
||||
return {
|
||||
chart: {
|
||||
id: 'نوسانات قیمت',
|
||||
type: 'area',
|
||||
height: 200,
|
||||
toolbar: { show: false },
|
||||
zoom: { autoScaleYaxis: true },
|
||||
redrawOnParentResize: true,
|
||||
},
|
||||
grid: {
|
||||
padding: { top: 0, right: 0, bottom: 0, left: 0 },
|
||||
yaxis: { lines: { show: false } },
|
||||
},
|
||||
stroke: { width: 2 },
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
style: {
|
||||
fontFamily,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
tickAmount: 8,
|
||||
show: false,
|
||||
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, 'ه.ریال')),
|
||||
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) => {
|
||||
selectedValue.value = timeline
|
||||
emit('updateData', timeline)
|
||||
}
|
||||
</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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user