Merge feature/responsive-layout-refactor into modular for desktop layouts.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<div class="w-full flex justify-evenly rounded-lg py-2 home_appear_fade_in mt-6 pb-45">
|
||||
<div
|
||||
class="w-full flex justify-evenly rounded-lg py-2 home_appear_fade_in"
|
||||
:class="[
|
||||
layout === 'boursecu' ? 'mt-8 pt-2' : 'mt-6',
|
||||
variant === 'mobile' ? 'pb-45' : 'mt-0 pb-0',
|
||||
]"
|
||||
>
|
||||
<div class="text-center w-full">
|
||||
<div>بالاترین قیمت امروز</div>
|
||||
<div v-if="price" class="flex flex-col items-center mt-3">
|
||||
@@ -26,13 +32,35 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { getTradeBuyPrice, getTradeSellPrice } from '@/utils/price'
|
||||
import { usePriceWithoutType } from '../../../../utils/usePrice'
|
||||
import type { ChartMetalType } from '../composables/usePriceChart'
|
||||
import { resolveMetalDayRangePrices } from '../composables/usePriceChart'
|
||||
|
||||
const props = defineProps<{
|
||||
price: any
|
||||
}>()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
price: any
|
||||
layout?: 'default' | 'boursecu'
|
||||
variant?: 'mobile' | 'desktop'
|
||||
selectedMetal?: ChartMetalType
|
||||
maftolRatio?: number
|
||||
}>(),
|
||||
{
|
||||
layout: 'default',
|
||||
variant: 'mobile',
|
||||
selectedMetal: 'katod',
|
||||
maftolRatio: 0,
|
||||
}
|
||||
)
|
||||
|
||||
const highPrice = computed(() => props.price?.up?.buy ?? getTradeBuyPrice(props.price))
|
||||
const lowPrice = computed(() => props.price?.up?.sell ?? getTradeSellPrice(props.price))
|
||||
const rangePrices = computed(() =>
|
||||
resolveMetalDayRangePrices({
|
||||
price: props.price,
|
||||
metal: props.selectedMetal,
|
||||
isBoursecuLayout: props.layout === 'boursecu',
|
||||
maftolRatio: props.maftolRatio,
|
||||
})
|
||||
)
|
||||
|
||||
const highPrice = computed(() => rangePrices.value.high)
|
||||
const lowPrice = computed(() => rangePrices.value.low)
|
||||
</script>
|
||||
|
||||
@@ -1,35 +1,32 @@
|
||||
<template>
|
||||
<div class="bg-primary rounded-lg p-2 py-1 mt-2">
|
||||
<div class="p-2">
|
||||
<div
|
||||
v-if="userStore.userInfo?.referral_id"
|
||||
class="text-tertiary font-bold w-full text-center flex justify-between gap-2 items-center"
|
||||
<div class="home-referral bg-primary rounded-lg">
|
||||
<div
|
||||
v-if="userStore.userInfo?.referral_id"
|
||||
class="home-referral__code text-tertiary font-bold"
|
||||
>
|
||||
<span>کد معرف:</span>
|
||||
<span class="home-referral__code-value">{{ userStore.userInfo?.referral_id }}</span>
|
||||
</div>
|
||||
|
||||
<div class="home-referral__actions">
|
||||
<BasicButton
|
||||
icon="vuesax-linear:share"
|
||||
size="small"
|
||||
class="home-referral__btn !bg-primary !border-tertiary !text-tertiary"
|
||||
icon-size="18"
|
||||
@click.stop="shareCode"
|
||||
>
|
||||
کد معرف:
|
||||
<div class="flex gap-2">
|
||||
<div class="mr-2">{{ userStore.userInfo?.referral_id }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex gap-2 mt-1">
|
||||
<BasicButton
|
||||
icon="vuesax-linear:share"
|
||||
size="small"
|
||||
class="flex-1 !bg-primary !border-tertiary !text-tertiary"
|
||||
icon-size="18"
|
||||
@click.stop="shareCode"
|
||||
>
|
||||
دعوت از دوستان
|
||||
</BasicButton>
|
||||
<BasicButton
|
||||
icon="vuesax-linear:copy"
|
||||
size="small"
|
||||
class="flex-1 !bg-primary !border-tertiary !text-tertiary"
|
||||
icon-size="18"
|
||||
@click.stop="copyCode"
|
||||
>
|
||||
رونوشت
|
||||
</BasicButton>
|
||||
</div>
|
||||
دعوت از دوستان
|
||||
</BasicButton>
|
||||
<BasicButton
|
||||
icon="vuesax-linear:copy"
|
||||
size="small"
|
||||
class="home-referral__btn !bg-primary !border-tertiary !text-tertiary"
|
||||
icon-size="18"
|
||||
@click.stop="copyCode"
|
||||
>
|
||||
رونوشت
|
||||
</BasicButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -58,4 +55,76 @@ const copyCode = () => {
|
||||
message.info('کد رونوشت شد')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.home-referral {
|
||||
container-type: inline-size;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
margin-top: 8px;
|
||||
padding: 10px 12px;
|
||||
overflow: hidden;
|
||||
|
||||
&__code {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__code-value {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
flex: 1 1 0;
|
||||
min-width: 0 !important;
|
||||
max-width: 100%;
|
||||
|
||||
:deep(.ant-btn) {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
padding-inline: 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
:deep(.ant-btn > div) {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 340px) {
|
||||
&__actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -42,10 +42,15 @@ import { message } from 'ant-design-vue'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { getReferralShareText, getReferralShareTitle } from '@/utils/config'
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
referralId: string
|
||||
}>()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
show: boolean
|
||||
referralId?: string
|
||||
}>(),
|
||||
{
|
||||
referralId: '',
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:show': [value: boolean]
|
||||
|
||||
@@ -2,62 +2,93 @@ import { ref } from 'vue'
|
||||
import { getConstByKey } from '@/api/const'
|
||||
|
||||
function parseJsonArray(raw: unknown): unknown[] {
|
||||
if (raw == null || raw === '') {
|
||||
return []
|
||||
}
|
||||
if (Array.isArray(raw)) {
|
||||
return raw
|
||||
}
|
||||
if (typeof raw !== 'string') {
|
||||
return []
|
||||
}
|
||||
const trimmed = raw.trim()
|
||||
if (!trimmed) {
|
||||
return []
|
||||
}
|
||||
if (raw == null || raw === '') {
|
||||
return []
|
||||
}
|
||||
if (Array.isArray(raw)) {
|
||||
return raw
|
||||
}
|
||||
if (typeof raw !== 'string') {
|
||||
return []
|
||||
}
|
||||
const trimmed = raw.trim()
|
||||
if (!trimmed) {
|
||||
return []
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed)
|
||||
return Array.isArray(parsed) ? parsed : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/** Module-level banner state — shared by mobile and desktop home layouts. */
|
||||
const banners = ref<unknown[]>([])
|
||||
const bannersLoading = ref(false)
|
||||
let bannersPromise: Promise<void> | null = null
|
||||
let bannersLoaded = false
|
||||
|
||||
async function fetchBanners(force = false) {
|
||||
if (!force && bannersLoaded) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!force && bannersPromise) {
|
||||
return bannersPromise
|
||||
}
|
||||
|
||||
bannersPromise = (async () => {
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed)
|
||||
return Array.isArray(parsed) ? parsed : []
|
||||
} catch {
|
||||
return []
|
||||
bannersLoading.value = true
|
||||
const response = await getConstByKey('header_banner')
|
||||
if (!response) {
|
||||
banners.value = []
|
||||
return
|
||||
}
|
||||
|
||||
let list: unknown[] = []
|
||||
if (typeof response === 'string') {
|
||||
list = parseJsonArray(response)
|
||||
} else if (typeof response === 'object' && !Array.isArray(response) && 'value' in response) {
|
||||
list = parseJsonArray((response as { value?: unknown }).value)
|
||||
} else if (Array.isArray(response) && response.length > 0) {
|
||||
const first = response[0] as { value?: unknown; icon?: unknown }
|
||||
if (first.value != null) {
|
||||
list = parseJsonArray(first.value)
|
||||
} else if (first.icon) {
|
||||
list = response
|
||||
}
|
||||
}
|
||||
|
||||
banners.value = list.filter((banner) => (banner as { show?: boolean }).show === true)
|
||||
bannersLoaded = true
|
||||
} catch (error) {
|
||||
console.error('Error loading banners:', error)
|
||||
banners.value = []
|
||||
} finally {
|
||||
bannersLoading.value = false
|
||||
bannersPromise = null
|
||||
}
|
||||
})()
|
||||
|
||||
return bannersPromise
|
||||
}
|
||||
|
||||
export function useBanners() {
|
||||
const banners = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
const loadBanners = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await getConstByKey('header_banner')
|
||||
if (!response) {
|
||||
banners.value = []
|
||||
return
|
||||
}
|
||||
|
||||
let list: unknown[] = []
|
||||
if (typeof response === 'string') {
|
||||
list = parseJsonArray(response)
|
||||
} else if (typeof response === 'object' && !Array.isArray(response) && 'value' in response) {
|
||||
list = parseJsonArray((response as { value?: unknown }).value)
|
||||
} else if (Array.isArray(response) && response.length > 0) {
|
||||
const first = response[0] as { value?: unknown; icon?: unknown }
|
||||
if (first.value != null) {
|
||||
list = parseJsonArray(first.value)
|
||||
} else if (first.icon) {
|
||||
list = response
|
||||
}
|
||||
}
|
||||
|
||||
banners.value = list.filter((b: { show?: boolean }) => b?.show === true)
|
||||
} catch (error) {
|
||||
console.error('Error loading banners:', error)
|
||||
banners.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return { banners, loading, loadBanners }
|
||||
return {
|
||||
banners,
|
||||
loading: bannersLoading,
|
||||
loadBanners: fetchBanners,
|
||||
}
|
||||
}
|
||||
|
||||
export function loadHomeBanners(force = false) {
|
||||
return fetchBanners(force)
|
||||
}
|
||||
|
||||
export function resetHomeBanners() {
|
||||
banners.value = []
|
||||
bannersLoaded = false
|
||||
bannersPromise = null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
import { computed, inject, onMounted, onUnmounted, provide, ref, type InjectionKey } from 'vue'
|
||||
import router from '@/router'
|
||||
import { loading, removeToken } from '@/utils'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { useWalletStore } from '@/store/walletStore'
|
||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
import type { WalletMetalTotal } from '@/models/walletAssets'
|
||||
import { formatMetalAssetsInfoText, formatRialAssetsInfoText, resolveAssetsRialValue } from '@/utils/productAssets'
|
||||
import { useConfigStore } from '@/store/configStore'
|
||||
import { getTradeBuyPrice, lastPrice, stopLastPricePolling } from '@/utils/price'
|
||||
import { getStoreClosedMessage, isStoreClosed } from '@/utils/storeHours'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { getReferralShareText, getReferralShareTitle } from '@/utils/config'
|
||||
import { useBanners } from './useBanners'
|
||||
import { usePriceChart, type ChartMetalType } from './usePriceChart'
|
||||
import type { HomeActionItem } from '../layouts/types'
|
||||
import { getHomeLayoutId } from '../layouts/registry'
|
||||
import { initHomePageData, resetHomePageData } from './useHomePageData'
|
||||
import { resetWalletPageData } from '../../wallet/composables/useWalletPageData'
|
||||
import { resetAddressPageData } from '../../address/composables/useAddressPageData'
|
||||
import { resetTransactionsPageData } from '../../transactions/composables/useTransactionsPageData'
|
||||
import { resetProductsPageData } from '../../products/composables/useProductsPageData'
|
||||
|
||||
export interface HomePageContext {
|
||||
loading: typeof loading
|
||||
isGaroosi: ReturnType<typeof computed<boolean>>
|
||||
limitedPrice: ReturnType<typeof computed>
|
||||
showClosedMessage: ReturnType<typeof computed<boolean>>
|
||||
closedMessage: ReturnType<typeof computed>
|
||||
mainActions: ReturnType<typeof computed<HomeActionItem[]>>
|
||||
banners: ReturnType<typeof useBanners>['banners']
|
||||
priceChartSeries: ReturnType<typeof usePriceChart>['priceChartSeries']
|
||||
chartSelection: ReturnType<typeof usePriceChart>['chartSelection']
|
||||
updateChartData: ReturnType<typeof usePriceChart>['updateChartData']
|
||||
selectedChartMetal: ReturnType<typeof usePriceChart>['selectedChartMetal']
|
||||
setChartMetal: (metal: ChartMetalType) => void
|
||||
isBoursecuHomeLayout: ReturnType<typeof computed<boolean>>
|
||||
handleBannerClick: (banner: { typeClick?: string; modalText?: string; link?: string }) => void
|
||||
openGoldModal: (metal: WalletMetalTotal) => void
|
||||
openRialModal: () => void
|
||||
}
|
||||
|
||||
export const HOME_PAGE_CONTEXT_KEY: InjectionKey<HomePageContext> = Symbol('homePageContext')
|
||||
|
||||
export function useHomePage() {
|
||||
const userStore = useUserStore()
|
||||
const walletStore = useWalletStore()
|
||||
const configStore = useConfigStore()
|
||||
const { assetsSummary, clearWalletAssets } = useWalletAssets()
|
||||
|
||||
const showLogoutConfirm = ref(false)
|
||||
const wholeBalanceVisible = ref(false)
|
||||
const showTextBanner = ref(false)
|
||||
const showShare = ref(false)
|
||||
const TextBanner = ref('')
|
||||
|
||||
const { banners } = useBanners()
|
||||
const {
|
||||
updateChartData,
|
||||
priceChartSeries,
|
||||
chartSelection,
|
||||
selectedChartMetal,
|
||||
setChartMetal,
|
||||
} = usePriceChart()
|
||||
const isBoursecuHomeLayout = computed(() => getHomeLayoutId() === 'boursecu')
|
||||
|
||||
const isGaroosi = computed(() => configStore.config?.name === 'garoosi')
|
||||
const closedMessage = computed(() => userStore.getValueByKey('closed_message'))
|
||||
const marketClosedText = computed(() => {
|
||||
if (!isStoreClosed()) return ''
|
||||
return closedMessage.value || getStoreClosedMessage()
|
||||
})
|
||||
const showClosedMessage = computed(() => !!marketClosedText.value)
|
||||
const limitedPrice = computed(() => walletStore.limitedPrice?.[0])
|
||||
const goldVal = computed(() => lastPrice.value?.price_last ?? 1)
|
||||
|
||||
const mainActions = computed<HomeActionItem[]>(() => [
|
||||
{
|
||||
name: 'buySell',
|
||||
icon: 'vuesax-linear:money-receive',
|
||||
label: 'خرید و فروش',
|
||||
handler: () => router.push({ name: 'buyProduct' }),
|
||||
},
|
||||
{
|
||||
name: 'deposit',
|
||||
icon: 'vuesax-linear:card-add',
|
||||
label: isGaroosi.value ? 'واریز و برداشت' : 'واریز نقدی',
|
||||
handler: () => router.push({ name: 'wallet', query: { state: 'deposit' } }),
|
||||
},
|
||||
{
|
||||
name: 'balance',
|
||||
icon: 'vuesax-linear:receipt-item',
|
||||
label: 'موجودی',
|
||||
handler: () => {
|
||||
wholeBalanceVisible.value = true
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'assets',
|
||||
icon: 'vuesax-linear:cup',
|
||||
label: isGaroosi.value ? 'دعوت از دوستان' : 'دارایی ها',
|
||||
handler: () => (isGaroosi.value ? shareCode() : router.push({ name: 'fund' })),
|
||||
},
|
||||
])
|
||||
|
||||
async function shareCode() {
|
||||
const text = getReferralShareText({
|
||||
userName: userStore.userInfo?.name,
|
||||
projectName: configStore.config.name,
|
||||
referralId: userStore.userInfo?.referral_id,
|
||||
})
|
||||
|
||||
try {
|
||||
await navigator.share({
|
||||
title: getReferralShareTitle(configStore.config.name),
|
||||
text,
|
||||
})
|
||||
} catch {
|
||||
copyCode()
|
||||
}
|
||||
}
|
||||
|
||||
function copyCode() {
|
||||
const code = userStore.userInfo?.referral_id
|
||||
if (code) {
|
||||
navigator.clipboard.writeText(code)
|
||||
message.info('کد رونوشت شد')
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
userStore.userInfo = undefined
|
||||
walletStore.walletInfo = undefined
|
||||
clearWalletAssets()
|
||||
resetHomePageData()
|
||||
resetWalletPageData()
|
||||
resetAddressPageData()
|
||||
resetTransactionsPageData()
|
||||
resetProductsPageData()
|
||||
removeToken()
|
||||
await router.push({ name: 'login' })
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
function openGoldModal(metal: WalletMetalTotal) {
|
||||
Modal.confirm({
|
||||
title: formatMetalAssetsInfoText(metal),
|
||||
okText: 'متوجه شدم',
|
||||
centered: true,
|
||||
cancelButtonProps: { style: { display: 'none' } },
|
||||
})
|
||||
}
|
||||
|
||||
function openRialModal() {
|
||||
const summary = assetsSummary.value
|
||||
const cash = summary?.cash_wallet ?? 0
|
||||
const metalRial = resolveAssetsRialValue(summary, getTradeBuyPrice(lastPrice.value))
|
||||
Modal.confirm({
|
||||
title: 'دارایی ریالی',
|
||||
content: formatRialAssetsInfoText(cash, metalRial),
|
||||
okText: 'متوجه شدم',
|
||||
centered: true,
|
||||
cancelButtonProps: { style: { display: 'none' } },
|
||||
})
|
||||
}
|
||||
|
||||
function handleBannerClick(banner: { typeClick?: string; modalText?: string; link?: string }) {
|
||||
const handlers: Record<string, () => void> = {
|
||||
share: () => {
|
||||
showShare.value = true
|
||||
},
|
||||
modal: () => {
|
||||
if (banner.modalText) {
|
||||
TextBanner.value = banner.modalText
|
||||
showTextBanner.value = true
|
||||
}
|
||||
},
|
||||
link: () => {
|
||||
if (banner.link) {
|
||||
if (banner.link.startsWith('http')) {
|
||||
window.open(banner.link, '_blank')
|
||||
} else {
|
||||
router.push(banner.link)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
handlers[banner.typeClick ?? '']?.()
|
||||
}
|
||||
|
||||
const context: HomePageContext = {
|
||||
loading,
|
||||
isGaroosi,
|
||||
limitedPrice,
|
||||
showClosedMessage,
|
||||
closedMessage: marketClosedText,
|
||||
mainActions,
|
||||
banners,
|
||||
priceChartSeries,
|
||||
chartSelection,
|
||||
updateChartData,
|
||||
selectedChartMetal,
|
||||
setChartMetal,
|
||||
isBoursecuHomeLayout,
|
||||
handleBannerClick,
|
||||
openGoldModal,
|
||||
openRialModal,
|
||||
}
|
||||
|
||||
provide(HOME_PAGE_CONTEXT_KEY, context)
|
||||
|
||||
onUnmounted(() => {
|
||||
stopLastPricePolling()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// Single shared fetch for mobile + desktop — safe to call on remount (deduped in useHomePageData)
|
||||
void initHomePageData()
|
||||
})
|
||||
|
||||
return {
|
||||
userStore,
|
||||
walletStore,
|
||||
showLogoutConfirm,
|
||||
wholeBalanceVisible,
|
||||
showTextBanner,
|
||||
showShare,
|
||||
TextBanner,
|
||||
goldVal,
|
||||
logout,
|
||||
}
|
||||
}
|
||||
|
||||
export function useHomePageContext() {
|
||||
const context = inject(HOME_PAGE_CONTEXT_KEY)
|
||||
if (!context) {
|
||||
throw new Error('useHomePageContext must be used within useHomePage')
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { ref } from 'vue'
|
||||
import { loading } from '@/utils'
|
||||
import { getLastPrices, startLastPricePolling, stopLastPricePolling } from '@/utils/price'
|
||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
import { clearProductsCatalog } from '@/composables/useProductsCatalog'
|
||||
import { getHomeLayoutId } from '../layouts/registry'
|
||||
import { fetchHomeChartPrices, loadHomeMaftolOffset, resetHomePriceChart } from './usePriceChart'
|
||||
import { loadHomeBanners, resetHomeBanners } from './useBanners'
|
||||
import { loadHomeFunds, resetHomeFunds } from './useUserFunds'
|
||||
|
||||
/**
|
||||
* Single entry point for all homepage API calls.
|
||||
* Mobile and desktop layouts must not fetch independently — they consume shared module state.
|
||||
*/
|
||||
let initPromise: Promise<void> | null = null
|
||||
const initialized = ref(false)
|
||||
|
||||
export function isHomePageDataInitialized() {
|
||||
return initialized.value
|
||||
}
|
||||
|
||||
export async function initHomePageData(force = false) {
|
||||
if (!force && initialized.value) {
|
||||
// Resume live price updates when returning to home without refetching datasets
|
||||
startLastPricePolling()
|
||||
return
|
||||
}
|
||||
|
||||
if (!force && initPromise) {
|
||||
return initPromise
|
||||
}
|
||||
|
||||
initPromise = (async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const { loadWalletAssets } = useWalletAssets()
|
||||
|
||||
await Promise.all([
|
||||
loadHomeFunds(force),
|
||||
loadWalletAssets(force),
|
||||
getLastPrices(),
|
||||
fetchHomeChartPrices(force),
|
||||
startLastPricePolling(),
|
||||
loadHomeBanners(force),
|
||||
])
|
||||
|
||||
if (getHomeLayoutId() === 'boursecu') {
|
||||
await loadHomeMaftolOffset(force)
|
||||
}
|
||||
|
||||
initialized.value = true
|
||||
} catch (error) {
|
||||
console.error('Home page initialization error:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
initPromise = null
|
||||
}
|
||||
})()
|
||||
|
||||
return initPromise
|
||||
}
|
||||
|
||||
/** Clears homepage caches — call on logout or when a full refresh is required. */
|
||||
export function resetHomePageData() {
|
||||
initialized.value = false
|
||||
initPromise = null
|
||||
stopLastPricePolling()
|
||||
resetHomeBanners()
|
||||
resetHomeFunds()
|
||||
resetHomePriceChart()
|
||||
clearProductsCatalog()
|
||||
}
|
||||
|
||||
export function useHomePageInitState() {
|
||||
return { initialized }
|
||||
}
|
||||
@@ -1,113 +1,245 @@
|
||||
import { ref } from 'vue'
|
||||
import { getAllPrices, getChart } from '@/api/home'
|
||||
import { ref, watch } from 'vue'
|
||||
import { getAllPrices } from '@/api/home'
|
||||
import type { Product } from '@/models/products'
|
||||
import { getProductsCatalog } from '@/composables/useProductsCatalog'
|
||||
import { getHomeLayoutId } from '../layouts/registry'
|
||||
import { getProductBuyPrice, resolvePriceOffsetRatio } from '@/utils/productPrice'
|
||||
import { isKatodMesGroup, isMaftolMesGroup } from '@/utils/productDealMethod'
|
||||
import { getTradeBuyPrice, getTradeSellPrice, lastPrice } from '@/utils/price'
|
||||
|
||||
export type ChartMetalType = 'katod' | 'maftol'
|
||||
|
||||
type ChartPoint = [number, number]
|
||||
|
||||
function mapTimelinePoints(items: unknown[]): ChartPoint[] {
|
||||
if (!Array.isArray(items)) {
|
||||
return []
|
||||
}
|
||||
/** Module-level chart state — one dataset shared by mobile and desktop PriceChart instances. */
|
||||
const priceChartSeries = ref<{ name: string; data: ChartPoint[] }[]>([
|
||||
{ name: 'قیمت', data: [] },
|
||||
])
|
||||
const rawChartPoints = ref<ChartPoint[]>([])
|
||||
const chartSelection = ref('DAILY')
|
||||
const chartLoading = ref(false)
|
||||
const selectedChartMetal = ref<ChartMetalType>('katod')
|
||||
const maftolChartRatio = ref(0)
|
||||
|
||||
return items
|
||||
.map((item) => {
|
||||
const row = item as { timestamp?: number; value?: number; price?: number }
|
||||
let timestamp = Number(row.timestamp)
|
||||
const value = Number(row.value ?? row.price ?? 0)
|
||||
let chartFetchPromise: Promise<void> | null = null
|
||||
let chartInitialized = false
|
||||
let maftolOffsetPromise: Promise<void> | null = null
|
||||
let maftolOffsetLoaded = false
|
||||
|
||||
if (!Number.isFinite(timestamp) || !Number.isFinite(value) || value <= 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (timestamp > 0 && timestamp < 1e11) {
|
||||
timestamp *= 1000
|
||||
}
|
||||
|
||||
return [timestamp, value] as ChartPoint
|
||||
})
|
||||
.filter((point): point is ChartPoint => point !== null)
|
||||
function findKatodProduct(products: Product[]) {
|
||||
return (
|
||||
products.find((product) => isKatodMesGroup(product.group_type))
|
||||
?? products.find((product) => (product.title ?? '').includes('کاتد'))
|
||||
)
|
||||
}
|
||||
|
||||
async function fetchCandleFallback(): Promise<ChartPoint[]> {
|
||||
function findMaftolProduct(products: Product[]) {
|
||||
return (
|
||||
products.find((product) => isMaftolMesGroup(product.group_type))
|
||||
?? products.find((product) => (product.title ?? '').includes('مفتول'))
|
||||
)
|
||||
}
|
||||
|
||||
function applyChartRatio(basePrice: number, ratio: number) {
|
||||
if (!ratio) {
|
||||
return basePrice
|
||||
}
|
||||
|
||||
return Math.round(basePrice + basePrice * ratio)
|
||||
}
|
||||
|
||||
/** بالاترین/پایینترین امروز — برای boursecu با نسبت مفتول نسبت به کاتد */
|
||||
export function resolveMetalDayRangePrices(options: {
|
||||
price: { up?: { buy?: number; sell?: number }; buy?: number; sell?: number; price_last?: number } | null | undefined
|
||||
metal: ChartMetalType
|
||||
isBoursecuLayout: boolean
|
||||
maftolRatio: number
|
||||
}) {
|
||||
const baseHigh = options.price?.up?.buy ?? getTradeBuyPrice(options.price as Parameters<typeof getTradeBuyPrice>[0])
|
||||
const baseLow = options.price?.up?.sell ?? getTradeSellPrice(options.price as Parameters<typeof getTradeSellPrice>[0])
|
||||
|
||||
if (!options.isBoursecuLayout || options.metal === 'katod') {
|
||||
return { high: baseHigh, low: baseLow }
|
||||
}
|
||||
|
||||
return {
|
||||
high: applyChartRatio(baseHigh, options.maftolRatio),
|
||||
low: applyChartRatio(baseLow, options.maftolRatio),
|
||||
}
|
||||
}
|
||||
|
||||
function deriveMaftolChartRatio(maftolProduct?: Product, katodProduct?: Product) {
|
||||
const offsetRatio = resolvePriceOffsetRatio(maftolProduct?.price_offset_percent)
|
||||
if (offsetRatio) {
|
||||
return offsetRatio
|
||||
}
|
||||
|
||||
if (!maftolProduct) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const basePrice = getTradeBuyPrice(lastPrice.value)
|
||||
const katodPrice = katodProduct ? getProductBuyPrice(katodProduct) : basePrice
|
||||
const maftolPrice = getProductBuyPrice(maftolProduct)
|
||||
const referencePrice = katodPrice || basePrice
|
||||
|
||||
if (!referencePrice || !maftolPrice || referencePrice === maftolPrice) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return (maftolPrice - referencePrice) / referencePrice
|
||||
}
|
||||
|
||||
function normalizeChartTimestamp(timestamp: number) {
|
||||
return timestamp < 1_000_000_000_000 ? timestamp * 1000 : timestamp
|
||||
}
|
||||
|
||||
function resolvePricesMap(response: unknown): Record<string, Array<{ timestamp: number; value: number }>> | null {
|
||||
if (!response || typeof response !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const body = response as Record<string, unknown>
|
||||
const nested = body.data
|
||||
|
||||
if (nested && typeof nested === 'object' && 'prices' in nested) {
|
||||
return (nested as { prices: Record<string, Array<{ timestamp: number; value: number }>> }).prices
|
||||
}
|
||||
|
||||
if ('prices' in body) {
|
||||
return body.prices as Record<string, Array<{ timestamp: number; value: number }>>
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function buildChartPoints(items: Array<{ timestamp: number; value: number }>): ChartPoint[] {
|
||||
return items.map((item) => [
|
||||
normalizeChartTimestamp(item.timestamp),
|
||||
item.value - 1,
|
||||
])
|
||||
}
|
||||
|
||||
function syncChartSeries() {
|
||||
const data =
|
||||
getHomeLayoutId() === 'boursecu' && selectedChartMetal.value === 'maftol'
|
||||
? rawChartPoints.value.map(
|
||||
([timestamp, value]) =>
|
||||
[timestamp, applyChartRatio(value, maftolChartRatio.value)] as ChartPoint
|
||||
)
|
||||
: rawChartPoints.value
|
||||
|
||||
priceChartSeries.value[0].data = data
|
||||
}
|
||||
|
||||
watch([selectedChartMetal, maftolChartRatio], () => {
|
||||
syncChartSeries()
|
||||
})
|
||||
|
||||
async function fetchPrices(force = false) {
|
||||
if (!force && chartInitialized && rawChartPoints.value.length > 0) {
|
||||
syncChartSeries()
|
||||
return
|
||||
}
|
||||
|
||||
if (!force && chartFetchPromise) {
|
||||
return chartFetchPromise
|
||||
}
|
||||
|
||||
chartFetchPromise = (async () => {
|
||||
try {
|
||||
const rows = await getChart()
|
||||
if (!Array.isArray(rows)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return rows
|
||||
.map((row) => {
|
||||
if (!row) {
|
||||
return null
|
||||
}
|
||||
|
||||
const dateRaw = Array.isArray(row) ? row[0] : row.date
|
||||
const candle = Array.isArray(row) ? row[1] : row.candle
|
||||
let value = 0
|
||||
|
||||
if (Array.isArray(candle)) {
|
||||
value = Number(candle[3] ?? candle[2] ?? candle[0] ?? 0)
|
||||
} else if (candle && typeof candle === 'object') {
|
||||
const candleRow = candle as { end?: number; high?: number; start?: number }
|
||||
value = Number(candleRow.end ?? candleRow.high ?? candleRow.start ?? 0)
|
||||
} else {
|
||||
value = Number(candle ?? 0)
|
||||
}
|
||||
|
||||
let timestamp = typeof dateRaw === 'number'
|
||||
? dateRaw
|
||||
: new Date(String(dateRaw)).getTime()
|
||||
|
||||
if (timestamp > 0 && timestamp < 1e11) {
|
||||
timestamp *= 1000
|
||||
}
|
||||
|
||||
if (!Number.isFinite(timestamp) || !Number.isFinite(value) || value <= 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return [timestamp, value] as ChartPoint
|
||||
})
|
||||
.filter((point): point is ChartPoint => point !== null)
|
||||
} catch {
|
||||
return []
|
||||
chartLoading.value = true
|
||||
const response = await getAllPrices()
|
||||
const data = resolvePricesMap(response)
|
||||
const selectedData = data?.[chartSelection.value] ?? []
|
||||
rawChartPoints.value = buildChartPoints(selectedData)
|
||||
syncChartSeries()
|
||||
chartInitialized = true
|
||||
} catch (error) {
|
||||
console.error('Error fetching prices:', error)
|
||||
} finally {
|
||||
chartLoading.value = false
|
||||
chartFetchPromise = null
|
||||
}
|
||||
})()
|
||||
|
||||
return chartFetchPromise
|
||||
}
|
||||
|
||||
async function loadMaftolOffset(force = false) {
|
||||
if (getHomeLayoutId() !== 'boursecu') {
|
||||
maftolChartRatio.value = 0
|
||||
syncChartSeries()
|
||||
return
|
||||
}
|
||||
|
||||
if (!force && maftolOffsetLoaded) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!force && maftolOffsetPromise) {
|
||||
return maftolOffsetPromise
|
||||
}
|
||||
|
||||
maftolOffsetPromise = (async () => {
|
||||
try {
|
||||
const products = await getProductsCatalog(force)
|
||||
const katodProduct = findKatodProduct(products)
|
||||
const maftolProduct = findMaftolProduct(products)
|
||||
maftolChartRatio.value = deriveMaftolChartRatio(maftolProduct, katodProduct)
|
||||
syncChartSeries()
|
||||
maftolOffsetLoaded = true
|
||||
} catch (error) {
|
||||
console.error('Error loading maftol chart ratio:', error)
|
||||
maftolChartRatio.value = 0
|
||||
syncChartSeries()
|
||||
} finally {
|
||||
maftolOffsetPromise = null
|
||||
}
|
||||
})()
|
||||
|
||||
return maftolOffsetPromise
|
||||
}
|
||||
|
||||
function updateChartData(timeline: string) {
|
||||
chartSelection.value = timeline
|
||||
void fetchPrices(true)
|
||||
}
|
||||
|
||||
function setChartMetal(metal: ChartMetalType) {
|
||||
selectedChartMetal.value = metal
|
||||
}
|
||||
|
||||
export function usePriceChart() {
|
||||
const priceChartSeries = ref([{ name: 'قیمت', data: [] as ChartPoint[] }])
|
||||
const selection = ref('DAILY')
|
||||
const chartLoading = ref(false)
|
||||
return {
|
||||
priceChartSeries,
|
||||
chartSelection,
|
||||
chartLoading,
|
||||
selectedChartMetal,
|
||||
maftolChartRatio,
|
||||
updateChartData,
|
||||
fetchPrices,
|
||||
loadMaftolOffset,
|
||||
setChartMetal,
|
||||
}
|
||||
}
|
||||
|
||||
const fetchPrices = async () => {
|
||||
try {
|
||||
chartLoading.value = true
|
||||
const response = await getAllPrices()
|
||||
const prices = response?.data?.prices ?? response?.prices
|
||||
export function fetchHomeChartPrices(force = false) {
|
||||
return fetchPrices(force)
|
||||
}
|
||||
|
||||
let points: ChartPoint[] = []
|
||||
if (prices && typeof prices === 'object') {
|
||||
const selectedData = prices[selection.value]
|
||||
points = mapTimelinePoints(Array.isArray(selectedData) ? selectedData : [])
|
||||
}
|
||||
export function loadHomeMaftolOffset(force = false) {
|
||||
return loadMaftolOffset(force)
|
||||
}
|
||||
|
||||
if (points.length === 0) {
|
||||
points = await fetchCandleFallback()
|
||||
}
|
||||
|
||||
priceChartSeries.value = [{ name: 'قیمت', data: points }]
|
||||
} catch (error) {
|
||||
console.error('Error fetching prices:', error)
|
||||
const fallback = await fetchCandleFallback()
|
||||
priceChartSeries.value = [{ name: 'قیمت', data: fallback }]
|
||||
} finally {
|
||||
chartLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const updateChartData = (timeline: string) => {
|
||||
selection.value = timeline
|
||||
fetchPrices()
|
||||
}
|
||||
|
||||
return { priceChartSeries, chartLoading, updateChartData, fetchPrices }
|
||||
export function resetHomePriceChart() {
|
||||
priceChartSeries.value = [{ name: 'قیمت', data: [] }]
|
||||
rawChartPoints.value = []
|
||||
chartSelection.value = 'DAILY'
|
||||
selectedChartMetal.value = 'katod'
|
||||
maftolChartRatio.value = 0
|
||||
chartInitialized = false
|
||||
chartFetchPromise = null
|
||||
maftolOffsetLoaded = false
|
||||
maftolOffsetPromise = null
|
||||
}
|
||||
|
||||
@@ -1,22 +1,54 @@
|
||||
import { ref } from 'vue'
|
||||
import { useFundUserStore } from '@/store/fundStore'
|
||||
|
||||
export function useUserFunds() {
|
||||
const funds = ref([])
|
||||
const loading = ref(false)
|
||||
const fundStore = useFundUserStore()
|
||||
/** Module-level fund list — shared across home layout variants. */
|
||||
const funds = ref<unknown[]>([])
|
||||
const fundsLoading = ref(false)
|
||||
let fundsPromise: Promise<void> | null = null
|
||||
let fundsLoaded = false
|
||||
|
||||
const loadFunds = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const result = await fundStore.getMyFund()
|
||||
funds.value = result?.rows || []
|
||||
} catch (error) {
|
||||
console.error('Error loading funds:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
async function fetchFunds(force = false) {
|
||||
if (!force && fundsLoaded) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!force && fundsPromise) {
|
||||
return fundsPromise
|
||||
}
|
||||
|
||||
const fundStore = useFundUserStore()
|
||||
|
||||
fundsPromise = (async () => {
|
||||
try {
|
||||
fundsLoading.value = true
|
||||
const result = await fundStore.getMyFund()
|
||||
funds.value = result?.rows || []
|
||||
fundsLoaded = true
|
||||
} catch (error) {
|
||||
console.error('Error loading funds:', error)
|
||||
} finally {
|
||||
fundsLoading.value = false
|
||||
fundsPromise = null
|
||||
}
|
||||
})()
|
||||
|
||||
return { funds, loading, loadFunds }
|
||||
}
|
||||
return fundsPromise
|
||||
}
|
||||
|
||||
export function useUserFunds() {
|
||||
return {
|
||||
funds,
|
||||
loading: fundsLoading,
|
||||
loadFunds: fetchFunds,
|
||||
}
|
||||
}
|
||||
|
||||
export function loadHomeFunds(force = false) {
|
||||
return fetchFunds(force)
|
||||
}
|
||||
|
||||
export function resetHomeFunds() {
|
||||
funds.value = []
|
||||
fundsLoaded = false
|
||||
fundsPromise = null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<BoursecuDesktopHome v-if="layoutId === 'boursecu'" @logout="$emit('logout')" />
|
||||
<DefaultDesktopHome v-else @logout="$emit('logout')" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { getHomeLayoutId } from '../layouts/registry'
|
||||
import BoursecuDesktopHome from './layouts/BoursecuDesktopHome.vue'
|
||||
import DefaultDesktopHome from './layouts/DefaultDesktopHome.vue'
|
||||
|
||||
defineEmits<{
|
||||
logout: []
|
||||
}>()
|
||||
|
||||
const layoutId = computed(() => getHomeLayoutId())
|
||||
</script>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import AssetsSection from '../../components/AssetsSection.vue'
|
||||
import DesktopHomeCard from './DesktopHomeCard.vue'
|
||||
import type { WalletMetalTotal } from '@/models/walletAssets'
|
||||
|
||||
defineEmits<{
|
||||
openGoldModal: [metal: WalletMetalTotal]
|
||||
openRialModal: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DesktopHomeCard title="داراییهای من">
|
||||
<AssetsSection
|
||||
plain
|
||||
class="home-desktop-assets"
|
||||
@open-gold-modal="$emit('openGoldModal', $event)"
|
||||
@open-rial-modal="$emit('openRialModal')"
|
||||
/>
|
||||
</DesktopHomeCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.home-desktop-assets) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
:deep(.home-desktop-assets > div) {
|
||||
margin-top: 0 !important;
|
||||
justify-content: flex-start !important;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.75rem;
|
||||
background: #eff5f3;
|
||||
border: 1px solid #d4e3de;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,128 @@
|
||||
<script setup lang="ts">
|
||||
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 { buildBoursecuHomeAssetRows, resolveSummaryMetalWeight } from '@/utils/productAssets'
|
||||
import { useHomePageContext } from '../../composables/useHomePage'
|
||||
|
||||
const emit = defineEmits<{
|
||||
openGoldModal: [metal: WalletMetalTotal]
|
||||
openRialModal: []
|
||||
}>()
|
||||
|
||||
const { assetsSummary } = useWalletAssets()
|
||||
const ctx = useHomePageContext()
|
||||
const isBoursecuLayout = computed(() => ctx.isBoursecuHomeLayout.value)
|
||||
|
||||
const meta = computed(() => assetsSummary.value?.meta)
|
||||
const metalTotals = computed(() => assetsSummary.value?.metal_totals ?? [])
|
||||
const inventoryCards = computed(() => assetsSummary.value?.inventory_cards ?? [])
|
||||
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),
|
||||
)
|
||||
|
||||
type MetricItem = {
|
||||
id: string
|
||||
label: string
|
||||
value: string
|
||||
metal?: WalletMetalTotal
|
||||
showInfo?: boolean
|
||||
onInfo?: 'gold' | 'rial'
|
||||
}
|
||||
|
||||
const metrics = computed<MetricItem[]>(() => {
|
||||
const items: MetricItem[] = []
|
||||
|
||||
if (meta.value?.has_count_based_products) {
|
||||
items.push({
|
||||
id: 'products',
|
||||
label: 'محصولات',
|
||||
value: `${countBasedInventory.value} عدد`,
|
||||
})
|
||||
}
|
||||
|
||||
if (isBoursecuLayout.value) {
|
||||
buildBoursecuHomeAssetRows(assetsSummary.value).forEach((row) => {
|
||||
items.push({
|
||||
id: row.id,
|
||||
label: row.label,
|
||||
value: row.value,
|
||||
metal: row.metal,
|
||||
showInfo: row.info !== 'none',
|
||||
onInfo: row.info === 'rial' ? 'rial' : 'gold',
|
||||
})
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
if (meta.value?.single_metal && metalTotals.value[0]) {
|
||||
const metal = metalTotals.value[0]
|
||||
items.push({
|
||||
id: `metal-${metal.metal_id}`,
|
||||
label: metal.label,
|
||||
value: formatMetalTotal(metal),
|
||||
metal,
|
||||
showInfo: true,
|
||||
onInfo: 'gold',
|
||||
})
|
||||
} else {
|
||||
metalTotals.value.forEach((metal, index) => {
|
||||
items.push({
|
||||
id: `metal-${metal.metal_id}`,
|
||||
label: metal.label,
|
||||
value: formatMetalTotal(metal),
|
||||
metal,
|
||||
showInfo: index === 0,
|
||||
onInfo: 'gold',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
items.push({
|
||||
id: 'rial',
|
||||
label: 'دارایی ریالی',
|
||||
value: `${usePriceWithoutType(totalRialValue.value)} ریال`,
|
||||
showInfo: true,
|
||||
onInfo: 'rial',
|
||||
})
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
function formatMetalTotal(metal: WalletMetalTotal) {
|
||||
return `${resolveSummaryMetalWeight(metal, inventoryCards.value)}${metal.unit}`
|
||||
}
|
||||
|
||||
function handleInfo(metric: MetricItem) {
|
||||
if (metric.onInfo === 'rial') {
|
||||
emit('openRialModal')
|
||||
return
|
||||
}
|
||||
if (metric.metal) {
|
||||
emit('openGoldModal', metric.metal)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dhome-metrics">
|
||||
<article v-for="metric in metrics" :key="metric.id" class="dhome-metric-tile">
|
||||
<div class="dhome-metric-tile__head">
|
||||
<span class="dhome-metric-tile__label">{{ metric.label }}</span>
|
||||
<button
|
||||
v-if="metric.showInfo"
|
||||
type="button"
|
||||
class="dhome-metric-tile__info"
|
||||
aria-label="اطلاعات بیشتر"
|
||||
@click="handleInfo(metric)"
|
||||
>
|
||||
<BasicIcon icon="vuesax-linear:info-circle" size="14" />
|
||||
</button>
|
||||
</div>
|
||||
<p class="dhome-metric-tile__value">{{ metric.value }}</p>
|
||||
</article>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import DesktopHomeSection from './DesktopHomeSection.vue'
|
||||
import BannerSlider from '../../components/BannerSlider.vue'
|
||||
import { HOME_PAGE_CONTEXT_KEY } from '../../composables/useHomePage'
|
||||
|
||||
const ctx = inject(HOME_PAGE_CONTEXT_KEY)!
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DesktopHomeSection
|
||||
v-if="ctx.isGaroosi.value && ctx.banners.value?.length"
|
||||
title="بنرها"
|
||||
>
|
||||
<BannerSlider
|
||||
:banners="ctx.banners.value"
|
||||
@bannerClick="ctx.handleBannerClick"
|
||||
/>
|
||||
</DesktopHomeSection>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, toValue } from 'vue'
|
||||
import { useHomePageContext } from '../../composables/useHomePage'
|
||||
import PriceChart from '../../components/PriceChart.vue'
|
||||
import DesktopHomeCard from '../components/DesktopHomeCard.vue'
|
||||
|
||||
const ctx = useHomePageContext()
|
||||
|
||||
const chartLayout = computed(() => (toValue(ctx.isBoursecuHomeLayout) ? 'boursecu' : 'default'))
|
||||
const priceChartSeries = computed(() => toValue(ctx.priceChartSeries))
|
||||
const chartSelection = computed(() => toValue(ctx.chartSelection))
|
||||
const selectedChartMetal = computed(() => toValue(ctx.selectedChartMetal))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DesktopHomeCard title="نمودار قیمت">
|
||||
<PriceChart
|
||||
variant="desktop"
|
||||
:price-chart-series="priceChartSeries"
|
||||
:layout="chartLayout"
|
||||
:chart-selection="chartSelection"
|
||||
:selected-metal="selectedChartMetal"
|
||||
@update-data="ctx.updateChartData"
|
||||
@update:selected-metal="ctx.setChartMetal"
|
||||
/>
|
||||
</DesktopHomeCard>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, toValue } from 'vue'
|
||||
import { useHomePageContext } from '../../composables/useHomePage'
|
||||
import PriceChart from '../../components/PriceChart.vue'
|
||||
import DesktopHomeSection from './DesktopHomeSection.vue'
|
||||
|
||||
const ctx = useHomePageContext()
|
||||
|
||||
const chartLayout = computed(() => (toValue(ctx.isBoursecuHomeLayout) ? 'boursecu' : 'default'))
|
||||
const priceChartSeries = computed(() => toValue(ctx.priceChartSeries))
|
||||
const chartSelection = computed(() => toValue(ctx.chartSelection))
|
||||
const selectedChartMetal = computed(() => toValue(ctx.selectedChartMetal))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DesktopHomeSection title="نمودار قیمت" class="dhome-chart">
|
||||
<PriceChart
|
||||
variant="desktop"
|
||||
:price-chart-series="priceChartSeries"
|
||||
:layout="chartLayout"
|
||||
:chart-selection="chartSelection"
|
||||
:selected-metal="selectedChartMetal"
|
||||
@update-data="ctx.updateChartData"
|
||||
@update:selected-metal="ctx.setChartMetal"
|
||||
/>
|
||||
</DesktopHomeSection>
|
||||
</template>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { toValue } from 'vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import DesktopHomeSection from './DesktopHomeSection.vue'
|
||||
import { useHomePageContext } from '../../composables/useHomePage'
|
||||
import type { HomeActionItem } from '../../layouts/types'
|
||||
|
||||
const ctx = useHomePageContext()
|
||||
|
||||
function runAction(action: HomeActionItem) {
|
||||
action.handler()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DesktopHomeSection title="عملیات">
|
||||
<div class="dhome-default-actions">
|
||||
<button
|
||||
v-for="action in toValue(ctx.mainActions)"
|
||||
:key="action.name"
|
||||
type="button"
|
||||
class="dhome-default-action"
|
||||
@click="runAction(action)"
|
||||
>
|
||||
<span class="dhome-default-action__icon">
|
||||
<BasicIcon :icon="action.icon" size="20" />
|
||||
</span>
|
||||
<span class="dhome-default-action__label">{{ action.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</DesktopHomeSection>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { usePriceWithoutType } from '@/utils/usePrice'
|
||||
import DesktopHomeSection from './DesktopHomeSection.vue'
|
||||
import { HOME_PAGE_CONTEXT_KEY } from '../../composables/useHomePage'
|
||||
|
||||
const ctx = inject(HOME_PAGE_CONTEXT_KEY)!
|
||||
const userStore = useUserStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DesktopHomeSection v-if="ctx.isGaroosi.value" title="داراییها">
|
||||
<div class="dhome-garoosi-assets">
|
||||
<div class="dhome-garoosi-assets__item">
|
||||
<span class="dhome-garoosi-assets__label">دارایی طلایی</span>
|
||||
<span class="dhome-garoosi-assets__value">
|
||||
{{ (userStore?.userInfo?.wallet?.count_gold ?? 0).toFixed(3) }} گرم
|
||||
</span>
|
||||
</div>
|
||||
<div class="dhome-garoosi-assets__divider" />
|
||||
<div class="dhome-garoosi-assets__item">
|
||||
<span class="dhome-garoosi-assets__label">دارایی ریالی</span>
|
||||
<span class="dhome-garoosi-assets__value">
|
||||
{{ usePriceWithoutType(userStore?.userInfo?.wallet?.count_cash ?? 0) }} ریال
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DesktopHomeSection>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { usePriceWithoutType } from '@/utils/usePrice'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { HOME_PAGE_CONTEXT_KEY } from '../../composables/useHomePage'
|
||||
|
||||
const ctx = inject(HOME_PAGE_CONTEXT_KEY)!
|
||||
const userStore = useUserStore()
|
||||
|
||||
const showLimitedPrice = computed(
|
||||
() =>
|
||||
ctx.limitedPrice.value !== undefined &&
|
||||
userStore.userInfo?.trade_active !== 1,
|
||||
)
|
||||
|
||||
const limitedPriceText = computed(() =>
|
||||
usePriceWithoutType(Number(ctx.limitedPrice.value?.value ?? 0)),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="showLimitedPrice || ctx.showClosedMessage.value" class="dhome-alerts">
|
||||
<div v-if="showLimitedPrice" class="dhome-alert dhome-alert--warning">
|
||||
<BasicIcon icon="vuesax-linear:info-circle" size="14" />
|
||||
<span>
|
||||
حداقل مبلغ اولیه برای ثبت سفارش {{ limitedPriceText }} ریال میباشد.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="ctx.showClosedMessage.value"
|
||||
class="dhome-alert dhome-alert--info"
|
||||
v-html="ctx.closedMessage.value"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<section class="rounded-xl border border-[#d4e3de] bg-white p-5 shadow-sm">
|
||||
<h2 v-if="title" class="mb-4 text-base font-bold text-primary">{{ title }}</h2>
|
||||
<slot />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
title?: string
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<header class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">خوش آمدید</p>
|
||||
<h1 class="text-xl font-bold text-gray-900">
|
||||
{{ userStore.userInfo?.name ?? 'کاربر' }}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-2 rounded-lg border border-[#d4e3de] bg-[#eff5f3] px-4 py-2 text-sm font-bold text-primary transition hover:bg-[#e4efea]"
|
||||
@click="router.push({ name: 'user' })"
|
||||
>
|
||||
<BasicIcon icon="vuesax-linear:user" size="18" />
|
||||
<span>حساب کاربری</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="showLogout"
|
||||
type="button"
|
||||
class="flex items-center gap-2 rounded-lg border border-gray-200 px-4 py-2 text-sm font-bold text-gray-600 transition hover:bg-gray-50"
|
||||
@click="$emit('logout')"
|
||||
>
|
||||
<BasicIcon icon="vuesax-linear:logout" size="18" />
|
||||
<span>خروج</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import router from '@/router'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
showLogout?: boolean
|
||||
}>(),
|
||||
{ showLogout: true },
|
||||
)
|
||||
|
||||
defineEmits<{
|
||||
logout: []
|
||||
}>()
|
||||
|
||||
const userStore = useUserStore()
|
||||
</script>
|
||||
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<section class="dhome-surface">
|
||||
<header v-if="title" class="dhome-section__header">
|
||||
<h2 class="dhome-section__title">{{ title }}</h2>
|
||||
<slot name="header-extra" />
|
||||
</header>
|
||||
<slot />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
title?: string
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="dhome" :class="{ 'dhome--boursecu': isBoursecu }">
|
||||
<div class="dhome__container">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { isBoursecuProject } from '@/utils/config'
|
||||
|
||||
const isBoursecu = isBoursecuProject()
|
||||
</script>
|
||||
|
||||
<style lang="less" src="../styles/desktop-home.less"></style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<header class="dhome-topbar">
|
||||
<div>
|
||||
<p class="dhome-topbar__eyebrow">نمای کلی</p>
|
||||
<h1 class="dhome-topbar__title">{{ userStore.userInfo?.name ?? 'کاربر' }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="dhome-topbar__actions">
|
||||
<button type="button" class="dhome-btn-ghost" @click="router.push({ name: 'user' })">
|
||||
<BasicIcon icon="vuesax-linear:user" size="16" />
|
||||
<span>حساب کاربری</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="showLogout"
|
||||
type="button"
|
||||
class="dhome-btn-ghost"
|
||||
@click="$emit('logout')"
|
||||
>
|
||||
<BasicIcon icon="vuesax-linear:logout" size="16" />
|
||||
<span>خروج</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import router from '@/router'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
showLogout?: boolean
|
||||
}>(),
|
||||
{ showLogout: true },
|
||||
)
|
||||
|
||||
defineEmits<{
|
||||
logout: []
|
||||
}>()
|
||||
|
||||
const userStore = useUserStore()
|
||||
</script>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="dhome-surface dhome-hero-price">
|
||||
<span class="dhome-label">قیمت لحظهای</span>
|
||||
<p class="dhome-hero-price__brand">{{ config.config?.name_fa }}</p>
|
||||
<div class="dhome-hero-price__value-row">
|
||||
<span
|
||||
v-if="percentChange !== 0"
|
||||
class="dhome-hero-price__delta"
|
||||
:class="percentChange >= 0 ? 'dhome-hero-price__delta--up' : 'dhome-hero-price__delta--down'"
|
||||
>
|
||||
{{ useTowDecimal(percentChange) }}%
|
||||
</span>
|
||||
<Vue3Odometer
|
||||
:value="displayGoldVal"
|
||||
class="dhome-hero-price__odometer o-text"
|
||||
style="font-family: var(--App-font-family)"
|
||||
/>
|
||||
<span class="dhome-hero-price__unit">ریال</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import Vue3Odometer from 'vue3-odometer'
|
||||
import 'odometer/themes/odometer-theme-default.css'
|
||||
import { useConfigStore } from '@/store/configStore'
|
||||
import { getTradeBuyPrice, lastPrice, percentPriceChanged } from '@/utils/price'
|
||||
import { useTowDecimal } from '@/utils/usePrice'
|
||||
|
||||
const config = useConfigStore()
|
||||
|
||||
const isMarketClosed = computed(
|
||||
() => lastPrice?.value?.goldir === 0 && getTradeBuyPrice(lastPrice.value) === 0,
|
||||
)
|
||||
|
||||
const displayGoldVal = computed(() => {
|
||||
if (isMarketClosed.value) return 0
|
||||
return getTradeBuyPrice(lastPrice.value)
|
||||
})
|
||||
|
||||
const percentChange = computed(() => {
|
||||
if (!getTradeBuyPrice(lastPrice.value)) return 0
|
||||
return percentPriceChanged()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, toValue } from 'vue'
|
||||
import { useHomePageContext } from '../../composables/useHomePage'
|
||||
import { price } from '@/utils/price'
|
||||
import CurrentPrice from '../../components/CurrentPrice.vue'
|
||||
import PriceStats from '../../components/PriceStats.vue'
|
||||
import DesktopHomeCard from './DesktopHomeCard.vue'
|
||||
|
||||
const ctx = useHomePageContext()
|
||||
const statsLayout = computed(() => (toValue(ctx.isBoursecuHomeLayout) ? 'boursecu' : 'default'))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<DesktopHomeCard title="قیمت لحظهای">
|
||||
<CurrentPrice class="home-desktop-current-price" />
|
||||
</DesktopHomeCard>
|
||||
|
||||
<DesktopHomeCard title="بازه قیمت امروز">
|
||||
<PriceStats variant="desktop" :price="price" :layout="statsLayout" />
|
||||
</DesktopHomeCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.home-desktop-current-price) {
|
||||
margin-top: 0;
|
||||
border: none;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { price } from '@/utils/price'
|
||||
import { usePriceWithoutType } from '@/utils/usePrice'
|
||||
import { getHomeLayoutId } from '../../layouts/registry'
|
||||
import { resolveMetalDayRangePrices, usePriceChart } from '../../composables/usePriceChart'
|
||||
|
||||
const { selectedChartMetal, maftolChartRatio } = usePriceChart()
|
||||
const isBoursecuLayout = computed(() => getHomeLayoutId() === 'boursecu')
|
||||
|
||||
const rangePrices = computed(() =>
|
||||
resolveMetalDayRangePrices({
|
||||
price: price.value,
|
||||
metal: selectedChartMetal.value,
|
||||
isBoursecuLayout: isBoursecuLayout.value,
|
||||
maftolRatio: maftolChartRatio.value,
|
||||
})
|
||||
)
|
||||
|
||||
const highPrice = computed(() => rangePrices.value.high)
|
||||
const lowPrice = computed(() => rangePrices.value.low)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dhome-surface dhome-price-range">
|
||||
<div class="dhome-price-range__item">
|
||||
<span class="dhome-label">بیشترین امروز</span>
|
||||
<span class="dhome-price-range__value dhome-price-range__value--high">
|
||||
{{ usePriceWithoutType(highPrice) }}
|
||||
<span class="text-xs font-medium text-gray-400 mr-1">ریال</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="dhome-price-range__divider" />
|
||||
<div class="dhome-price-range__item">
|
||||
<span class="dhome-label">کمترین امروز</span>
|
||||
<span class="dhome-price-range__value dhome-price-range__value--low">
|
||||
{{ usePriceWithoutType(lowPrice) }}
|
||||
<span class="text-xs font-medium text-gray-400 mr-1">ریال</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import router from '@/router'
|
||||
import HomeQuickActionBox from '../../components/HomeQuickActionBox.vue'
|
||||
import DesktopHomeCard from './DesktopHomeCard.vue'
|
||||
import { boursecuQuickActions } from '../../layouts/configs/boursecuQuickActions'
|
||||
import type { HomeQuickActionItem } from '../../layouts/types'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
actions?: HomeQuickActionItem[]
|
||||
}>(),
|
||||
{
|
||||
actions: () => boursecuQuickActions,
|
||||
},
|
||||
)
|
||||
|
||||
function handleClick(action: HomeQuickActionItem) {
|
||||
router.push(action.route)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DesktopHomeCard title="دسترسی سریع">
|
||||
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 xl:grid-cols-6">
|
||||
<HomeQuickActionBox
|
||||
v-for="action in props.actions"
|
||||
:key="action.id"
|
||||
:action="action"
|
||||
class="home-desktop-quick-action"
|
||||
@click="handleClick(action)"
|
||||
/>
|
||||
</div>
|
||||
</DesktopHomeCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.home-desktop-quick-action) {
|
||||
min-height: 108px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import router from '@/router'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import DesktopHomeSection from './DesktopHomeSection.vue'
|
||||
import { boursecuQuickActions } from '../../layouts/configs/boursecuQuickActions'
|
||||
import type { HomeQuickActionItem } from '../../layouts/types'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
actions?: HomeQuickActionItem[]
|
||||
title?: string
|
||||
}>(),
|
||||
{
|
||||
actions: () => boursecuQuickActions,
|
||||
title: 'میانبرها',
|
||||
},
|
||||
)
|
||||
|
||||
function navigate(action: HomeQuickActionItem) {
|
||||
router.push(action.route)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DesktopHomeSection :title="title">
|
||||
<nav class="dhome-actions" aria-label="میانبرهای صفحه اصلی">
|
||||
<button
|
||||
v-for="action in props.actions"
|
||||
:key="action.id"
|
||||
type="button"
|
||||
class="dhome-action"
|
||||
@click="navigate(action)"
|
||||
>
|
||||
<span class="dhome-action__icon">
|
||||
<BasicIcon :icon="action.icon" size="18" />
|
||||
</span>
|
||||
<span class="dhome-action__label">{{ action.label }}</span>
|
||||
<BasicIcon icon="vuesax-linear:arrow-left" size="14" class="dhome-action__chevron" />
|
||||
</button>
|
||||
</nav>
|
||||
</DesktopHomeSection>
|
||||
</template>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import DesktopHomeSection from './DesktopHomeSection.vue'
|
||||
import ReferralSection from '../../components/ReferralSection.vue'
|
||||
import { HOME_PAGE_CONTEXT_KEY } from '../../composables/useHomePage'
|
||||
|
||||
const ctx = inject(HOME_PAGE_CONTEXT_KEY)!
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DesktopHomeSection v-if="ctx.isGaroosi.value" title="دعوت از دوستان">
|
||||
<ReferralSection />
|
||||
</DesktopHomeSection>
|
||||
</template>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { loading } from '@/utils'
|
||||
import { useHomePageContext } from '../../composables/useHomePage'
|
||||
import HomeSkeleton from '../../components/HomeSkeleton.vue'
|
||||
import DesktopHomeShell from '../components/DesktopHomeShell.vue'
|
||||
import DesktopHomeTopbar from '../components/DesktopHomeTopbar.vue'
|
||||
import DesktopHomeAlerts from '../components/DesktopHomeAlerts.vue'
|
||||
import DesktopLivePrice from '../components/DesktopLivePrice.vue'
|
||||
import DesktopPriceRange from '../components/DesktopPriceRange.vue'
|
||||
import DesktopAssetsMetrics from '../components/DesktopAssetsMetrics.vue'
|
||||
import DesktopChartSection from '../components/DesktopChartSection.vue'
|
||||
import DesktopQuickActionsPanel from '../components/DesktopQuickActionsPanel.vue'
|
||||
|
||||
defineEmits<{
|
||||
logout: []
|
||||
}>()
|
||||
|
||||
const userStore = useUserStore()
|
||||
const ctx = useHomePageContext()
|
||||
|
||||
const showSkeleton = computed(() => loading.value || !userStore.userInfo)
|
||||
const showContent = computed(() => !loading.value && Boolean(userStore.userInfo))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DesktopHomeShell>
|
||||
<DesktopHomeTopbar @logout="$emit('logout')" />
|
||||
<DesktopHomeAlerts />
|
||||
|
||||
<HomeSkeleton v-if="showSkeleton" />
|
||||
|
||||
<template v-else-if="showContent">
|
||||
<section class="dhome-hero-row">
|
||||
<DesktopLivePrice />
|
||||
<DesktopPriceRange />
|
||||
</section>
|
||||
|
||||
<DesktopAssetsMetrics
|
||||
@open-gold-modal="ctx.openGoldModal"
|
||||
@open-rial-modal="ctx.openRialModal"
|
||||
/>
|
||||
|
||||
<section class="dhome-main-grid">
|
||||
<DesktopChartSection />
|
||||
<DesktopQuickActionsPanel />
|
||||
</section>
|
||||
</template>
|
||||
</DesktopHomeShell>
|
||||
</template>
|
||||
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { loading } from '@/utils'
|
||||
import { useHomePageContext } from '../../composables/useHomePage'
|
||||
import HomeSkeleton from '../../components/HomeSkeleton.vue'
|
||||
import DesktopHomeShell from '../components/DesktopHomeShell.vue'
|
||||
import DesktopHomeTopbar from '../components/DesktopHomeTopbar.vue'
|
||||
import DesktopHomeAlerts from '../components/DesktopHomeAlerts.vue'
|
||||
import DesktopLivePrice from '../components/DesktopLivePrice.vue'
|
||||
import DesktopPriceRange from '../components/DesktopPriceRange.vue'
|
||||
import DesktopAssetsMetrics from '../components/DesktopAssetsMetrics.vue'
|
||||
import DesktopChartSection from '../components/DesktopChartSection.vue'
|
||||
import DesktopDefaultActions from '../components/DesktopDefaultActions.vue'
|
||||
import DesktopGaroosiAssets from '../components/DesktopGaroosiAssets.vue'
|
||||
import DesktopReferralPanel from '../components/DesktopReferralPanel.vue'
|
||||
import DesktopBannersPanel from '../components/DesktopBannersPanel.vue'
|
||||
|
||||
defineEmits<{
|
||||
logout: []
|
||||
}>()
|
||||
|
||||
const userStore = useUserStore()
|
||||
const ctx = useHomePageContext()
|
||||
|
||||
const showSkeleton = computed(() => loading.value || !userStore.userInfo)
|
||||
const showContent = computed(() => !loading.value && Boolean(userStore.userInfo))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DesktopHomeShell>
|
||||
<DesktopHomeTopbar @logout="$emit('logout')" />
|
||||
<DesktopHomeAlerts />
|
||||
|
||||
<HomeSkeleton v-if="showSkeleton" />
|
||||
|
||||
<template v-else-if="showContent">
|
||||
<section class="dhome-hero-row">
|
||||
<DesktopLivePrice />
|
||||
<DesktopPriceRange />
|
||||
</section>
|
||||
|
||||
<DesktopAssetsMetrics
|
||||
v-if="!ctx.isGaroosi.value"
|
||||
@open-gold-modal="ctx.openGoldModal"
|
||||
@open-rial-modal="ctx.openRialModal"
|
||||
/>
|
||||
|
||||
<div v-if="ctx.isGaroosi.value" class="dhome-default-grid">
|
||||
<div class="dhome-default-grid__main">
|
||||
<DesktopDefaultActions />
|
||||
<DesktopBannersPanel />
|
||||
<DesktopChartSection />
|
||||
</div>
|
||||
|
||||
<aside class="dhome-default-grid__side">
|
||||
<DesktopGaroosiAssets />
|
||||
<DesktopReferralPanel />
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<DesktopDefaultActions />
|
||||
<DesktopChartSection />
|
||||
</template>
|
||||
</template>
|
||||
</DesktopHomeShell>
|
||||
</template>
|
||||
@@ -0,0 +1,516 @@
|
||||
/* Premium desktop home — SaaS dashboard design tokens */
|
||||
|
||||
.dhome {
|
||||
min-height: 100%;
|
||||
background: #f6f7f9;
|
||||
color: #111827;
|
||||
font-feature-settings: 'ss01' on;
|
||||
}
|
||||
|
||||
.dhome__container {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
padding: 28px 36px 40px;
|
||||
}
|
||||
|
||||
.dhome-label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
color: #9ca3af;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.dhome-surface {
|
||||
background: #ffffff;
|
||||
border-radius: 14px;
|
||||
padding: 22px 24px;
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.home-referral {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dhome-section {
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Topbar */
|
||||
.dhome-topbar {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
margin-bottom: 28px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
|
||||
|
||||
&__eyebrow {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #9ca3af;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.03em;
|
||||
color: #111827;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.dhome-btn-ghost {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s ease, background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: #111827;
|
||||
background: rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.dhome-alerts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.dhome-alert {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
|
||||
&--warning {
|
||||
background: #fef3f2;
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
&--info {
|
||||
background: #f0f4ff;
|
||||
color: #344054;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hero price */
|
||||
.dhome-hero-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 300px;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dhome-hero-price {
|
||||
&__brand {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
&__value-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&__odometer {
|
||||
font-size: 32px !important;
|
||||
font-weight: 600 !important;
|
||||
letter-spacing: -0.03em;
|
||||
color: #111827;
|
||||
direction: ltr;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__unit {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
&__delta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
direction: ltr;
|
||||
|
||||
&--up {
|
||||
background: #ecfdf3;
|
||||
color: #027a48;
|
||||
}
|
||||
|
||||
&--down {
|
||||
background: #fef3f2;
|
||||
color: #b42318;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dhome-price-range {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
direction: ltr;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&__value--high {
|
||||
color: #027a48;
|
||||
}
|
||||
|
||||
&__value--low {
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
&__divider {
|
||||
height: 1px;
|
||||
background: rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
}
|
||||
|
||||
/* Asset metrics */
|
||||
.dhome-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(148px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dhome-metric-tile {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 16px 18px;
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
transition: box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
}
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
color: #111827;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
|
||||
/* Main dashboard grid */
|
||||
.dhome-main-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 272px;
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.dhome-chart {
|
||||
:deep(.bg-gray-50) {
|
||||
background: transparent !important;
|
||||
border-radius: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.w-full.text-center.pt-2) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
:deep(.chart-metal-segment) {
|
||||
display: inline-flex;
|
||||
background: #f3f4f6;
|
||||
border-radius: 8px;
|
||||
padding: 3px;
|
||||
margin-bottom: 0 !important;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:deep(.chart-metal-segment__item) {
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.chart-timeline-group) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
:deep(.chart-timeline-group .ant-radio-button-wrapper) {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Quick actions */
|
||||
.dhome-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.dhome-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 11px 10px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
text-align: right;
|
||||
font-family: inherit;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
background: #f3f4f6;
|
||||
color: @App-primary-color;
|
||||
flex-shrink: 0;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
&:hover &__icon {
|
||||
background: #eef2f6;
|
||||
}
|
||||
|
||||
&__label {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
&__chevron {
|
||||
color: #d1d5db;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Default layout grid */
|
||||
.dhome-default-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 300px;
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.dhome-default-grid__main,
|
||||
.dhome-default-grid__side {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dhome-default-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dhome-default-action {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 18px 12px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: #f9fafb;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: background 0.15s ease, transform 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f3f4f6;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
color: @App-primary-color;
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
/* Garoosi assets strip */
|
||||
.dhome-garoosi-assets {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
color: #111827;
|
||||
direction: ltr;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&__divider {
|
||||
height: 1px;
|
||||
background: rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
}
|
||||
|
||||
/* Boursecu subtle accent */
|
||||
.dhome--boursecu {
|
||||
.dhome-action__icon {
|
||||
background: rgba(19, 48, 80, 0.06);
|
||||
color: #133050;
|
||||
}
|
||||
|
||||
.dhome-hero-price__odometer {
|
||||
color: #133050;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.dhome__container {
|
||||
padding: 24px 28px 36px;
|
||||
}
|
||||
|
||||
.dhome-hero-row,
|
||||
.dhome-main-grid,
|
||||
.dhome-default-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dhome-default-actions {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
+32
-277
@@ -1,296 +1,51 @@
|
||||
<template>
|
||||
<div class="mt-10 p-5">
|
||||
<div v-if="userStore.userInfo && !loading">
|
||||
<!-- هشدار حداقل مبلغ -->
|
||||
<div
|
||||
v-if="limitedPrice !== undefined && userStore?.userInfo.trade_active !== 1"
|
||||
class="text-red text-xs flex gap-1 items-center justify-center mb-2"
|
||||
>
|
||||
<Icon icon="vuesax-linear:info-circle" size="16"/>
|
||||
حداقل مبلغ اولیه برای ثبت سفارش {{ usePriceWithoutType(limitedPrice.value as number ?? 0) }} ریال می باشد.
|
||||
</div>
|
||||
<div class="home-page-root">
|
||||
<HomePageShell @logout="showLogoutConfirm = true" />
|
||||
|
||||
<!-- بخش اصلی -->
|
||||
<div class="p-1 from-primary rounded-lg p-px to-transparent" :class="isGaroosi ? 'mt-0' : 'mt-5'">
|
||||
<!-- داراییها (فقط گاروسی) -->
|
||||
<div v-if="isGaroosi" class="flex justify-between px-2 mb-2">
|
||||
<AssetItem label="دارایی طلایی:" :value="`${(userStore?.userInfo?.wallet?.count_gold ?? 0).toFixed(3)} گرم`"/>
|
||||
<AssetItem label="دارایی ریالی:"
|
||||
:value="`${usePriceWithoutType(userStore?.userInfo?.wallet?.count_cash ?? 0)} ریال`"/>
|
||||
</div>
|
||||
<LogoutConfirmDialog v-model:show="showLogoutConfirm" @confirm="logout" />
|
||||
|
||||
<!-- دکمههای اصلی -->
|
||||
<div class="p-2 py-3 rounded-lg bg-tertiary">
|
||||
<div class="flex justify-between">
|
||||
<ActionButton
|
||||
v-for="action in mainActions"
|
||||
:key="action.name"
|
||||
:action="action"
|
||||
@click="action.handler"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<BalanceSheet
|
||||
v-model:show="wholeBalanceVisible"
|
||||
:wallet-info="walletStore.walletInfo"
|
||||
:gold-val="goldVal"
|
||||
/>
|
||||
|
||||
<!-- بخش کد معرف (گاروسی) -->
|
||||
<ReferralSection v-if="isGaroosi"/>
|
||||
|
||||
<!-- بخش داراییها (سایر پروژهها) -->
|
||||
<AssetsSection v-else @openGoldModal="openGoldModal" @openRialModal="openRialModal"/>
|
||||
|
||||
<!-- پیام بسته بودن بازار -->
|
||||
<MarketClosedMessage v-if="showClosedMessage" :message="marketClosedText"/>
|
||||
|
||||
<!-- قیمت فعلی -->
|
||||
<CurrentPrice/>
|
||||
|
||||
<!-- بنرها -->
|
||||
<BannerSlider v-if="isGaroosi && banners?.length" :banners="banners" @bannerClick="handleBannerClick"/>
|
||||
|
||||
<!-- نمودار قیمت -->
|
||||
<PriceChart @updateData="updateChartData" :price-chart-series="priceChartSeries"/>
|
||||
|
||||
<!-- آمار بالا و پایین -->
|
||||
<PriceStats :price="price"/>
|
||||
</div>
|
||||
|
||||
<!-- اسکلت لودینگ -->
|
||||
<HomeSkeleton v-if="loading" class="w-full"/>
|
||||
</div>
|
||||
|
||||
<!-- هدر -->
|
||||
<AppHeader @logout="showLogoutConfirm = true"/>
|
||||
|
||||
<!-- دیالوگ خروج -->
|
||||
<LogoutConfirmDialog v-model:show="showLogoutConfirm" @confirm="logout"/>
|
||||
|
||||
<!-- شیت موجودی -->
|
||||
<BalanceSheet v-model:show="wholeBalanceVisible" :walletInfo="walletStore.walletInfo" :goldVal="goldVal"/>
|
||||
|
||||
<!-- مودالها -->
|
||||
<BasicModal v-model:visible="showTextBanner" :custom-footer="true" destroyOnClose>
|
||||
<div class="max-h-[54vh] ql-editor" v-html="TextBanner"></div>
|
||||
<BasicModal v-model:visible="showTextBanner" :custom-footer="true" destroy-on-close>
|
||||
<div class="max-h-[54vh] ql-editor" v-html="TextBanner" />
|
||||
</BasicModal>
|
||||
|
||||
<ShareModal v-model:visible="showShare" :referralId="userStore.userInfo?.referral_id"/>
|
||||
<ShareModal v-model:show="showShare" :referral-id="userStore.userInfo?.referral_id" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="tsx" setup>
|
||||
import {computed, onMounted, onUnmounted, ref} from 'vue'
|
||||
import router from '@/router'
|
||||
import {useWindowScroll} from '@vueuse/core'
|
||||
import {loading, removeToken} from '@/utils'
|
||||
import {useUserStore} from '@/store/userStore'
|
||||
import {useWalletStore} from '@/store/walletStore'
|
||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
import type { WalletMetalTotal } from '@/models/walletAssets'
|
||||
import { formatMetalAssetsInfoText, formatRialAssetsInfoText, resolveAssetsRialValue } from '@/utils/productAssets'
|
||||
import {getLastPrices, getTradeBuyPrice, lastPrice, price, startLastPricePolling, stopLastPricePolling} from '@/utils/price'
|
||||
import {useConfigStore} from '@/store/configStore'
|
||||
import {useFundUserStore} from '@/store/fundStore'
|
||||
import {usePriceWithoutType} from '../../../utils/usePrice'
|
||||
import { getStoreClosedMessage, isStoreClosed } from '@/utils/storeHours'
|
||||
import {getConstByKey} from '@/api/const'
|
||||
import {message, Modal} from 'ant-design-vue'
|
||||
|
||||
// Components
|
||||
import Icon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import HomeSkeleton from '@/views/base/home/components/HomeSkeleton.vue'
|
||||
import BasicModal from '@/components/BasicPacks/BasicModal.vue'
|
||||
import AssetItem from './components/AssetItem.vue'
|
||||
import ActionButton from './components/ActionButton.vue'
|
||||
import ReferralSection from './components/ReferralSection.vue'
|
||||
import AssetsSection from './components/AssetsSection.vue'
|
||||
import MarketClosedMessage from './components/MarketClosedMessage.vue'
|
||||
import CurrentPrice from './components/CurrentPrice.vue'
|
||||
import BannerSlider from './components/BannerSlider.vue'
|
||||
import PriceChart from './components/PriceChart.vue'
|
||||
import PriceStats from './components/PriceStats.vue'
|
||||
import AppHeader from './components/AppHeader.vue'
|
||||
import LogoutConfirmDialog from './components/LogoutConfirmDialog.vue'
|
||||
import BalanceSheet from './components/BalanceSheet.vue'
|
||||
import ShareModal from './components/ShareModal.vue'
|
||||
import HomePageShell from './shell/HomePageShell.vue'
|
||||
import { useHomePage } from './composables/useHomePage'
|
||||
|
||||
// Composables
|
||||
import {useBanners} from './composables/useBanners'
|
||||
import {usePriceChart} from './composables/usePriceChart'
|
||||
import {useUserFunds} from './composables/useUserFunds'
|
||||
import {getReferralShareText, getReferralShareTitle} from '@/utils/config'
|
||||
|
||||
// Stores
|
||||
const userStore = useUserStore()
|
||||
const walletStore = useWalletStore()
|
||||
const configStore = useConfigStore()
|
||||
const fundStore = useFundUserStore()
|
||||
const { assetsSummary, loadWalletAssets } = useWalletAssets()
|
||||
|
||||
// Config
|
||||
const isGaroosi = computed(() => configStore.config.name === 'garoosi')
|
||||
const closedMessage = computed(() => userStore.getValueByKey('closed_message'))
|
||||
const marketClosedText = computed(() => {
|
||||
if (!isStoreClosed()) return ''
|
||||
return closedMessage.value || getStoreClosedMessage()
|
||||
})
|
||||
const showClosedMessage = computed(() => !!marketClosedText.value)
|
||||
|
||||
|
||||
// State
|
||||
const showLogoutConfirm = ref(false)
|
||||
const wholeBalanceVisible = ref(false)
|
||||
const showTextBanner = ref(false)
|
||||
const showShare = ref(false)
|
||||
const TextBanner = ref('')
|
||||
const odometerKey = ref(0)
|
||||
const {x, y} = useWindowScroll()
|
||||
|
||||
// Composables
|
||||
const {banners, loadBanners} = useBanners()
|
||||
const {updateChartData, priceChartSeries, fetchPrices} = usePriceChart()
|
||||
const {funds, loadFunds} = useUserFunds()
|
||||
|
||||
// Computed
|
||||
const limitedPrice = computed(() => walletStore.limitedPrice?.[0])
|
||||
const goldVal = computed(() => {
|
||||
return lastPrice.value?.price_last ?? 1
|
||||
})
|
||||
|
||||
// Actions
|
||||
const mainActions = computed(() => [
|
||||
{
|
||||
name: 'buySell',
|
||||
icon: 'vuesax-linear:money-receive',
|
||||
label: 'خرید و فروش',
|
||||
handler: () => router.push({name: 'buyProduct'})
|
||||
},
|
||||
{
|
||||
name: 'deposit',
|
||||
icon: 'vuesax-linear:card-add',
|
||||
label: isGaroosi.value ? 'واریز و برداشت' : 'واریز نقدی',
|
||||
handler: () => router.push({name: 'wallet', query: {state: 'deposit'}})
|
||||
},
|
||||
{
|
||||
name: 'balance',
|
||||
icon: 'vuesax-linear:receipt-item',
|
||||
label: 'موجودی',
|
||||
handler: () => wholeBalanceVisible.value = true
|
||||
},
|
||||
{
|
||||
name: 'assets',
|
||||
icon: 'vuesax-linear:cup',
|
||||
label: isGaroosi.value ? 'دعوت از دوستان' : 'دارایی ها',
|
||||
handler: () => isGaroosi.value ? shareCode() : router.push({name: 'fund'})
|
||||
}
|
||||
])
|
||||
|
||||
// Methods
|
||||
async function shareCode() {
|
||||
const text = getReferralShareText({
|
||||
userName: userStore.userInfo?.name,
|
||||
projectName: configStore.config.name,
|
||||
referralId: userStore.userInfo?.referral_id,
|
||||
})
|
||||
|
||||
try {
|
||||
await navigator.share({
|
||||
title: getReferralShareTitle(configStore.config.name),
|
||||
text,
|
||||
})
|
||||
} catch {
|
||||
copyCode()
|
||||
}
|
||||
}
|
||||
|
||||
function copyCode() {
|
||||
const code = userStore.userInfo?.referral_id
|
||||
if (code) {
|
||||
navigator.clipboard.writeText(code)
|
||||
message.info('کد رونوشت شد')
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
userStore.userInfo = undefined
|
||||
walletStore.walletInfo = undefined
|
||||
useWalletAssets().clearWalletAssets()
|
||||
removeToken()
|
||||
await router.push({name: 'login'})
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
function openGoldModal(metal: WalletMetalTotal) {
|
||||
Modal.confirm({
|
||||
title: formatMetalAssetsInfoText(metal),
|
||||
okText: 'متوجه شدم',
|
||||
centered: true,
|
||||
cancelButtonProps: {style: {display: 'none'}}
|
||||
})
|
||||
}
|
||||
|
||||
function openRialModal() {
|
||||
const summary = assetsSummary.value
|
||||
const cash = summary?.cash_wallet ?? 0
|
||||
const metalRial = resolveAssetsRialValue(summary, getTradeBuyPrice(lastPrice.value))
|
||||
Modal.confirm({
|
||||
title: 'دارایی ریالی',
|
||||
content: formatRialAssetsInfoText(cash, metalRial),
|
||||
okText: 'متوجه شدم',
|
||||
centered: true,
|
||||
cancelButtonProps: {style: {display: 'none'}}
|
||||
})
|
||||
}
|
||||
|
||||
function handleBannerClick(banner) {
|
||||
const handlers = {
|
||||
share: () => showShare.value = true,
|
||||
modal: () => {
|
||||
if (banner.modalText) {
|
||||
TextBanner.value = banner.modalText
|
||||
showTextBanner.value = true
|
||||
}
|
||||
},
|
||||
link: () => {
|
||||
if (banner.link) {
|
||||
if (banner.link.startsWith('http')) {
|
||||
window.open(banner.link, '_blank')
|
||||
} else {
|
||||
router.push(banner.link)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
handlers[banner.typeClick]?.()
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
await Promise.all([
|
||||
loadFunds(),
|
||||
loadWalletAssets(),
|
||||
getLastPrices(),
|
||||
fetchPrices(),
|
||||
startLastPricePolling(),
|
||||
loadBanners()
|
||||
])
|
||||
|
||||
// شروع آپدیت قیمت
|
||||
const updateInterval = setInterval(() => {
|
||||
odometerKey.value++
|
||||
}, 1000)
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(updateInterval)
|
||||
stopLastPricePolling()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Initialization error:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
const {
|
||||
userStore,
|
||||
walletStore,
|
||||
showLogoutConfirm,
|
||||
wholeBalanceVisible,
|
||||
showTextBanner,
|
||||
showShare,
|
||||
TextBanner,
|
||||
goldVal,
|
||||
logout,
|
||||
} = useHomePage()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-page-root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.odometer {
|
||||
font-size: 60px;
|
||||
@@ -317,4 +72,4 @@ label:focus-within {
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { loading } from '@/utils'
|
||||
import HomeSkeleton from '../components/HomeSkeleton.vue'
|
||||
import { getHomeLayoutConfig } from './registry'
|
||||
import { HOME_SECTION_COMPONENTS } from './sections'
|
||||
import type { HomeLayoutConfig, HomeSectionId } from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
layoutConfig?: HomeLayoutConfig
|
||||
}>()
|
||||
|
||||
const userStore = useUserStore()
|
||||
const layoutConfig = computed(() => props.layoutConfig ?? getHomeLayoutConfig())
|
||||
|
||||
const outerSections = computed(() =>
|
||||
(layoutConfig.value.outerSections ?? []).filter((section) => section.visible !== false)
|
||||
)
|
||||
|
||||
const innerSections = computed(() =>
|
||||
layoutConfig.value.sections.filter((section) => section.visible !== false)
|
||||
)
|
||||
|
||||
function resolveSectionComponent(sectionId: HomeSectionId) {
|
||||
return HOME_SECTION_COMPONENTS[sectionId]
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="userStore.userInfo && !loading">
|
||||
<component
|
||||
:is="resolveSectionComponent(section.id)"
|
||||
v-for="section in outerSections"
|
||||
:key="`outer-${section.id}`"
|
||||
/>
|
||||
|
||||
<div v-if="layoutConfig.wrapperClass" :class="layoutConfig.wrapperClass">
|
||||
<component
|
||||
:is="resolveSectionComponent(section.id)"
|
||||
v-for="section in innerSections"
|
||||
:key="section.id"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<component
|
||||
:is="resolveSectionComponent(section.id)"
|
||||
v-for="section in innerSections"
|
||||
:key="section.id"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<HomeSkeleton v-if="loading" class="w-full" />
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { HomeLayoutConfig } from '../types'
|
||||
|
||||
/**
|
||||
* چیدمان اختصاصی boursecu — فقط وقتی home_layout=boursecu یا پروژه boursecu باشد.
|
||||
* شامل: دارایی بدون بکگراند، گرید ۶ باکس، نمودار کاتد/مفتول، و ...
|
||||
*/
|
||||
export const boursecuHomeLayoutConfig: HomeLayoutConfig = {
|
||||
id: 'boursecu',
|
||||
wrapperClass: 'rounded-lg mt-5',
|
||||
outerSections: [
|
||||
{ id: 'assets' },
|
||||
{ id: 'boursecuQuickActions' },
|
||||
{ id: 'limitedPriceAlert' },
|
||||
],
|
||||
sections: [
|
||||
{ id: 'marketClosed' },
|
||||
{ id: 'currentPrice' },
|
||||
{ id: 'priceChart' },
|
||||
{ id: 'priceStats' },
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { HomeQuickActionItem } from '../types'
|
||||
|
||||
export const boursecuQuickActions: HomeQuickActionItem[] = [
|
||||
{
|
||||
id: 'buySell',
|
||||
label: 'خرید و فروش',
|
||||
icon: 'vuesax-linear:money-receive',
|
||||
route: { name: 'buyProduct' },
|
||||
},
|
||||
{
|
||||
id: 'wallet',
|
||||
label: 'واریز و برداشت',
|
||||
icon: 'vuesax-linear:wallet-1',
|
||||
route: { name: 'wallet' },
|
||||
},
|
||||
{
|
||||
id: 'assets',
|
||||
label: 'دارایی ها',
|
||||
icon: 'vuesax-linear:cup',
|
||||
route: { name: 'fund' },
|
||||
},
|
||||
{
|
||||
id: 'transactions',
|
||||
label: 'تراکنش ها',
|
||||
icon: 'vuesax-linear:receipt-item',
|
||||
route: { name: 'transactions' },
|
||||
},
|
||||
{
|
||||
id: 'physicalReceive',
|
||||
label: 'تحویل فیزیکی',
|
||||
icon: 'vuesax-linear:direct-inbox',
|
||||
route: { name: 'physicalReceive' },
|
||||
},
|
||||
{
|
||||
id: 'convert',
|
||||
label: 'تبدیل',
|
||||
icon: 'vuesax-linear:refresh-circle',
|
||||
route: { name: 'buyProduct' },
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { HomeLayoutConfig } from '../types'
|
||||
|
||||
/** چیدمان پیشفرض — همه پروژهها بهجز boursecu */
|
||||
export const defaultHomeLayoutConfig: HomeLayoutConfig = {
|
||||
id: 'default',
|
||||
wrapperClass: 'p-1 from-primary rounded-lg p-px to-transparent mt-5',
|
||||
outerSections: [{ id: 'limitedPriceAlert' }],
|
||||
sections: [
|
||||
{ id: 'garoosiAssets' },
|
||||
{ id: 'mainActions' },
|
||||
{ id: 'referral' },
|
||||
{ id: 'assets' },
|
||||
{ id: 'marketClosed' },
|
||||
{ id: 'currentPrice' },
|
||||
{ id: 'banners' },
|
||||
{ id: 'priceChart' },
|
||||
{ id: 'priceStats' },
|
||||
],
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user