fix kashkool
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
export interface AboutSection {
|
||||
title?: string
|
||||
paragraphs?: string[]
|
||||
items?: string[]
|
||||
}
|
||||
|
||||
export function getAboutSections(projectName: string): AboutSection[] {
|
||||
return [
|
||||
{
|
||||
paragraphs: [`${projectName} وبسایت رسمی است.`],
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div
|
||||
class="dabout-content dabout-content--default"
|
||||
:class="variant === 'desktop' ? 'dabout-content--default' : '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="variant === 'desktop' ? 'dabout-content__divider' : 'w-full h-1px my-3 bg-tertiary'"
|
||||
/>
|
||||
<div
|
||||
v-for="(paragraph, paragraphIndex) in section.paragraphs ?? []"
|
||||
:key="`p-${sectionIndex}-${paragraphIndex}`"
|
||||
:class="[variant === 'desktop' ? 'dabout-content__paragraph' : '', paragraphIndex > 0 ? 'mt-3' : '']"
|
||||
>
|
||||
{{ paragraph }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useAboutPageContext } from '../composables/useAboutPage'
|
||||
|
||||
withDefaults(defineProps<{ variant?: 'mobile' | 'desktop' }>(), { variant: 'mobile' })
|
||||
|
||||
const { aboutSections } = useAboutPageContext()
|
||||
</script>
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div :class="variant === 'desktop' ? 'dabout-support' : 'wrap-fix bottom-0 px-10 py-6 flex gap-2 bg-white items-center'">
|
||||
<a class="w-full" :href="supportHref" target="_blank" rel="noopener noreferrer">
|
||||
<BasicButton class="w-full" type="primary">تماس با پشتیبانی</BasicButton>
|
||||
</a>
|
||||
<a :href="supportHref" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
src="@/assets/img/rubika.webp"
|
||||
alt=""
|
||||
:class="variant === 'desktop' ? 'dabout-support__bale' : 'w-12'"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import { useAboutPageContext } from '../composables/useAboutPage'
|
||||
|
||||
withDefaults(defineProps<{ variant?: 'mobile' | 'desktop' }>(), { variant: 'mobile' })
|
||||
|
||||
const { supportHref } = useAboutPageContext()
|
||||
</script>
|
||||
@@ -0,0 +1,29 @@
|
||||
import { computed, inject, provide, type InjectionKey, type ComputedRef } from 'vue'
|
||||
import { getConfig } from '@/config/app-config'
|
||||
import { getAboutSections, type AboutSection } from '../aboutContent'
|
||||
|
||||
export interface AboutPageContext {
|
||||
projectName: ComputedRef<string>
|
||||
aboutSections: ComputedRef<AboutSection[]>
|
||||
supportHref: ComputedRef<string>
|
||||
}
|
||||
|
||||
export const ABOUT_PAGE_CONTEXT_KEY: InjectionKey<AboutPageContext> = Symbol('aboutPageContext')
|
||||
|
||||
const SUPPORT_LINK = 'https://web.rubika.ir/#c=u0I8F1U0a939b0df5ba9f73618d33d7c'
|
||||
|
||||
export function useAboutPage() {
|
||||
const projectName = computed(() => getConfig('name_fa', 'کشکول گلد'))
|
||||
const aboutSections = computed(() => getAboutSections(projectName.value))
|
||||
const supportHref = computed(() => SUPPORT_LINK)
|
||||
|
||||
const context: AboutPageContext = { projectName, aboutSections, supportHref }
|
||||
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 useAboutPage')
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="dabout">
|
||||
<div class="dabout__container">
|
||||
<DesktopPageTopbar eyebrow="اطلاعات" title="درباره ما" show-back />
|
||||
|
||||
<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">{{ projectName }}</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 DesktopPageTopbar from '@/layouts/components/DesktopPageTopbar.vue'
|
||||
import AboutSectionsContent from '../components/AboutSectionsContent.vue'
|
||||
import AboutSupportActions from '../components/AboutSupportActions.vue'
|
||||
import { useAboutPageContext } from '../composables/useAboutPage'
|
||||
|
||||
const { projectName } = useAboutPageContext()
|
||||
</script>
|
||||
|
||||
<style lang="less" src="./styles/desktop-about.less"></style>
|
||||
@@ -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
-184
@@ -1,195 +1,23 @@
|
||||
<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">
|
||||
<div class="bg-primary text-white rounded-[12px] p-4 text-xs leading-5">
|
||||
<div class="w-full h-1px my-3 bg-tertiary"></div>
|
||||
<div>
|
||||
کشکول گلد وبسایت رسمی تست.
|
||||
</div>
|
||||
<!-- <div class="mt-3">-->
|
||||
<!-- این وبسایت با پشتوانه سه مجموعه معتبر نور هشتم فعالیت میکند و امکان خرید شمش طلا را از وزنهای خرد تا معاملات عمده، با قیمت شفاف و امنیت کامل فراهم میسازد.-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="mt-3">-->
|
||||
<!-- تمامی فرآیندهای خرید در کشکول گلد بر اساس استانداردهای حرفهای بازار طلا و با اتکا به اعتبار مجموعههای نور هشتم مشهد، نورهشتم فردوس و نورهشتم (داماس) بجستان انجام میشود تا کاربران با آرامش و اطمینان کامل اقدام به خرید کنند.-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="mt-3">-->
|
||||
<!-- کشکول گلدکشکول گلد، بانکی از طلا بساز...-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="mt-3">-->
|
||||
<!-- برای ارتباط حضوری، دریافت مشاوره و اطمینان بیشتر، کاربران میتوانند از طریق آدرسهای سه شعبه رسمی نور هشتم با مجموعه در ارتباط باشند و از پشتیبانی مستقیم مجموعه بهرهمند شوند.-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="w-full h-1px my-3 bg-tertiary"></div>-->
|
||||
<!-- <div class="mt-3 text-xl font-bold">چرا کشکول گلد؟</div>
|
||||
<div class="mt-3">
|
||||
۱. خرید بدون اجرت و مالیات
|
||||
طلایی که از اپلیکیشن کشکول گلد خریداری میکنید، بدون اجرت ساخت و مالیات محاسبه میشود و تنها ۰.۵٪ کارمزد از شما دریافت خواهد شد.
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
۲. خرید بدون محدودیت
|
||||
در کشکول گلد میتوانید از ۵۰ سوت تا چند کیلوگرم طلا را بهصورت آنلاین و بدون هیچگونه محدودیت مکانی خریداری کنید
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
۳. فعالیت در ساعات رسمی معاملات طلا
|
||||
برای رعایت قوانین صنف طلا و جلوگیری از خالیفروشی، فعالیت اپلیکیشن کشکول گلد منطبق با ساعات رسمی معاملات اتحادیه طلافروشان کشور است.
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
۴. امکان تحویل فیزیکی طلا
|
||||
کشکول گلد دارای شعب حضوری معتبر با مجوز خردهفروشی طلاست تا کاربران بتوانند در صورت تمایل، طلای خریداریشده خود را بهصورت فیزیکی تحویل بگیرند و از نزدیک از اصالت آن اطمینان حاصل کنند.
|
||||
</div>
|
||||
<div class="w-full h-1px my-3 bg-tertiary"></div>-->
|
||||
<!-- <div class="mt-3">-->
|
||||
<!-- آدرس شعب ما:-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="mt-3">-->
|
||||
<!-- مشهد : خیابان امام خمینی / برج طلا و جواهر مرمر / طبقه همکف / واحد 16 / طلا و سکه نورهشتم-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="mt-3">-->
|
||||
<!-- فردوس : خیابان امام خمینی / بعد از امام خمینی 11 / طلا و سکه نورهشتم-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="mt-3">-->
|
||||
<!-- بجستان : خیابان امام خمینی / چهارراه وحدت / طلا و جواهر داماس [طلا و سکه نورهشتم]-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="w-full h-1px my-3 bg-tertiary"></div>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrap-fix bottom-0 px-10 py-6 flex gap-2 bg-white items-center">
|
||||
<a class="w-full" href="https://web.rubika.ir/#">
|
||||
<BasicButton class="w-full" type="primary">تماس با پشتیبانی</BasicButton>
|
||||
</a>
|
||||
<a href="https://web.rubika.ir/#">
|
||||
<img src="@/assets/img/rubika.webp" alt="" class=" w-12"/>
|
||||
</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 } 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 BaseDatePicker from '@/components/BasicPacks/BaseDatePicker'
|
||||
import img_pattern from "@/views/login/assets/img/pattern.svg";
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import { useAboutPage } from './composables/useAboutPage'
|
||||
import DesktopAboutView from './desktop/DesktopAboutView.vue'
|
||||
import MobileAboutView from './mobile/MobileAboutView.vue'
|
||||
|
||||
const userFormState = ref<User>({
|
||||
fname: '',
|
||||
lname: '',
|
||||
mobile: undefined,
|
||||
birth_date: '',
|
||||
phone_number: '',
|
||||
province: '',
|
||||
city: '',
|
||||
n_code: undefined
|
||||
})
|
||||
const formRef = ref<FormInstance>()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// const editPassVisible = ref<boolean>(false)
|
||||
const 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() {
|
||||
// nextTick(() => {
|
||||
const val = `⭐️الان طلا بخر؛ آخر ماه سودشو ببر!
|
||||
با خرید طلای آبشده در مقدارهای کم ولی مستمر، سود بیشتری ببر!
|
||||
|
||||
این دعوت نامه از طرف "${userStore.userInfo?.name}" عزیز برای شما ارسال شده تا شما هم از بازار طلای کشکول گلد جا نمونید.
|
||||
روی لینک زیر کلیک کنید و ثبت نام خود را تکمیل کنید.
|
||||
|
||||
kh.kashkolgold.ir/login?id=${userStore.userInfo?.referral_id}`
|
||||
navigator.share({
|
||||
title: 'کشکول گلد',
|
||||
text: 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
|
||||
}
|
||||
})
|
||||
}
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
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,23 @@
|
||||
<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 pb-24">
|
||||
<div class="px-5">
|
||||
<AboutSectionsContent variant="mobile" />
|
||||
</div>
|
||||
<AboutSupportActions variant="mobile" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import router from '@/router'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import AboutSectionsContent from '../components/AboutSectionsContent.vue'
|
||||
import AboutSupportActions from '../components/AboutSupportActions.vue'
|
||||
</script>
|
||||
@@ -0,0 +1,309 @@
|
||||
import { computed, inject, onMounted, onUnmounted, provide, ref, watch, type InjectionKey } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { useCartStore } from '@/store/cartStore'
|
||||
import { useWalletStore } from '@/store/walletStore'
|
||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
import { checkoutCart } from '@/api/card'
|
||||
import { registerCodeDiscount } from '@/api/giftCard'
|
||||
import { getProductBaseBuyPrice } from '@/utils/productPrice'
|
||||
import { subscribeProductListStream } from '@/utils/productStreamBridge'
|
||||
import {
|
||||
getLastPrices,
|
||||
lastPrice,
|
||||
startLastPricePolling,
|
||||
stopPriceInterval,
|
||||
} from '@/utils/price'
|
||||
import {
|
||||
priceSocketConnected,
|
||||
priceStreamLifecycle,
|
||||
priceWssEnabled,
|
||||
shouldUsePricePolling,
|
||||
} from '@/utils/priceSocketState'
|
||||
import { getToken, loading } from '@/utils'
|
||||
import router from '@/router'
|
||||
import type { Price } from '@/models/home'
|
||||
import type { Order } from '@/models/order'
|
||||
import type { Product } from '@/models/products'
|
||||
|
||||
export interface CartPageContext {
|
||||
cartStore: ReturnType<typeof useCartStore>
|
||||
loading: typeof loading
|
||||
infoVisible: ReturnType<typeof ref<boolean>>
|
||||
showDiscountModal: ReturnType<typeof ref<boolean>>
|
||||
discountCode: ReturnType<typeof ref<string>>
|
||||
discountInfo: ReturnType<typeof ref<any>>
|
||||
price: ReturnType<typeof computed<Price>>
|
||||
totalPrice: ReturnType<typeof computed<number>>
|
||||
displaytotalPrice: ReturnType<typeof computed<number | string>>
|
||||
discountPercentLabel: ReturnType<typeof computed<string>>
|
||||
formatWeight: (weight?: number) => string
|
||||
handleRemoveItem: (itemId: string) => void
|
||||
handleCheckout: () => Promise<void>
|
||||
submitDiscount: () => Promise<void>
|
||||
removeDiscount: () => void
|
||||
}
|
||||
|
||||
export const CART_PAGE_CONTEXT_KEY: InjectionKey<CartPageContext> = Symbol('cartPageContext')
|
||||
|
||||
export function useCartPage() {
|
||||
const cartStore = useCartStore()
|
||||
const walletStore = useWalletStore()
|
||||
const { loadWalletAssets } = useWalletAssets()
|
||||
const infoVisible = ref(false)
|
||||
const showDiscountModal = ref(false)
|
||||
const discountCode = ref('')
|
||||
const discountInfo = ref<any>()
|
||||
|
||||
let cartRefreshInterval: ReturnType<typeof setInterval> | undefined
|
||||
let unsubscribeProductStream: (() => void) | undefined
|
||||
|
||||
const price = computed((): Price => <Price>lastPrice.value ?? 0)
|
||||
|
||||
function syncCartFromProducts(products: Product[]) {
|
||||
if (cartStore.itemCount === 0) return
|
||||
const removedCount = cartStore.syncProductsFromServer(products)
|
||||
cartStore.updateCartItemPrices()
|
||||
if (removedCount > 0) {
|
||||
message.warning(`${removedCount} محصول از سبد خرید حذف شد (دیگر در دسترس نیست)`)
|
||||
}
|
||||
}
|
||||
|
||||
watch(lastPrice, () => {
|
||||
if (lastPrice.value && cartStore.itemCount > 0) {
|
||||
cartStore.updateCartItemPrices()
|
||||
}
|
||||
}, { deep: true, immediate: false })
|
||||
|
||||
const totalPrice = computed(() =>
|
||||
parseInt(String(cartStore.cartItems.reduce((sum, item) => sum + item.totalPrice, 0)))
|
||||
)
|
||||
|
||||
const displaytotalPrice = computed(() => {
|
||||
if (discountInfo.value?.amount > 0) {
|
||||
return (totalPrice.value - totalPrice.value * discountInfo.value.amount).toFixed(0)
|
||||
}
|
||||
return totalPrice.value
|
||||
})
|
||||
|
||||
const discountPercentLabel = computed(() => {
|
||||
const amount = Number(discountInfo.value?.amount ?? 0)
|
||||
if (amount <= 0) return ''
|
||||
return `${Math.round(amount * 100)}٪`
|
||||
})
|
||||
|
||||
function removeDiscount() {
|
||||
discountInfo.value = undefined
|
||||
discountCode.value = ''
|
||||
message.success('کد تخفیف حذف شد')
|
||||
}
|
||||
|
||||
async function submitDiscount() {
|
||||
try {
|
||||
loading.value = true
|
||||
const temp = await registerCodeDiscount(discountCode.value)
|
||||
discountInfo.value = temp.giftcard
|
||||
message.success('کد تخفیف با موفقیت اعمال شد')
|
||||
showDiscountModal.value = false
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatWeight(weight?: number) {
|
||||
if (!weight) return ''
|
||||
if (weight < 1) {
|
||||
const sot = weight * 1000
|
||||
return sot < 1 ? '1 سوت' : `${Math.round(sot)} سوت`
|
||||
}
|
||||
return `${weight} گرم`
|
||||
}
|
||||
|
||||
function handleRemoveItem(itemId: string) {
|
||||
Modal.confirm({
|
||||
title: 'حذف از سبد خرید',
|
||||
content: 'آیا از حذف این آیتم از سبد خرید اطمینان دارید؟',
|
||||
okText: 'تایید',
|
||||
okType: 'danger',
|
||||
centered: true,
|
||||
cancelText: 'انصراف',
|
||||
onOk() {
|
||||
cartStore.removeFromCart(itemId)
|
||||
message.success('آیتم از سبد خرید حذف شد')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function handleCheckout() {
|
||||
if (!getToken()) {
|
||||
message.warning('برای تسویه حساب، لطفا ابتدا وارد حساب کاربری خود شوید')
|
||||
await router.push({ name: 'login' })
|
||||
return
|
||||
}
|
||||
if (cartStore.itemCount === 0) {
|
||||
message.warning('سبد خرید شما خالی است')
|
||||
return
|
||||
}
|
||||
if (!lastPrice.value) {
|
||||
message.error('لطفا منتظر بمانید تا قیمتها بارگذاری شوند')
|
||||
return
|
||||
}
|
||||
|
||||
let cashItemsTotal = cartStore.cartItems
|
||||
.filter((item) => item.method === 'cash' && !item.gateway)
|
||||
.reduce((sum, item) => parseInt(String(sum + item.totalPrice)), 0)
|
||||
|
||||
if (cashItemsTotal > 0) {
|
||||
if (discountInfo.value?.amount > 0) {
|
||||
cashItemsTotal -= cashItemsTotal * discountInfo.value.amount
|
||||
}
|
||||
if (!walletStore.walletInfo) await walletStore.getMyWallet()
|
||||
if (!walletStore.walletInfo?.cash || cashItemsTotal > walletStore.walletInfo.cash) {
|
||||
message.error('موجودی کیف پول شما ناکافی است')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const goldItemsTotal = cartStore.cartItems
|
||||
.filter((item) => item.method === 'gold')
|
||||
.reduce((sum, item) => sum + (item.goldVazn ? item.goldVazn * item.quantity : 0), 0)
|
||||
|
||||
if (goldItemsTotal > 0) {
|
||||
if (!walletStore.walletInfo) await walletStore.getMyWallet()
|
||||
if (!walletStore.walletInfo?.gold || goldItemsTotal > walletStore.walletInfo.gold) {
|
||||
message.error('موجودی کیف طلا شما ناکافی است')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
infoVisible.value = true
|
||||
const orders: Order[] = cartStore.cartItems.map((item) => ({
|
||||
direction: 1,
|
||||
price: getProductBaseBuyPrice(item.product),
|
||||
amount: item.product.amount,
|
||||
gateway: item.gateway,
|
||||
product_id: item.product.id,
|
||||
method: item.method,
|
||||
number: item.quantity,
|
||||
discount_code: discountInfo.value?.code,
|
||||
}))
|
||||
|
||||
const data = await checkoutCart(orders)
|
||||
let returnUrl: string | null = null
|
||||
if (Array.isArray(data)) {
|
||||
const itemWithReturnUrl = data.find((item) => item?.returnUrl)
|
||||
if (itemWithReturnUrl?.returnUrl) returnUrl = itemWithReturnUrl.returnUrl
|
||||
} else if (data?.returnUrl) {
|
||||
returnUrl = data.returnUrl
|
||||
}
|
||||
|
||||
if (returnUrl) {
|
||||
window.location.href = returnUrl
|
||||
return
|
||||
}
|
||||
|
||||
await walletStore.getMyWallet()
|
||||
cartStore.clearCart()
|
||||
await router.push({ name: 'transactions' })
|
||||
message.success('سفارشات شما با موفقیت ثبت شد')
|
||||
await loadWalletAssets(true)
|
||||
} catch (e) {
|
||||
console.error('Error in checkout:', e)
|
||||
message.error('خطا در ثبت سفارش. لطفا دوباره تلاش کنید.')
|
||||
} finally {
|
||||
loading.value = false
|
||||
infoVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function initPrice() {
|
||||
try {
|
||||
if (shouldUsePricePolling()) await getLastPrices()
|
||||
startLastPricePolling()
|
||||
cartStore.updateCartItemPrices()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshCart() {
|
||||
const removedCount = await cartStore.refreshCartFromServer()
|
||||
if (removedCount > 0) {
|
||||
message.warning(`${removedCount} محصول از سبد خرید حذف شد (دیگر در دسترس نیست)`)
|
||||
}
|
||||
}
|
||||
|
||||
function startCartRefreshPolling() {
|
||||
if (cartRefreshInterval || !shouldUsePricePolling()) return
|
||||
cartRefreshInterval = setInterval(() => {
|
||||
if (shouldUsePricePolling()) void refreshCart()
|
||||
}, 10000)
|
||||
}
|
||||
|
||||
function stopCartRefreshPolling() {
|
||||
if (cartRefreshInterval) {
|
||||
clearInterval(cartRefreshInterval)
|
||||
cartRefreshInterval = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function syncCartRefreshPolling() {
|
||||
if (shouldUsePricePolling()) startCartRefreshPolling()
|
||||
else stopCartRefreshPolling()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [priceWssEnabled.value, priceSocketConnected.value, priceStreamLifecycle.value] as const,
|
||||
() => {
|
||||
syncCartRefreshPolling()
|
||||
if (shouldUsePricePolling() && cartStore.itemCount > 0) void refreshCart()
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
cartStore.loadCartFromStorage()
|
||||
unsubscribeProductStream = subscribeProductListStream(syncCartFromProducts)
|
||||
loading.value = true
|
||||
try {
|
||||
await initPrice()
|
||||
if (shouldUsePricePolling()) await refreshCart()
|
||||
syncCartRefreshPolling()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unsubscribeProductStream?.()
|
||||
unsubscribeProductStream = undefined
|
||||
stopCartRefreshPolling()
|
||||
stopPriceInterval()
|
||||
})
|
||||
|
||||
const context: CartPageContext = {
|
||||
cartStore,
|
||||
loading,
|
||||
infoVisible,
|
||||
showDiscountModal,
|
||||
discountCode,
|
||||
discountInfo,
|
||||
price,
|
||||
totalPrice,
|
||||
displaytotalPrice,
|
||||
discountPercentLabel,
|
||||
formatWeight,
|
||||
handleRemoveItem,
|
||||
handleCheckout,
|
||||
submitDiscount,
|
||||
removeDiscount,
|
||||
}
|
||||
|
||||
provide(CART_PAGE_CONTEXT_KEY, context)
|
||||
return context
|
||||
}
|
||||
|
||||
export function useCartPageContext() {
|
||||
const context = inject(CART_PAGE_CONTEXT_KEY)
|
||||
if (!context) throw new Error('useCartPageContext must be used within useCartPage')
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<div class="dinner cart-page">
|
||||
<div class="dinner__container">
|
||||
<DesktopPageTopbar title="سبد خرید" show-back>
|
||||
<template #trailing>
|
||||
<div v-if="price" class="dcart-header-price number-ltr">
|
||||
<span class="dcart-header-price__label">قیمت خرید از کشکول گلد:</span>
|
||||
<strong>{{ usePriceWithoutType(price?.price_last ?? 0) }}</strong>
|
||||
<span>ریال</span>
|
||||
</div>
|
||||
</template>
|
||||
</DesktopPageTopbar>
|
||||
|
||||
<GoldLoading v-model:show="infoVisible" title="" />
|
||||
|
||||
<template v-if="loading">
|
||||
<TransactionSkeleton v-for="i in 3" :key="i" class="mt-4" />
|
||||
</template>
|
||||
|
||||
<div v-else-if="cartStore.itemCount === 0" class="dinner-surface dcart-empty">
|
||||
<p>سبد خرید شما خالی است</p>
|
||||
<BasicButton type="primary" @click="router.push({ name: 'buyProduct' })">مشاهده محصولات</BasicButton>
|
||||
</div>
|
||||
|
||||
<div v-else class="dcart-grid">
|
||||
<div class="dcart-items">
|
||||
<article v-for="item in cartStore.cartItems" :key="item.id" class="dcart-item">
|
||||
<div class="dcart-item__image">
|
||||
<ProductImage :product="item.product" img-class="max-w-full max-h-full object-contain" />
|
||||
</div>
|
||||
<div class="dcart-item__main">
|
||||
<h3 class="dcart-item__title">{{ item.product.title }}</h3>
|
||||
<p class="dcart-item__meta">
|
||||
روش پرداخت: {{ item.method === 'cash' ? 'نقدی' : 'طلایی' }}
|
||||
<span v-if="item.method === 'cash' && item.gateway"> (با درگاه)</span>
|
||||
</p>
|
||||
<p class="dcart-item__meta">
|
||||
تعداد: {{ item.quantity }} عدد
|
||||
<span v-if="item.goldVazn"> | وزن: {{ formatWeight(item.goldVazn) }}</span>
|
||||
</p>
|
||||
<div class="dcart-item__footer">
|
||||
<span class="dcart-item__price">{{ usePrice(item.totalPrice) }}</span>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:trash"
|
||||
class="text-red-500 cursor-pointer"
|
||||
@click.stop="handleRemoveItem(item.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<aside class="dinner-surface dcart-summary">
|
||||
<h2 class="dcart-summary__title">خلاصه سفارش</h2>
|
||||
|
||||
<div
|
||||
v-if="discountInfo?.amount > 0"
|
||||
class="dcart-summary__discount"
|
||||
>
|
||||
<div>
|
||||
<span>کد تخفیف: </span>
|
||||
<strong>{{ discountInfo.code }}</strong>
|
||||
<div class="text-xs text-green-600">{{ discountPercentLabel }} تخفیف اعمال شد</div>
|
||||
</div>
|
||||
<button type="button" class="dcart-summary__remove-discount" @click="removeDiscount">
|
||||
<BasicIcon icon="vuesax-linear:close-circle" size="18" />
|
||||
حذف
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="dcart-summary__row">
|
||||
<span>مجموع:</span>
|
||||
<div class="text-left">
|
||||
<div class="dcart-summary__total">{{ usePrice(displaytotalPrice) }}</div>
|
||||
<div v-if="discountInfo?.amount > 0" class="text-xs text-gray-400 line-through">
|
||||
{{ usePrice(totalPrice) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="!discountInfo?.amount"
|
||||
type="button"
|
||||
class="dcart-summary__discount-link"
|
||||
@click="showDiscountModal = true"
|
||||
>
|
||||
کد تخفیف دارم
|
||||
</button>
|
||||
|
||||
<BasicButton class="w-full" type="primary" size="large" @click="handleCheckout">
|
||||
تسویه حساب
|
||||
</BasicButton>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import router from '@/router'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import GoldLoading from '@/components/GoldLoading.vue'
|
||||
import ProductImage from '@/components/ProductImage.vue'
|
||||
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
|
||||
import DesktopPageTopbar from '@/layouts/components/DesktopPageTopbar.vue'
|
||||
import { usePrice, usePriceWithoutType } from '@/utils/usePrice'
|
||||
import { useCartPageContext } from '../composables/useCartPage'
|
||||
|
||||
const {
|
||||
cartStore,
|
||||
loading,
|
||||
infoVisible,
|
||||
showDiscountModal,
|
||||
discountInfo,
|
||||
price,
|
||||
totalPrice,
|
||||
displaytotalPrice,
|
||||
discountPercentLabel,
|
||||
formatWeight,
|
||||
handleRemoveItem,
|
||||
handleCheckout,
|
||||
removeDiscount,
|
||||
} = useCartPageContext()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dcart-header-price {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
|
||||
strong {
|
||||
font-size: 16px;
|
||||
color: #111827;
|
||||
}
|
||||
}
|
||||
|
||||
.dcart-empty {
|
||||
padding: 48px 24px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.dcart-summary__discount {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
background: rgba(167, 43, 119, 0.06);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dcart-summary__remove-discount {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #ef4444;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.dcart-summary__discount-link {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: @App-primary-color;
|
||||
font-size: 13px;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
+26
-453
@@ -1,464 +1,37 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="absolute inset-0 bg-cover bg-center opacity-60 z--1" :style="{ backgroundImage: `url(${img})` }"></div>
|
||||
<div class="bg-white drop-shadow z-[1999] wrap-fix">
|
||||
<div class="py-3 px-3 font-bold flex items-center justify-between">
|
||||
<div>سبد خرید</div>
|
||||
<!-- Price Display in Header -->
|
||||
<div v-if="price" class="flex gap-1 items-center text-xs flex-1 justify-end">
|
||||
<span class="text-gray-600 text-xs">قیمت خرید از کشکول گلد:</span>
|
||||
<div class="font-bold text-base" style="font-family: iran-sans; direction: ltr">
|
||||
{{ usePriceWithoutType(price?.price_last ?? 0) }}
|
||||
</div>
|
||||
<span class="text-xs">ریال</span>
|
||||
</div>
|
||||
<div class="cart-page-root">
|
||||
<DesktopCartView v-show="isDesktop" />
|
||||
<MobileCartView v-show="!isDesktop" />
|
||||
|
||||
<AModal
|
||||
v-model:visible="showDiscountModal"
|
||||
ok-text="اعمال کد تخفیف"
|
||||
cancel-text="انصراف"
|
||||
@ok="submitDiscount"
|
||||
@cancel="showDiscountModal = false"
|
||||
>
|
||||
<div class="p-5">
|
||||
<div>کد تخفیف</div>
|
||||
<AInput v-model:value="discountCode" />
|
||||
<div class="font-bold text-xs mt-3">*حساس به حروف بزرگ و کوچک</div>
|
||||
</div>
|
||||
</div>
|
||||
<GoldLoading v-model:show="infoVisible" title=""/>
|
||||
<div class="px-5 pt-26 pb-30" :class="loading ? 'h-[60vh]' : ''">
|
||||
<template v-if="loading">
|
||||
<TransactionSkeleton v-for="i in 3" :key="i" class="mt-4" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- Empty Cart Message -->
|
||||
<div v-if="cartStore.itemCount === 0" class="flex flex-col items-center justify-center h-[60vh]">
|
||||
<div class="text-gray-500 text-lg font-semibold mb-4">سبد خرید شما خالی است</div>
|
||||
<BasicButton type="primary" @click="$router.push({ name: 'buyProduct' })">
|
||||
مشاهده محصولات
|
||||
</BasicButton>
|
||||
</div>
|
||||
|
||||
<!-- Cart Items -->
|
||||
<div v-else>
|
||||
<div class="space-y-4 mb-20">
|
||||
<div
|
||||
v-for="item in cartStore.cartItems"
|
||||
:key="item.id"
|
||||
class="bg-white rounded-lg border border-gray-200 p-4 shadow-sm"
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<!-- Product Image -->
|
||||
<div class="bg-gray-100 rounded-lg flex justify-center items-center p-1 min-w-[80px] h-[80px]">
|
||||
<ProductImage
|
||||
:product="item.product"
|
||||
img-class="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Product Info -->
|
||||
<div class="flex-1 flex flex-col justify-between">
|
||||
<div>
|
||||
<div class="text-base font-semibold mb-1">{{ item.product.title }}</div>
|
||||
<!-- <div class="text-xs text-gray-500 mb-2">-->
|
||||
<!-- {{ getProductAmount(item.product.amount) }}-->
|
||||
<!-- </div>-->
|
||||
<div class="text-xs text-gray-500 mb-2">
|
||||
روش پرداخت: {{ item.method === 'cash' ? 'نقدی' : 'طلایی' }}
|
||||
<span v-if="item.method === 'cash' && item.gateway" class="mr-2">(با درگاه)</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
تعداد: {{ item.quantity }} عدد
|
||||
<span v-if="item.goldVazn" class="mr-2">| وزن: {{ formatWeight(item.goldVazn) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between mt-2">
|
||||
<div class="text-primary font-bold">
|
||||
{{ usePrice(item.totalPrice) }}
|
||||
</div>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:trash"
|
||||
class="text-red-500 cursor-pointer"
|
||||
@click.stop="handleRemoveItem(item.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total and Checkout -->
|
||||
<div class=" flex justify-center fixed bottom-20 left-0 right-0 z-1">
|
||||
<div class="md:w-[426px] w-full bg-white border-t border-gray-200 p-4 shadow-lg">
|
||||
<div
|
||||
v-if="discountInfo?.amount > 0"
|
||||
class="mb-3 flex items-center justify-between rounded-lg bg-purple-50 px-3 py-2"
|
||||
>
|
||||
<div class="flex flex-col gap-0.5 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-gray-600">کد تخفیف:</span>
|
||||
<span class="font-semibold">{{ discountInfo.code }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-green-600">
|
||||
{{ discountPercentLabel }} تخفیف اعمال شد
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1 text-xs text-red-500"
|
||||
@click="removeDiscount"
|
||||
>
|
||||
<BasicIcon icon="vuesax-linear:close-circle" size="18" />
|
||||
<span>حذف</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div class="text-lg font-semibold">مجموع:</div>
|
||||
<div class="flex flex-col items-end gap-1">
|
||||
<div class="text-primary font-bold text-xl">
|
||||
{{ usePrice(displaytotalPrice) }}
|
||||
</div>
|
||||
<div
|
||||
v-if="discountInfo?.amount > 0"
|
||||
class="text-xs text-gray-400 line-through"
|
||||
>
|
||||
{{ usePrice(totalPrice) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!discountInfo?.amount"
|
||||
class="mb-3 text-center"
|
||||
>
|
||||
<span
|
||||
class="text-xs text-primary underline cursor-pointer"
|
||||
@click="showDiscountModal = true"
|
||||
>
|
||||
کد تخفیف دارم
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<BasicButton
|
||||
class="w-full py-3"
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="handleCheckout"
|
||||
>
|
||||
تسویه حساب
|
||||
</BasicButton>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</AModal>
|
||||
</div>
|
||||
<AModal v-model:visible="showDiscountModal" ok-text="اعمال کد تخفیف" cancel-text="انصراف" @ok="submitDiscount"
|
||||
@cancel="showDiscountModal=false">
|
||||
<div class="p-5">
|
||||
<div>کد تخفیف</div>
|
||||
<AInput v-model:value="discountCode"></AInput>
|
||||
<div class="font-bold text-xs mt-3">
|
||||
*حساس به حروف بزرگ و کوچک
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</AModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, onMounted, onUnmounted, watch, computed} from 'vue'
|
||||
import {useCartStore} from '@/store/cartStore'
|
||||
import {usePrice, usePriceWithoutType} from '@/utils/usePrice'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import {message, Modal} from 'ant-design-vue'
|
||||
import img from '@/assets/img/user_ly1.svg'
|
||||
import {
|
||||
getLastPrices,
|
||||
lastPrice,
|
||||
stopPriceInterval,
|
||||
setIntervalGetLastPrice
|
||||
} from '@/utils/price'
|
||||
import {checkoutCart} from '@/api/card'
|
||||
import router from '@/router'
|
||||
import {useWalletStore} from '@/store/walletStore'
|
||||
import {useFundUserStore} from '@/store/fundStore'
|
||||
import type {Price} from '@/models/home'
|
||||
import type {Order} from '@/models/order'
|
||||
<script lang="ts" setup>
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import { useCartPage } from './composables/useCartPage'
|
||||
import DesktopCartView from './desktop/DesktopCartView.vue'
|
||||
import MobileCartView from './mobile/MobileCartView.vue'
|
||||
|
||||
import {loading, getToken} from '@/utils'
|
||||
import GoldLoading from '@/components/GoldLoading.vue'
|
||||
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
|
||||
import ProductImage from '@/components/ProductImage.vue'
|
||||
import {registerCodeDiscount} from "@/api/giftCard";
|
||||
|
||||
const cartStore = useCartStore()
|
||||
const walletStore = useWalletStore()
|
||||
const fundStore = useFundUserStore()
|
||||
const infoVisible = ref(false)
|
||||
const showDiscountModal = ref(false)
|
||||
const discountCode = ref<string>('')
|
||||
const discountInfo = ref()
|
||||
let totalPriceWithDiscount = 0
|
||||
|
||||
const price = computed((): Price => <Price>lastPrice.value ?? 0)
|
||||
|
||||
// Watch for price updates and update cart items
|
||||
watch(lastPrice, () => {
|
||||
if (lastPrice.value) {
|
||||
cartStore.recalculateCartPrices(lastPrice.value)
|
||||
}
|
||||
}, {deep: true, immediate: false})
|
||||
|
||||
// Computed total price that updates with price changes
|
||||
const totalPrice = computed(() => {
|
||||
return parseInt(cartStore.cartItems.reduce((sum, item) => sum + item.totalPrice, 0))
|
||||
})
|
||||
const displaytotalPrice = computed(() => {
|
||||
if (discountInfo.value?.amount > 0) {
|
||||
return (totalPrice.value - (totalPrice.value * discountInfo.value?.amount)).toFixed(0)
|
||||
|
||||
} else
|
||||
return totalPrice.value
|
||||
})
|
||||
|
||||
const discountPercentLabel = computed(() => {
|
||||
const amount = Number(discountInfo.value?.amount ?? 0)
|
||||
if (amount <= 0) return ''
|
||||
return `${Math.round(amount * 100)}٪`
|
||||
})
|
||||
|
||||
function removeDiscount() {
|
||||
discountInfo.value = undefined
|
||||
discountCode.value = ''
|
||||
message.success('کد تخفیف حذف شد')
|
||||
}
|
||||
|
||||
async function submitDiscount() {
|
||||
try {
|
||||
loading.value = true
|
||||
const temp = await registerCodeDiscount(discountCode.value)
|
||||
discountInfo.value = temp.giftcard
|
||||
message.success('کد تخفیف با موفقیت اعمال شد')
|
||||
showDiscountModal.value = false
|
||||
} catch (e) {
|
||||
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function getProductAmount(amount?: number) {
|
||||
if (!amount) return ''
|
||||
if (amount < 1) {
|
||||
// کمتر از 1 گرم را به سوت تبدیل کن (مشابه ProductItem)
|
||||
const sot = amount * 1000
|
||||
// اگر نتیجه کمتر از 1 شد، حداقل 1 نمایش بده
|
||||
if (sot < 1) {
|
||||
return `1 سوت`
|
||||
}
|
||||
// در غیر این صورت، عدد را به عدد صحیح گرد کن
|
||||
return `${Math.round(sot)} سوت`
|
||||
}
|
||||
return `${amount} گرم`
|
||||
}
|
||||
|
||||
function formatWeight(weight?: number) {
|
||||
if (!weight) return ''
|
||||
if (weight < 1) {
|
||||
// کمتر از 1 گرم را به سوت تبدیل کن
|
||||
const sot = weight * 1000
|
||||
// اگر نتیجه کمتر از 1 شد، حداقل 1 نمایش بده
|
||||
if (sot < 1) {
|
||||
return `1 سوت`
|
||||
}
|
||||
// در غیر این صورت، عدد را به عدد صحیح گرد کن
|
||||
return `${Math.round(sot)} سوت`
|
||||
}
|
||||
return `${weight} گرم`
|
||||
}
|
||||
|
||||
function handleRemoveItem(itemId: string) {
|
||||
Modal.confirm({
|
||||
title: 'حذف از سبد خرید',
|
||||
content: 'آیا از حذف این آیتم از سبد خرید اطمینان دارید؟',
|
||||
okText: 'تایید',
|
||||
okType: 'danger',
|
||||
centered: true,
|
||||
cancelText: 'انصراف',
|
||||
onOk() {
|
||||
cartStore.removeFromCart(itemId)
|
||||
message.success('آیتم از سبد خرید حذف شد')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function handleCheckout() {
|
||||
// Check if user is logged in
|
||||
if (!getToken()) {
|
||||
message.warning('برای تسویه حساب، لطفا ابتدا وارد حساب کاربری خود شوید')
|
||||
await router.push({name: 'login'})
|
||||
return
|
||||
}
|
||||
|
||||
if (cartStore.itemCount === 0) {
|
||||
message.warning('سبد خرید شما خالی است')
|
||||
return
|
||||
}
|
||||
|
||||
if (!lastPrice.value) {
|
||||
message.error('لطفا منتظر بمانید تا قیمتها بارگذاری شوند')
|
||||
return
|
||||
}
|
||||
|
||||
// Check wallet balance for cash method items (without gateway)
|
||||
let cashItemsTotal = cartStore.cartItems
|
||||
.filter(item => item.method === 'cash' && !item.gateway)
|
||||
.reduce((sum, item) => parseInt(sum + item.totalPrice), 0)
|
||||
|
||||
if (cashItemsTotal > 0) {
|
||||
// Ensure wallet info is loaded
|
||||
if (discountInfo.value?.amount && discountInfo.value?.amount>0){
|
||||
cashItemsTotal=cashItemsTotal-cashItemsTotal*discountInfo.value?.amount
|
||||
}
|
||||
if (!walletStore.walletInfo) {
|
||||
await walletStore.getMyWallet()
|
||||
}
|
||||
|
||||
if (!walletStore.walletInfo?.cash || cashItemsTotal > walletStore.walletInfo.cash) {
|
||||
message.error('موجودی کیف پول شما ناکافی است')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check gold balance for gold method items
|
||||
const goldItemsTotal = cartStore.cartItems
|
||||
.filter(item => item.method === 'gold')
|
||||
.reduce((sum, item) => {
|
||||
// Calculate total gold weight needed
|
||||
const goldWeight = item.goldVazn ? item.goldVazn * item.quantity : 0
|
||||
return sum + goldWeight
|
||||
}, 0)
|
||||
|
||||
if (goldItemsTotal > 0) {
|
||||
// Ensure wallet info is loaded
|
||||
if (!walletStore.walletInfo) {
|
||||
await walletStore.getMyWallet()
|
||||
}
|
||||
|
||||
if (!walletStore.walletInfo?.gold || goldItemsTotal > walletStore.walletInfo.gold) {
|
||||
message.error('موجودی کیف طلا شما ناکافی است')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
infoVisible.value = true
|
||||
|
||||
// Prepare orders array with same structure as createOrder for each product
|
||||
const orders: Order[] = cartStore.cartItems.map(item => {
|
||||
console.log(item)
|
||||
const product = item.product
|
||||
// Calculate price similar to createOrder (buy price)
|
||||
let productPrice = 0
|
||||
if (product.group_type == 'آبشده') {
|
||||
productPrice = lastPrice.value?.price_last ?? 0
|
||||
} else {
|
||||
productPrice = (product.amount ?? 0) * (lastPrice.value?.price_last ?? 0)
|
||||
}
|
||||
|
||||
return {
|
||||
direction: 1, // 1 for buy (cart items are always buy orders)
|
||||
price: productPrice,
|
||||
amount: product.amount,
|
||||
gateway: item.gateway,
|
||||
product_id: product.id,
|
||||
method: item.method,
|
||||
number: item.quantity,
|
||||
discount_code:discountInfo.value?.code
|
||||
}
|
||||
})
|
||||
|
||||
// Call checkoutCart API with orders array
|
||||
const data = await checkoutCart(orders)
|
||||
|
||||
// Check for returnUrl (gateway payment)
|
||||
let returnUrl = null
|
||||
if (Array.isArray(data)) {
|
||||
const itemWithReturnUrl = data.find(item => item?.returnUrl)
|
||||
if (itemWithReturnUrl?.returnUrl) {
|
||||
returnUrl = itemWithReturnUrl.returnUrl
|
||||
}
|
||||
} else if (data?.returnUrl) {
|
||||
returnUrl = data.returnUrl
|
||||
}
|
||||
|
||||
// If returnUrl exists, redirect to gateway and stop execution
|
||||
if (returnUrl) {
|
||||
window.location.href = returnUrl
|
||||
return // Stop execution here
|
||||
}
|
||||
|
||||
// If no gateway, continue with normal flow
|
||||
await walletStore.getMyWallet()
|
||||
cartStore.clearCart()
|
||||
await router.push({name: 'transactions'})
|
||||
message.success('سفارشات شما با موفقیت ثبت شد')
|
||||
await fundStore.getMyFund()
|
||||
} catch (e: any) {
|
||||
console.error('Error in checkout:', e)
|
||||
message.error('خطا در ثبت سفارش. لطفا دوباره تلاش کنید.')
|
||||
} finally {
|
||||
loading.value = false
|
||||
infoVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function initPrice() {
|
||||
try {
|
||||
await getLastPrices()
|
||||
await setIntervalGetLastPrice()
|
||||
if (lastPrice.value) {
|
||||
cartStore.recalculateCartPrices(lastPrice.value)
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshCart() {
|
||||
const removedCount = await cartStore.refreshCartFromServer()
|
||||
if (removedCount > 0) {
|
||||
message.warning(`${removedCount} محصول از سبد خرید حذف شد (دیگر در دسترس نیست)`)
|
||||
}
|
||||
if (lastPrice.value) {
|
||||
cartStore.recalculateCartPrices(lastPrice.value)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
cartStore.loadCartFromStorage()
|
||||
loading.value = true
|
||||
try {
|
||||
await initPrice()
|
||||
await refreshCart()
|
||||
const refreshCartInfo=setInterval(async () => {
|
||||
await refreshCart()
|
||||
// await getUpPrice()
|
||||
}, 10000)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopPriceInterval()
|
||||
})
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
const { showDiscountModal, discountCode, submitDiscount } = useCartPage()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-primary {
|
||||
color: #d3a233;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 426px) {
|
||||
.wrap-fix {
|
||||
width: 426px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.cart-page-root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="absolute inset-0 bg-cover bg-center opacity-60 z--1" :style="{ backgroundImage: `url(${img})` }" />
|
||||
<div class="bg-white drop-shadow z-[1999] wrap-fix">
|
||||
<div class="py-3 px-3 font-bold flex items-center justify-between">
|
||||
<div>سبد خرید</div>
|
||||
<div v-if="price" class="flex gap-1 items-center text-xs flex-1 justify-end">
|
||||
<span class="text-gray-600 text-xs">قیمت خرید از کشکول گلد:</span>
|
||||
<div class="font-bold text-base" style="font-family: iran-sans; direction: ltr">
|
||||
{{ usePriceWithoutType(price?.price_last ?? 0) }}
|
||||
</div>
|
||||
<span class="text-xs">ریال</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GoldLoading v-model:show="infoVisible" title="" />
|
||||
<div class="px-5 pt-26 pb-30" :class="loading ? 'h-[60vh]' : ''">
|
||||
<template v-if="loading">
|
||||
<TransactionSkeleton v-for="i in 3" :key="i" class="mt-4" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="cartStore.itemCount === 0" class="flex flex-col items-center justify-center h-[60vh]">
|
||||
<div class="text-gray-500 text-lg font-semibold mb-4">سبد خرید شما خالی است</div>
|
||||
<BasicButton type="primary" @click="router.push({ name: 'buyProduct' })">مشاهده محصولات</BasicButton>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="space-y-4 mb-20">
|
||||
<div
|
||||
v-for="item in cartStore.cartItems"
|
||||
:key="item.id"
|
||||
class="bg-white rounded-lg border border-gray-200 p-4 shadow-sm"
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<div class="bg-gray-100 rounded-lg flex justify-center items-center p-1 min-w-[80px] h-[80px]">
|
||||
<ProductImage :product="item.product" img-class="max-w-full max-h-full object-contain" />
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col justify-between">
|
||||
<div>
|
||||
<div class="text-base font-semibold mb-1">{{ item.product.title }}</div>
|
||||
<div class="text-xs text-gray-500 mb-2">
|
||||
روش پرداخت: {{ item.method === 'cash' ? 'نقدی' : 'طلایی' }}
|
||||
<span v-if="item.method === 'cash' && item.gateway" class="mr-2">(با درگاه)</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
تعداد: {{ item.quantity }} عدد
|
||||
<span v-if="item.goldVazn" class="mr-2">| وزن: {{ formatWeight(item.goldVazn) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between mt-2">
|
||||
<div class="text-primary font-bold">{{ usePrice(item.totalPrice) }}</div>
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:trash"
|
||||
class="text-red-500 cursor-pointer"
|
||||
@click.stop="handleRemoveItem(item.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center fixed bottom-20 left-0 right-0 z-1">
|
||||
<div class="md:w-[426px] w-full bg-white border-t border-gray-200 p-4 shadow-lg">
|
||||
<div
|
||||
v-if="discountInfo?.amount > 0"
|
||||
class="mb-3 flex items-center justify-between rounded-lg bg-purple-50 px-3 py-2"
|
||||
>
|
||||
<div class="flex flex-col gap-0.5 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-gray-600">کد تخفیف:</span>
|
||||
<span class="font-semibold">{{ discountInfo.code }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-green-600">{{ discountPercentLabel }} تخفیف اعمال شد</div>
|
||||
</div>
|
||||
<button type="button" class="flex items-center gap-1 text-xs text-red-500" @click="removeDiscount">
|
||||
<BasicIcon icon="vuesax-linear:close-circle" size="18" />
|
||||
<span>حذف</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div class="text-lg font-semibold">مجموع:</div>
|
||||
<div class="flex flex-col items-end gap-1">
|
||||
<div class="text-primary font-bold text-xl">{{ usePrice(displaytotalPrice) }}</div>
|
||||
<div v-if="discountInfo?.amount > 0" class="text-xs text-gray-400 line-through">
|
||||
{{ usePrice(totalPrice) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!discountInfo?.amount" class="mb-3 text-center">
|
||||
<span class="text-xs text-primary underline cursor-pointer" @click="showDiscountModal = true">
|
||||
کد تخفیف دارم
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<BasicButton class="w-full py-3" type="primary" size="large" @click="handleCheckout">
|
||||
تسویه حساب
|
||||
</BasicButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import router from '@/router'
|
||||
import img from '@/assets/img/user_ly1.svg'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import GoldLoading from '@/components/GoldLoading.vue'
|
||||
import ProductImage from '@/components/ProductImage.vue'
|
||||
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
|
||||
import { usePrice, usePriceWithoutType } from '@/utils/usePrice'
|
||||
import { useCartPageContext } from '../composables/useCartPage'
|
||||
|
||||
const {
|
||||
cartStore,
|
||||
loading,
|
||||
infoVisible,
|
||||
showDiscountModal,
|
||||
discountInfo,
|
||||
price,
|
||||
totalPrice,
|
||||
displaytotalPrice,
|
||||
discountPercentLabel,
|
||||
formatWeight,
|
||||
handleRemoveItem,
|
||||
handleCheckout,
|
||||
removeDiscount,
|
||||
} = useCartPageContext()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-primary {
|
||||
color: #d3a233;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,79 @@
|
||||
import { inject, onMounted, provide, ref, type InjectionKey } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { getEarnMoney } from '@/api/earnMoney'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { loading } from '@/utils'
|
||||
import type { EarnMoney } from '@/models/earnMoney'
|
||||
|
||||
export interface EarnMoneyPageContext {
|
||||
data: ReturnType<typeof ref<EarnMoney | undefined>>
|
||||
userStore: ReturnType<typeof useUserStore>
|
||||
isTransaction: ReturnType<typeof ref<boolean>>
|
||||
setIsTransaction: (value: boolean) => void
|
||||
copyCode: () => void
|
||||
shareCode: () => void
|
||||
}
|
||||
|
||||
export const EARN_MONEY_PAGE_CONTEXT_KEY: InjectionKey<EarnMoneyPageContext> = Symbol('earnMoneyPageContext')
|
||||
|
||||
export function useEarnMoneyPage() {
|
||||
const data = ref<EarnMoney>()
|
||||
const userStore = useUserStore()
|
||||
const isTransaction = ref(false)
|
||||
|
||||
function setIsTransaction(value: boolean) {
|
||||
isTransaction.value = value
|
||||
}
|
||||
|
||||
function copyCode() {
|
||||
const val = userStore.userInfo?.referral_id
|
||||
if (val) {
|
||||
navigator.clipboard.writeText(val)
|
||||
message.info('کد رونوشت شد')
|
||||
}
|
||||
}
|
||||
|
||||
function shareCode() {
|
||||
const val = `⭐️الان طلا بخر؛ آخر ماه سودشو ببر!
|
||||
با خرید طلای آبشده در مقدارهای کم ولی مستمر، سود بیشتری ببر!
|
||||
|
||||
این دعوت نامه از طرف "${userStore.userInfo?.name}" عزیز برای شما ارسال شده تا شما هم از بازار طلای کشکول گلد جا نمونید.
|
||||
روی لینک زیر کلیک کنید و ثبت نام خود را تکمیل کنید.
|
||||
|
||||
kh.kashkolgold.ir/login?id=${userStore.userInfo?.referral_id}`
|
||||
navigator.share({ title: 'کشکول گلد', text: val })
|
||||
}
|
||||
|
||||
async function earnMoney() {
|
||||
try {
|
||||
loading.value = true
|
||||
data.value = await getEarnMoney()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void earnMoney()
|
||||
})
|
||||
|
||||
const context: EarnMoneyPageContext = {
|
||||
data,
|
||||
userStore,
|
||||
isTransaction,
|
||||
setIsTransaction,
|
||||
copyCode,
|
||||
shareCode,
|
||||
}
|
||||
|
||||
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 useEarnMoneyPage')
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div class="dinner earn-money-page">
|
||||
<div class="dinner__container">
|
||||
<DesktopPageTopbar title="کسب درآمد" show-back />
|
||||
|
||||
<section class="dinner-surface p-5 mb-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="bg-[#DBEAFE] h-10 w-10 rounded-full flex justify-center items-center flex-shrink-0">
|
||||
<img :src="coin" alt="" class="w-6 h-6" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h2 class="font-bold mb-2">دعوت دوستان</h2>
|
||||
<p class="text-sm text-gray-600 leading-6">
|
||||
با دعوت دوستانتان به اپلیکیشن کشکول گلد، دوست شما اولین خرید خود را بدون کارمزد انجام میدهد
|
||||
و شما نیز ۱۰٪ از کارمزد ۱۰ معاملهی اول او را بهعنوان پاداش دریافت میکنید.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-center gap-2 mt-4">
|
||||
<span class="text-sm text-gray-500">کد بازاریابی</span>
|
||||
<strong class="text-lg tracking-wide">{{ userStore.userInfo?.referral_id }}</strong>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2 mt-4 max-w-md mx-auto">
|
||||
<BasicButton icon="vuesax-linear:share" size="small" icon-size="18" @click="shareCode">اشتراک گذاری</BasicButton>
|
||||
<BasicButton icon="vuesax-linear:copy" size="small" icon-size="18" @click="copyCode">رونوشت</BasicButton>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="dearn-stats">
|
||||
<div class="dearn-stat dearn-stat--blue">
|
||||
<div class="dearn-stat__value">{{ data?.usersSum }} عدد</div>
|
||||
<div class="dearn-stat__label">تعداد زیر مجموعه</div>
|
||||
</div>
|
||||
<div class="dearn-stat dearn-stat--yellow">
|
||||
<div class="dearn-stat__value">{{ data?.giftsSum }} سوت</div>
|
||||
<div class="dearn-stat__label">میزان کل پاداش ها</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dinner-surface p-2 mb-4 flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="dearn-tab"
|
||||
:class="{ 'dearn-tab--active': isTransaction }"
|
||||
@click="setIsTransaction(true)"
|
||||
>
|
||||
پاداش ها
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="dearn-tab"
|
||||
:class="{ 'dearn-tab--active': !isTransaction }"
|
||||
@click="setIsTransaction(false)"
|
||||
>
|
||||
فهرست زیر مجموعه
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="dearn-list">
|
||||
<template v-if="isTransaction">
|
||||
<GiftCard v-for="(val, i) in data?.gifts" :key="`gift-${i}`" :item="val" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<EarnCard v-for="(val, i) in data?.users" :key="`user-${i}`" :item="val" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import coin from '@/assets/img/coin.png'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import DesktopPageTopbar from '@/layouts/components/DesktopPageTopbar.vue'
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
.dearn-tab {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
|
||||
&--active {
|
||||
background: #fff;
|
||||
color: @App-primary-color;
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,163 +1,23 @@
|
||||
<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=" p-3 border-solid border-1 rounded-[8px] border-[#a72b77] ">
|
||||
<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-2">
|
||||
<div class="flex">
|
||||
<div class="font-bold">دعوت دوستان</div>
|
||||
<!-- <div class="text-xs ">راهنمای کسب درآمد</div>-->
|
||||
</div>
|
||||
<div class=" mt-2 text-xs">با دعوت دوستانتان به اپلیکیشن کشکول گلد،
|
||||
دوست شما اولین خرید خود را بدون کارمزد انجام میدهد
|
||||
و شما نیز ۱۰٪ از کارمزد ۱۰ معاملهی اول او را بهعنوان پاداش دریافت میکنید. 💰</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-[#a72b77] p-2 rounded-[8px] w-full">-->
|
||||
<!-- <div class="text-white text-center">فهرست زیرمجموعه</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="bg-[#a72b77] 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 class="h-20rem overflow-y-scroll" v-if="isTransaction">
|
||||
<GiftCard v-for="(val,i) in data?.gifts" :key="i" :item="val"/>
|
||||
</div>
|
||||
<div v-else class="h-20rem overflow-y-scroll">
|
||||
<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'
|
||||
|
||||
const data = ref<EarnMoney>()
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
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() {
|
||||
// nextTick(() => {
|
||||
const val = `⭐️الان طلا بخر؛ آخر ماه سودشو ببر!
|
||||
با خرید طلای آبشده در مقدارهای کم ولی مستمر، سود بیشتری ببر!
|
||||
|
||||
این دعوت نامه از طرف "${userStore.userInfo?.name}" عزیز برای شما ارسال شده تا شما هم از بازار طلای کشکول گلد جا نمونید.
|
||||
روی لینک زیر کلیک کنید و ثبت نام خود را تکمیل کنید.
|
||||
|
||||
kh.kashkolgold.ir/login?id=${userStore.userInfo?.referral_id}`
|
||||
navigator.share({
|
||||
title: 'کشکول گلد',
|
||||
text: val
|
||||
})
|
||||
// message.info('کد رونوشت شد')
|
||||
// })
|
||||
}
|
||||
|
||||
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,86 @@
|
||||
<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="p-4">
|
||||
<div class="p-3 border-solid border-1 rounded-[8px] border-[#a72b77]">
|
||||
<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-2">
|
||||
<div class="font-bold">دعوت دوستان</div>
|
||||
<div class="mt-2 text-xs">
|
||||
با دعوت دوستانتان به اپلیکیشن کشکول گلد، دوست شما اولین خرید خود را بدون کارمزد انجام میدهد
|
||||
و شما نیز ۱۰٪ از کارمزد ۱۰ معاملهی اول او را بهعنوان پاداش دریافت میکنید. 💰
|
||||
</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="shareCode">
|
||||
اشتراک گذاری
|
||||
</BasicButton>
|
||||
<BasicButton icon="vuesax-linear:copy" size="small" class="flex-1" icon-size="18" @click="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="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="setIsTransaction(false)"
|
||||
>
|
||||
فهرست زیر مجموعه
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-solid border-1 border-[#E5E7EB] mt-6" />
|
||||
<div v-if="isTransaction" class="h-20rem overflow-y-scroll">
|
||||
<GiftCard v-for="(val, i) in data?.gifts" :key="i" :item="val" />
|
||||
</div>
|
||||
<div v-else class="h-20rem overflow-y-scroll">
|
||||
<EarnCard v-for="(val, i) in data?.users" :key="i" :item="val" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
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 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>
|
||||
@@ -1,69 +1,61 @@
|
||||
<template>
|
||||
<div v-if="item" class="bg-white flex flex-col items-center p-3 rounded-xl">
|
||||
<img v-lazy="{ src: itemImg, loading: logo,error: logo}" alt="" class="w-[87px] ">
|
||||
<div class="mt-3">{{props.item?.title?? '-'}}</div>
|
||||
<div class="mt-1 flex gap-1"><div class="font-bold">{{props.item?.wallet[0]?.gold?? '-'}}</div> گرم</div>
|
||||
<!-- {{props.item?.wallet[0]?.cash}}-->
|
||||
<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 text-center line-clamp-2">{{ item.title ?? '-' }}</div>
|
||||
<div class="mt-1 flex flex-col items-center gap-0.5 text-center">
|
||||
<div class="flex gap-1">
|
||||
<div class="font-bold">{{ item.quantity ?? '-' }}</div>
|
||||
{{ item.unit }}
|
||||
</div>
|
||||
<div v-if="item.unit_weight && item.display_type === 'count'" class="text-xs text-gray-500">
|
||||
هر عدد {{ item.unit_weight }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
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 {computed} from "vue";
|
||||
import type {Fund} from "@/models/fund";
|
||||
import logo from "@/assets/img/logo_full_.png";
|
||||
/*function itemImg() {
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import type { WalletInventoryCard } from '@/models/walletAssets'
|
||||
import { getProductDisplayImage } from '@/utils/productAssets'
|
||||
|
||||
}*/
|
||||
interface Prop {
|
||||
item: Fund
|
||||
item: WalletInventoryCard
|
||||
variant?: 'mobile' | 'desktop'
|
||||
}
|
||||
|
||||
const props = defineProps<Prop>()
|
||||
|
||||
const itemImg = computed(() => {
|
||||
switch (props.item?.title) {
|
||||
case 'خرید لوازم منزل':
|
||||
return homeImg
|
||||
case 'هدیه':
|
||||
return giftImg
|
||||
case 'گردش':
|
||||
return circulationImg
|
||||
case 'ازدواج':
|
||||
return loveImg
|
||||
case 'تعمیرات':
|
||||
return repairsImg
|
||||
case 'پس انداز':
|
||||
return savingImg
|
||||
default: return savingImg
|
||||
}
|
||||
const props = withDefaults(defineProps<Prop>(), {
|
||||
variant: 'mobile',
|
||||
})
|
||||
const imageSrc = ref('')
|
||||
|
||||
/*const itemName = computed(() => {
|
||||
switch (props.type) {
|
||||
case 'home':
|
||||
return 'خرید لوازم منزل'
|
||||
case 'gift':
|
||||
return 'هدیه'
|
||||
case 'circulation':
|
||||
return 'گردش'
|
||||
case 'love':
|
||||
return 'ازدواج'
|
||||
case 'repairs':
|
||||
return 'تعمیرات'
|
||||
case 'saving':
|
||||
return 'پس انداز'
|
||||
default: return null
|
||||
}
|
||||
})*/
|
||||
const displayImage = computed(() => imageSrc.value || resolveImage())
|
||||
|
||||
watch(
|
||||
() => props.item,
|
||||
() => {
|
||||
imageSrc.value = resolveImage()
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
function resolveImage() {
|
||||
return getProductDisplayImage({
|
||||
title: props.item.title,
|
||||
group_type: props.item.group_type,
|
||||
file: props.item.file,
|
||||
})
|
||||
}
|
||||
|
||||
function onImageError() {
|
||||
imageSrc.value = getProductDisplayImage({
|
||||
title: props.item.title,
|
||||
group_type: props.item.group_type,
|
||||
file: null,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,99 @@
|
||||
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 { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
import type { WalletMetalTotal } from '@/models/walletAssets'
|
||||
import { fundPageInventoryCards, formatMetalTotal as buildMetalTotalLabel } from '@/utils/productAssets'
|
||||
import { getLastPrices } from '@/utils/price'
|
||||
import img from '@/assets/img/cash_img.svg'
|
||||
import empty from '@/assets/img/cash_empty.svg'
|
||||
|
||||
export interface FundPageContext {
|
||||
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
|
||||
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 { assetsSummary, loadWalletAssets } = useWalletAssets()
|
||||
|
||||
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 buildMetalTotalLabel(metal, inventoryCards.value)
|
||||
}
|
||||
|
||||
function openFundInfo(productId: number) {
|
||||
const id = Number(productId)
|
||||
if (!Number.isFinite(id) || id <= 0) return
|
||||
router.push({ name: 'fund_info', params: { id: String(id) } })
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
await Promise.all([loadWalletAssets(true), getLastPrices()])
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const context: FundPageContext = {
|
||||
meta,
|
||||
inventoryCards,
|
||||
metalTotals,
|
||||
primaryMetalTotal,
|
||||
cashWallet,
|
||||
totalRialValue,
|
||||
countBasedInventory,
|
||||
fundHeaderBackground: img,
|
||||
fundEmptyImage: empty,
|
||||
fundEmptyText: 'هنوز محصولی خریداری نکردهاید',
|
||||
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,114 @@
|
||||
<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 dfund-summary--ghatreh">
|
||||
<img :src="fundHeaderBackground" alt="" class="dfund-summary__bg" />
|
||||
<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="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" 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>{{ 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 Item from '../components/Item.vue'
|
||||
import { useFundPageContext } from '../composables/useFundPage'
|
||||
|
||||
const {
|
||||
meta,
|
||||
inventoryCards,
|
||||
metalTotals,
|
||||
primaryMetalTotal,
|
||||
cashWallet,
|
||||
totalRialValue,
|
||||
countBasedInventory,
|
||||
fundHeaderBackground,
|
||||
fundEmptyImage,
|
||||
fundEmptyText,
|
||||
formatMetalTotal,
|
||||
openFundInfo,
|
||||
goBack,
|
||||
} = useFundPageContext()
|
||||
</script>
|
||||
|
||||
<style lang="less" src="./styles/desktop-fund.less"></style>
|
||||
@@ -0,0 +1,506 @@
|
||||
/* 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;
|
||||
}
|
||||
|
||||
&--ghatreh {
|
||||
background: #efced9;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
&__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;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.dfund-info__layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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%;
|
||||
}
|
||||
+14
-113
@@ -1,122 +1,23 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="bg-white w-full py-3 px-3 text-black 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="bg-[#EFCED9] w-screen md:w-full h-[70vh] fixed md:absolute">
|
||||
<div class="pt-15 flex flex-col">
|
||||
<div class="text-gray-500 font-bold mr-5 z-20">موجودی قلک ها</div>
|
||||
<div class="flex items-baseline justify-center mt-2 w-full z-20">
|
||||
<div class="text-3xl font-bold">{{ inventory }}</div>
|
||||
/گرم
|
||||
</div>
|
||||
<img :src="img" alt="" class="mt-[-4rem] mr-10 max-w-[22rem] self-end">
|
||||
</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="items.length === 0 && !loading" class="w-full h-full flex justify-center items-center">
|
||||
<div>
|
||||
<img :src="empty" alt="" class="w-70 ">
|
||||
<div class="text-center text-lg">یک قلک جدید بساز!</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 items" :item="item"
|
||||
@click="router.push({name: 'fund_info', params:{id:item.id}})"/>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fixed w-full flex justify-center bottom-23 z-200 md:right-0">
|
||||
<div class="rounded-3xl bg-primary text-white px-5 py-2" @click="router.push({name: 'fund_add'})">افزودن قلک
|
||||
</div>
|
||||
</div>
|
||||
<div class="fund-page-root">
|
||||
<DesktopFundView v-show="isDesktop" />
|
||||
<MobileFundView v-show="!isDesktop" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import { useFundPage } from './composables/useFundPage'
|
||||
import DesktopFundView from './desktop/DesktopFundView.vue'
|
||||
import MobileFundView from './mobile/MobileFundView.vue'
|
||||
|
||||
import router from "@/router";
|
||||
import BasicIcon from "@/components/BasicPacks/BasicIcon.vue";
|
||||
import img from '@/assets/img/cash_img.svg'
|
||||
import empty from '@/assets/img/cash_empty.svg'
|
||||
import Item from './components/Item.vue'
|
||||
import {onMounted, ref} from "vue";
|
||||
import {loading} from "@/utils";
|
||||
import type {TableOutput} from "@/models/table";
|
||||
import type {Fund} from "@/models/fund";
|
||||
import {useFundUserStore} from "@/store/fundStore";
|
||||
|
||||
const items = ref<Fund[]>([])
|
||||
const tableOutput = ref<TableOutput<Fund> | null>(null)
|
||||
const inventory = ref<number>(0)
|
||||
const fundStore = useFundUserStore()
|
||||
|
||||
onMounted(() => {
|
||||
initTable()
|
||||
})
|
||||
|
||||
async function initTable() {
|
||||
try {
|
||||
loading.value = true
|
||||
tableOutput.value = fundStore.myFunds ?? await fundStore.getMyFund() ?? null
|
||||
// console.log(tableOutput.value)
|
||||
items.value.push(...tableOutput.value?.rows ?? [])
|
||||
items.value.forEach(x => {
|
||||
// console.log(x.wallet[0]?.gold)
|
||||
inventory.value += x?.wallet[0]?.gold ?? 0
|
||||
// console.log(inventory.value)
|
||||
})
|
||||
// console.log(tableOutput.value)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/*watch(()=> router.currentRoute.value.name, () =>{
|
||||
console.log('salammmmmmmmmmmmmmmmmmmmmmmmmmm')
|
||||
})*/
|
||||
/*
|
||||
const items = [
|
||||
{
|
||||
type: 'home',
|
||||
amount: 2.7
|
||||
},
|
||||
{
|
||||
type: 'gift',
|
||||
amount: 13.1
|
||||
},
|
||||
{
|
||||
type: 'saving',
|
||||
amount: 6.4
|
||||
},
|
||||
{
|
||||
type: 'repairs',
|
||||
amount: 16.6
|
||||
},
|
||||
{
|
||||
type: 'circulation',
|
||||
amount: 5.4
|
||||
},
|
||||
{
|
||||
type: 'love',
|
||||
amount: 3.7
|
||||
},
|
||||
]
|
||||
*/
|
||||
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
useFundPage(isDesktop)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
.fund-page-root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,69 +1,63 @@
|
||||
<template>
|
||||
|
||||
<div @click="infoVisible = true">
|
||||
<div class="flex">
|
||||
<div :class="isUp? 'bg-green-50':'bg-red-50'"
|
||||
class="rounded-lg p-3 h-fit w-fit flex justify-center items-center ">
|
||||
<Icon :class="isUp? 'text-green-600':'text-red-600'"
|
||||
:icon="`vuesax-linear:${isUp? 'arrow-up-2':'arrow-down-2'}`"/>
|
||||
<div
|
||||
:class="isUp ? 'bg-green-50' : 'bg-red-50'"
|
||||
class="rounded-lg p-3 h-fit w-fit flex justify-center items-center"
|
||||
>
|
||||
<Icon
|
||||
:class="isUp ? 'text-green-600' : 'text-red-600'"
|
||||
:icon="`vuesax-linear:${isUp ? 'arrow-up-2' : 'arrow-down-2'}`"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full mr-3">
|
||||
<div class="flex justify-between w-full">
|
||||
<div>{{ item.type_str }}</div>
|
||||
<div>{{ item.title }}</div>
|
||||
<div>{{ toJalaliNotime(item.expire_at) }}</div>
|
||||
</div>
|
||||
<div class="flex justify-between mt-2 ">
|
||||
<div :class="item.gold >= 0? 'text-green-600':'text-red-600'" class="text-gray-500 text-xs number-ltr">
|
||||
</div>
|
||||
<div class="flex justify-between mt-2">
|
||||
<div
|
||||
:class="item.gold >= 0 ? 'text-green-600' : 'text-red-600'"
|
||||
class="text-gray-500 text-xs number-ltr"
|
||||
/>
|
||||
<div class="text-gray-500 flex gap-1 text-xs items-center">
|
||||
<div :class="item.gold > 0 ? 'text-green-600':'text-red-600'" class="text-base number-ltr">
|
||||
{{ item.gold }}
|
||||
<div
|
||||
:class="item.gold > 0 ? 'text-green-600' : 'text-red-600'"
|
||||
class="text-base number-ltr"
|
||||
>
|
||||
{{ formatGoldAmount(item.gold) }}
|
||||
</div>
|
||||
گرم
|
||||
{{ item.unit }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div v-if="isGold" class="flex w-full justify-between mt-2">
|
||||
<div class="text-gray-500">قیمت معامله</div>
|
||||
<div class="text-gray-500 flex gap-1 text-xs items-center">
|
||||
<div class="text-base">{{ usePriceWithoutType(Math.abs(item.cash) / Math.abs(item.gold)) }}</div>
|
||||
ریال
|
||||
</div>
|
||||
</div>-->
|
||||
<!-- <div v-else class="h-8"></div>-->
|
||||
<a-divider class="!my-3"/>
|
||||
<a-divider class="!my-3" />
|
||||
<BasicActionSheet v-model:show="infoVisible">
|
||||
<div class="font-bold text-lg">{{ props.item.type_str }}</div>
|
||||
<div class="font-bold text-lg">{{ props.item.title }}</div>
|
||||
<div class="pb-20 mt-5">
|
||||
<div class="flex justify-between mb-3">
|
||||
<div class="text-black">مقدار طلا مبادله شده:</div>
|
||||
<div :class="item.gold >= 0? 'text-green-600' : 'text-red-600'" class="flex text-xs items-center">
|
||||
<div class="text-base ml-1 number-ltr">{{ item.gold }}</div>
|
||||
گرم
|
||||
<div class="text-black">مقدار {{ props.title }} مبادله شده:</div>
|
||||
<div
|
||||
:class="item.gold >= 0 ? 'text-green-600' : 'text-red-600'"
|
||||
class="flex text-xs items-center"
|
||||
>
|
||||
<div class="text-base ml-1 number-ltr">{{ formatGoldAmount(item.gold) }}</div>
|
||||
{{ item.unit }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mb-3">
|
||||
<div class="text-black">موجودی کیف طلا:</div>
|
||||
<div class="text-black">موجودی {{ props.title }}:</div>
|
||||
<div class="flex text-xs text-yellow-700 items-center">
|
||||
<div class="text-base ml-1">{{ item.balance_gold }}</div>
|
||||
گرم
|
||||
<div class="text-base ml-1">{{ formatGoldAmount(item.balance_gold) }}</div>
|
||||
{{ item.unit }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="flex justify-between mb-3">
|
||||
<div class="text-black">موجودی کیف پول:</div>
|
||||
<div class="flex text-xs items-center" :class="isUp? 'text-green-600' : 'text-red-600'"><div class="text-base">{{usePriceWithoutType(12000000)}}</div>ریال</div>
|
||||
</div>
|
||||
<div class="flex justify-between mb-3">
|
||||
<div class="text-black">موجودی کیف طلا:</div>
|
||||
<div class="flex items-center" :class="isUp? 'text-red-600' : 'text-green-600'">60 گرم</div>
|
||||
</div>-->
|
||||
<div class="flex justify-between mb-3">
|
||||
<div class="text-black">وضعیت:</div>
|
||||
<div class="flex text-xs items-center">
|
||||
<ATag :color="item.active? 'blue': 'red'" class="!py-1 !px-6 !ml-0">{{
|
||||
item.active ? 'موفق' : 'نا موفق'
|
||||
}}
|
||||
<ATag :color="item.active ? 'blue' : 'red'" class="!py-1 !px-6 !ml-0">
|
||||
{{ item.active ? 'موفق' : 'نا موفق' }}
|
||||
</ATag>
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,43 +73,24 @@
|
||||
</div>
|
||||
</BasicActionSheet>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Icon from "@/components/BasicPacks/BasicIcon.vue";
|
||||
import BasicActionSheet from "@/components/BasicPacks/BasicActionSheet.vue";
|
||||
import {computed, ref} from "vue";
|
||||
import BasicButton from "@/components/BasicPacks/BasicButton.vue";
|
||||
import type {Transaction} from "@/models/transaction";
|
||||
import {toJalaliNotime, toTime} from "../../../../../utils/useDateTime";
|
||||
import Icon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import type { Transaction } from '@/models/transaction'
|
||||
import { formatGoldAmount } from '@/utils/usePrice'
|
||||
import { toJalaliNotime, toTime } from '@/utils/useDateTime'
|
||||
|
||||
const infoVisible = ref<boolean>(false)
|
||||
|
||||
interface Prop {
|
||||
item: Transaction
|
||||
title?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Prop>(), {})
|
||||
const isUp = computed(() => props.item.type === 3 || props.item.type === 1)
|
||||
const isGold = computed(() => props.item.type === 1 || props.item.type === 2)
|
||||
/*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,93 @@
|
||||
import { computed, inject, provide, ref, watch, type InjectionKey } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import router from '@/router'
|
||||
import { loading } from '@/utils'
|
||||
import { getFundTransactionTable } from '@/api/fund'
|
||||
import { getOneProductWallet } from '@/api/products'
|
||||
import type { Product } from '@/models/products'
|
||||
import type { TableInput, TableOutput } from '@/models/table'
|
||||
import type { Transaction } from '@/models/transaction'
|
||||
|
||||
export interface FundInfoPageContext {
|
||||
info: ReturnType<typeof ref<Product | undefined>>
|
||||
items: ReturnType<typeof ref<Transaction[]>>
|
||||
infoId: ReturnType<typeof computed<number | null>>
|
||||
goBack: () => void
|
||||
}
|
||||
|
||||
export const FUND_INFO_PAGE_KEY: InjectionKey<FundInfoPageContext> = Symbol('fundInfoPage')
|
||||
|
||||
function parseRouteProductId(raw: string | string[] | undefined): number | null {
|
||||
const value = Array.isArray(raw) ? raw[0] : raw
|
||||
if (value == null || value === '') return null
|
||||
const id = Number.parseInt(String(value), 10)
|
||||
return Number.isFinite(id) && id > 0 ? id : null
|
||||
}
|
||||
|
||||
export function useFundInfoPage() {
|
||||
const route = useRoute()
|
||||
const info = ref<Product>()
|
||||
const items = ref<Transaction[]>([])
|
||||
const tableOutput = ref<TableOutput<Transaction> | null>(null)
|
||||
const pagination = ref<TableInput>({})
|
||||
|
||||
const infoId = computed(() => parseRouteProductId(route.params.id))
|
||||
|
||||
async function initTable() {
|
||||
if (!info.value?.wallet?.id) return
|
||||
|
||||
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 initInfo() {
|
||||
const id = infoId.value
|
||||
if (id == null) {
|
||||
console.error('Invalid fund product id in route')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
info.value = await getOneProductWallet(id)
|
||||
await initTable()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(infoId, (id) => {
|
||||
if (id != null) void initInfo()
|
||||
}, { immediate: true })
|
||||
|
||||
function goBack() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
const context: FundInfoPageContext = {
|
||||
info,
|
||||
items,
|
||||
infoId,
|
||||
goBack,
|
||||
}
|
||||
|
||||
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,61 @@
|
||||
<template>
|
||||
<div class="dfund-info">
|
||||
<div class="dfund-info__container">
|
||||
<DesktopPageTopbar
|
||||
eyebrow="جزئیات دارایی"
|
||||
:title="info?.title ?? '—'"
|
||||
show-back
|
||||
sticky
|
||||
/>
|
||||
|
||||
<div class="dfund-info__layout">
|
||||
<section class="dfund-info__hero dfund-surface">
|
||||
<a-spin :spinning="loading">
|
||||
<div class="dfund-info__image">
|
||||
<ProductImage
|
||||
v-if="info"
|
||||
:product="info"
|
||||
img-class="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
</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 DesktopPageTopbar from '@/layouts/components/DesktopPageTopbar.vue'
|
||||
import ProductImage from '@/components/ProductImage.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 { useFundInfoPageContext } from '../composables/useFundInfoPage'
|
||||
|
||||
const { info, items } = useFundInfoPageContext()
|
||||
</script>
|
||||
|
||||
<style lang="less" src="../../desktop/styles/desktop-fund.less"></style>
|
||||
@@ -1,291 +1,23 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="bg-white wrap-fix py-3 px-3 text-black 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="">
|
||||
</div>
|
||||
</a-spin>
|
||||
<div class="mt-2 font-bold flex text-xs items-center text-gray-500">
|
||||
<div class="ml-1 text-base text-black">{{ info?.wallet[0]?.gold }}</div>
|
||||
گرم
|
||||
</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" />
|
||||
</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>موجودی کیف طلا: {{ walletStore?.walletInfo?.gold }} گرم</div>
|
||||
<div v-if="info?.wallet[0]?.gold !== 0">
|
||||
{{ `موجودی قابل برداشت قلک: ${info?.wallet[0]?.gold ?? 0} گرم` }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="mt-2 text-xs font-bold">موجودی قلک {{ info.title }}: {{ info?.wallet[0]?.gold }} گرم
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<BasicButton
|
||||
:disabled="transButtonDisable"
|
||||
class=" w-full py-2 mt-2"
|
||||
html-type="submit"
|
||||
type="primary"
|
||||
>
|
||||
<div>
|
||||
<a-spin v-if="loading" size="small" />
|
||||
<div v-else>{{ isDeposit ? 'واریز' : 'برداشت' }}</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
<BasicButton class="mb-20 mt-2 w-full" @click="transactionShow = false">بازگشت</BasicButton>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</BasicActionSheet>
|
||||
<BasicActionSheet v-model:show="editShow">
|
||||
<div class="font-bold text-lg">قلک {{ info?.title }}</div>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<template v-slot:button>
|
||||
<div class="text-center text-red" @click="deleteFundFunction">
|
||||
حذف
|
||||
</div>
|
||||
<div class="text-center" @click="showDeleteConfirm = false">
|
||||
انصراف
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</DialogVant>
|
||||
<div class="fund-info-page-root">
|
||||
<DesktopFundInfoView v-show="isDesktop" />
|
||||
<MobileFundInfoView v-show="!isDesktop" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import { useFundInfoPage } from './composables/useFundInfoPage'
|
||||
import DesktopFundInfoView from './desktop/DesktopFundInfoView.vue'
|
||||
import MobileFundInfoView from './mobile/MobileFundInfoView.vue'
|
||||
|
||||
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 BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
||||
import { useWalletStore } from '@/store/walletStore'
|
||||
import ListAnim from '@/components/ListAnim.vue'
|
||||
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
|
||||
import DialogVant from '@/components/DialogVant.vue'
|
||||
import { useFundUserStore } from '@/store/fundStore'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
const info = ref<Fund>()
|
||||
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[0] === undefined || info.value?.wallet[0]?.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[0]?.gold
|
||||
}
|
||||
// return isDeposit.value ? userFormState.value > walletStore?.walletInfo?.gold : userFormState.value > info.value?.wallet[0]?.gold
|
||||
} else return true
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
async function initInfo() {
|
||||
try {
|
||||
loading.value = true
|
||||
info.value = await getFundById(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[0]?.id })
|
||||
// pagination.value.fund_id = infoId
|
||||
tableOutput.value = await getFundTransactionTable(pagination.value)
|
||||
items.value.push(...tableOutput.value.rows)
|
||||
// console.log(tableOutput.value)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function transaction() {
|
||||
formRef.value?.validateFields().then(async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
userFormState.fund_id = infoId
|
||||
isDeposit.value ? await fundDeposit(userFormState) : await fundWithdraw(userFormState)
|
||||
await initInfo()
|
||||
// await initTable()
|
||||
await fundStore.getMyFund()
|
||||
await walletStore.getMyWallet()
|
||||
fundStore = useWalletStore()
|
||||
walletStore = useWalletStore()
|
||||
message.success(` با موفقیت${isDeposit.value ? ' واریز' : ' برداشت'} شد`)
|
||||
transactionShow.value = false
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const itemImg = computed(() => {
|
||||
switch (info.value?.title) {
|
||||
case 'خرید لوازم منزل':
|
||||
return homeImg
|
||||
case 'هدیه':
|
||||
return giftImg
|
||||
case 'گردش':
|
||||
return circulationImg
|
||||
case 'ازدواج':
|
||||
return loveImg
|
||||
case 'تعمیرات':
|
||||
return repairsImg
|
||||
case 'پس انداز':
|
||||
return savingImg
|
||||
default:
|
||||
return savingImg
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
useFundInfoPage()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.fund-info-page-root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="bg-white wrap-fix py-3 px-3 text-black font-bold drop-shadow flex h-12 z-20">
|
||||
<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-gray-100 w-[120px] aspect-square flex justify-center items-center rounded-xl p-2">
|
||||
<ProductImage
|
||||
v-if="info"
|
||||
:product="info"
|
||||
img-class="max-h-full max-w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
</a-spin>
|
||||
<div class="mt-2 font-bold flex text-xs items-center text-gray-500">
|
||||
<div class="ml-1 text-base text-black">{{ 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 ProductImage from '@/components/ProductImage.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 { useFundInfoPageContext } from '../composables/useFundInfoPage'
|
||||
|
||||
const { info, items, goBack } = useFundInfoPageContext()
|
||||
</script>
|
||||
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="bg-white w-full py-3 px-3 text-black 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="bg-[#EFCED9] w-screen md:w-full h-[70vh] fixed md:absolute overflow-hidden">
|
||||
<img
|
||||
:src="fundHeaderBackground"
|
||||
alt=""
|
||||
class="mt-[-4rem] mr-10 max-w-[22rem] self-end pointer-events-none absolute bottom-0 left-0 opacity-20"
|
||||
/>
|
||||
<div class="pt-15 flex flex-col relative z-10">
|
||||
<div class="text-gray-700 font-bold mr-5 z-20">موجودی دارایی ها</div>
|
||||
|
||||
<div
|
||||
v-if="meta?.has_count_based_products"
|
||||
class="flex justify-center mt-4 w-full z-20"
|
||||
>
|
||||
محصولات:
|
||||
<div class="text-l mr-4 font-bold">{{ countBasedInventory }} عدد</div>
|
||||
</div>
|
||||
|
||||
<template v-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"
|
||||
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="w-70" />
|
||||
<div 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 Item from '../components/Item.vue'
|
||||
import { useFundPageContext } from '../composables/useFundPage'
|
||||
|
||||
const {
|
||||
meta,
|
||||
inventoryCards,
|
||||
metalTotals,
|
||||
primaryMetalTotal,
|
||||
cashWallet,
|
||||
totalRialValue,
|
||||
countBasedInventory,
|
||||
fundHeaderBackground,
|
||||
fundEmptyImage,
|
||||
fundEmptyText,
|
||||
formatMetalTotal,
|
||||
openFundInfo,
|
||||
goBack,
|
||||
} = useFundPageContext()
|
||||
</script>
|
||||
@@ -0,0 +1,41 @@
|
||||
<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-1'">
|
||||
کد کارت هدیه خود را در فیلد بالا وارد کنید و سپس دکمه ثبت را بزنید.
|
||||
</p>
|
||||
<p :class="variant === 'desktop' ? 'dgift-form__case-note' : '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 { loading } from '@/utils'
|
||||
import { required } from '@/utils/inputRules'
|
||||
import { useGiftCardPageContext } from '../composables/useGiftCardPage'
|
||||
|
||||
withDefaults(defineProps<{ variant?: 'mobile' | 'desktop' }>(), { variant: 'mobile' })
|
||||
|
||||
const { form, disableButton, registerGiftCard } = useGiftCardPageContext()
|
||||
</script>
|
||||
@@ -0,0 +1,42 @@
|
||||
import { computed, inject, provide, ref, type InjectionKey } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { registerCodeGiftCard } from '@/api/giftCard'
|
||||
import { loading } from '@/utils'
|
||||
|
||||
export interface GiftCardPageContext {
|
||||
form: ReturnType<typeof ref<{ code: string }>>
|
||||
disableButton: ReturnType<typeof computed<boolean>>
|
||||
registerGiftCard: () => Promise<void>
|
||||
}
|
||||
|
||||
export const GIFT_CARD_PAGE_CONTEXT_KEY: InjectionKey<GiftCardPageContext> = Symbol('giftCardPageContext')
|
||||
|
||||
export function useGiftCardPage() {
|
||||
const form = ref({ code: '' })
|
||||
|
||||
const disableButton = computed(() => {
|
||||
if (loading.value) return true
|
||||
return !form.value.code || form.value.code.trim().length === 0
|
||||
})
|
||||
|
||||
async function registerGiftCard() {
|
||||
try {
|
||||
loading.value = true
|
||||
await registerCodeGiftCard(form.value.code)
|
||||
message.success('کد کارت هدیه با موفقیت ثبت شد')
|
||||
form.value.code = ''
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const context: GiftCardPageContext = { form, disableButton, 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 useGiftCardPage')
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div class="dgift gift-card-page">
|
||||
<div class="dgift__container">
|
||||
<DesktopPageTopbar eyebrow="مالی" title="کارت هدیه" show-back sticky />
|
||||
|
||||
<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 DesktopPageTopbar from '@/layouts/components/DesktopPageTopbar.vue'
|
||||
import GiftCardForm from '../components/GiftCardForm.vue'
|
||||
</script>
|
||||
|
||||
<style lang="less" src="./styles/desktop-gift-card.less"></style>
|
||||
@@ -0,0 +1,144 @@
|
||||
/* 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;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dgift-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 300px);
|
||||
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(167, 43, 119, 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 8px;
|
||||
}
|
||||
|
||||
&__case-note {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #9ca3af;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
&__submit {
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.dgift-guide {
|
||||
padding: 24px;
|
||||
position: sticky;
|
||||
top: 88px;
|
||||
|
||||
&__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;
|
||||
}
|
||||
|
||||
.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,81 +1,23 @@
|
||||
<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-1">
|
||||
کد کارت هدیه خود را در فیلد بالا وارد کنید و سپس دکمه ثبت را بزنید.
|
||||
</div>
|
||||
<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 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
|
||||
}
|
||||
}
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
useGiftCardPage()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.gift-card-page-root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<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-1">
|
||||
کد کارت هدیه خود را در فیلد بالا وارد کنید و سپس دکمه ثبت را بزنید.
|
||||
</div>
|
||||
<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"
|
||||
>
|
||||
<a-spin v-if="loading" size="small" />
|
||||
<div v-else>ثبت کد</div>
|
||||
</BasicButton>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { loading } from '@/utils'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import Bg_main from '@/components/bg_main.vue'
|
||||
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
||||
import { required } from '@/utils/inputRules'
|
||||
import { useGiftCardPageContext } from '../composables/useGiftCardPage'
|
||||
|
||||
const { form, disableButton, registerGiftCard } = useGiftCardPageContext()
|
||||
</script>
|
||||
@@ -0,0 +1,31 @@
|
||||
<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 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'
|
||||
|
||||
defineEmits<{
|
||||
logout: []
|
||||
}>()
|
||||
|
||||
const userStore = useUserStore()
|
||||
</script>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,102 @@
|
||||
import { ref } from 'vue'
|
||||
import { getConstByKey } from '@/api/const'
|
||||
import { useSettingsStore } from '@/store/settingsStore'
|
||||
|
||||
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 []
|
||||
}
|
||||
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) {
|
||||
const settingsStore = useSettingsStore()
|
||||
if (!settingsStore.isSettingTruthy('banner_enabled')) {
|
||||
banners.value = []
|
||||
bannersLoaded = true
|
||||
return
|
||||
}
|
||||
|
||||
if (!force && bannersLoaded) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!force && bannersPromise) {
|
||||
return bannersPromise
|
||||
}
|
||||
|
||||
bannersPromise = (async () => {
|
||||
try {
|
||||
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() {
|
||||
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,81 @@
|
||||
import { ref } from 'vue'
|
||||
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'
|
||||
|
||||
/** فقط لودینگ صفحه اصلی — نه loading سراسری @/utils */
|
||||
const homePageLoading = ref(false)
|
||||
|
||||
let initPromise: Promise<void> | null = null
|
||||
const initialized = ref(false)
|
||||
|
||||
export function useHomePageLoading() {
|
||||
return { homePageLoading }
|
||||
}
|
||||
|
||||
export function isHomePageDataInitialized() {
|
||||
return initialized.value
|
||||
}
|
||||
|
||||
export async function initHomePageData(force = false) {
|
||||
if (!force && initialized.value) {
|
||||
startLastPricePolling()
|
||||
|
||||
if (getHomeLayoutId() === 'boursecu') {
|
||||
void loadHomeMaftolOffset(false)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!force && initPromise) {
|
||||
return initPromise
|
||||
}
|
||||
|
||||
initPromise = (async () => {
|
||||
try {
|
||||
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 {
|
||||
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
|
||||
homePageLoading.value = false
|
||||
stopLastPricePolling()
|
||||
resetHomeBanners()
|
||||
resetHomeFunds()
|
||||
resetHomePriceChart()
|
||||
clearProductsCatalog()
|
||||
}
|
||||
|
||||
export function useHomePageInitState() {
|
||||
return { initialized }
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { getAllPrices } from '@/api/home'
|
||||
import type { Product } from '@/models/products'
|
||||
import { getProductsCatalog, getProductsCatalogRef } from '@/composables/useProductsCatalog'
|
||||
import { getHomeLayoutId } from '../layouts/registry'
|
||||
import { getProductBuyPrice, resolvePriceOffsetRatio } from '@/utils/productPrice'
|
||||
import { isKatodMesGroup, isMaftolMesGroup, isShamshMesGroup } from '@/utils/productDealMethod'
|
||||
import { getTradeBuyPrice, getTradeSellPrice, lastPrice } from '@/utils/price'
|
||||
|
||||
export type ChartMetalType = 'katod' | 'maftol' | 'shamsh'
|
||||
|
||||
type ChartPoint = [number, number]
|
||||
|
||||
/** 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 catalogProducts = getProductsCatalogRef()
|
||||
|
||||
function resolveChartProducts(products: Product[]) {
|
||||
return {
|
||||
katod: findKatodProduct(products),
|
||||
maftol: findMaftolProduct(products),
|
||||
shamsh: findShamshProduct(products),
|
||||
}
|
||||
}
|
||||
|
||||
/** نسبت مفتول به کاتد — reactive به قیمت لحظهای و کاتالوگ محصولات */
|
||||
const maftolChartRatio = computed(() => {
|
||||
if (getHomeLayoutId() !== 'boursecu') {
|
||||
return 0
|
||||
}
|
||||
|
||||
void lastPrice.value?.price_last
|
||||
|
||||
const { katod, maftol } = resolveChartProducts(catalogProducts.value)
|
||||
return deriveMetalChartRatio(maftol, katod)
|
||||
})
|
||||
|
||||
/** نسبت شمش مس به کاتد */
|
||||
const shamshChartRatio = computed(() => {
|
||||
if (getHomeLayoutId() !== 'boursecu') {
|
||||
return 0
|
||||
}
|
||||
|
||||
void lastPrice.value?.price_last
|
||||
|
||||
const { katod, shamsh } = resolveChartProducts(catalogProducts.value)
|
||||
return deriveMetalChartRatio(shamsh, katod)
|
||||
})
|
||||
|
||||
const selectedMetalChartRatio = computed(() => {
|
||||
if (selectedChartMetal.value === 'maftol') {
|
||||
return maftolChartRatio.value
|
||||
}
|
||||
if (selectedChartMetal.value === 'shamsh') {
|
||||
return shamshChartRatio.value
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
let chartFetchPromise: Promise<void> | null = null
|
||||
let chartInitialized = false
|
||||
let metalOffsetPromise: Promise<void> | null = null
|
||||
let metalOffsetLoaded = false
|
||||
|
||||
function findKatodProduct(products: Product[]) {
|
||||
return (
|
||||
products.find((product) => isKatodMesGroup(product.group_type))
|
||||
?? products.find((product) => (product.title ?? '').includes('کاتد'))
|
||||
)
|
||||
}
|
||||
|
||||
function findMaftolProduct(products: Product[]) {
|
||||
return (
|
||||
products.find((product) => isMaftolMesGroup(product.group_type))
|
||||
?? products.find((product) => (product.title ?? '').includes('مفتول'))
|
||||
)
|
||||
}
|
||||
|
||||
function findShamshProduct(products: Product[]) {
|
||||
return products.find((product) =>
|
||||
isShamshMesGroup(product.group_type, product.title)
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
metalRatio: 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.metalRatio),
|
||||
low: applyChartRatio(baseLow, options.metalRatio),
|
||||
}
|
||||
}
|
||||
|
||||
function deriveMetalChartRatio(metalProduct?: Product, katodProduct?: Product) {
|
||||
const metalOffset = resolvePriceOffsetRatio(metalProduct?.price_offset_percent)
|
||||
if (metalOffset) {
|
||||
return metalOffset
|
||||
}
|
||||
|
||||
if (!metalProduct) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const liveBuy = getTradeBuyPrice(lastPrice.value)
|
||||
const katodLive = katodProduct ? getProductBuyPrice(katodProduct) : liveBuy
|
||||
const metalLive = getProductBuyPrice(metalProduct)
|
||||
|
||||
const katodStatic = Number(katodProduct?.price ?? 0) + Number(katodProduct?.price_profit ?? 0)
|
||||
const metalStatic = Number(metalProduct.price ?? 0) + Number(metalProduct.price_profit ?? 0)
|
||||
|
||||
const referencePrice = katodLive || liveBuy || katodStatic
|
||||
const derivedMetalPrice = metalLive || metalStatic
|
||||
|
||||
if (!referencePrice || !derivedMetalPrice || referencePrice === derivedMetalPrice) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return (derivedMetalPrice - 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 ratio = selectedMetalChartRatio.value
|
||||
const data =
|
||||
getHomeLayoutId() === 'boursecu' && selectedChartMetal.value !== 'katod'
|
||||
? rawChartPoints.value.map(
|
||||
([timestamp, value]) =>
|
||||
[timestamp, applyChartRatio(value, ratio)] as ChartPoint
|
||||
)
|
||||
: rawChartPoints.value
|
||||
|
||||
priceChartSeries.value[0].data = data
|
||||
}
|
||||
|
||||
watch([selectedChartMetal, maftolChartRatio, shamshChartRatio], () => {
|
||||
syncChartSeries()
|
||||
})
|
||||
|
||||
async function fetchPrices(force = false) {
|
||||
if (!force && chartInitialized && rawChartPoints.value.length > 0) {
|
||||
syncChartSeries()
|
||||
return
|
||||
}
|
||||
|
||||
if (!force && chartFetchPromise) {
|
||||
return chartFetchPromise
|
||||
}
|
||||
|
||||
chartFetchPromise = (async () => {
|
||||
try {
|
||||
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 loadMetalChartOffsets(force = false) {
|
||||
if (getHomeLayoutId() !== 'boursecu') {
|
||||
syncChartSeries()
|
||||
return
|
||||
}
|
||||
|
||||
if (!force && metalOffsetLoaded && catalogProducts.value.length > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!force && metalOffsetPromise) {
|
||||
return metalOffsetPromise
|
||||
}
|
||||
|
||||
metalOffsetPromise = (async () => {
|
||||
try {
|
||||
await getProductsCatalog(force)
|
||||
syncChartSeries()
|
||||
metalOffsetLoaded = true
|
||||
} catch (error) {
|
||||
console.error('Error loading boursecu chart metal ratios:', error)
|
||||
syncChartSeries()
|
||||
} finally {
|
||||
metalOffsetPromise = null
|
||||
}
|
||||
})()
|
||||
|
||||
return metalOffsetPromise
|
||||
}
|
||||
|
||||
function updateChartData(timeline: string) {
|
||||
chartSelection.value = timeline
|
||||
void fetchPrices(true)
|
||||
}
|
||||
|
||||
function setChartMetal(metal: ChartMetalType) {
|
||||
selectedChartMetal.value = metal
|
||||
|
||||
if (metal !== 'katod' && getHomeLayoutId() === 'boursecu' && !catalogProducts.value.length) {
|
||||
void loadMetalChartOffsets(true)
|
||||
}
|
||||
}
|
||||
|
||||
export function usePriceChart() {
|
||||
return {
|
||||
priceChartSeries,
|
||||
chartSelection,
|
||||
chartLoading,
|
||||
selectedChartMetal,
|
||||
maftolChartRatio,
|
||||
shamshChartRatio,
|
||||
selectedMetalChartRatio,
|
||||
updateChartData,
|
||||
fetchPrices,
|
||||
loadMaftolOffset: loadMetalChartOffsets,
|
||||
loadMetalChartOffsets,
|
||||
setChartMetal,
|
||||
}
|
||||
}
|
||||
|
||||
export function fetchHomeChartPrices(force = false) {
|
||||
return fetchPrices(force)
|
||||
}
|
||||
|
||||
export function loadHomeMaftolOffset(force = false) {
|
||||
return loadMetalChartOffsets(force)
|
||||
}
|
||||
|
||||
export function resetHomePriceChart() {
|
||||
priceChartSeries.value = [{ name: 'قیمت', data: [] }]
|
||||
rawChartPoints.value = []
|
||||
chartSelection.value = 'DAILY'
|
||||
selectedChartMetal.value = 'katod'
|
||||
chartInitialized = false
|
||||
chartFetchPromise = null
|
||||
metalOffsetLoaded = false
|
||||
metalOffsetPromise = null
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
<template>
|
||||
<div class="dhome">
|
||||
<div class="dhome__container">
|
||||
<DesktopHomeTopbar @logout="$emit('logout')" />
|
||||
|
||||
<HomeSkeleton v-if="loading && !userStore.userInfo" />
|
||||
|
||||
<template v-else-if="userStore.userInfo">
|
||||
<section class="dhome-hero-row">
|
||||
<div class="dhome-surface dhome-hero-price">
|
||||
<span class="dhome-label">قیمت خرید از کشکول گلد</span>
|
||||
<div class="dhome-hero-price__value-row">
|
||||
<span
|
||||
v-if="percentPriceChanged() !== 0"
|
||||
class="dhome-hero-price__delta"
|
||||
:class="percentPriceChanged() >= 0 ? 'dhome-hero-price__delta--up' : 'dhome-hero-price__delta--down'"
|
||||
>
|
||||
<BasicIcon
|
||||
:icon="percentPriceChanged() >= 0 ? 'vuesax-linear:arrow-up-2' : 'vuesax-linear:arrow-down-2'"
|
||||
size="14"
|
||||
/>
|
||||
<span class="dhome-hero-price__delta-value">{{ useTowDecimal(percentPriceChanged()) }}%</span>
|
||||
</span>
|
||||
<Vue3Odometer
|
||||
:value="goldVal ?? 0"
|
||||
class="dhome-hero-price__odometer o-text"
|
||||
/>
|
||||
<span class="dhome-hero-price__unit">ریال</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dhome-surface dhome-price-range">
|
||||
<article class="dhome-price-range__item">
|
||||
<span class="dhome-price-range__label">بیشترین امروز</span>
|
||||
<p class="dhome-price-range__value dhome-price-range__value--high">
|
||||
{{ usePriceWithoutType(price?.buy ?? 0) }} ریال
|
||||
</p>
|
||||
</article>
|
||||
<article class="dhome-price-range__item">
|
||||
<span class="dhome-price-range__label">کمترین امروز</span>
|
||||
<p class="dhome-price-range__value dhome-price-range__value--low">
|
||||
{{ usePriceWithoutType(price?.sell ?? 0) }} ریال
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="dhome-default-grid">
|
||||
<div class="dhome-default-grid__main">
|
||||
<section class="dhome-surface">
|
||||
<header class="dhome-section__header">
|
||||
<h2 class="dhome-section__title">عملیات</h2>
|
||||
</header>
|
||||
<div class="dhome-default-actions">
|
||||
<button
|
||||
v-for="action in mainActions"
|
||||
:key="action.name"
|
||||
type="button"
|
||||
class="dhome-default-action"
|
||||
@click="action.handler()"
|
||||
>
|
||||
<span class="dhome-default-action__icon">
|
||||
<BasicIcon :icon="action.icon" size="20" />
|
||||
</span>
|
||||
<span class="dhome-default-action__label">{{ action.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="dhome-surface dhome-chart-banner-combined">
|
||||
<div class="dhome-chart-banner-combined__grid">
|
||||
<div class="dhome-chart-panel">
|
||||
<header class="dhome-section__header dhome-chart-panel__header">
|
||||
<h2 class="dhome-section__title">نمودار قیمت</h2>
|
||||
</header>
|
||||
<div class="dhome-chart-panel__tabs">
|
||||
<button
|
||||
type="button"
|
||||
class="dhome-chart-panel__tab"
|
||||
:class="{ 'dhome-chart-panel__tab--active': selection === 'DAILY' }"
|
||||
@click="updateData('DAILY')"
|
||||
>
|
||||
24 ساعت اخیر
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="dhome-chart-panel__tab"
|
||||
:class="{ 'dhome-chart-panel__tab--active': selection === 'WEEKLY' }"
|
||||
@click="updateData('WEEKLY')"
|
||||
>
|
||||
هفته گذشته
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="dhome-chart-panel__tab"
|
||||
:class="{ 'dhome-chart-panel__tab--active': selection === 'MONTHLY' }"
|
||||
@click="updateData('MONTHLY')"
|
||||
>
|
||||
ماه گذشته
|
||||
</button>
|
||||
</div>
|
||||
<apexchart
|
||||
type="area"
|
||||
height="280"
|
||||
:options="priceChartOptions"
|
||||
:series="priceChartSeries"
|
||||
/>
|
||||
</div>
|
||||
<div class="dhome-banner-panel">
|
||||
<img :src="bannerImg" alt="" class="dhome-banner-panel__img" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<aside class="dhome-default-grid__side">
|
||||
<section class="dhome-surface dhome-assets-card">
|
||||
<h3 class="dhome-assets-card__title">داراییها</h3>
|
||||
<div class="dhome-assets-card__rows">
|
||||
<div class="dhome-assets-card__row">
|
||||
<span>دارایی طلایی</span>
|
||||
<strong>{{ balanceLabel }}</strong>
|
||||
</div>
|
||||
<div class="dhome-assets-card__row">
|
||||
<span>دارایی ریالی</span>
|
||||
<strong class="number-ltr">{{ usePriceWithoutType(totalRialDisplay) }} ریال</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="userStore.userInfo?.referral_id" class="dhome-surface dhome-referral">
|
||||
<h3 class="dhome-referral__title">دعوت از دوستان</h3>
|
||||
<p class="dhome-referral__code">{{ userStore.userInfo.referral_id }}</p>
|
||||
<div class="dhome-referral__actions">
|
||||
<button type="button" class="dhome-referral__btn" @click="shareReferral">
|
||||
<BasicIcon icon="vuesax-linear:share" size="16" />
|
||||
<span>دعوت</span>
|
||||
</button>
|
||||
<button type="button" class="dhome-referral__btn" @click="copyReferralCode">
|
||||
<BasicIcon icon="vuesax-linear:copy" size="16" />
|
||||
<span>رونوشت</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import Vue3Odometer from 'vue3-odometer'
|
||||
import 'odometer/themes/odometer-theme-default.css'
|
||||
import router from '@/router'
|
||||
import { getAllPrices } from '@/api/home'
|
||||
import { loading } from '@/utils'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { useWalletStore } from '@/store/walletStore'
|
||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
import { resolveAssetsRialValue } from '@/utils/productAssets'
|
||||
import { usePrice, usePriceWithoutType, useTowDecimal } from '@/utils/usePrice'
|
||||
import {
|
||||
getLastPrices,
|
||||
getTradeBuyPrice,
|
||||
getUpPrice,
|
||||
percentPriceChanged,
|
||||
price,
|
||||
setIntervalGetAllPrice,
|
||||
} from '@/utils/price'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import DesktopHomeTopbar from '@/views/base/home/components/DesktopHomeTopbar.vue'
|
||||
import HomeSkeleton from '@/views/base/home/components/HomeSkeleton.vue'
|
||||
import bannerImg from '@/assets/img/banner/2.png'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
const emit = defineEmits<{ logout: []; openBalance: [] }>()
|
||||
const userStore = useUserStore()
|
||||
const walletStore = useWalletStore()
|
||||
const { assetsSummary, loadWalletAssets } = useWalletAssets()
|
||||
|
||||
const goldVal = ref<number | undefined>()
|
||||
const selection = ref<'DAILY' | 'WEEKLY' | 'MONTHLY'>('DAILY')
|
||||
const priceChartSeries = ref([{ name: 'قیمت', data: [] as [number, number][] }])
|
||||
const priceChartOptions = ref({
|
||||
chart: { toolbar: { show: false }, zoom: { autoScaleYaxis: true } },
|
||||
stroke: { width: 2 },
|
||||
dataLabels: { enabled: false },
|
||||
yaxis: { show: false },
|
||||
xaxis: { type: 'datetime', labels: { show: false }, axisBorder: { show: false } },
|
||||
colors: ['#a72b77'],
|
||||
fill: {
|
||||
type: 'gradient',
|
||||
gradient: { shadeIntensity: 1, opacityFrom: 0.7, opacityTo: 0.9, stops: [0, 100] },
|
||||
},
|
||||
tooltip: { x: { format: 'dd/MM/yy HH:mm' } },
|
||||
})
|
||||
|
||||
const countBasedInventory = computed(() =>
|
||||
(assetsSummary.value?.count_products ?? []).reduce((sum, item) => sum + Number(item.quantity ?? 0), 0)
|
||||
)
|
||||
|
||||
const primaryMetalTotal = computed(() => assetsSummary.value?.metal_totals?.[0])
|
||||
|
||||
const balanceLabel = computed(() => {
|
||||
const meta = assetsSummary.value?.meta
|
||||
if (meta?.has_count_based_products && countBasedInventory.value > 0) {
|
||||
return `${countBasedInventory.value} عدد`
|
||||
}
|
||||
const metal = primaryMetalTotal.value
|
||||
if (metal && Number(metal.total_weight) > 0) {
|
||||
return `${Number(metal.total_weight).toFixed(3)} ${metal.unit}`
|
||||
}
|
||||
const fallback = walletStore.walletInfo?.gold_sum ?? userStore.userInfo?.wallet?.count_gold ?? 0
|
||||
return `${Number(fallback).toFixed(3)} گرم`
|
||||
})
|
||||
|
||||
const totalRialDisplay = computed(() =>
|
||||
resolveAssetsRialValue(assetsSummary.value, getTradeBuyPrice(price.value) || Number(goldVal.value ?? 0))
|
||||
)
|
||||
|
||||
const mainActions = [
|
||||
{ name: 'buySell', icon: 'vuesax-linear:money-receive', label: 'خرید و فروش', handler: () => router.push({ name: 'gold' }) },
|
||||
{ name: 'deposit', icon: 'vuesax-linear:card-add', label: 'واریز نقدی', handler: () => router.push({ name: 'wallet', query: { state: 'deposit' } }) },
|
||||
{ name: 'balance', icon: 'vuesax-linear:receipt-item', label: 'موجودی', handler: () => emitBalance() },
|
||||
{ name: 'fund', icon: 'vuesax-linear:archive-1', label: 'دارایی ها', handler: () => router.push({ name: 'fund' }) },
|
||||
{ name: 'earn', icon: 'vuesax-linear:dollar-circle', label: 'کسب درآمد', handler: () => router.push({ name: 'earnMoney' }) },
|
||||
]
|
||||
|
||||
function emitBalance() {
|
||||
emit('openBalance')
|
||||
}
|
||||
|
||||
async function fetchPrices() {
|
||||
try {
|
||||
const response = await getAllPrices()
|
||||
const data = response?.data?.prices ?? {}
|
||||
const selectedData = data[selection.value] ?? []
|
||||
priceChartSeries.value[0].data = selectedData.map((item: { timestamp: number; value: number }) => [
|
||||
item.timestamp,
|
||||
item.value - 1,
|
||||
])
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
function updateData(timeline: 'DAILY' | 'WEEKLY' | 'MONTHLY') {
|
||||
selection.value = timeline
|
||||
void fetchPrices()
|
||||
}
|
||||
|
||||
function copyReferralCode() {
|
||||
const code = userStore.userInfo?.referral_id
|
||||
if (code) {
|
||||
navigator.clipboard.writeText(code)
|
||||
message.info('کد رونوشت شد')
|
||||
}
|
||||
}
|
||||
|
||||
function shareReferral() {
|
||||
const text = `⭐️الان طلا بخر؛ آخر ماه سودشو ببر!
|
||||
این دعوت نامه از طرف "${userStore.userInfo?.name}" برای شما ارسال شده.
|
||||
kh.kashkolgold.ir/login?id=${userStore.userInfo?.referral_id}`
|
||||
navigator.share({ title: 'کشکول گلد', text })
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
await Promise.all([
|
||||
getLastPrices(),
|
||||
getUpPrice(),
|
||||
setIntervalGetAllPrice(),
|
||||
fetchPrices(),
|
||||
loadWalletAssets(true),
|
||||
userStore.userInfo ? Promise.resolve() : userStore.getMyInfo(),
|
||||
])
|
||||
goldVal.value = price.value?.price_last
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" src="./styles/desktop-home.less"></style>
|
||||
@@ -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,121 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import BoursecuAssetsSummary from '../../components/BoursecuAssetsSummary.vue'
|
||||
import { formatWeightAmount, usePriceWithoutType } from '@/utils/usePrice'
|
||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
import type { WalletMetalTotal } from '@/models/walletAssets'
|
||||
import { resolveSummaryMetalWeight } from '@/utils/productAssets'
|
||||
import { useHomePageContext } from '../../composables/useHomePage'
|
||||
|
||||
const emit = defineEmits<{
|
||||
openGoldModal: [metal: WalletMetalTotal, modalText?: string, modalTitle?: string, modalContent?: string]
|
||||
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 cashWallet = computed(() => assetsSummary.value?.cash_wallet ?? 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: `${usePriceWithoutType(countBasedInventory.value, false)} عدد`,
|
||||
})
|
||||
}
|
||||
|
||||
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(cashWallet.value)} ریال`,
|
||||
showInfo: true,
|
||||
onInfo: 'rial',
|
||||
})
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
function formatMetalTotal(metal: WalletMetalTotal) {
|
||||
return `${formatWeightAmount(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>
|
||||
<BoursecuAssetsSummary
|
||||
v-if="isBoursecuLayout"
|
||||
variant="desktop"
|
||||
icon-color="tertiary"
|
||||
/>
|
||||
|
||||
<div v-else 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,32 @@
|
||||
<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'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
embedded?: boolean
|
||||
}>(),
|
||||
{
|
||||
embedded: false,
|
||||
},
|
||||
)
|
||||
|
||||
const ctx = inject(HOME_PAGE_CONTEXT_KEY)!
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="embedded ? 'div' : DesktopHomeSection"
|
||||
v-if="ctx.bannerEnabled.value && ctx.banners.value?.length"
|
||||
:title="embedded ? undefined : 'بنرها'"
|
||||
class="dhome-banners"
|
||||
:class="{ 'dhome-banners--embedded': embedded }"
|
||||
>
|
||||
<BannerSlider
|
||||
:banners="ctx.banners.value"
|
||||
@bannerClick="ctx.handleBannerClick"
|
||||
/>
|
||||
</component>
|
||||
</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,40 @@
|
||||
<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 props = withDefaults(
|
||||
defineProps<{
|
||||
embedded?: boolean
|
||||
}>(),
|
||||
{
|
||||
embedded: false,
|
||||
},
|
||||
)
|
||||
|
||||
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>
|
||||
<component
|
||||
:is="props.embedded ? 'div' : DesktopHomeSection"
|
||||
:title="props.embedded ? undefined : 'نمودار قیمت'"
|
||||
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"
|
||||
/>
|
||||
</component>
|
||||
</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,38 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { usePriceWithoutType } from '@/utils/usePrice'
|
||||
import { isBoursecuProject } from '@/utils/config'
|
||||
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 isBoursecu = isBoursecuProject()
|
||||
|
||||
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="!isBoursecu && 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,169 @@
|
||||
<template>
|
||||
<div
|
||||
class="dhome-surface dhome-hero-price"
|
||||
:class="{
|
||||
'dhome-hero-price--boursecu-open': isBoursecu && displayState === 'open',
|
||||
'dhome-hero-price--boursecu-closed': isBoursecu && displayState !== 'open',
|
||||
}"
|
||||
>
|
||||
<template v-if="displayState !== 'open'">
|
||||
<div class="dhome-hero-price__closed" :class="{ 'dhome-hero-price__closed--default': !isBoursecu }">
|
||||
<div class="dhome-hero-price__closed-icon">
|
||||
<BasicIcon
|
||||
:icon="displayState === 'closed' ? 'vuesax-linear:lock' : 'vuesax-linear:info-circle'"
|
||||
size="22"
|
||||
:color="isBoursecu ? '#ffffff' : '#64748b'"
|
||||
/>
|
||||
</div>
|
||||
<p class="dhome-hero-price__closed-title">{{ statusTitle }}</p>
|
||||
<p class="dhome-hero-price__closed-hint">{{ statusHint }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<span class="dhome-label" :class="{ 'dhome-label--boursecu': isBoursecu }">
|
||||
{{ isBoursecu ? 'قیمت لحظهای کاتد مس' : 'قیمت لحظهای' }}
|
||||
</span>
|
||||
<p v-if="!isBoursecu" 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'"
|
||||
>
|
||||
<BasicIcon
|
||||
:icon="percentChange > 0 ? 'vuesax-linear:arrow-up-2' : 'vuesax-linear:arrow-down-2'"
|
||||
size="14"
|
||||
/>
|
||||
<span class="dhome-hero-price__delta-value">{{ formatSignedPercentChange(percentChange) }}</span>
|
||||
</span>
|
||||
<Vue3Odometer
|
||||
:value="displayGoldVal"
|
||||
class="dhome-hero-price__odometer o-text"
|
||||
:class="{ 'dhome-hero-price__odometer--boursecu': isBoursecu }"
|
||||
/>
|
||||
<span class="dhome-hero-price__unit" :class="{ 'dhome-hero-price__unit--boursecu': isBoursecu }">ریال</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import Vue3Odometer from 'vue3-odometer'
|
||||
import 'odometer/themes/odometer-theme-default.css'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { useConfigStore } from '@/store/configStore'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { isBoursecuProject } from '@/utils/config'
|
||||
import { getTradeBuyPrice, lastPrice, percentPriceChanged } from '@/utils/price'
|
||||
import {
|
||||
NO_QUOTE_MESSAGE,
|
||||
getPriceDisplayState,
|
||||
getStoreClosedMessage,
|
||||
} from '@/utils/storeHours'
|
||||
import { formatSignedPercentChange } from '@/utils/usePrice'
|
||||
|
||||
const config = useConfigStore()
|
||||
const userStore = useUserStore()
|
||||
const isBoursecu = isBoursecuProject()
|
||||
|
||||
const displayState = computed(() => getPriceDisplayState())
|
||||
|
||||
const statusTitle = computed(() =>
|
||||
displayState.value === 'closed' ? 'فروشگاه بسته است' : NO_QUOTE_MESSAGE
|
||||
)
|
||||
|
||||
const statusHint = computed(() => {
|
||||
if (displayState.value === 'closed') {
|
||||
const custom = userStore.getValueByKey('closed_message')
|
||||
if (typeof custom === 'string' && custom.trim()) {
|
||||
return custom.replace(/<[^>]*>/g, '').trim()
|
||||
}
|
||||
return getStoreClosedMessage()
|
||||
}
|
||||
return 'در حال حاضر مظنهای برای نمایش وجود ندارد.'
|
||||
})
|
||||
|
||||
const displayGoldVal = computed(() => {
|
||||
if (displayState.value !== 'open') return 0
|
||||
return getTradeBuyPrice(lastPrice.value)
|
||||
})
|
||||
|
||||
const percentChange = computed(() => {
|
||||
if (!getTradeBuyPrice(lastPrice.value)) return 0
|
||||
return percentPriceChanged()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.dhome-hero-price--boursecu-open {
|
||||
background: linear-gradient(135deg, #c98a4a 0%, #a86528 100%);
|
||||
border-color: transparent;
|
||||
box-shadow: 0 4px 14px rgba(168, 101, 40, 0.28);
|
||||
}
|
||||
|
||||
.dhome-hero-price--boursecu-closed {
|
||||
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
||||
border-color: transparent;
|
||||
box-shadow: 0 4px 14px rgba(45, 55, 72, 0.22);
|
||||
}
|
||||
|
||||
.dhome-label--boursecu {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.dhome-hero-price__odometer--boursecu {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.dhome-hero-price__unit--boursecu {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.dhome-hero-price__closed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dhome-hero-price__closed-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dhome-hero-price__closed-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.dhome-hero-price__closed-hint {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
}
|
||||
|
||||
.dhome-hero-price__closed--default {
|
||||
.dhome-hero-price__closed-icon {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.dhome-hero-price__closed-title {
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.dhome-hero-price__closed-hint {
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import CurrentPrice from '../../components/CurrentPrice.vue'
|
||||
import PriceStats from '../../components/PriceStats.vue'
|
||||
import DesktopHomeCard from './DesktopHomeCard.vue'
|
||||
</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" />
|
||||
</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, selectedMetalChartRatio } = usePriceChart()
|
||||
const isBoursecuLayout = computed(() => getHomeLayoutId() === 'boursecu')
|
||||
|
||||
const rangePrices = computed(() =>
|
||||
resolveMetalDayRangePrices({
|
||||
price: price.value,
|
||||
metal: selectedChartMetal.value,
|
||||
isBoursecuLayout: isBoursecuLayout.value,
|
||||
metalRatio: selectedMetalChartRatio.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,48 @@
|
||||
<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">
|
||||
<img
|
||||
v-if="action.iconSrc"
|
||||
:src="action.iconSrc"
|
||||
alt=""
|
||||
class="dhome-action__icon-image"
|
||||
/>
|
||||
<BasicIcon v-else :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,53 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { getToken } 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 { userInfo } = storeToRefs(userStore)
|
||||
const ctx = useHomePageContext()
|
||||
|
||||
const showContent = computed(() => Boolean(userInfo.value))
|
||||
const showSkeleton = computed(() => Boolean(getToken()) && !userInfo.value)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DesktopHomeShell>
|
||||
<DesktopHomeTopbar @logout="$emit('logout')" />
|
||||
<DesktopHomeAlerts />
|
||||
|
||||
<HomeSkeleton v-if="showSkeleton" />
|
||||
|
||||
<template v-if="showContent">
|
||||
<DesktopAssetsMetrics
|
||||
@open-gold-modal="ctx.openGoldModal"
|
||||
@open-rial-modal="ctx.openRialModal"
|
||||
/>
|
||||
|
||||
<section class="dhome-hero-row">
|
||||
<DesktopLivePrice />
|
||||
<DesktopPriceRange />
|
||||
</section>
|
||||
|
||||
<section class="dhome-main-grid">
|
||||
<DesktopChartSection />
|
||||
<DesktopQuickActionsPanel />
|
||||
</section>
|
||||
</template>
|
||||
</DesktopHomeShell>
|
||||
</template>
|
||||
@@ -0,0 +1,93 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { getToken } 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 { userInfo } = storeToRefs(userStore)
|
||||
const ctx = useHomePageContext()
|
||||
|
||||
const showContent = computed(() => Boolean(userInfo.value))
|
||||
const showSkeleton = computed(() => Boolean(getToken()) && !userInfo.value)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DesktopHomeShell>
|
||||
<DesktopHomeTopbar @logout="$emit('logout')" />
|
||||
<DesktopHomeAlerts />
|
||||
|
||||
<HomeSkeleton v-if="showSkeleton" />
|
||||
|
||||
<template v-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 />
|
||||
<section
|
||||
v-if="ctx.bannerEnabled.value && ctx.banners.value?.length"
|
||||
class="dhome-surface dhome-chart-banner-combined"
|
||||
>
|
||||
<header class="dhome-section__header">
|
||||
<h2 class="dhome-section__title">نمودار قیمت</h2>
|
||||
</header>
|
||||
<div class="dhome-chart-banner-combined__grid">
|
||||
<DesktopChartSection embedded />
|
||||
<DesktopBannersPanel embedded />
|
||||
</div>
|
||||
</section>
|
||||
<DesktopChartSection v-else />
|
||||
</div>
|
||||
|
||||
<aside class="dhome-default-grid__side">
|
||||
<DesktopGaroosiAssets />
|
||||
<DesktopReferralPanel />
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<DesktopDefaultActions />
|
||||
<section
|
||||
v-if="ctx.bannerEnabled.value && ctx.banners.value?.length"
|
||||
class="dhome-surface dhome-chart-banner-combined"
|
||||
>
|
||||
<header class="dhome-section__header">
|
||||
<h2 class="dhome-section__title">نمودار قیمت</h2>
|
||||
</header>
|
||||
<div class="dhome-chart-banner-combined__grid">
|
||||
<DesktopChartSection embedded />
|
||||
<DesktopBannersPanel embedded />
|
||||
</div>
|
||||
</section>
|
||||
<DesktopChartSection v-else />
|
||||
</template>
|
||||
</template>
|
||||
</DesktopHomeShell>
|
||||
</template>
|
||||
@@ -0,0 +1,714 @@
|
||||
/* 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;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
|
||||
&-value {
|
||||
direction: ltr;
|
||||
unicode-bidi: isolate;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
&--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;
|
||||
}
|
||||
|
||||
/* Chart + banners in one card */
|
||||
.dhome-chart-banner-combined {
|
||||
&__grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(320px, 38%);
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
}
|
||||
}
|
||||
|
||||
.dhome-banners {
|
||||
min-width: 0;
|
||||
|
||||
&--embedded {
|
||||
align-self: center;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
:deep(.home_appear_fade_in) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
:deep(.banner-swiper) {
|
||||
width: 100%;
|
||||
aspect-ratio: 2.8 / 1;
|
||||
max-height: 13rem;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.swiper-slide),
|
||||
:deep(.banner-img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.banner-img) {
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
&__icon-image {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
object-fit: contain;
|
||||
transform: scale(2.2);
|
||||
}
|
||||
|
||||
&: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(5, 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;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sidebar: assets + referral */
|
||||
.dhome-assets-card {
|
||||
padding: 20px;
|
||||
|
||||
&__title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
&__rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
|
||||
strong {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dhome-referral {
|
||||
padding: 20px;
|
||||
|
||||
&__title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
&__code {
|
||||
display: block;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-align: center;
|
||||
color: #111827;
|
||||
margin: 0 0 16px;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
padding: 10px 8px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||
border-radius: 10px;
|
||||
background: #f9fafb;
|
||||
color: #374151;
|
||||
font-family: inherit;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dhome-chart-panel {
|
||||
min-width: 0;
|
||||
|
||||
&__tabs {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
background: #f3f4f6;
|
||||
border-radius: 8px;
|
||||
padding: 3px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
&__tab {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 8px 14px;
|
||||
border-radius: 6px;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
|
||||
&--active {
|
||||
background: @App-primary-color;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dhome-banner-panel {
|
||||
align-self: stretch;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #f9fafb;
|
||||
min-height: 220px;
|
||||
|
||||
&__img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 220px;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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-chart-banner-combined__grid,
|
||||
.dhome-default-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dhome-default-actions {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
+126
-123
@@ -1,12 +1,14 @@
|
||||
<template>
|
||||
<div class=" mt-20">
|
||||
<!-- <div class="flex gap-7 overflow-x-scroll px-5">
|
||||
<ProductInventory v-for="item in productInventorys" :ProductItem="item" class="shadow-lg my-5" :key="item.id"></ProductInventory>
|
||||
</div>-->
|
||||
<div>
|
||||
<div class="home-page-root">
|
||||
<GhatrehHomeDesktopView
|
||||
v-show="isDesktop"
|
||||
@logout="showLogoutConfirm = true"
|
||||
@open-balance="openWholeBalanceVisible()"
|
||||
/>
|
||||
<div v-show="!isDesktop" class="mt-20">
|
||||
<div
|
||||
v-if="userStore.userInfo && !loading"
|
||||
class="bg-white w-screen md:w-auto h-fit px-5 m-[-1px]"
|
||||
v-if="!loading"
|
||||
class="bg-white w-full md:w-auto h-fit px-5 m-[-1px]"
|
||||
>
|
||||
<div
|
||||
v-if="limitedPrice !== undefined && userStore?.userInfo.trade_active !== 1"
|
||||
@@ -14,7 +16,7 @@
|
||||
>
|
||||
<Icon icon="vuesax-linear:info-circle" size="16" />
|
||||
حداقل مبلغ اولیه برای ثبت سفارش
|
||||
{{ usePriceWithoutType(limitedPrice.value as number ?? 0) }} ریال می باشد.
|
||||
{{ usePriceWithoutType(parseInt(limitedPrice.value) ?? 0) }} ریال می باشد.
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -34,7 +36,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-end mb-3">
|
||||
<div class="text-sm "></div>
|
||||
<div class="text-sm" />
|
||||
<div>
|
||||
<div class="text-xs opacity-75" style="justify-self: end;">شناسه شما در کشکول گلد</div>
|
||||
<div class="flex">
|
||||
@@ -53,14 +55,13 @@
|
||||
<div class="flex justify-between items-center font-semibold text-base">
|
||||
<div>
|
||||
<div class="text-xs opacity-75 mb-1">موجودی</div>
|
||||
<div>{{ (userStore?.userInfo?.wallet?.count_gold ?? 0).toFixed(3) }} گرم</div>
|
||||
<div>{{ balanceLabel }}</div>
|
||||
</div>
|
||||
<div class="w-px h-6 bg-white/30 mx-3"></div>
|
||||
<div>
|
||||
<div class="text-xs opacity-75 mb-1" style="justify-self: end;">معادل ریالی</div>
|
||||
<div>{{ usePriceWithoutType( ((userStore?.userInfo?.wallet?.count_gold ?? 0) * (goldVal ?? 0)).toFixed(0) ) }} ریال</div>
|
||||
<div>{{ usePriceWithoutType(totalRialDisplay) }} ریال</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -141,7 +142,7 @@
|
||||
>
|
||||
<Icon class="text-white font-bold" icon="vuesax-linear:archive-1" />
|
||||
</div>
|
||||
<div class="text-primary">قلک</div>
|
||||
<div class="text-primary">دارایی ها</div>
|
||||
</div>
|
||||
<div
|
||||
class="text-center w-full flex-flex-col items-center home_appear_fade_in_down"
|
||||
@@ -278,13 +279,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<swiper
|
||||
:slides-per-view="1"
|
||||
:space-between="5"
|
||||
>
|
||||
<swiper-slide><img class="img rounded-lg" :src="img2" /></swiper-slide>
|
||||
<swiper-slide><img class="img rounded-lg" :src="img2" /></swiper-slide>
|
||||
</swiper>
|
||||
<div class="home-banner w-full mt-5 overflow-hidden rounded-lg">
|
||||
<swiper
|
||||
:slides-per-view="1"
|
||||
:space-between="5"
|
||||
class="home-banner__swiper"
|
||||
>
|
||||
<swiper-slide class="home-banner__slide">
|
||||
<img class="home-banner__img rounded-lg" :src="img2" alt="" />
|
||||
</swiper-slide>
|
||||
<swiper-slide class="home-banner__slide">
|
||||
<img class="home-banner__img rounded-lg" :src="img2" alt="" />
|
||||
</swiper-slide>
|
||||
</swiper>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex mt-5 gap-1 bg-primary text-white font-bold justify-center cursor-pointer rounded-lg py-5"
|
||||
@@ -352,24 +360,12 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div v-else class="bg-white w-screen md:w-auto h-[80vh] px-5 m-[-1px] flex justify-center ">
|
||||
<div v-else class="bg-white w-full md:w-auto h-[80vh] px-5 m-[-1px] flex justify-center">
|
||||
<HomeSkeleton class="w-full" />
|
||||
</div>
|
||||
<!-- <div class="relative pt-15 overflow-hidden ">-->
|
||||
<!-- <div class="absolute left--12 top-0">-->
|
||||
<!-- <img :src="image">-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="bg-primary pb-30"-->
|
||||
<!-- style="background: linear-gradient(to bottom, #fff,#fff, #a72b77, #a72b77);"-->
|
||||
<!-- >-->
|
||||
<!-- <div class=" text-tertiary text-2xl pt-6 pr-5 pl-50" style="font-size: 26px;font-weight: 600; color: #a72b77" >ارسال کالا بعد از ثبت سفارش</div>-->
|
||||
<!-- <div class="pr-5 pl-8 pt-10 text-tertiary ">میتوانید با خرید محصولات بعد از ثبت سفارش درخواست ارسال را-->
|
||||
<!-- ثبت کرده و زیر 24 ساعت خرید خود را دریافت کنید-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
<div
|
||||
v-if="!isDesktop"
|
||||
:class="y > 50 ? 'bg-white shadow opacity-95' : ''"
|
||||
class="px-4 py-2 top-0 z-5 wrap-fix background-color-transition"
|
||||
>
|
||||
@@ -417,7 +413,6 @@
|
||||
<div class="text-center" @click="showLogoutConfirm = false">انصراف</div>
|
||||
</template>
|
||||
</DialogVant>
|
||||
</div>
|
||||
<BasicActionSheet v-model:show="wholeBalanceVisible">
|
||||
<div class="pb-20">
|
||||
<div>
|
||||
@@ -428,30 +423,22 @@
|
||||
class="flex justify-between"
|
||||
title="مجموعه کل دارایی طلایی"
|
||||
is-line
|
||||
:info="`در این قسمت مجموع کل وزن طلای آبشده (${(walletStore.walletInfo?.gold ?? 0)?.toFixed(3)}) + ارزش دارایی ها (${(walletStore.walletInfo?.treasure_sum ?? 0)?.toFixed(3)} ) را مشاهده میفرمایید.`"
|
||||
infoHead="مجموع کل دارایی طلایی"
|
||||
:value="parseFloat((walletStore.walletInfo?.gold_sum ?? 0).toFixed(2)) + ' گرم'"
|
||||
:value="balanceLabel"
|
||||
/>
|
||||
<!-- <a-divider class="!my-1" />
|
||||
<BasicInfoItem
|
||||
class="flex justify-between"
|
||||
title="میزان طلای آبشده"
|
||||
is-line
|
||||
:value="parseFloat((walletStore.walletInfo?.gold ?? 0)?.toFixed(3)) + ' گرم'"
|
||||
/>
|
||||
<a-divider class="!my-1" />
|
||||
<BasicInfoItem
|
||||
class="flex justify-between"
|
||||
title="ارزش گنج ها"
|
||||
is-line
|
||||
:value="parseFloat((walletStore.walletInfo?.treasure_sum ?? 0)?.toFixed(3)) + ' گرم'"
|
||||
/>-->
|
||||
<a-divider class="!my-1" />
|
||||
<BasicInfoItem
|
||||
v-if="countBasedInventory > 0"
|
||||
class="flex justify-between"
|
||||
title="تعداد محصولات"
|
||||
is-line
|
||||
:value="`${countBasedInventory} عدد`"
|
||||
/>
|
||||
<a-divider v-if="countBasedInventory > 0" class="!my-1" />
|
||||
<BasicInfoItem
|
||||
class="flex justify-between"
|
||||
title="دارایی کیف پول ریالی"
|
||||
is-line
|
||||
:value="usePrice(walletStore.walletInfo?.cash ?? 0)"
|
||||
:value="usePrice(assetsSummary?.cash_wallet ?? walletStore.walletInfo?.cash ?? 0)"
|
||||
/>
|
||||
<a-divider class="!my-1" />
|
||||
<BasicInfoItem
|
||||
@@ -468,14 +455,7 @@
|
||||
class="flex justify-between"
|
||||
title="ارزش کل دارایی شما"
|
||||
is-line
|
||||
:value="
|
||||
usePrice(
|
||||
(
|
||||
(walletStore.walletInfo?.gold_sum ?? 0) * (goldVal ?? 0) +
|
||||
(walletStore.walletInfo?.cash ?? 0)
|
||||
).toFixed(0)
|
||||
)
|
||||
"
|
||||
:value="usePrice(totalRialDisplay)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -502,7 +482,7 @@
|
||||
</a-button>
|
||||
</template>
|
||||
</BasicModal>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="tsx" setup>
|
||||
@@ -511,7 +491,7 @@ import 'swiper/swiper-bundle.css';
|
||||
import img2 from '@/assets/img/banner/2.png'
|
||||
import Icon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { usePrice, usePriceWithoutType, useTowDecimal } from '../../../utils/usePrice'
|
||||
import {computed, createVNode, onMounted, onUnmounted, ref, watch} from 'vue'
|
||||
import {computed, createVNode, onActivated, onMounted, onUnmounted, ref, watch} from 'vue'
|
||||
import router from '@/router'
|
||||
import * as echarts from 'echarts'
|
||||
import { useIntersectionObserver, useWindowScroll } from '@vueuse/core'
|
||||
@@ -523,8 +503,10 @@ import { getAllPrices, getChart } from '@/api/home'
|
||||
import Vue3Odometer from 'vue3-odometer'
|
||||
import 'odometer/themes/odometer-theme-default.css'
|
||||
import HomeSkeleton from '@/views/base/home/components/HomeSkeleton.vue'
|
||||
import GhatrehHomeDesktopView from '@/views/base/home/desktop/GhatrehHomeDesktopView.vue'
|
||||
import {
|
||||
getLastPrices,
|
||||
getTradeBuyPrice,
|
||||
getUpPrice,
|
||||
lastPrice,
|
||||
percentPriceChanged,
|
||||
@@ -537,9 +519,9 @@ import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
||||
import BasicInfoItem from '@/components/BasicPacks/BasicInfoItem.vue'
|
||||
import ProductInventory from '@/views/base/home/components/ProductInventory.vue'
|
||||
import type { PInventory, Product } from '@/models/products'
|
||||
import type { Fund } from '@/models/fund'
|
||||
import type { TableOutput } from '@/models/table'
|
||||
import { useFundUserStore } from '@/store/fundStore'
|
||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
import { resolveAssetsRialValue } from '@/utils/productAssets'
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import BasicModal from '@/components/BasicPacks/BasicModal.vue'
|
||||
@@ -547,6 +529,7 @@ import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const walletStore = useWalletStore()
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
const chartLoading = ref<boolean>(false)
|
||||
const openModal = ref<boolean>(false)
|
||||
const wholeBalanceVisible = ref<boolean>(false)
|
||||
@@ -557,23 +540,41 @@ const main = ref(null)
|
||||
const logoutLoading = ref(false)
|
||||
const productInventorys = ref<PInventory[] | undefined>()
|
||||
const value = ref<string>('a')
|
||||
const items = ref<Fund[]>([])
|
||||
const tableOutput = ref<TableOutput<Fund> | null>(null)
|
||||
const inventory = ref<number>(0)
|
||||
const sumGold = ref<number>(0)
|
||||
const fundStore = useFundUserStore()
|
||||
const { assetsSummary, loadWalletAssets } = useWalletAssets()
|
||||
|
||||
async function initTable() {
|
||||
try {
|
||||
loading.value = true
|
||||
tableOutput.value = fundStore.myFunds ?? await fundStore.getMyFund() ?? null
|
||||
items.value.push(...tableOutput.value?.rows ?? [])
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
const countBasedInventory = computed(() =>
|
||||
(assetsSummary.value?.count_products ?? []).reduce(
|
||||
(sum, item) => sum + Number(item.quantity ?? 0),
|
||||
0
|
||||
)
|
||||
)
|
||||
|
||||
const primaryMetalTotal = computed(() => assetsSummary.value?.metal_totals?.[0])
|
||||
|
||||
const balanceLabel = computed(() => {
|
||||
const meta = assetsSummary.value?.meta
|
||||
if (meta?.has_count_based_products && countBasedInventory.value > 0) {
|
||||
return `${countBasedInventory.value} عدد`
|
||||
}
|
||||
}
|
||||
|
||||
const metal = primaryMetalTotal.value
|
||||
if (metal && Number(metal.total_weight) > 0) {
|
||||
return `${Number(metal.total_weight).toFixed(3)} ${metal.unit}`
|
||||
}
|
||||
|
||||
const fallback =
|
||||
walletStore.walletInfo?.gold_sum ??
|
||||
userStore.userInfo?.wallet?.count_gold ??
|
||||
0
|
||||
return `${Number(fallback).toFixed(3)} گرم`
|
||||
})
|
||||
|
||||
const totalRialDisplay = computed(() =>
|
||||
resolveAssetsRialValue(
|
||||
assetsSummary.value,
|
||||
getTradeBuyPrice(price.value) || Number(goldVal.value ?? 0)
|
||||
)
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
stopPriceInterval()
|
||||
@@ -588,20 +589,9 @@ onMounted(() => {
|
||||
const goldVal = ref<number | undefined>()
|
||||
|
||||
function openWholeBalanceVisible() {
|
||||
items.value.forEach(x => {
|
||||
if (x.title !== 'آبشده نقدی')
|
||||
inventory.value += x?.wallet?.gold ?? 0
|
||||
})
|
||||
wholeBalanceVisible.value = true
|
||||
}
|
||||
|
||||
function sumOfGold() {
|
||||
items.value.forEach(x => {
|
||||
if (x.title !== 'آبشده نقدی')
|
||||
return sumGold.value += x?.wallet?.gold ?? 0
|
||||
})
|
||||
}
|
||||
|
||||
function update() {
|
||||
setInterval(() => {
|
||||
goldVal.value = lastPrice?.value.price_last
|
||||
@@ -859,42 +849,35 @@ async function initPrice() {
|
||||
}
|
||||
}
|
||||
|
||||
async function initProductInventory() {
|
||||
try {
|
||||
// loading.value = true
|
||||
|
||||
// let { rows } = await getProductInventory()
|
||||
// fundStore.myFunds
|
||||
// productInventorys.value = fundStore.myFunds?.rows
|
||||
productInventorys.value = items.value.sort((a, b) => {
|
||||
if (a.wallet?.gold ?? 0 < b.wallet?.gold ?? 0)
|
||||
return -1
|
||||
if (a.wallet?.gold ?? 0 > b.wallet?.gold ?? 0)
|
||||
return 1
|
||||
return 0
|
||||
})
|
||||
// console.log(productInventorys.value)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
// loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
openModal.value = false
|
||||
localStorage.setItem('hasSeenIntroModal', 'true')
|
||||
router.push({ name: 'earnMoney' })
|
||||
}
|
||||
|
||||
onActivated(() => {
|
||||
void loadWalletAssets(true)
|
||||
if (!userStore.userInfo) {
|
||||
void userStore.getMyInfo()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
await initTable()
|
||||
await initProductInventory()
|
||||
// await initChart()
|
||||
await initPrice()
|
||||
await fetchPrices()
|
||||
const tasks: Promise<unknown>[] = [
|
||||
initPrice(),
|
||||
fetchPrices(),
|
||||
loadWalletAssets(true),
|
||||
]
|
||||
|
||||
if (!userStore.userInfo) {
|
||||
tasks.push(userStore.getMyInfo())
|
||||
} else if (!walletStore.walletInfo && userStore.userInfo.wallet) {
|
||||
walletStore.walletInfo = userStore.userInfo.wallet
|
||||
}
|
||||
|
||||
await Promise.all(tasks)
|
||||
update()
|
||||
const hasSeenModal = localStorage.getItem('hasSeenIntroModal')
|
||||
if (!hasSeenModal) {
|
||||
@@ -957,6 +940,29 @@ function copyCode() {
|
||||
|
||||
<style lang="less">
|
||||
|
||||
.home-banner {
|
||||
width: 100%;
|
||||
|
||||
&__swiper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__slide {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 0 auto;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.odometer {
|
||||
font-size: 60px;
|
||||
text-align: center;
|
||||
@@ -982,7 +988,4 @@ apexchart {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
</style>
|
||||
@@ -0,0 +1,51 @@
|
||||
import { defineAsyncComponent, type Component } from 'vue'
|
||||
import { getConfigApp, isBoursecuProject } from '@/utils/config'
|
||||
import type { HomeLayoutConfig, HomeLayoutId } from './types'
|
||||
import { defaultHomeLayoutConfig } from './configs/default'
|
||||
import { boursecuHomeLayoutConfig } from './configs/boursecu'
|
||||
|
||||
export const DEFAULT_HOME_LAYOUT_ID: HomeLayoutId = 'default'
|
||||
|
||||
const HOME_LAYOUT_CONFIGS: Record<HomeLayoutId, HomeLayoutConfig> = {
|
||||
default: defaultHomeLayoutConfig,
|
||||
boursecu: boursecuHomeLayoutConfig,
|
||||
}
|
||||
|
||||
let cachedLayoutRenderer: Component | null = null
|
||||
|
||||
function getLayoutRenderer(): Component {
|
||||
if (!cachedLayoutRenderer) {
|
||||
cachedLayoutRenderer = defineAsyncComponent(() => import('./HomeLayoutRenderer.vue'))
|
||||
}
|
||||
|
||||
return cachedLayoutRenderer
|
||||
}
|
||||
|
||||
export function isHomeLayoutId(value: unknown): value is HomeLayoutId {
|
||||
return typeof value === 'string' && value in HOME_LAYOUT_CONFIGS
|
||||
}
|
||||
|
||||
export function getHomeLayoutId(): HomeLayoutId {
|
||||
const adminLayout = getConfigApp('home_layout')
|
||||
if (isHomeLayoutId(adminLayout)) {
|
||||
return adminLayout
|
||||
}
|
||||
|
||||
if (isBoursecuProject()) {
|
||||
return 'boursecu'
|
||||
}
|
||||
|
||||
return DEFAULT_HOME_LAYOUT_ID
|
||||
}
|
||||
|
||||
export function getHomeLayoutConfig(layoutId = getHomeLayoutId()): HomeLayoutConfig {
|
||||
return HOME_LAYOUT_CONFIGS[layoutId] ?? HOME_LAYOUT_CONFIGS[DEFAULT_HOME_LAYOUT_ID]
|
||||
}
|
||||
|
||||
export function resolveHomeLayoutComponent(_layoutId = getHomeLayoutId()): Component {
|
||||
return getLayoutRenderer()
|
||||
}
|
||||
|
||||
export function registerHomeLayoutConfig(layoutId: HomeLayoutId, config: HomeLayoutConfig) {
|
||||
HOME_LAYOUT_CONFIGS[layoutId] = config
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/** چیدمانهای صفحه اصلی — هر پروژه/ادمین میتواند یکی را انتخاب کند */
|
||||
export type HomeLayoutId = 'default' | 'boursecu'
|
||||
|
||||
export type HomeSectionId =
|
||||
| 'limitedPriceAlert'
|
||||
| 'garoosiAssets'
|
||||
| 'mainActions'
|
||||
| 'boursecuQuickActions'
|
||||
| 'referral'
|
||||
| 'assets'
|
||||
| 'marketClosed'
|
||||
| 'currentPrice'
|
||||
| 'banners'
|
||||
| 'priceChart'
|
||||
| 'priceStats'
|
||||
|
||||
export interface HomeQuickActionItem {
|
||||
id: string
|
||||
label: string
|
||||
icon: string
|
||||
iconSrc?: string
|
||||
route: { name: string; query?: Record<string, string> }
|
||||
}
|
||||
|
||||
export interface HomeLayoutSectionConfig {
|
||||
id: HomeSectionId
|
||||
visible?: boolean
|
||||
}
|
||||
|
||||
export interface HomeLayoutConfig {
|
||||
id: HomeLayoutId
|
||||
wrapperClass?: string
|
||||
outerSections?: HomeLayoutSectionConfig[]
|
||||
sections: HomeLayoutSectionConfig[]
|
||||
}
|
||||
|
||||
export interface HomeActionItem {
|
||||
name: string
|
||||
icon: string
|
||||
label: string
|
||||
handler: () => void
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<!-- v-show: breakpoint switches must not destroy layouts or re-run child mount hooks -->
|
||||
<MobileHomeView v-show="!isDesktop" @logout="$emit('logout')" />
|
||||
<HomeDesktopView v-show="isDesktop" @logout="$emit('logout')" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import MobileHomeView from '../mobile/MobileHomeView.vue'
|
||||
import HomeDesktopView from '../desktop/HomeDesktopView.vue'
|
||||
|
||||
defineEmits<{
|
||||
logout: []
|
||||
}>()
|
||||
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
</script>
|
||||
+2
-346
@@ -1,351 +1,7 @@
|
||||
<template>
|
||||
<div >
|
||||
<Transition>
|
||||
<div v-if="popVisible" class="top-0 bottom-0 bg-black wrap-fix z-2000 opacity-60"
|
||||
@click="popVisible = false">
|
||||
</div>
|
||||
</Transition>
|
||||
<Transition name="slide-fade">
|
||||
<div v-if="popVisible" class="wrap-fix bottom-11vh z-2000">
|
||||
<div class="relative flex flex-col items-center ">
|
||||
<div class="flex items-center justify-center bg-white rounded-full w-[30px] h-[30px] mb-2"
|
||||
@click="popVisible = !popVisible">
|
||||
<BasicIcon
|
||||
icon="vuesax-linear:close-circle"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<BasicButton icon-size="16" class="!font-bold w-15rem"
|
||||
@click="$router.push({name: 'user'}); popVisible = false">
|
||||
<div class="flex gap-2 items-center">
|
||||
<BasicIcon icon="vuesax-linear:user" size="16" class="absolute right-2" />
|
||||
<div>حساب کاربری</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
<!-- <BasicButton v-if="isToken()" icon-size="16" class="!font-bold w-15rem"-->
|
||||
<!-- @click="$router.push({name: 'notifyTrigger'}); popVisible = false">-->
|
||||
<!-- <div class="flex gap-2 items-center">-->
|
||||
<!-- <BasicIcon icon="vuesax-linear:bill" size="16" class="absolute right-2" />-->
|
||||
<!-- <div>هشدار قیمت</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </BasicButton>-->
|
||||
<!-- <BasicButton v-if="isToken()" icon-size="16" class="!font-bold w-15rem"-->
|
||||
<!-- @click="$router.push({name: 'transfer'}); popVisible = false">-->
|
||||
<!-- <div class="flex gap-2 items-center">-->
|
||||
<!-- <BasicIcon icon="vuesax-linear:card-send" size="16" class="absolute right-2" />-->
|
||||
<!-- <div>انتقال کشکول گلد</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </BasicButton>-->
|
||||
<BasicButton v-if="isToken()" icon-size="16" class="!font-bold w-15rem"
|
||||
@click="$router.push({name: 'physicalReceive'}); popVisible = false">
|
||||
<div class="flex gap-2 items-center">
|
||||
<BasicIcon icon="vuesax-linear:direct-inbox" size="16" class="absolute right-2" />
|
||||
<div>دریافت طلای فیزیکی</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
<BasicButton v-if="isToken()" icon-size="16" class="!font-bold w-15rem"
|
||||
@click="$router.push({name: 'buyProduct'}); popVisible = false">
|
||||
<div class="flex gap-2 items-center">
|
||||
<BasicIcon icon="vuesax-linear:coin" size="16" class="absolute right-2" />
|
||||
<div>خرید محصول</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
<BasicButton v-if="isToken()" icon-size="16" class="!font-bold w-15rem"
|
||||
@click="openReceivesList(); popVisible = false">
|
||||
<div class="flex gap-2 items-center">
|
||||
<BasicIcon icon="vuesax-linear:receipt-item" size="16" class="absolute right-2" />
|
||||
<div>لیست دریافتی ها</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
<BasicButton v-if="isToken()" icon-size="16" class="!font-bold w-15rem"
|
||||
@click="$router.push({name: 'giftCard'}); popVisible = false">
|
||||
<div class="flex gap-2 items-center">
|
||||
<BasicIcon icon="vuesax-linear:gift" size="16" class="absolute right-2" />
|
||||
<div>ثبت کارت هدیه</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
<BasicButton icon-size="16" class="!font-bold w-15rem"
|
||||
@click="$router.push({name: 'about'}); popVisible = false">
|
||||
<div class="flex gap-2 items-center">
|
||||
<BasicIcon icon="vuesax-linear:info-circle" size="16" class="absolute right-2" />
|
||||
<div>درباره ما</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
<BasicButton v-if="isToken()" icon-size="16" class="!font-bold w-15rem"
|
||||
@click="$router.push({name: 'rules'}); popVisible = false">
|
||||
<div class="flex gap-2 items-center">
|
||||
<BasicIcon icon="vuesax-linear:task" size="16" class="absolute right-2" />
|
||||
<div>قوانین و مقررات</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
<!-- <BasicButton icon-size="16" class="!font-bold w-50vw md:w-full"
|
||||
@click="$router.push({name: 'permissions'}); popVisible = false">
|
||||
<div class="flex gap-2 items-center">
|
||||
<BasicIcon icon="vuesax-linear:archive-book" size="16" class="absolute right-2" />
|
||||
<div>مجوز ها</div>
|
||||
</div>
|
||||
</BasicButton>-->
|
||||
<!-- <BasicButton icon-size="16" class="!font-bold w-50vw md:w-full"
|
||||
@click="$router.push({name: 'earnMoney'}); popVisible = false">
|
||||
<div class="flex gap-2 items-center">
|
||||
<BasicIcon icon="vuesax-linear:dollar-circle" size="16" class="absolute right-2" />
|
||||
<div>کسب درآمد</div>
|
||||
</div>
|
||||
</BasicButton>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
<div class=" !h-[calc(100vh-72px)] ">
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<!-- Use any custom transition and to `fade` -->
|
||||
<transition :name="route.meta.transition || 'fade'">
|
||||
<component :is="Component" class="!h-[calc(100vh-72px)]" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
<!-- <canvas ref="canvas" id="canvas1" width="400" height="80" class="w-full"></canvas>-->
|
||||
<div v-if="isToken()" class="bottom-appbar w-auto mx-auto">
|
||||
<div class="tabs">
|
||||
<div :class="setNavColor('home')" class="tab" @click="setRoute('home')">
|
||||
<Icon icon="vuesax-linear:home" />
|
||||
<div class="text-xs mt-1 font-bold">صفحه اصلی</div>
|
||||
</div>
|
||||
<!-- <div :class="setNavColor('buyProduct')" class="tab" @click="setRoute('buyProduct')">-->
|
||||
<!-- <Icon icon="vuesax-linear:coin" />-->
|
||||
<!-- <div class="text-xs mt-1 font-bold">فروشگاه</div>-->
|
||||
<!-- </div>-->
|
||||
<div :class="setNavColor('transactions')" class="tab" @click="setRoute('transactions')">
|
||||
<Icon icon="vuesax-linear:receipt-item" />
|
||||
<div class="text-xs mt-1 font-bold">گزارش</div>
|
||||
</div>
|
||||
<div class="main_in">
|
||||
<div class="text-white circle_in">
|
||||
<div class="circle_into " @click="popVisible = !popVisible">
|
||||
<img
|
||||
class="p-2"
|
||||
src="@/assets/img/logo.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div :class="setNavColor('buyProduct')" class="tab" @click="setRoute('buyProduct')">
|
||||
<Icon icon="vuesax-linear:coin" />
|
||||
<div class="text-xs mt-1 font-bold">خرید محصول</div>
|
||||
</div>-->
|
||||
<div :class="setNavColor('wallet')" class="tab" @click="setRoute('wallet')">
|
||||
<Icon icon="vuesax-linear:wallet-1" />
|
||||
<div class="text-xs mt-1 font-bold">کیف پول</div>
|
||||
</div>
|
||||
<!-- <div :class="setNavColor('physicalReceive')" class="tab" @click="setRoute('physicalReceive')">-->
|
||||
<!-- <Icon icon="vuesax-linear:direct-inbox" />-->
|
||||
<!-- <div class="text-xs mt-1 font-bold">دریافت فیزیکی</div>-->
|
||||
<!-- </div>-->
|
||||
<div :class="setNavColor('cart')" class="tab" @click="setRoute('cart')">
|
||||
<a-badge :count="cartStore.itemCount">
|
||||
<Icon icon="vuesax-linear:shopping-cart" />
|
||||
</a-badge>
|
||||
<div class="text-xs mt-1 font-bold">سبد خرید</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="circle">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="bottom-appbar w-auto mx-auto">
|
||||
<div class="tabs">
|
||||
<div :class="setNavColor('buyProduct')" class="tab" @click="setRoute('buyProduct')">
|
||||
<Icon icon="vuesax-linear:coin" />
|
||||
<div class="text-xs mt-1 font-bold">فروشگاه</div>
|
||||
</div>
|
||||
<div :class="setNavColor('about')" class="tab" @click="setRoute('about')">
|
||||
<Icon icon="vuesax-linear:info-circle" />
|
||||
<div class="text-xs mt-1 font-bold">درباره ما</div>
|
||||
</div>
|
||||
<div class="main_in">
|
||||
<div class="text-white circle_in">
|
||||
<div class="circle_into " @click="popVisible = !popVisible">
|
||||
<img
|
||||
class="p-2"
|
||||
src="@/assets/img/logo.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div :class="setNavColor('buyProduct')" class="tab" @click="setRoute('buyProduct')">
|
||||
<Icon icon="vuesax-linear:coin" />
|
||||
<div class="text-xs mt-1 font-bold">خرید محصول</div>
|
||||
</div>-->
|
||||
<div :class="setNavColor('cart')" class="tab" @click="setRoute('cart')">
|
||||
<a-badge :count="cartStore.itemCount">
|
||||
<Icon icon="vuesax-linear:shopping-cart" />
|
||||
</a-badge>
|
||||
<div class="text-xs mt-1 font-bold">سبد خرید</div>
|
||||
</div>
|
||||
<div :class="setNavColor('login')" class="tab" @click="setRoute('login')">
|
||||
<Icon icon="vuesax-linear:login" />
|
||||
<div class="text-xs mt-1 font-bold">ورود</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="circle">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<AppLayoutShell />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {computed, onMounted, ref} from 'vue'
|
||||
import Icon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import router from '@/router'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import type { Transaction } from '@/models/transaction'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import {isToken} from "@/utils";
|
||||
import { useCartStore } from '@/store/cartStore'
|
||||
|
||||
const cartStore = useCartStore()
|
||||
|
||||
const popVisible = ref(false)
|
||||
|
||||
const currentRoute = computed(() => router.currentRoute.value.name)
|
||||
|
||||
function setNavColor(val: string) {
|
||||
if (currentRoute.value === val) {
|
||||
return 'text-primary'
|
||||
} else {
|
||||
return 'text-gray-400'
|
||||
}
|
||||
}
|
||||
|
||||
function setRoute(val: string) {
|
||||
popVisible.value = false
|
||||
router.push({ name: val })
|
||||
}
|
||||
|
||||
function openReceivesList() {
|
||||
router.push({ name: 'list' })
|
||||
}
|
||||
onMounted(()=>{
|
||||
console.log(isToken())
|
||||
})
|
||||
import AppLayoutShell from '@/layouts/AppLayoutShell.vue'
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="less">
|
||||
|
||||
.bottom-appbar {
|
||||
z-index: 5000;
|
||||
height: 80px;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left:0;
|
||||
@media screen and (min-width: 426px) {
|
||||
width: 426px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
height: 90%;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
background: white;
|
||||
//background-color: red;
|
||||
justify-content: space-between;
|
||||
position: absolute;
|
||||
z-index: 400;
|
||||
//border: 1px dashed primary;
|
||||
box-shadow: 0px -2px 4px -2px #0000001A;
|
||||
|
||||
.tab {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
flex-flow: column;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.main_in {
|
||||
height: 70px;
|
||||
width: 70px;
|
||||
|
||||
.circle_in {
|
||||
margin-top: -10px;
|
||||
border-radius: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
//border: 1px dashed primary;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
flex-wrap: wrap;
|
||||
background-color: #a72b77;
|
||||
|
||||
|
||||
|
||||
.circle_into {
|
||||
overflow:hidden;
|
||||
@apply bg-primary;
|
||||
border-radius: 100%;
|
||||
width: 90%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
flex-wrap: wrap;
|
||||
height: 90%;
|
||||
border: white 1px solid;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.main {
|
||||
height: 70px;
|
||||
width: 70px;
|
||||
position: relative;
|
||||
|
||||
.circle {
|
||||
border-radius: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 20;
|
||||
box-shadow: 0px -5px 4px -2px #0000001A;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-enter-active,
|
||||
.v-leave-active {
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.v-enter-from,
|
||||
.v-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-fade-enter-active {
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.slide-fade-leave-active {
|
||||
transition: all 0.4s cubic-bezier(1, 0.5, 0.8, 1);
|
||||
}
|
||||
|
||||
.slide-fade-enter-from,
|
||||
.slide-fade-leave-to {
|
||||
transform: translateX(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,44 +1,59 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="absolute inset-0 bg-cover bg-center opacity-60" :style="{ backgroundImage: `url(${img})` }"></div>
|
||||
<div
|
||||
class="bg-white w-full transaction-list py-3 px-3 font-bold drop-shadow flex justify-between z-10"
|
||||
>
|
||||
<div>لیست دریافت فیزیکی</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 pt-13 pb-35 px-2">
|
||||
<RequestItem v-for="item in requestItems" :item="item"></RequestItem>
|
||||
</div>
|
||||
<Transition mode="out-in" name="fade">
|
||||
<div
|
||||
class="wrap-fix bottom-0 h-[19vh] bg-[#FBFBFC] shadow-lg rounded-3xl flex justify-center pt-3 z-200"
|
||||
style="box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 26px 11px"
|
||||
@click="openAdd()">
|
||||
<BasicIcon icon="vuesax-linear:add" />
|
||||
<div class="cursor-pointer">افزودن درخواست جدید</div>
|
||||
<div class="physical-receive-page-root">
|
||||
<DesktopPhysicalReceiveListView
|
||||
v-show="isDesktop"
|
||||
:request-items="requestItems"
|
||||
@add="openAdd"
|
||||
/>
|
||||
<div v-show="!isDesktop">
|
||||
<div class="absolute inset-0 bg-cover bg-center opacity-60" :style="{ backgroundImage: `url(${img})` }" />
|
||||
<div class="bg-white w-full transaction-list py-3 px-3 font-bold drop-shadow flex justify-between z-10">
|
||||
<div>لیست دریافت فیزیکی</div>
|
||||
</div>
|
||||
</Transition>
|
||||
<div class="flex flex-col gap-4 pt-13 pb-35 px-2">
|
||||
<RequestItem v-for="item in requestItems" :key="item.id" :item="item" />
|
||||
</div>
|
||||
<Transition mode="out-in" name="fade">
|
||||
<div
|
||||
class="wrap-fix bottom-0 h-[19vh] bg-[#FBFBFC] shadow-lg rounded-3xl flex justify-center pt-3 z-200"
|
||||
style="box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 26px 11px"
|
||||
@click="openAdd"
|
||||
>
|
||||
<BasicIcon icon="vuesax-linear:add" />
|
||||
<div class="cursor-pointer">افزودن درخواست جدید</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {onMounted, ref} from "vue";
|
||||
import {getPhysicalReceiveList} from "@/api/physicalreceive";
|
||||
import type {Physicalreceive} from "@/models/physicalreceive";
|
||||
import RequestItem from "@/views/base/physicalReceive/components/RequestItem.vue";
|
||||
import router from "@/router";
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getPhysicalReceiveList } from '@/api/physicalreceive'
|
||||
import type { Physicalreceive } from '@/models/physicalreceive'
|
||||
import RequestItem from '@/views/base/physicalReceive/components/RequestItem.vue'
|
||||
import router from '@/router'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import img from '@/assets/img/user_ly1.svg'
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import DesktopPhysicalReceiveListView from '../desktop/DesktopPhysicalReceiveListView.vue'
|
||||
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
const requestItems = ref<Physicalreceive[]>([])
|
||||
|
||||
const openAdd = () => {
|
||||
router.push('physicalReceive/new')
|
||||
router.push({ name: 'new' })
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
let {rows} = await getPhysicalReceiveList()
|
||||
const { rows } = await getPhysicalReceiveList()
|
||||
requestItems.value = rows
|
||||
console.log('rows',requestItems.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
.physical-receive-page-root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="dinner physical-receive-page">
|
||||
<div class="dinner__container">
|
||||
<DesktopPageTopbar title="لیست دریافت فیزیکی" show-back />
|
||||
|
||||
<div class="dphysical-list">
|
||||
<RequestItem v-for="item in requestItems" :key="item.id" :item="item" />
|
||||
<div v-if="requestItems.length === 0" class="dinner-surface p-8 text-center text-gray-500">
|
||||
هنوز درخواستی ثبت نشده است.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="dphysical-add" @click="$emit('add')">
|
||||
<BasicIcon icon="vuesax-linear:add" />
|
||||
افزودن درخواست جدید
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Physicalreceive } from '@/models/physicalreceive'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import DesktopPageTopbar from '@/layouts/components/DesktopPageTopbar.vue'
|
||||
import RequestItem from '../components/RequestItem.vue'
|
||||
|
||||
defineProps<{
|
||||
requestItems: Physicalreceive[]
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
add: []
|
||||
}>()
|
||||
</script>
|
||||
@@ -262,11 +262,12 @@ import type { Transaction } from '@/models/transaction'
|
||||
import type { TableEngineInput, TableOutput } from '@/models/table'
|
||||
import { getTransactionTable } from '@/api/transaction'
|
||||
import { getLastPrices, lastPrice } from '@/utils/price'
|
||||
import { getProductBaseBuyPrice, getProductBaseSellPrice } from '@/utils/productPrice'
|
||||
import { Product } from '@/models/products'
|
||||
import BasicSelect from '@/components/BasicPacks/BasicSelect.vue'
|
||||
import { SelectItem } from '@/models/SelectItem'
|
||||
import Bg_main from '@/components/bg_main.vue'
|
||||
import { useFundUserStore } from '@/store/fundStore'
|
||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
import { useCartStore } from '@/store/cartStore'
|
||||
|
||||
const typeDeal: SelectItem[] = [
|
||||
@@ -295,7 +296,7 @@ const sellItems = ref<Transaction[]>([])
|
||||
const priceErrorVisible = ref<boolean>(false)
|
||||
const buyTableOutput = ref<TableOutput<Transaction> | null>(null)
|
||||
const sellTableOutput = ref<TableOutput<Transaction> | null>(null)
|
||||
const fundStore = useFundUserStore()
|
||||
const { loadWalletAssets } = useWalletAssets()
|
||||
const refInput = ref<FormInstance>()
|
||||
const el = ref()
|
||||
|
||||
@@ -653,18 +654,9 @@ const price = computed((): Price => <Price>lastPrice.value ?? 0)
|
||||
|
||||
async function initPrice() {
|
||||
try {
|
||||
// chartLoading.value = true
|
||||
await getLastPrices()
|
||||
// await setIntervalGetPrice()
|
||||
loading.value = true
|
||||
price.value = lastPrice.value
|
||||
props.ProductItem.price = lastPrice.value?.buy * props.ProductItem?.amount
|
||||
props.ProductItem.sellPrice = lastPrice.value?.sell * props.ProductItem?.amount
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
// chartLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -715,7 +707,9 @@ async function ok() {
|
||||
infoVisible.value = true
|
||||
const data = await createOrder({
|
||||
direction: isBuy.value ? 1 : 2,
|
||||
price: isBuy.value ? price.value?.buy : price.value?.sell,
|
||||
price: isBuy.value
|
||||
? getProductBaseBuyPrice(props.ProductItem)
|
||||
: getProductBaseSellPrice(props.ProductItem),
|
||||
amount: form.value.goldValue ?? 0,
|
||||
gateway: form.value.gateway,
|
||||
product_id: props.ProductItem.id,
|
||||
@@ -753,7 +747,7 @@ async function ok() {
|
||||
}
|
||||
// refreshTable()
|
||||
message.success('سفارش شما با موفقیت انجام شد')
|
||||
await fundStore.getMyFund()
|
||||
await loadWalletAssets(true)
|
||||
} catch (e) {
|
||||
// console.log(e)
|
||||
// await initPrice()
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-2 !text-primary text-sm font-bold">
|
||||
{{ usePrice((props.ProductItem.price ?? 0) + (props.ProductItem.cash_fee_buy ?? 0)) }}
|
||||
{{ usePrice(getProductBuyPrice(props.ProductItem)) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,6 +104,7 @@
|
||||
import { computed } from 'vue'
|
||||
import type { Product } from '@/models/products'
|
||||
import { usePrice } from '@/utils/usePrice'
|
||||
import { getProductBuyPrice } from '@/utils/productPrice'
|
||||
import ProductImage from '@/components/ProductImage.vue'
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-auto !text-primary text-sm font-bold">
|
||||
{{ usePrice((props.ProductItem.price ?? 0) + (props.ProductItem.cash_fee_buy ?? 0)) }}
|
||||
{{ usePrice(getProductBuyPrice(props.ProductItem)) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -36,6 +36,7 @@
|
||||
import { computed } from 'vue'
|
||||
import type { Product } from '@/models/products'
|
||||
import { usePrice } from '@/utils/usePrice'
|
||||
import { getProductBuyPrice } from '@/utils/productPrice'
|
||||
import ProductImage from '@/components/ProductImage.vue'
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import { ref, watch } from 'vue'
|
||||
import { getProductsCatalog, getProductsCatalogRef } from '@/composables/useProductsCatalog'
|
||||
import type { Product } from '@/models/products'
|
||||
import { loadProductCategoryTabNames } from '@/utils/categoryTabs'
|
||||
import { getLastPrices, startLastPricePolling, stopLastPricePolling } from '@/utils/price'
|
||||
import {
|
||||
priceSocketConnected,
|
||||
priceStreamLifecycle,
|
||||
priceWssEnabled,
|
||||
shouldUsePricePolling,
|
||||
} from '@/utils/priceSocketState'
|
||||
import { subscribeProductListStream } from '@/utils/productStreamBridge'
|
||||
|
||||
let initPromise: Promise<void> | null = null
|
||||
let productListInterval: ReturnType<typeof setInterval> | undefined
|
||||
let providerMountCount = 0
|
||||
let pricePollingMountCount = 0
|
||||
let unsubscribeProductStream: (() => void) | undefined
|
||||
|
||||
const initialized = ref(false)
|
||||
|
||||
export const productsActiveKey = ref('همه')
|
||||
export const productsTypeItems = ref<string[]>([])
|
||||
export const productsFirstItems = ref<Product[]>()
|
||||
export const productsItems = ref<Product[]>()
|
||||
export const productsPageLoading = ref(true)
|
||||
|
||||
function sortProductsByInventory(products: Product[]) {
|
||||
return [...products].sort((a, b) => {
|
||||
const aInStock = Number(a.inventory ?? 0) > 0 ? 0 : 1
|
||||
const bInStock = Number(b.inventory ?? 0) > 0 ? 0 : 1
|
||||
if (aInStock !== bInStock) return aInStock - bInStock
|
||||
return Number(b.inventory ?? 0) - Number(a.inventory ?? 0)
|
||||
})
|
||||
}
|
||||
|
||||
export function changeProductsTab() {
|
||||
if (productsActiveKey.value !== 'همه') {
|
||||
productsItems.value = productsFirstItems.value?.filter(
|
||||
(val) => val.group_type === productsActiveKey.value,
|
||||
)
|
||||
} else {
|
||||
productsItems.value = productsFirstItems.value
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCategoryTabs() {
|
||||
try {
|
||||
productsTypeItems.value = await loadProductCategoryTabNames()
|
||||
} catch (error) {
|
||||
console.error('Product category tabs error:', error)
|
||||
productsTypeItems.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function applyProductsList(products: Product[]) {
|
||||
const sorted = sortProductsByInventory(products)
|
||||
productsFirstItems.value = sorted
|
||||
changeProductsTab()
|
||||
productsPageLoading.value = false
|
||||
}
|
||||
|
||||
export async function refreshProductsPageData() {
|
||||
try {
|
||||
const fresh = await getProductsCatalog(true)
|
||||
if (!fresh?.length) return
|
||||
|
||||
applyProductsList(fresh)
|
||||
} catch (error) {
|
||||
console.error('Products refresh error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export function applyProductsListFromSocket(products: Product[]) {
|
||||
if (!products?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
getProductsCatalogRef().value = products
|
||||
applyProductsList(products)
|
||||
}
|
||||
|
||||
function startProductListPolling() {
|
||||
if (productListInterval || !shouldUsePricePolling()) {
|
||||
return
|
||||
}
|
||||
|
||||
productListInterval = setInterval(() => {
|
||||
if (shouldUsePricePolling()) {
|
||||
void refreshProductsPageData()
|
||||
}
|
||||
}, 10000)
|
||||
}
|
||||
|
||||
function stopProductListPolling() {
|
||||
if (productListInterval) {
|
||||
clearInterval(productListInterval)
|
||||
productListInterval = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function syncProductListPolling() {
|
||||
if (shouldUsePricePolling()) {
|
||||
if (initialized.value) {
|
||||
startProductListPolling()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
stopProductListPolling()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [priceWssEnabled.value, priceSocketConnected.value, priceStreamLifecycle.value] as const,
|
||||
() => {
|
||||
syncProductListPolling()
|
||||
if (shouldUsePricePolling() && initialized.value && !productsFirstItems.value?.length) {
|
||||
void refreshProductsPageData()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export function onProductsPageProviderMounted() {
|
||||
providerMountCount += 1
|
||||
pricePollingMountCount += 1
|
||||
|
||||
unsubscribeProductStream = subscribeProductListStream(applyProductsListFromSocket)
|
||||
|
||||
if (pricePollingMountCount === 1) {
|
||||
if (shouldUsePricePolling()) {
|
||||
void getLastPrices()
|
||||
}
|
||||
startLastPricePolling()
|
||||
}
|
||||
}
|
||||
|
||||
export function onProductsPageProviderUnmounted() {
|
||||
unsubscribeProductStream?.()
|
||||
unsubscribeProductStream = undefined
|
||||
|
||||
providerMountCount = Math.max(0, providerMountCount - 1)
|
||||
pricePollingMountCount = Math.max(0, pricePollingMountCount - 1)
|
||||
|
||||
if (pricePollingMountCount === 0) {
|
||||
stopLastPricePolling()
|
||||
}
|
||||
|
||||
if (providerMountCount === 0) {
|
||||
stopProductListPolling()
|
||||
}
|
||||
}
|
||||
|
||||
export async function initProductsPageData(force = false) {
|
||||
if (!force && initialized.value) {
|
||||
syncProductListPolling()
|
||||
if (productsFirstItems.value?.length) {
|
||||
productsPageLoading.value = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!force && initPromise) {
|
||||
return initPromise
|
||||
}
|
||||
|
||||
initPromise = (async () => {
|
||||
try {
|
||||
if (!productsFirstItems.value?.length) {
|
||||
productsPageLoading.value = true
|
||||
}
|
||||
await loadCategoryTabs()
|
||||
|
||||
// stream-first: polling فقط وقتی SSE قطع/غیرفعال است
|
||||
if (shouldUsePricePolling()) {
|
||||
await refreshProductsPageData()
|
||||
}
|
||||
|
||||
initialized.value = true
|
||||
syncProductListPolling()
|
||||
} catch (error) {
|
||||
console.error('Products page initialization error:', error)
|
||||
} finally {
|
||||
// اگر استریم قبل از init لیست را داده، loading را دوباره true نکردهایم؛
|
||||
// اگر API/polling تمام شد یا محصول داریم، اسپینر را قطع کن.
|
||||
if (productsFirstItems.value?.length || shouldUsePricePolling()) {
|
||||
productsPageLoading.value = false
|
||||
}
|
||||
initPromise = null
|
||||
}
|
||||
})()
|
||||
|
||||
return initPromise
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
<template>
|
||||
<div class="bottom-0 fixed top-0 -z-10 pointer-events-none">
|
||||
<img :src="img" alt="" class=" w-inherit "
|
||||
:style="{ backgroundRepeat: 'repeat', }"
|
||||
/>
|
||||
</div>
|
||||
<a-spin :spinning="loading">
|
||||
<div :class="isDesktop ? 'dpage products-page' : ''">
|
||||
<div class="bottom-0 fixed top-0 -z-10 pointer-events-none">
|
||||
<img :src="img" alt="" class=" w-inherit "
|
||||
:style="{ backgroundRepeat: 'repeat', }"
|
||||
/>
|
||||
</div>
|
||||
<a-spin :spinning="productsPageLoading">
|
||||
<div class="relative z-0 !h-full overflow-y-scroll">
|
||||
<div class="w-full flex justify-center top-0 py-5" style="align-self: center">
|
||||
<ARadioGroup @change="changeTab" v-model:value="activekey" button-style="solid">
|
||||
<ARadioGroup @change="changeProductsTab" v-model:value="productsActiveKey" button-style="solid">
|
||||
<ARadioButton class="cats" value="همه">همه</ARadioButton>
|
||||
<ARadioButton class="cats" v-for="item in typeItems" :value="item">{{ item }}</ARadioButton>
|
||||
<ARadioButton class="cats" v-for="item in productsTypeItems" :key="item" :value="item">{{ item }}</ARadioButton>
|
||||
</ARadioGroup>
|
||||
</div>
|
||||
<div class="mx-5 flex justify-end" @click="viewVertical=!viewVertical">
|
||||
@@ -20,7 +21,7 @@
|
||||
<div class="mx-5 pb-30 pt-2 grid grid-cols-2 gap-3 items-stretch">
|
||||
<template v-if="viewVertical">
|
||||
<ProductItemVertical
|
||||
v-for="item in items"
|
||||
v-for="item in productsItems"
|
||||
:key="item.id"
|
||||
class="h-full"
|
||||
:ProductItem="item"
|
||||
@@ -29,7 +30,7 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<ProductItem
|
||||
v-for="item in items"
|
||||
v-for="item in productsItems"
|
||||
:key="item.id"
|
||||
class="col-span-2 h-full"
|
||||
:ProductItem="item"
|
||||
@@ -40,9 +41,6 @@
|
||||
|
||||
</div>
|
||||
</a-spin>
|
||||
<!-- <BasicActionSheet v-model:show="showFilter">-->
|
||||
<!-- <indexGold ></indexGold>-->
|
||||
<!-- </BasicActionSheet>-->
|
||||
|
||||
<BasicActionSheet placement="bottom" v-model:show="showFilter">
|
||||
<div class="">
|
||||
@@ -51,191 +49,65 @@
|
||||
</div>
|
||||
</BasicActionSheet>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import router from '@/router'
|
||||
import ProductItem from '@/views/base/products/components/ProductItem.vue'
|
||||
import { getProducts } from '@/api/products'
|
||||
import {
|
||||
getLastPrices,
|
||||
lastPrice,
|
||||
stopPriceInterval,
|
||||
setIntervalGetLastPrice
|
||||
} from '@/utils/price'
|
||||
import { loading, getToken } from '@/utils'
|
||||
import { Price } from '@/models/home'
|
||||
import type { Product } from '@/models/products'
|
||||
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
||||
import indexGold from '@/views/base/gold/index.vue'
|
||||
import DealProduct from '@/views/base/products/components/DealProduct.vue'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import img from "@/assets/img/user_ly1.svg";
|
||||
import { message } from 'ant-design-vue'
|
||||
import BasicButton from "@/App.vue";
|
||||
import BasicIcon from "@/components/BasicPacks/BasicIcon.vue";
|
||||
import ProductItemVertical from "@/views/base/products/components/ProductItemVertical.vue";
|
||||
import { loadProductCategoryTabNames } from '@/utils/categoryTabs'
|
||||
import { subscribeProductListStream } from '@/utils/productStreamBridge'
|
||||
import { isProductVisible } from '@/utils/productPrice'
|
||||
import {
|
||||
productsActiveKey,
|
||||
productsTypeItems,
|
||||
productsItems,
|
||||
productsPageLoading,
|
||||
changeProductsTab,
|
||||
initProductsPageData,
|
||||
onProductsPageProviderMounted,
|
||||
onProductsPageProviderUnmounted,
|
||||
} from '@/views/base/products/composables/useProductsPageData'
|
||||
|
||||
const activekey = ref('همه')
|
||||
const showFilter = ref()
|
||||
const selectedProduct = ref<Product>()
|
||||
const viewVertical = ref(true)
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
|
||||
function clickProduct(item: Product) {
|
||||
if(item?.show){
|
||||
// Navigate to product detail page
|
||||
if (isProductVisible(item)) {
|
||||
router.push({ name: 'productDetail', params: { id: item.id } })
|
||||
}
|
||||
}
|
||||
|
||||
function changeTab() {
|
||||
if (activekey.value != 'همه') {
|
||||
items.value = firstItems.value?.filter((val) =>
|
||||
val.group_type == activekey.value
|
||||
)
|
||||
} else items.value = firstItems.value
|
||||
}
|
||||
|
||||
function sortProductsByInventory(products: Product[] | undefined) {
|
||||
if (!products?.length) return products ?? []
|
||||
|
||||
return [...products].sort((a, b) => {
|
||||
const aInStock = Number(a.inventory ?? 0) > 0 ? 0 : 1
|
||||
const bInStock = Number(b.inventory ?? 0) > 0 ? 0 : 1
|
||||
if (aInStock !== bInStock) return aInStock - bInStock
|
||||
return Number(b.inventory ?? 0) - Number(a.inventory ?? 0)
|
||||
})
|
||||
}
|
||||
|
||||
async function loadCategoryTabs() {
|
||||
try {
|
||||
typeItems.value = await loadProductCategoryTabNames()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
typeItems.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const firstItems = ref<Product[]>()
|
||||
const price = ref<Price>()
|
||||
const items = ref<Product[]>()
|
||||
const typeItems = ref([])
|
||||
let unsubscribeProductStream: (() => void) | undefined
|
||||
|
||||
function applyStreamProducts(products: Product[]) {
|
||||
const sorted = sortProductsByInventory(products)
|
||||
firstItems.value = sorted
|
||||
changeTab()
|
||||
applyLivePricesToProducts(items.value)
|
||||
applyLivePricesToProducts(firstItems.value)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
unsubscribeProductStream = subscribeProductListStream(applyStreamProducts)
|
||||
onProductsPageProviderMounted()
|
||||
await initProductsPageData()
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const products = sortProductsByInventory(await getProducts())
|
||||
items.value = products
|
||||
firstItems.value = products
|
||||
await loadCategoryTabs()
|
||||
await initPrice()
|
||||
// await setIntervalGetLastPrice()
|
||||
// console.log('run product')
|
||||
|
||||
} catch (e: any) {
|
||||
console.log(e)
|
||||
// If 401 error, set empty items to show page without products
|
||||
if (e?.response?.status === 401) {
|
||||
items.value = []
|
||||
firstItems.value = []
|
||||
typeItems.value = []
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (router.currentRoute.value.query.cat=="coin"){
|
||||
activekey.value="سکه"
|
||||
changeTab()
|
||||
}
|
||||
|
||||
if (router.currentRoute.value.query.state) {
|
||||
if (router.currentRoute.value.query.product) {
|
||||
clickProduct(items.value?.find(x => x.title === 'سکه'))
|
||||
} else {
|
||||
clickProduct(items.value?.find(x => x.title === 'آبشده نقدی'))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
unsubscribeProductStream?.()
|
||||
stopPriceInterval()
|
||||
})
|
||||
|
||||
function applyLivePricesToProducts(products: Product[] | undefined) {
|
||||
if (!lastPrice.value || !products?.length) {
|
||||
return
|
||||
if (router.currentRoute.value.query.cat === 'coin') {
|
||||
productsActiveKey.value = 'سکه'
|
||||
changeProductsTab()
|
||||
}
|
||||
|
||||
products.forEach((val) => {
|
||||
if (val.group_type == 'آبشده') {
|
||||
val.price = parseInt(String(lastPrice.value?.price_last ?? 0))
|
||||
val.sellPrice = parseInt(String(lastPrice.value?.sell ?? 0))
|
||||
if (router.currentRoute.value.query.state) {
|
||||
if (router.currentRoute.value.query.product) {
|
||||
clickProduct(productsItems.value?.find((x) => x.title === 'سکه'))
|
||||
} else {
|
||||
val.price = parseInt(String(val.amount * lastPrice.value?.price_last))
|
||||
val.sellPrice = parseInt(String(val.amount * lastPrice.value?.sell))
|
||||
clickProduct(productsItems.value?.find((x) => x.title === 'آبشده نقدی'))
|
||||
}
|
||||
|
||||
syncProductShow(val)
|
||||
})
|
||||
}
|
||||
|
||||
/** show از API سرور — بعد از محاسبه قیمت زنده فقط اگر غیرفعال یا قیمت صفر باشد خاموش میشود */
|
||||
function syncProductShow(product: Product) {
|
||||
if (!product.active) {
|
||||
product.show = false
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
const displayPrice = Number(product.price ?? 0) + Number(product.cash_fee_buy ?? 0)
|
||||
if (displayPrice > 0) {
|
||||
product.show = true
|
||||
return
|
||||
}
|
||||
|
||||
product.show = Number(lastPrice.value?.price_last ?? 0) > 0
|
||||
}
|
||||
|
||||
watch(lastPrice, () => {
|
||||
applyLivePricesToProducts(items.value)
|
||||
applyLivePricesToProducts(firstItems.value)
|
||||
}, { deep: true })
|
||||
|
||||
async function initPrice() {
|
||||
try {
|
||||
// chartLoading.value = true
|
||||
await getLastPrices()
|
||||
await setIntervalGetLastPrice()
|
||||
loading.value = true
|
||||
price.value = lastPrice.value
|
||||
|
||||
applyLivePricesToProducts(items.value)
|
||||
applyLivePricesToProducts(firstItems.value)
|
||||
} catch (e: any) {
|
||||
console.log(e)
|
||||
// If 401 error, just skip price initialization
|
||||
if (e?.response?.status === 401) {
|
||||
// Price will not be available, but page can still be shown
|
||||
price.value = undefined
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
// chartLoading.value = false
|
||||
}
|
||||
}
|
||||
onUnmounted(() => {
|
||||
onProductsPageProviderUnmounted()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@@ -247,4 +119,4 @@ async function initPrice() {
|
||||
color: rgba(206, 163, 76, var(--un-text-opacity))!important;
|
||||
}
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,271 +1,31 @@
|
||||
<template>
|
||||
<div class="bottom-0 fixed top-0">
|
||||
<img :src="img" alt="" class="w-inherit" :style="{ backgroundRepeat: 'repeat' }"/>
|
||||
<div class="product-detail-page-root">
|
||||
<DesktopProductDetailView v-show="isDesktop" />
|
||||
<MobileProductDetailView v-show="!isDesktop" />
|
||||
|
||||
<BasicActionSheet placement="bottom" v-model:show="showFilter">
|
||||
<indexGold v-if="product?.group_type == 'آبشده'" :ProductItem="product" />
|
||||
<DealProduct v-else :ProductItem="product" @close="showFilter = false" />
|
||||
</BasicActionSheet>
|
||||
</div>
|
||||
<a-spin :spinning="loading">
|
||||
<div class="!h-full overflow-y-scroll">
|
||||
<div class="mx-5 pb-30 pt-5" style="color: #1b2b07">
|
||||
<!-- Product Image -->
|
||||
<div class="bg-gray-100 rounded-lg w-full flex justify-center mb-4">
|
||||
<ProductImage
|
||||
:product="product"
|
||||
:dimmed="isOutOfStock"
|
||||
img-class="max-w-full h-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Product Title -->
|
||||
<div class="text-2xl font-bold mb-4">{{ product?.title }}</div>
|
||||
<div class=" flex items-center justify-between mb-4 gap-4">
|
||||
<!-- Product Price -->
|
||||
<div class="text-xl font-semibold text-primary">
|
||||
{{ usePrice((product?.price ?? 0) + (product?.cash_fee_buy ?? 0)) }}
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<!-- Buy Button -->
|
||||
<BasicButton
|
||||
v-if="product?.show && product?.open_buy &&(product?.inventory || product?.inventory >0)"
|
||||
class="w-full py-3 "
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="handleBuyClick"
|
||||
>
|
||||
خرید محصول
|
||||
</BasicButton>
|
||||
<div v-else-if="!product?.show" class="text-center text-gray-500 py-4">
|
||||
این محصول در حال حاضر در دسترس نیست
|
||||
</div>
|
||||
<div v-else-if="!product?.open_buy" class="text-center text-gray-500 py-4">
|
||||
خرید این محصول در حال حاضر امکان پذیر نیست
|
||||
</div>
|
||||
<div v-else-if="!product?.inventory || product?.inventory==0" class="text-center text-gray-500 py-4">
|
||||
ناموجود
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Product Description -->
|
||||
<div v-if="product?.description" class="mb-6">
|
||||
<div class="text-lg font-semibold mb-2">توضیحات محصول:</div>
|
||||
<div class="text-base text-gray-700 whitespace-pre-line">{{ product?.description }}</div>
|
||||
<!-- <div class="text-base text-gray-700 whitespace-pre-line">شمش طلای نقش برجسته ونوس ۱ گرمی انتخابی ظریف و ارزشمند برای سرمایهگذاری و هدیه است. این شمش از طلای خالص با عیار بالا ساخته شده و نقش برجستهی الهه ونوس، نمادی از زیبایی و ظرافت، جلوهای هنری و خاص به آن بخشیده است. وزن دقیق ۱ گرم، ابعاد مناسب و بستهبندی شکیل، این محصول را به گزینهای ایدهآل برای نگهداری ارزش، هدیههای خاص و کلکسیونهای طلا تبدیل میکند. </div>-->
|
||||
</div>
|
||||
<!-- Similar Products -->
|
||||
<div v-if="product?.similar?.length" class="mt-8">
|
||||
<div class="text-lg font-semibold mb-4">محصولات مشابه</div>
|
||||
|
||||
<swiper
|
||||
:slides-per-view="2.25"
|
||||
:space-between="16"
|
||||
class="similar-products-swiper"
|
||||
>
|
||||
<swiper-slide
|
||||
v-for="item in product.similar"
|
||||
:key="item.id"
|
||||
>
|
||||
<div
|
||||
class="bg-white rounded-lg shadow p-4 cursor-pointer h-full"
|
||||
@click="goToProduct(item.id)"
|
||||
>
|
||||
<!-- Image -->
|
||||
<div class="bg-gray-100 rounded-md flex justify-center p-4 mb-2">
|
||||
<ProductImage
|
||||
:product="item"
|
||||
:dimmed="!item.inventory || item.inventory === 0"
|
||||
img-class="h-20 object-contain"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<div class="text-sm font-semibold mb-1 line-clamp-2">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<!-- Price -->
|
||||
<div class="text-sm text-primary font-bold mb-3">
|
||||
{{ usePrice((item.price ?? 0) + (item.cash_fee_buy ?? 0)) }}
|
||||
</div>
|
||||
<span
|
||||
v-if="item.inventory || item.inventory==0"
|
||||
class="text-xs bg-gray-300 text-gray-600 px-1 py-0.5 rounded-md "
|
||||
>
|
||||
ناموجود
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</swiper-slide>
|
||||
</swiper>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
|
||||
<!-- Basic Action Sheet for Buy -->
|
||||
<BasicActionSheet placement="bottom" v-model:show="showFilter">
|
||||
<div class="">
|
||||
<indexGold v-if="product?.group_type == 'آبشده'" :ProductItem="product"></indexGold>
|
||||
<DealProduct v-else :ProductItem="product" @close="showFilter = false"></DealProduct>
|
||||
</div>
|
||||
</BasicActionSheet>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {onMounted, onUnmounted, ref, computed, watch} from 'vue'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {getProductById, getProducts} from '@/api/products'
|
||||
import {loading, getToken} from '@/utils'
|
||||
import type {Product} from '@/models/products'
|
||||
<script lang="ts" setup>
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
||||
import indexGold from '@/views/base/gold/index.vue'
|
||||
import DealProduct from '@/views/base/products/components/DealProduct.vue'
|
||||
import {cloneDeep} from 'lodash'
|
||||
import img from '@/assets/img/user_ly1.svg'
|
||||
import {message} from 'ant-design-vue'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import {usePrice} from '@/utils/usePrice'
|
||||
import {
|
||||
getLastPrices,
|
||||
lastPrice,
|
||||
stopPriceInterval
|
||||
} from '@/utils/price'
|
||||
import {Swiper, SwiperSlide} from 'swiper/vue'
|
||||
import 'swiper/swiper-bundle.css'
|
||||
import ProductImage from '@/components/ProductImage.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const product = ref<Product>()
|
||||
const showFilter = ref(false)
|
||||
|
||||
const productId = computed(() => {
|
||||
const id = route.params.id
|
||||
return typeof id === 'string' ? parseInt(id) : id
|
||||
})
|
||||
|
||||
const isOutOfStock = computed(
|
||||
() => !product.value?.inventory || product.value.inventory === 0
|
||||
)
|
||||
|
||||
watch(
|
||||
() => productId.value,
|
||||
(newId, oldId) => {
|
||||
if (newId && newId !== oldId) {
|
||||
loadProduct()
|
||||
window.scrollTo({top: 0, behavior: 'smooth'})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
async function loadProduct() {
|
||||
try {
|
||||
loading.value = true
|
||||
// const products = await getProducts()
|
||||
const products = await getProductById(Number(productId.value))
|
||||
const foundProduct = products
|
||||
if (!foundProduct) {
|
||||
message.error('محصول یافت نشد')
|
||||
router.push({name: 'buyProduct'})
|
||||
return
|
||||
}
|
||||
product.value = foundProduct
|
||||
await initPrice()
|
||||
} catch (e: any) {
|
||||
console.log(e)
|
||||
if (e?.response?.status === 401) {
|
||||
message.warning('برای مشاهده محصولات، لطفا ابتدا وارد حساب کاربری خود شوید')
|
||||
router.push({name: 'login'})
|
||||
} else {
|
||||
message.error('خطا در بارگذاری محصول')
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(lastPrice, async () => {
|
||||
// console.log(items);
|
||||
if (lastPrice.value && product.value.similar) {
|
||||
product.value.similar.forEach((val) => {
|
||||
// console.log(lastPrice.value)
|
||||
if (val.group_type == 'آبشده') {
|
||||
val.price = lastPrice.value?.price_last
|
||||
val.sellPrice = lastPrice.value?.sell
|
||||
} else {
|
||||
val.price = parseInt(val.amount * lastPrice.value?.price_last)
|
||||
val.sellPrice = parseInt(val.amount * lastPrice.value?.sell)
|
||||
|
||||
// console.log(lastPrice.value?.price_last + "*" + val.amount + ' ' + val.price)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}, {deep: true})
|
||||
|
||||
async function initPrice() {
|
||||
try {
|
||||
await getLastPrices()
|
||||
loading.value = true
|
||||
if (lastPrice.value && product.value) {
|
||||
if (product.value.group_type == 'آبشده') {
|
||||
product.value.price = parseInt(lastPrice.value?.price_last)
|
||||
product.value.sellPrice = parseInt(lastPrice.value?.sell)
|
||||
} else {
|
||||
product.value.price = parseInt(product.value.amount * lastPrice.value?.price_last)
|
||||
product.value.sellPrice = parseInt(product.value.amount * lastPrice.value?.sell)
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleBuyClick() {
|
||||
if (!product.value?.show) {
|
||||
return
|
||||
}
|
||||
|
||||
// // Check if user is logged in
|
||||
// if (!getToken()) {
|
||||
// message.warning('برای خرید یا فروش محصولات، لطفا ابتدا وارد حساب کاربری خود شوید')
|
||||
// router.push({ name: 'login' })
|
||||
// return
|
||||
// }
|
||||
//
|
||||
showFilter.value = true
|
||||
product.value = cloneDeep(product.value)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadProduct()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopPriceInterval()
|
||||
})
|
||||
|
||||
function goToProduct(id: number) {
|
||||
router.push({name: 'productDetail', params: {id}})
|
||||
}
|
||||
import { useProductDetailPage } from './composables/useProductDetailPage'
|
||||
import DesktopProductDetailView from './desktop/DesktopProductDetailView.vue'
|
||||
import MobileProductDetailView from './mobile/MobileProductDetailView.vue'
|
||||
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
const { product, showFilter } = useProductDetailPage()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-primary {
|
||||
color: #d3a233;
|
||||
}
|
||||
|
||||
.similar-products-swiper {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.similar-products-swiper :deep(.swiper-slide) {
|
||||
height: auto;
|
||||
.product-detail-page-root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { computed, inject, onMounted, onUnmounted, provide, ref, watch, type InjectionKey } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { getProductById } from '@/api/products'
|
||||
import { findProductInCatalog, getProductsCatalog } from '@/composables/useProductsCatalog'
|
||||
import { getLastPrices, stopPriceInterval } from '@/utils/price'
|
||||
import { getProductBuyPrice } from '@/utils/productPrice'
|
||||
import { loading } from '@/utils'
|
||||
import type { Product } from '@/models/products'
|
||||
|
||||
export interface ProductDetailPageContext {
|
||||
product: ReturnType<typeof ref<Product | undefined>>
|
||||
showFilter: ReturnType<typeof ref<boolean>>
|
||||
isOutOfStock: ReturnType<typeof computed<boolean>>
|
||||
handleBuyClick: () => void
|
||||
goToProduct: (id: number) => void
|
||||
getProductBuyPrice: typeof getProductBuyPrice
|
||||
}
|
||||
|
||||
export const PRODUCT_DETAIL_PAGE_CONTEXT_KEY: InjectionKey<ProductDetailPageContext> =
|
||||
Symbol('productDetailPageContext')
|
||||
|
||||
export function useProductDetailPage() {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const product = ref<Product>()
|
||||
const showFilter = ref(false)
|
||||
|
||||
const productId = computed(() => {
|
||||
const id = route.params.id
|
||||
return typeof id === 'string' ? parseInt(id) : Number(id)
|
||||
})
|
||||
|
||||
const isOutOfStock = computed(() => !product.value?.inventory || product.value.inventory === 0)
|
||||
|
||||
watch(productId, (newId, oldId) => {
|
||||
if (newId && newId !== oldId) {
|
||||
void loadProduct()
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
})
|
||||
|
||||
async function loadProduct() {
|
||||
try {
|
||||
loading.value = true
|
||||
await getProductsCatalog()
|
||||
let foundProduct = findProductInCatalog(Number(productId.value))
|
||||
if (!foundProduct) foundProduct = await getProductById(Number(productId.value))
|
||||
if (!foundProduct) {
|
||||
message.error('محصول یافت نشد')
|
||||
await router.push({ name: 'buyProduct' })
|
||||
return
|
||||
}
|
||||
product.value = foundProduct
|
||||
await getLastPrices()
|
||||
} catch (e: any) {
|
||||
if (e?.response?.status === 401) {
|
||||
message.warning('برای مشاهده محصولات، لطفا ابتدا وارد حساب کاربری خود شوید')
|
||||
await router.push({ name: 'login' })
|
||||
} else {
|
||||
message.error('خطا در بارگذاری محصول')
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleBuyClick() {
|
||||
if (!product.value?.show) return
|
||||
showFilter.value = true
|
||||
product.value = cloneDeep(product.value)
|
||||
}
|
||||
|
||||
function goToProduct(id: number) {
|
||||
router.push({ name: 'productDetail', params: { id } })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadProduct()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopPriceInterval()
|
||||
})
|
||||
|
||||
const context: ProductDetailPageContext = {
|
||||
product,
|
||||
showFilter,
|
||||
isOutOfStock,
|
||||
handleBuyClick,
|
||||
goToProduct,
|
||||
getProductBuyPrice,
|
||||
}
|
||||
|
||||
provide(PRODUCT_DETAIL_PAGE_CONTEXT_KEY, context)
|
||||
return context
|
||||
}
|
||||
|
||||
export function useProductDetailPageContext() {
|
||||
const context = inject(PRODUCT_DETAIL_PAGE_CONTEXT_KEY)
|
||||
if (!context) throw new Error('useProductDetailPageContext must be used within useProductDetailPage')
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="dinner product-detail-page">
|
||||
<div class="dinner__container">
|
||||
<DesktopPageTopbar :title="product?.title ?? 'جزئیات محصول'" show-back sticky />
|
||||
|
||||
<a-spin :spinning="loading">
|
||||
<div v-if="product" class="dproduct-grid">
|
||||
<div class="dinner-surface dproduct-gallery">
|
||||
<ProductImage
|
||||
:product="product"
|
||||
:dimmed="isOutOfStock"
|
||||
img-class="max-w-full max-h-[320px] object-contain"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="dproduct-info">
|
||||
<div class="dinner-surface dproduct-info__panel">
|
||||
<div class="dproduct-info__price-row">
|
||||
<span class="dproduct-info__price">{{ usePrice(getProductBuyPrice(product)) }}</span>
|
||||
<BasicButton
|
||||
v-if="product.show && product.open_buy && product.inventory"
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="handleBuyClick"
|
||||
>
|
||||
خرید محصول
|
||||
</BasicButton>
|
||||
<div v-else-if="!product.show" class="text-gray-500">این محصول در حال حاضر در دسترس نیست</div>
|
||||
<div v-else-if="!product.open_buy" class="text-gray-500">خرید این محصول در حال حاضر امکان پذیر نیست</div>
|
||||
<div v-else class="text-gray-500">ناموجود</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="product.description" class="dinner-surface dproduct-info__desc">
|
||||
<h3 class="font-semibold mb-3">توضیحات محصول:</h3>
|
||||
<div class="whitespace-pre-line">{{ product.description }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="product.similar?.length" class="dinner-surface p-5">
|
||||
<h3 class="font-semibold mb-4">محصولات مشابه</h3>
|
||||
<div class="dproduct-similar-grid">
|
||||
<article
|
||||
v-for="item in product.similar"
|
||||
:key="item.id"
|
||||
class="dproduct-similar-card"
|
||||
@click="goToProduct(item.id)"
|
||||
>
|
||||
<div class="dproduct-similar-card__image">
|
||||
<ProductImage
|
||||
:product="item"
|
||||
:dimmed="!item.inventory"
|
||||
img-class="h-20 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-sm font-semibold line-clamp-2">{{ item.title }}</div>
|
||||
<div class="text-sm text-primary font-bold">{{ usePrice(getProductBuyPrice(item)) }}</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import ProductImage from '@/components/ProductImage.vue'
|
||||
import DesktopPageTopbar from '@/layouts/components/DesktopPageTopbar.vue'
|
||||
import { loading } from '@/utils'
|
||||
import { usePrice } from '@/utils/usePrice'
|
||||
import { useProductDetailPageContext } from '../composables/useProductDetailPage'
|
||||
|
||||
const { product, isOutOfStock, handleBuyClick, goToProduct, getProductBuyPrice } = useProductDetailPageContext()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dproduct-info__panel {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dproduct-similar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dproduct-similar-card {
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.06);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
&__image {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #d3a233;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="bottom-0 fixed top-0">
|
||||
<img :src="img" alt="" class="w-inherit" :style="{ backgroundRepeat: 'repeat' }" />
|
||||
</div>
|
||||
<a-spin :spinning="loading">
|
||||
<div class="!h-full overflow-y-scroll">
|
||||
<div class="mx-5 pb-30 pt-5" style="color: #1b2b07">
|
||||
<div class="bg-gray-100 rounded-lg w-full flex justify-center mb-4">
|
||||
<ProductImage :product="product" :dimmed="isOutOfStock" img-class="max-w-full h-auto" />
|
||||
</div>
|
||||
|
||||
<div class="text-2xl font-bold mb-4">{{ product?.title }}</div>
|
||||
<div class="flex items-center justify-between mb-4 gap-4">
|
||||
<div class="text-xl font-semibold text-primary">
|
||||
{{ usePrice(getProductBuyPrice(product)) }}
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<BasicButton
|
||||
v-if="product?.show && product?.open_buy && product?.inventory"
|
||||
class="w-full py-3"
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="handleBuyClick"
|
||||
>
|
||||
خرید محصول
|
||||
</BasicButton>
|
||||
<div v-else-if="!product?.show" class="text-center text-gray-500 py-4">
|
||||
این محصول در حال حاضر در دسترس نیست
|
||||
</div>
|
||||
<div v-else-if="!product?.open_buy" class="text-center text-gray-500 py-4">
|
||||
خرید این محصول در حال حاضر امکان پذیر نیست
|
||||
</div>
|
||||
<div v-else class="text-center text-gray-500 py-4">ناموجود</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="product?.description" class="mb-6">
|
||||
<div class="text-lg font-semibold mb-2">توضیحات محصول:</div>
|
||||
<div class="text-base text-gray-700 whitespace-pre-line">{{ product.description }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="product?.similar?.length" class="mt-8">
|
||||
<div class="text-lg font-semibold mb-4">محصولات مشابه</div>
|
||||
<swiper :slides-per-view="2.25" :space-between="16" class="similar-products-swiper">
|
||||
<swiper-slide v-for="item in product.similar" :key="item.id">
|
||||
<div class="bg-white rounded-lg shadow p-4 cursor-pointer h-full" @click="goToProduct(item.id)">
|
||||
<div class="bg-gray-100 rounded-md flex justify-center p-4 mb-2">
|
||||
<ProductImage
|
||||
:product="item"
|
||||
:dimmed="!item.inventory"
|
||||
img-class="h-20 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-sm font-semibold mb-1 line-clamp-2">{{ item.title }}</div>
|
||||
<div class="text-sm text-primary font-bold mb-3">
|
||||
{{ usePrice(getProductBuyPrice(item)) }}
|
||||
</div>
|
||||
</div>
|
||||
</swiper-slide>
|
||||
</swiper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import img from '@/assets/img/user_ly1.svg'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import ProductImage from '@/components/ProductImage.vue'
|
||||
import { loading } from '@/utils'
|
||||
import { usePrice } from '@/utils/usePrice'
|
||||
import { Swiper, SwiperSlide } from 'swiper/vue'
|
||||
import 'swiper/swiper-bundle.css'
|
||||
import { useProductDetailPageContext } from '../composables/useProductDetailPage'
|
||||
|
||||
const { product, isOutOfStock, handleBuyClick, goToProduct, getProductBuyPrice } = useProductDetailPageContext()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-primary {
|
||||
color: #d3a233;
|
||||
}
|
||||
.similar-products-swiper {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.similar-products-swiper :deep(.swiper-slide) {
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div
|
||||
:class="variant === 'desktop' ? 'drules-content' : 'rules-panel mt-5 border-solid border-2 border-primary text-primary rounded-[12px] p-4'"
|
||||
>
|
||||
<div :class="variant === 'desktop' ? 'drules-content__title' : 'font-bold text-sm text-primary'">
|
||||
{{ rulesTitle }}
|
||||
</div>
|
||||
<div :class="variant === 'desktop' ? 'drules-content__divider' : 'w-full h-1px my-3 bg-secondary'" />
|
||||
<div
|
||||
v-for="(block, blockIndex) in rulesBlocks"
|
||||
:key="`block-${blockIndex}`"
|
||||
:class="variant === 'desktop' ? 'drules-content__block' : 'rules-block'"
|
||||
>
|
||||
<p
|
||||
v-for="(rule, index) in block.items"
|
||||
:key="`rule-${blockIndex}-${index}`"
|
||||
:class="variant === 'desktop' ? 'drules-content__paragraph' : 'rules-paragraph'"
|
||||
>
|
||||
{{ rule }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRulesPageContext } from '../composables/useRulesPage'
|
||||
|
||||
withDefaults(defineProps<{ variant?: 'mobile' | 'desktop' }>(), { variant: 'mobile' })
|
||||
|
||||
const { rulesTitle, rulesBlocks } = useRulesPageContext()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rules-panel {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
.rules-paragraph {
|
||||
margin: 0 0 1rem;
|
||||
text-align: right;
|
||||
line-height: 1.9;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div :class="variant === 'desktop' ? 'drules-support' : 'wrap-fix bottom-0 px-10 py-6 flex gap-2 bg-white items-center'">
|
||||
<a class="w-full" :href="supportHref" target="_blank" rel="noopener noreferrer">
|
||||
<BasicButton class="w-full" type="primary">تماس با پشتیبانی</BasicButton>
|
||||
</a>
|
||||
<a :href="supportHref" target="_blank" rel="noopener noreferrer">
|
||||
<img src="@/assets/img/rubika.webp" alt="" :class="variant === 'desktop' ? 'drules-support__bale' : 'w-12'" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import { useRulesPageContext } from '../composables/useRulesPage'
|
||||
|
||||
withDefaults(defineProps<{ variant?: 'mobile' | 'desktop' }>(), { variant: 'mobile' })
|
||||
|
||||
const { supportHref } = useRulesPageContext()
|
||||
</script>
|
||||
@@ -0,0 +1,36 @@
|
||||
import { computed, inject, provide, type ComputedRef, type InjectionKey } from 'vue'
|
||||
import { getConfig } from '@/config/app-config'
|
||||
import { getRulesBlocks, getRulesTitle, type RulesBlock } from '../rulesContent'
|
||||
|
||||
export interface RulesPageContext {
|
||||
nameProject: ComputedRef<string>
|
||||
rulesTitle: ComputedRef<string>
|
||||
rulesBlocks: ComputedRef<RulesBlock[]>
|
||||
supportHref: ComputedRef<string>
|
||||
}
|
||||
|
||||
export const RULES_PAGE_CONTEXT_KEY: InjectionKey<RulesPageContext> = Symbol('rulesPageContext')
|
||||
|
||||
const SUPPORT_LINK = 'https://web.rubika.ir/#c=u0I8F1U0a939b0df5ba9f73618d33d7c'
|
||||
|
||||
export function useRulesPage() {
|
||||
const nameProject = computed(() => getConfig('name_fa', 'کشکول گلد'))
|
||||
const rulesTitle = computed(() => getRulesTitle(nameProject.value, false))
|
||||
const rulesBlocks = computed(() => getRulesBlocks(nameProject.value, false))
|
||||
const supportHref = computed(() => SUPPORT_LINK)
|
||||
|
||||
const context: RulesPageContext = {
|
||||
nameProject,
|
||||
rulesTitle,
|
||||
rulesBlocks,
|
||||
supportHref,
|
||||
}
|
||||
provide(RULES_PAGE_CONTEXT_KEY, context)
|
||||
return context
|
||||
}
|
||||
|
||||
export function useRulesPageContext() {
|
||||
const context = inject(RULES_PAGE_CONTEXT_KEY)
|
||||
if (!context) throw new Error('useRulesPageContext must be used within useRulesPage')
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="drules">
|
||||
<div class="drules__container">
|
||||
<DesktopPageTopbar eyebrow="حقوقی" title="قوانین و مقررات" show-back />
|
||||
|
||||
<div class="drules-layout">
|
||||
<section class="drules-surface drules-main">
|
||||
<div class="drules-main__intro">
|
||||
<div class="drules-main__logo">
|
||||
<BasicIcon icon="vuesax-linear:task" size="28" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="drules-main__project">{{ nameProject }}</h2>
|
||||
<p class="drules-main__tagline">شرایط استفاده از خدمات و تعهدات کاربران</p>
|
||||
</div>
|
||||
</div>
|
||||
<RulesContentPanel variant="desktop" />
|
||||
</section>
|
||||
|
||||
<aside class="drules-surface drules-side">
|
||||
<h3 class="drules-side__title">پشتیبانی و ارتباط</h3>
|
||||
<p class="drules-side__text">در صورت نیاز به توضیح بیشتر درباره قوانین، با پشتیبانی تماس بگیرید.</p>
|
||||
<RulesSupportActions variant="desktop" />
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import DesktopPageTopbar from '@/layouts/components/DesktopPageTopbar.vue'
|
||||
import RulesContentPanel from '../components/RulesContentPanel.vue'
|
||||
import RulesSupportActions from '../components/RulesSupportActions.vue'
|
||||
import { useRulesPageContext } from '../composables/useRulesPage'
|
||||
|
||||
const { nameProject } = useRulesPageContext()
|
||||
</script>
|
||||
|
||||
<style lang="less" src="./styles/desktop-rules.less"></style>
|
||||
@@ -0,0 +1,156 @@
|
||||
/* Premium desktop rules page */
|
||||
|
||||
.drules {
|
||||
min-height: 100%;
|
||||
background: #f6f7f9;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.drules__container {
|
||||
max-width: 1080px;
|
||||
margin: 0 auto;
|
||||
padding: 28px 36px 40px;
|
||||
}
|
||||
|
||||
.drules-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 300px);
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.drules-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);
|
||||
}
|
||||
|
||||
.drules-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(167, 43, 119, 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;
|
||||
}
|
||||
}
|
||||
|
||||
.drules-content {
|
||||
padding: 20px;
|
||||
border: 1px solid rgba(167, 43, 119, 0.15);
|
||||
border-radius: 12px;
|
||||
background: rgba(167, 43, 119, 0.03);
|
||||
color: @App-primary-color;
|
||||
|
||||
&__title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: rgba(167, 43, 119, 0.12);
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
&__block + &__block {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid rgba(167, 43, 119, 0.08);
|
||||
}
|
||||
|
||||
&__paragraph {
|
||||
margin: 0 0 14px;
|
||||
font-size: 14px;
|
||||
line-height: 1.9;
|
||||
text-align: right;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drules-side {
|
||||
padding: 24px;
|
||||
position: sticky;
|
||||
top: 88px;
|
||||
|
||||
&__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;
|
||||
}
|
||||
}
|
||||
|
||||
.drules-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) {
|
||||
.drules-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.drules-side {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
+12
-274
@@ -1,285 +1,23 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="wrap-fix z-1 bg-white 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">
|
||||
<!-- <div class="h-20vh flex justify-center">
|
||||
<img :src="logoFull" class="mb-20 h-15vh mt-5" />
|
||||
</div>-->
|
||||
<div class="overflow-y-auto h-60vh pb-20 border-primary">
|
||||
<div
|
||||
class="mt-5 text-5 border-solid border-2 border-primary text-primary rounded-[12px] p-4"
|
||||
>
|
||||
<div class="font-bold text-sm text-primary">
|
||||
قوانین و مقررات معاملات طلا در بستر کشکول گلد
|
||||
</div>
|
||||
<div class="w-full h-1px my-3 bg-secondary"></div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• کاربران موظفند قبل از ثبت نام، تمامی مفاد این تعهدنامه را مطالعه نموده و صرفاً در
|
||||
صورت پذیرش آن اقدام به ثبتنام نمایند.
|
||||
</div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• کاربران کشکول گلد میپذیرند که کلیهی فعالیتهای آنها در کشکول گلد، در چارچوب قوانین
|
||||
جمهوری اسلامی ایران بوده و هیچ گونه فعالیتی خارج از این چارچوب انجام نخواهد شد.
|
||||
</div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• احراز هویت برای استفاده از خدمات کشکول گلد ضروری بوده و کاربران موظفند اطلاعات صحیح
|
||||
خود را بارگذاری نمایند؛ بدیهی است در صورت وجود هرگونه تخلف در احراز هویت، مسئولیت به
|
||||
عهدهی فرد متخلف بوده و کشکول گلد حق توقف ارائه خدمات به کاربر مذکور و ارجاع موارد
|
||||
تخلف به مراجع ذیصلاح را خواهد داشت.
|
||||
</div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• کشکول گلد خود را ملزم به حفظ اطلاعات شخصی کاربران در چارچوب قوانین قضایی کشور
|
||||
میداند.
|
||||
</div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• کاربران کشکول گلد تعهد میدهند که از قرار دادن حساب خود در اختیار دیگران خودداری
|
||||
نمایند. از خدمات سایت، تنها برای حساب شخصی خود استفاده نموده و مسئولیت استفاده از
|
||||
خدمات کشکول گلد برای فردی دیگر، به عهده کاربر خواهد بود. شماره حساب اعلام شده به سایت
|
||||
باید متعلق به کاربر بوده و کاربران مجاز به دادن شماره حساب متعلق به اشخاص دیگر
|
||||
نیستند.
|
||||
</div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• کاربران میپذیرند در صورت تشخیص کارشناسان پشتیبانی، جهت حفظ امنیت دارایی حساب
|
||||
کاربریشان با اطلاع قبلی نسبت به برقراری تماس تصویری با تیم پشتیبان همکاری نمایند.
|
||||
</div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• هرگونه سهلانگاری کاربران در حفظ اطلاعات شخصی و امنیتی از جمله: رمز عبور کاربر،
|
||||
سرقت گوشی کاربر، سرقت کارت ملی و مدارک شناسایی کاربر، سپردن مدارک شخصی و موبایل به
|
||||
فرد ثالث و ... به عهدهی شخص کاربر بوده و کشکول گلد هیچ گونه مسئولیتی در این موارد
|
||||
نخواهد داشت.
|
||||
</div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• اکیداً توصیه میشود از رمز عبوری پیچیده، امن و همچنین کد شناسایی دو عاملی استفاده
|
||||
شود.
|
||||
</div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• کشکول گلد این اطمینان را میدهد که داراییهای کاربران را نزد خود به امانت و به
|
||||
بهترین شکل و با بالاترین استانداردهای امنیتی ممکن حفظ نماید؛ در صورت بروز هرگونه
|
||||
مشکل امنیتی، کشکول گلد متعهد به جبران خسارت خواهد بود.
|
||||
</div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• به عنوان پلتفرم معاملات برخط فلزات گرانبها، هیچگونه مسئولیتی در قبال تصمیمات
|
||||
اقتصادی فی مابین کاربران و سود و زیان حاصل از آن بر عهده کشکول گلد نمیباشد.
|
||||
</div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• کشکول گلد حق اصلاح و تغییر قوانین و مقررات ذکر شده در این صفحه را چه به صورت کلی و
|
||||
چه به صورت جزئی دارد و نسخه نهایی را در این صفحه در معرض دید قرار میدهد. استفاده از
|
||||
خدمات کشکول گلد پس از اعمال تغییرات، به معنای پذیرش همه تغییرات میباشد.
|
||||
</div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• از لحظه خرید کاربران تا زمان تحویل طلای خریداری شده، مسئولیت حفظ و نگهداری اصل طلا
|
||||
بر عهده کشکول گلد میباشد.
|
||||
</div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• کاربران کشکول گلد اذعان میدارند که از ریسکهای اقتصادی مربوط به سرمایهگذاری در
|
||||
حوزه طلا مطلع بوده و با علم بر این موضوع اقدام به تبادل و سرمایهگذاری میکنند.
|
||||
</div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• کشکول گلد متعهد میشود در حد توان از حریم شخصی شما محافظت کند و در این راستا،
|
||||
تکنولوژی مورد نیاز در سایت و پلتفرم را برای هر چه مطمئنتر و امنتر شدن استفاده شما،
|
||||
توسعه دهد.
|
||||
</div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• در صورت بروز نوسان شدید در قیمت طلا و هرگونه رویداد برهمزننده نظم حاکم بر بازار
|
||||
طلا، کشکول گلد میتواند برای خرید و فروش طلا توسط کاربر سقف تعیین کند تا دو طرف متضرر
|
||||
نشوند.
|
||||
</div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• کشکول گلد مجاز است بدون اطلاع قبلی و به صلاحدید شخصی نسبت به توقف سفارشگیری جدید،
|
||||
معاملات (از جمله سفارشهای باز) را متوقف کند؛ بدیهی است که کلیه سفارشهای تکمیلشده
|
||||
قبل از توقف سفارشگیری، پردازش و تحویل داده خواهد شد.
|
||||
</div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• در صورت بروز هرگونه اختلال در اینترنت و خدمات زیرساخت به دلایل حاکمیتی، هیچگونه
|
||||
مسئولیتی به عهده کشکول گلد نمیباشد.
|
||||
</div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• درخواست برداشت ریال از حساب کاربری در سریعترین زمان ممکن پس از ثبت، بررسی خواهد
|
||||
شد. زمان واریز پول به حساب کاربران بر اساس محدودیتهای انتقال وجه بین بانکی، متفاوت
|
||||
خواهد بود. برای اطلاعات بیشتر، به قوانین انتقال وجه بین بانکی (پایا، ساتنا) مراجعه
|
||||
فرمایید.
|
||||
</div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• کاربر میپذیرد که به جز در موارد مورد تعهد، حق هیچگونه داعیه، طلب و شکایت از
|
||||
سایت، مدیران، کارمندان و افراد مرتبط با این سایت را نخواهد داشت.
|
||||
</div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• اگر کاربر تحت هر عنوان اشتباهاً یا من غیر حق، وجوهی را به حساب کاربر منظور یا در
|
||||
محاسبات خود هر نوع اشتباهی نماید، کشکول گلد هر زمان مجاز و مختار است راساً و مستقلاً و
|
||||
بدون انجام هیچگونه تشریفات اداری و قضایی و دریافت اجازه کتبی از متعهد (صاحب حساب)
|
||||
در رفع اشتباه و برداشت از حسابهای وی اقدام نماید و تشخیص نسبت به وقوع اشتباه یا
|
||||
پرداخت بدون حق و لزوم برداشت از حساب معتبر خواهد بود و کاربر حق هرگونه اعتراض و
|
||||
ادعایی را در خصوص نحوه عملکرد کشکول گلد از هر جهت از خود ساقط مینماید.
|
||||
</div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• در صورت بروز هرگونه مشکل یا ابهام در هر یک از معاملات (مانند دستکاری قیمت، اختلال
|
||||
در وبسرویسها، انجام معامله خارج از دامنه نوسان و ...)، کشکول گلد حق دارد مستقلاً آن
|
||||
معامله را ابطال کرده و داراییهای هر یک از طرفین را به حساب خودشان عودت دهد. بدیهی
|
||||
است در صورتی که اشکال از سمت کشکول گلد باشد، شرکت موظف خواهد بود که جبران خسارت نماید
|
||||
و در غیر این صورت، کاربر حق هرگونه اعتراض و ادعایی در خصوص نحوه عملکرد از هر جهت را
|
||||
از خود ساقط مینماید.
|
||||
</div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• در صورت فوت یا حجر کاربر، کشکول گلد پیرو قوانین جمهوری اسلامی ایران میباشد. کاربر
|
||||
با علم و اطلاع از مقررات این بند نسبت به افتتاح حساب کاربری در کشکول گلد اقدام نموده و
|
||||
کاربر، اشخاص ثالث، ورثه یا قائممقام قانونی ایشان حق اعتراض و یا ادعایی در آینده در
|
||||
این خصوص نخواهند داشت.
|
||||
</div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• هزینه خدمات و کارمزد تراکنشهای مالی انجام شده در مطابق با توضیحات ارائه شده در
|
||||
صفحه راهنمای استفاده از کشکول گلد میباشد.
|
||||
</div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• تمامی علائم تجاری، طرحهای گرافیکی، رابط کاربری، رابط بصری، عکس، لوگو، کدهای
|
||||
نرمافزار، ساختار، نحوه بیان، تنظیم مطالب و محتوای موجود در کشکول گلد و شبکههای متعلق
|
||||
به کشکول گلد و بهطور کلی، سایر حقوق و امتیازات مربوطه منحصراً در مالکیت کشکول گلد
|
||||
میباشد و هیچ شخصی حق استفاده از آن را بدون رضایت کشکول گلد ندارد.
|
||||
</div>
|
||||
<div class="mt-5 text-xs leading-5">
|
||||
• اگر هر یک از مفاد این موافقتنامه بر اساس هر قانون لازمالاجرا، نامعتبر یا غیرقابل
|
||||
اجرا تشخیص داده شود، این امر بر اعتبار هیچ ماده دیگری تأثیری نخواهد داشت و بقیه
|
||||
مقررات اجرا میشود.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrap-fix bottom-0 px-10 py-6 flex gap-2 bg-white items-center">
|
||||
<a class="w-full" href="https://web.rubika.ir/#c=u0I8F1U0a939b0df5ba9f73618d33d7c">
|
||||
<BasicButton class="w-full" type="primary">تماس با پشتیبانی</BasicButton>
|
||||
</a>
|
||||
<!-- <BasicButton type="primary" @click="$router.push({name: 'permissions'})">
|
||||
<div>مجوز ها</div>
|
||||
</BasicButton>-->
|
||||
<a href="https://web.rubika.ir/#c=u0I8F1U0a939b0df5ba9f73618d33d7c">
|
||||
<img src="@/assets/img/rubika.webp" alt="" class="w-12"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <ActionSheet v-model:show="editPassVisible">salam</ActionSheet>-->
|
||||
<div class="rules-page-root">
|
||||
<DesktopRulesView v-show="isDesktop" />
|
||||
<MobileRulesView v-show="!isDesktop" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, ref } 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 BaseDatePicker from '@/components/BasicPacks/BaseDatePicker'
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import { useRulesPage } from './composables/useRulesPage'
|
||||
import DesktopRulesView from './desktop/DesktopRulesView.vue'
|
||||
import MobileRulesView from './mobile/MobileRulesView.vue'
|
||||
|
||||
const userFormState = ref<User>({
|
||||
fname: '',
|
||||
lname: '',
|
||||
mobile: undefined,
|
||||
birth_date: '',
|
||||
phone_number: '',
|
||||
province: '',
|
||||
city: '',
|
||||
n_code: undefined
|
||||
})
|
||||
const formRef = ref<FormInstance>()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// const editPassVisible = ref<boolean>(false)
|
||||
const 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() {
|
||||
// nextTick(() => {
|
||||
const val = `⭐️الان طلا بخر؛ آخر ماه سودشو ببر!
|
||||
با خرید طلای آبشده در مقدارهای کم ولی مستمر، سود بیشتری ببر!
|
||||
|
||||
این دعوت نامه از طرف "${userStore.userInfo?.name}" عزیز برای شما ارسال شده تا شما هم از بازار طلای کشکول گلد جا نمونید.
|
||||
روی لینک زیر کلیک کنید و ثبت نام خود را تکمیل کنید.
|
||||
|
||||
kh.kashkolgold.ir/login?id=${userStore.userInfo?.referral_id}`
|
||||
navigator.share({
|
||||
title: 'کشکول گلد',
|
||||
text: 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
|
||||
}
|
||||
})
|
||||
}
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
useRulesPage()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/*.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
.rules-page-root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}*/
|
||||
/*.trans{
|
||||
transition: height 1s;
|
||||
}*/
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="wrap-fix z-1 bg-white 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 pb-24 px-5">
|
||||
<RulesContentPanel variant="mobile" />
|
||||
<RulesSupportActions variant="mobile" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import router from '@/router'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import RulesContentPanel from '../components/RulesContentPanel.vue'
|
||||
import RulesSupportActions from '../components/RulesSupportActions.vue'
|
||||
</script>
|
||||
@@ -0,0 +1,49 @@
|
||||
export interface RulesBlock {
|
||||
title?: string
|
||||
items: string[]
|
||||
}
|
||||
|
||||
function resolveProjectName(nameProject?: string | null) {
|
||||
return (nameProject ?? '').trim() || 'کشکول گلد'
|
||||
}
|
||||
|
||||
function getDefaultRulesItems(nameProject: string): string[] {
|
||||
const brand = resolveProjectName(nameProject)
|
||||
|
||||
return [
|
||||
`کاربران موظفند قبل از ثبتنام، تمامی مفاد این تعهدنامه را مطالعه نموده و صرفاً در صورت پذیرش آن اقدام به ثبتنام نمایند.`,
|
||||
`کاربران ${brand} میپذیرند که کلیه فعالیتهای آنها در ${brand}، در چارچوب قوانین جمهوری اسلامی ایران بوده و هیچ گونه فعالیتی خارج از این چارچوب انجام نخواهد شد.`,
|
||||
`احراز هویت برای استفاده از خدمات ${brand} ضروری بوده و کاربران موظفند اطلاعات صحیح خود را بارگذاری نمایند. در صورت تخلف در احراز هویت، مسئولیت بر عهده فرد متخلف بوده و ${brand} حق توقف ارائه خدمات و ارجاع موارد تخلف به مراجع ذیصلاح را خواهد داشت.`,
|
||||
`${brand} خود را ملزم به حفظ اطلاعات شخصی کاربران در چارچوب قوانین قضایی کشور میداند.`,
|
||||
`کاربران ${brand} تعهد میدهند حساب خود را در اختیار دیگران قرار ندهند و تنها برای حساب شخصی خود از خدمات استفاده کنند. شماره حساب اعلامشده باید متعلق به خود کاربر باشد.`,
|
||||
`کاربران میپذیرند در صورت تشخیص پشتیبانی، برای حفظ امنیت دارایی حساب، با اطلاع قبلی در تماس تصویری همکاری نمایند.`,
|
||||
`سهلانگاری در حفظ اطلاعات و امنیت حساب از جمله رمز عبور، گوشی، کارت ملی و مدارک شناسایی، بر عهده کاربر است و ${brand} مسئولیتی در این موارد ندارد.`,
|
||||
`استفاده از رمز عبور پیچیده و کد شناسایی دو عاملی توصیه میشود.`,
|
||||
`${brand} دارایی کاربران را با بالاترین استانداردهای امنیتی حفظ میکند و در صورت بروز مشکل امنیتی متعهد به جبران خسارت خواهد بود.`,
|
||||
`به عنوان پلتفرم معاملات برخط فلزات گرانبها، ${brand} مسئولیتی در قبال تصمیمات اقتصادی کاربران و سود و زیان حاصل از آن ندارد.`,
|
||||
`${brand} حق اصلاح قوانین این صفحه را دارد و استفاده از خدمات پس از اعمال تغییرات، به معنای پذیرش آنهاست.`,
|
||||
`از لحظه خرید تا زمان تحویل طلای خریداریشده، مسئولیت حفظ و نگهداری آن بر عهده ${brand} است.`,
|
||||
`کاربران ${brand} از ریسکهای سرمایهگذاری در حوزه طلا مطلع بوده و با علم به این موضوع اقدام به معامله میکنند.`,
|
||||
`${brand} متعهد است از حریم شخصی کاربران محافظت کند.`,
|
||||
`در صورت نوسان شدید قیمت طلا، ${brand} میتواند برای خرید و فروش سقف تعیین کند.`,
|
||||
`${brand} مجاز است بدون اطلاع قبلی سفارشگیری جدید را متوقف کند. سفارشهای تکمیلشده قبل از توقف، پردازش میشوند.`,
|
||||
`در صورت اختلال اینترنت یا زیرساخت به دلایل حاکمیتی، ${brand} مسئولیتی ندارد.`,
|
||||
`درخواست برداشت ریال پس از ثبت بررسی میشود. زمان واریز مطابق محدودیتهای انتقال وجه بین بانکی (پایا، ساتنا) خواهد بود.`,
|
||||
`کاربر حق داعیه و شکایت از سایت، مدیران و کارمندان را جز در موارد تعهد ندارد.`,
|
||||
`در صورت واریز اشتباه وجه، ${brand} مجاز است نسبت به رفع اشتباه و برداشت از حساب اقدام کند و کاربر حق اعتراض را از خود ساقط میداند.`,
|
||||
`در صورت ابهام در معاملات، ${brand} حق ابطال معامله و عودت داراییها را دارد. در صورت اشکال از سمت ${brand}، جبران خسارت انجام میشود.`,
|
||||
`در صورت فوت یا حجر کاربر، ${brand} پیرو قوانین جمهوری اسلامی ایران است.`,
|
||||
`هزینه خدمات و کارمزد مطابق راهنمای استفاده از ${brand} محاسبه میشود.`,
|
||||
`کلیه حقوق مادی و معنوی ${brand} محفوظ است و استفاده بدون رضایت مجاز نیست.`,
|
||||
`ابطال هر بند، بر سایر بندها اثر ندارد.`,
|
||||
]
|
||||
}
|
||||
|
||||
export function getRulesTitle(nameProject: string, _isBoursecu = false) {
|
||||
const brand = resolveProjectName(nameProject)
|
||||
return `قوانین و مقررات معاملات طلا در بستر ${brand}`
|
||||
}
|
||||
|
||||
export function getRulesBlocks(nameProject: string, _isBoursecu = false): RulesBlock[] {
|
||||
return [{ items: getDefaultRulesItems(nameProject) }]
|
||||
}
|
||||
@@ -1,10 +1,48 @@
|
||||
<template>
|
||||
<div @click="infoVisible = true">
|
||||
<!-- {{ item}}-->
|
||||
<button
|
||||
v-if="variant === 'desktop'"
|
||||
type="button"
|
||||
class="dtxn-row"
|
||||
@click="infoVisible = true"
|
||||
>
|
||||
<span
|
||||
class="dtxn-row__icon"
|
||||
:class="isUp ? 'dtxn-row__icon--up' : 'dtxn-row__icon--down'"
|
||||
>
|
||||
<Icon :icon="`vuesax-linear:${isUp ? 'arrow-up-2' : 'arrow-down-2'}`" size="18" />
|
||||
</span>
|
||||
<div class="dtxn-row__main">
|
||||
<p class="dtxn-row__title">{{ item.title }}</p>
|
||||
<div class="dtxn-row__meta">
|
||||
<span>{{ toJalaliOutOfUTC(item.expire_at) }}</span>
|
||||
<span :class="goldAmount >= 0 ? 'text-green-600' : 'text-red-600'">
|
||||
{{ formatGoldAmount(goldAmount) }} {{ item.unit }}
|
||||
</span>
|
||||
<span v-if="isGold">
|
||||
قیمت معامله: {{ usePriceWithoutType(tradePrice) }} ریال
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="dtxn-row__amount"
|
||||
:class="item.cash > 0 ? 'dtxn-row__amount--up' : 'dtxn-row__amount--down'"
|
||||
>
|
||||
{{ usePriceWithoutType(item.cash ?? 0) }} ریال
|
||||
</div>
|
||||
<div class="dtxn-row__status">
|
||||
<ATag color="blue" class="!py-1 !px-4 !ml-0 !text-xs">
|
||||
{{ item.order?.sts_str ?? item.withdraw?.sts_str ?? item.title }}
|
||||
</ATag>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div v-else @click="infoVisible = true">
|
||||
<div class="flex">
|
||||
<div :class="isUp? 'bg-green-50':'bg-red-50'" class="rounded-lg p-3 h-fit w-fit flex justify-center items-center">
|
||||
<Icon :class="isUp? 'text-green-600':'text-red-600'"
|
||||
:icon="`vuesax-linear:${isUp? 'arrow-up-2':'arrow-down-2'}`" />
|
||||
<div :class="isUp ? 'bg-green-50' : 'bg-red-50'" class="rounded-lg p-3 h-fit w-fit flex justify-center items-center">
|
||||
<Icon
|
||||
:class="isUp ? 'text-green-600' : 'text-red-600'"
|
||||
:icon="`vuesax-linear:${isUp ? 'arrow-up-2' : 'arrow-down-2'}`"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full mr-3">
|
||||
<div class="flex justify-between w-full">
|
||||
@@ -12,13 +50,16 @@
|
||||
<div>{{ toJalaliOutOfUTC(item.expire_at) }}</div>
|
||||
</div>
|
||||
<div class="flex justify-between mt-2">
|
||||
<div :class="getTransactionGoldAmount(item) >= 0? 'text-green-600':'text-red-600'"
|
||||
class="text-gray-500 text-xs number-ltr">{{ formatGoldAmount(getTransactionGoldAmount(item)) }}
|
||||
<div>{{item.unit}}</div>
|
||||
<div
|
||||
:class="getTransactionGoldAmount(item) >= 0 ? 'text-green-600' : 'text-red-600'"
|
||||
class="text-gray-500 text-xs number-ltr"
|
||||
>
|
||||
{{ formatGoldAmount(goldAmount) }}
|
||||
<div>{{ item.unit }}</div>
|
||||
</div>
|
||||
<div v-if="!isShopProduct" class="text-gray-500 flex gap-1 text-xs items-center number-ltr">
|
||||
<div :class="item.cash > 0 ? 'text-green-600':'text-red-600'" class="text-base">
|
||||
{{ usePriceWithoutType(item.cash?? 0) }}
|
||||
<div :class="item.cash > 0 ? 'text-green-600' : 'text-red-600'" class="text-base">
|
||||
{{ usePriceWithoutType(item.cash ?? 0) }}
|
||||
</div>
|
||||
ریال
|
||||
</div>
|
||||
@@ -48,8 +89,7 @@
|
||||
<div v-else-if="isGold" class="flex w-full justify-between mt-2">
|
||||
<div class="text-gray-500">قیمت معامله:</div>
|
||||
<div class="text-gray-500 flex gap-1 text-xs items-center">
|
||||
<!-- <div class="text-base">{{ typeof parseInt((Math.abs(item.cash) / Math.abs(item.gold)).toFixed(0)) }}</div>-->
|
||||
<div class="text-base">{{ usePriceWithoutType(getTransactionTradePrice(item)) }}</div>
|
||||
<div class="text-base">{{ usePriceWithoutType(tradePrice) }}</div>
|
||||
ریال
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,167 +97,69 @@
|
||||
<div class="text-gray-500">وضعیت :</div>
|
||||
<div class="text-gray-500 flex gap-1 text-xs items-center">
|
||||
<ATag class="!py-1 !px-6 !ml-0" color="blue">
|
||||
{{ (item.order?.sts_str ?? item.withdraw?.sts_str) ?? item?.title }}
|
||||
{{ item.order?.sts_str ?? item.withdraw?.sts_str ?? item?.title }}
|
||||
</ATag>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="item.note" class="flex w-full justify-between mt-1">
|
||||
<div class="text-gray-500 flex gap-1 text-xs items-center">
|
||||
<div class="!py-1 !ml-0">
|
||||
{{ item.note }}
|
||||
</div>
|
||||
<div class="!py-1 !ml-0">{{ item.note }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div v-else class="h-8"></div>-->
|
||||
<a-divider class="!my-3" />
|
||||
<BasicActionSheet v-model:show="infoVisible">
|
||||
<div class="font-bold text-lg">{{ props.item.title }}</div>
|
||||
<div class="pb-20 mt-5">
|
||||
<div class="flex justify-between mb-3">
|
||||
<div class="text-primary">مبلغ:</div>
|
||||
<div :class="item.cash > 0? 'text-green-600' : 'text-red-600'" class="flex text-xs items-center number-ltr">
|
||||
<div class="text-base ml-1">{{ usePriceWithoutType(item.cash?? 0) }}</div>
|
||||
ریال
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="isShopProduct">
|
||||
<div class="flex justify-between mb-3">
|
||||
<div class="text-primary">تعداد:</div>
|
||||
<div class="flex text-xs items-center number-ltr">
|
||||
<div class="text-base ml-1">{{ item.order?.number ?? 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mb-3">
|
||||
<div class="text-primary">قیمت کل:</div>
|
||||
<div class="flex text-xs items-center number-ltr">
|
||||
<div class="text-base ml-1">{{ usePriceWithoutType(item.order?.total_price ?? 0) }}</div>
|
||||
ریال
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mb-3">
|
||||
<div class="text-primary">قیمت واحد:</div>
|
||||
<div class="flex text-xs items-center number-ltr">
|
||||
<div class="text-base ml-1">{{ usePriceWithoutType(item.order?.price ?? 0) }}</div>
|
||||
ریال
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="item.order" class="flex justify-between mb-3">
|
||||
<div class="text-primary">قیمت هر گرم طلا حین معامله:</div>
|
||||
<div class="flex text-xs items-center number-ltr">
|
||||
<div class="text-base ml-1">{{ usePriceWithoutType(item.order?.price?? 0) }}</div>
|
||||
ریال
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mb-3">
|
||||
<div class="text-primary">مقدار {{item.wallet?.product? item.wallet.product.title: 'آبشده' }} مبادله شده:</div>
|
||||
<div :class="getTransactionGoldAmount(item) >= 0? 'text-green-600' : 'text-red-600'" class="flex text-xs items-center number-ltr">
|
||||
<div class="text-base ml-1">{{ formatGoldAmount(getTransactionGoldAmount(item)) }}</div>
|
||||
{{item.unit}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex justify-between mb-3">
|
||||
<div class="text-primary">موجودی کیف پول:</div>
|
||||
<div :class="item.cash > 0? 'text-green-600' : 'text-red-600'" class="flex text-xs items-center number-ltr">
|
||||
<div class="text-base">{{ usePriceWithoutType(item.balance_cash ?? 0) }}</div>
|
||||
ریال
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mb-3" v-if="item.wallet?.product">
|
||||
<div class="text-primary">موجودی {{item.wallet.product.title}}:</div>
|
||||
<div class="flex text-xs text-yellow-700 items-center">
|
||||
<div class="text-base ml-1">{{ item.balance_gold }}</div>
|
||||
{{item.wallet.product.unit}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mb-3" v-else>
|
||||
<div class="text-primary">موجودی کیف آبشده:</div>
|
||||
<div class="flex text-xs text-yellow-700 items-center">
|
||||
<div class="text-base ml-1">{{ item.balance_gold }}</div>
|
||||
گرم
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="flex justify-between mb-3">
|
||||
<div class="text-primary">موجودی کیف پول:</div>
|
||||
<div class="flex text-xs items-center" :class="isUp? 'text-green-600' : 'text-red-600'"><div class="text-base">{{usePriceWithoutType(12000000)}}</div>ریال</div>
|
||||
</div>
|
||||
<div class="flex justify-between mb-3">
|
||||
<div class="text-primary">موجودی کیف طلا:</div>
|
||||
<div class="flex items-center" :class="isUp? 'text-red-600' : 'text-green-600'">60 گرم</div>
|
||||
</div>-->
|
||||
<div class="flex justify-between mb-3">
|
||||
<div class="text-primary">وضعیت:</div>
|
||||
<div class="flex text-xs items-center">
|
||||
<ATag :color="item.active? 'blue': 'blue'" class="!py-1 !px-6 !ml-0">
|
||||
{{ item.order?.sts_str ?? item.withdraw?.sts_str }}
|
||||
</ATag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mb-3">
|
||||
<div class="text-primary">تاریخ:</div>
|
||||
<div>{{ toJalaliNotime(item.expire_at) }}</div>
|
||||
</div>
|
||||
<div class="flex justify-between mb-3">
|
||||
<div class="text-primary">زمان:</div>
|
||||
<div>{{ toTime(item.expire_at) }}</div>
|
||||
</div>
|
||||
<!-- <div class="flex justify-between mb-3">
|
||||
<div class="text-primary">وضعیت معامله:</div>
|
||||
<div class="flex text-xs items-center">
|
||||
<ATag class="!py-1 !px-6 !ml-0" color="orange">
|
||||
معاملات باز
|
||||
</ATag>
|
||||
</div>
|
||||
</div>-->
|
||||
<div v-if="item.eaj" class="flex justify-between mb-3">
|
||||
<div class="text-primary">مهلت ارسال فیش:</div>
|
||||
<div>{{ item.eaj }}</div>
|
||||
</div>
|
||||
<BasicButton class="w-full mt-5" @click="infoVisible = false">بازگشت</BasicButton>
|
||||
</div>
|
||||
</BasicActionSheet>
|
||||
</div>
|
||||
|
||||
<BasicActionSheet v-if="variant !== 'desktop'" v-model:show="infoVisible">
|
||||
<div class="font-bold text-lg">{{ item.title }}</div>
|
||||
<TransactionDetailPanel :item="item" variant="mobile" />
|
||||
<BasicButton class="w-full mt-5" @click="infoVisible = false">بازگشت</BasicButton>
|
||||
</BasicActionSheet>
|
||||
|
||||
<BasicModal
|
||||
v-else
|
||||
v-model:visible="infoVisible"
|
||||
:title="item.title"
|
||||
width="520px"
|
||||
custom-footer
|
||||
destroy-on-close
|
||||
>
|
||||
<TransactionDetailPanel :item="item" variant="desktop" />
|
||||
<template #footer>
|
||||
<BasicButton class="w-full" @click="infoVisible = false">بستن</BasicButton>
|
||||
</template>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Icon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { formatGoldAmount, getTransactionGoldAmount, getTransactionTradePrice, usePriceWithoutType } from '../../../../utils/usePrice'
|
||||
import {
|
||||
formatGoldAmount,
|
||||
getTransactionGoldAmount,
|
||||
getTransactionTradePrice,
|
||||
usePriceWithoutType,
|
||||
} from '@/utils/usePrice'
|
||||
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import BasicModal from '@/components/BasicPacks/BasicModal.vue'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import TransactionDetailPanel from './TransactionDetailPanel.vue'
|
||||
import type { Transaction } from '@/models/transaction'
|
||||
import { toJalaliNotime, toJalaliOutOfUTC, toTime } from '../../../../utils/useDateTime'
|
||||
import { toJalaliOutOfUTC } from '@/utils/useDateTime'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const infoVisible = ref<boolean>(false)
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
item: Transaction
|
||||
variant?: 'mobile' | 'desktop'
|
||||
}>(),
|
||||
{ variant: 'mobile' },
|
||||
)
|
||||
|
||||
interface Prop {
|
||||
item: Transaction
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Prop>(), {})
|
||||
const isUp = computed(() => props.item.type === 3 || props.item.type === 1 || props.item.type === 8 || props.item.type === 9)
|
||||
const infoVisible = ref(false)
|
||||
const goldAmount = computed(() => getTransactionGoldAmount(props.item))
|
||||
const tradePrice = computed(() => getTransactionTradePrice(props.item))
|
||||
const isUp = computed(
|
||||
() => props.item.type === 3 || props.item.type === 1 || props.item.type === 8 || props.item.type === 9,
|
||||
)
|
||||
const isGold = computed(() => props.item.type === 1 || props.item.type === 2 || props.item.type === 10)
|
||||
const isShopProduct = computed(() => !!props.item?.product_id)
|
||||
/*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 @@
|
||||
<template>
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail' : 'mb-8rem mt-5'">
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__row' : 'flex justify-between mb-3'">
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__label' : 'text-primary'">مبلغ:</div>
|
||||
<div
|
||||
:class="[
|
||||
variant === 'desktop' ? 'dtxn-detail__value' : '',
|
||||
item.cash > 0 ? 'text-green-600' : 'text-red-600',
|
||||
'flex text-xs items-center number-ltr',
|
||||
]"
|
||||
>
|
||||
<div class="text-base ml-1">{{ usePriceWithoutType(item.cash ?? 0) }}</div>
|
||||
ریال
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="isShopProduct">
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__row' : 'flex justify-between mb-3'">
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__label' : 'text-primary'">تعداد:</div>
|
||||
<div :class="['flex text-xs items-center number-ltr', variant === 'desktop' ? 'dtxn-detail__value' : '']">
|
||||
<div class="text-base ml-1">{{ item.order?.number ?? 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__row' : 'flex justify-between mb-3'">
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__label' : 'text-primary'">قیمت کل:</div>
|
||||
<div :class="['flex text-xs items-center number-ltr', variant === 'desktop' ? 'dtxn-detail__value' : '']">
|
||||
<div class="text-base ml-1">{{ usePriceWithoutType(item.order?.total_price ?? 0) }}</div>
|
||||
ریال
|
||||
</div>
|
||||
</div>
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__row' : 'flex justify-between mb-3'">
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__label' : 'text-primary'">قیمت واحد:</div>
|
||||
<div :class="['flex text-xs items-center number-ltr', variant === 'desktop' ? 'dtxn-detail__value' : '']">
|
||||
<div class="text-base ml-1">{{ usePriceWithoutType(item.order?.price ?? 0) }}</div>
|
||||
ریال
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
v-if="item.order"
|
||||
:class="variant === 'desktop' ? 'dtxn-detail__row' : 'flex justify-between mb-3'"
|
||||
>
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__label' : 'text-primary'">قیمت هر گرم طلا حین معامله:</div>
|
||||
<div :class="['flex text-xs items-center number-ltr', variant === 'desktop' ? 'dtxn-detail__value' : '']">
|
||||
<div class="text-base ml-1">{{ usePriceWithoutType(item.order?.price ?? 0) }}</div>
|
||||
ریال
|
||||
</div>
|
||||
</div>
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__row' : 'flex justify-between mb-3'">
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__label' : 'text-primary'">
|
||||
مقدار {{ item.wallet?.product ? item.wallet.product.title : 'آبشده' }} مبادله شده:
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
goldAmount >= 0 ? 'text-green-600' : 'text-red-600',
|
||||
'flex text-xs items-center number-ltr',
|
||||
variant === 'desktop' ? 'dtxn-detail__value' : '',
|
||||
]"
|
||||
>
|
||||
<div class="text-base ml-1">{{ formatGoldAmount(goldAmount) }}</div>
|
||||
{{ item.unit }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__row' : 'flex justify-between mb-3'">
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__label' : 'text-primary'">موجودی کیف پول:</div>
|
||||
<div
|
||||
:class="[
|
||||
item.cash > 0 ? 'text-green-600' : 'text-red-600',
|
||||
'flex text-xs items-center number-ltr',
|
||||
variant === 'desktop' ? 'dtxn-detail__value' : '',
|
||||
]"
|
||||
>
|
||||
<div class="text-base">{{ usePriceWithoutType(item.balance_cash ?? 0) }}</div>
|
||||
ریال
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="item.wallet?.product"
|
||||
:class="variant === 'desktop' ? 'dtxn-detail__row' : 'flex justify-between mb-3'"
|
||||
>
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__label' : 'text-primary'">
|
||||
موجودی {{ item.wallet.product.title }}:
|
||||
</div>
|
||||
<div class="flex text-xs text-yellow-700 items-center">
|
||||
<div class="text-base ml-1">{{ item.balance_gold }}</div>
|
||||
{{ item.wallet.product.unit }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else :class="variant === 'desktop' ? 'dtxn-detail__row' : 'flex justify-between mb-3'">
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__label' : 'text-primary'">موجودی کیف آبشده:</div>
|
||||
<div class="flex text-xs text-yellow-700 items-center">
|
||||
<div class="text-base ml-1">{{ item.balance_gold }}</div>
|
||||
گرم
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__row' : 'flex justify-between mb-3'">
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__label' : 'text-primary'">وضعیت:</div>
|
||||
<div class="flex text-xs items-center">
|
||||
<ATag color="blue" class="!py-1 !px-6 !ml-0">
|
||||
{{ item.order?.sts_str ?? item.withdraw?.sts_str }}
|
||||
</ATag>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__row' : 'flex justify-between mb-3'">
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__label' : 'text-primary'">تاریخ:</div>
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__value' : ''">{{ toJalaliNotime(item.expire_at) }}</div>
|
||||
</div>
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__row' : 'flex justify-between mb-3'">
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__label' : 'text-primary'">زمان:</div>
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__value' : ''">{{ toTime(item.expire_at) }}</div>
|
||||
</div>
|
||||
<div v-if="item.eaj" :class="variant === 'desktop' ? 'dtxn-detail__row' : 'flex justify-between mb-3'">
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__label' : 'text-primary'">مهلت ارسال فیش:</div>
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__value' : ''">{{ item.eaj }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import {
|
||||
formatGoldAmount,
|
||||
getTransactionGoldAmount,
|
||||
usePriceWithoutType,
|
||||
} from '@/utils/usePrice'
|
||||
import type { Transaction } from '@/models/transaction'
|
||||
import { toJalaliNotime, toTime } from '@/utils/useDateTime'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
item: Transaction
|
||||
variant?: 'mobile' | 'desktop'
|
||||
}>(),
|
||||
{ variant: 'mobile' },
|
||||
)
|
||||
|
||||
const goldAmount = computed(() => getTransactionGoldAmount(props.item))
|
||||
const isShopProduct = computed(() => !!props.item?.product_id)
|
||||
</script>
|
||||
@@ -0,0 +1,134 @@
|
||||
import { computed, inject, onMounted, provide, ref, type ComponentPublicInstance, type InjectionKey } from 'vue'
|
||||
import { useIntersectionObserver } from '@vueuse/core'
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import { loading, loadingMore } from '@/utils'
|
||||
import { getTransactionTable } from '@/api/transaction'
|
||||
import type { Transaction } from '@/models/transaction'
|
||||
import type { TableEngineInput, TableOutput } from '@/models/table'
|
||||
|
||||
export interface TransactionsPageContext {
|
||||
items: ReturnType<typeof ref<Transaction[]>>
|
||||
dateFilter: ReturnType<typeof ref<string | undefined>>
|
||||
emptyText: ReturnType<typeof computed<string>>
|
||||
applyFilter: () => Promise<void>
|
||||
reloadTable: () => Promise<void>
|
||||
bindMobileObserver: (el: Element | ComponentPublicInstance | null) => void
|
||||
bindDesktopObserver: (el: Element | ComponentPublicInstance | null) => void
|
||||
showFilter: ReturnType<typeof ref<boolean>>
|
||||
}
|
||||
|
||||
export const TRANSACTIONS_PAGE_KEY: InjectionKey<TransactionsPageContext> = Symbol('transactionsPage')
|
||||
|
||||
export function useTransactionsPageProvider() {
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
const pagination = ref<TableEngineInput>({ filters: [], limit: 7, page: 0 })
|
||||
const items = ref<Transaction[]>([])
|
||||
const tableOutput = ref<TableOutput<Transaction> | null>(null)
|
||||
const showFilter = ref(false)
|
||||
const dateFilter = ref<string>()
|
||||
const mobileObserverEl = ref<HTMLElement | null>(null)
|
||||
const desktopObserverEl = ref<HTMLElement | null>(null)
|
||||
|
||||
const activeObserverEl = computed(() =>
|
||||
isDesktop.value ? desktopObserverEl.value : mobileObserverEl.value
|
||||
)
|
||||
|
||||
const emptyText = computed(() => 'تراکنشی موجود نیست')
|
||||
|
||||
async function initTable() {
|
||||
try {
|
||||
if (dateFilter.value) {
|
||||
pagination.value.filters = [
|
||||
{
|
||||
field: 'created_at',
|
||||
cr: 'and',
|
||||
type: 'date',
|
||||
op: 'eq',
|
||||
value: dateFilter.value,
|
||||
},
|
||||
]
|
||||
} else {
|
||||
pagination.value.filters = []
|
||||
}
|
||||
pagination.value.page === 0 ? (loading.value = true) : (loadingMore.value = true)
|
||||
tableOutput.value = await getTransactionTable(pagination.value)
|
||||
items.value.push(...tableOutput.value.rows)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadTable() {
|
||||
pagination.value.page = 0
|
||||
items.value = []
|
||||
await initTable()
|
||||
}
|
||||
|
||||
async function applyFilter() {
|
||||
await reloadTable()
|
||||
showFilter.value = false
|
||||
}
|
||||
|
||||
function resolveObserverElement(el: Element | ComponentPublicInstance | null) {
|
||||
if (!el) return null
|
||||
if (el instanceof HTMLElement) return el
|
||||
const root = (el as ComponentPublicInstance).$el
|
||||
return root instanceof HTMLElement ? root : null
|
||||
}
|
||||
|
||||
function bindMobileObserver(el: Element | ComponentPublicInstance | null) {
|
||||
mobileObserverEl.value = resolveObserverElement(el)
|
||||
}
|
||||
|
||||
function bindDesktopObserver(el: Element | ComponentPublicInstance | null) {
|
||||
desktopObserverEl.value = resolveObserverElement(el)
|
||||
}
|
||||
|
||||
useIntersectionObserver(
|
||||
activeObserverEl,
|
||||
async ([entry]) => {
|
||||
if (
|
||||
entry?.isIntersecting &&
|
||||
tableOutput.value &&
|
||||
(pagination.value.page + 1) * pagination.value.limit < tableOutput.value.query.total
|
||||
) {
|
||||
pagination.value.page++
|
||||
await initTable()
|
||||
}
|
||||
},
|
||||
{
|
||||
root: null,
|
||||
rootMargin: '300px',
|
||||
threshold: 0.01,
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
initTable()
|
||||
})
|
||||
|
||||
const context: TransactionsPageContext = {
|
||||
items,
|
||||
dateFilter,
|
||||
emptyText,
|
||||
applyFilter,
|
||||
reloadTable,
|
||||
bindMobileObserver,
|
||||
bindDesktopObserver,
|
||||
showFilter,
|
||||
}
|
||||
|
||||
provide(TRANSACTIONS_PAGE_KEY, context)
|
||||
return context
|
||||
}
|
||||
|
||||
export function useTransactionsPageContext() {
|
||||
const context = inject(TRANSACTIONS_PAGE_KEY)
|
||||
if (!context) {
|
||||
throw new Error('useTransactionsPageContext must be used within useTransactionsPageProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="dtxn">
|
||||
<div class="dtxn__container">
|
||||
<DesktopPageTopbar title="تراکنشها" />
|
||||
|
||||
<div class="dtxn-filters">
|
||||
<div class="dtxn-filters__label">
|
||||
<BasicIcon icon="vuesax-linear:filter" size="16" />
|
||||
<span>فیلتر تاریخ:</span>
|
||||
</div>
|
||||
<BaseDatePicker
|
||||
v-model:value="dateFilter"
|
||||
class="dtxn-filters__date"
|
||||
placeholder="تاریخ"
|
||||
size="large"
|
||||
/>
|
||||
<BasicButton type="primary" @click="applyFilter">اعمال فیلتر</BasicButton>
|
||||
<BasicButton v-if="dateFilter" @click="clearFilter">حذف فیلتر</BasicButton>
|
||||
</div>
|
||||
|
||||
<div :class="loading ? 'min-h-[40vh]' : ''" class="mt-5">
|
||||
<div v-if="!loading" class="dtxn-list">
|
||||
<list-anim>
|
||||
<Item
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
variant="desktop"
|
||||
/>
|
||||
</list-anim>
|
||||
<div :ref="bindDesktopObserver" class="w-full opacity-0 relative">
|
||||
در حال بارگذاری
|
||||
</div>
|
||||
<TransactionSkeleton v-if="loadingMore" class="mt-1 mb-8" />
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<TransactionSkeleton v-for="i in 8" :key="i" class="mt-1" />
|
||||
</template>
|
||||
|
||||
<div v-if="items.length <= 0 && !loading" class="dtxn-empty">
|
||||
<DotLottieVue
|
||||
class="w-44 h-44"
|
||||
autoplay
|
||||
src="https://lottie.host/51d3315a-cf88-4c18-bb4d-a31511fd4325/KjSBzcMbi6.json"
|
||||
/>
|
||||
<p class="dtxn-empty__text">{{ emptyText }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { loading, loadingMore } from '@/utils'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import DesktopPageTopbar from '@/layouts/components/DesktopPageTopbar.vue'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import BaseDatePicker from '@/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue'
|
||||
import ListAnim from '@/components/ListAnim.vue'
|
||||
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
|
||||
import { DotLottieVue } from '@lottiefiles/dotlottie-vue'
|
||||
import Item from '../components/Item.vue'
|
||||
import { useTransactionsPageContext } from '../composables/useTransactionsPage'
|
||||
|
||||
const {
|
||||
items,
|
||||
dateFilter,
|
||||
emptyText,
|
||||
applyFilter,
|
||||
reloadTable,
|
||||
bindDesktopObserver,
|
||||
} = useTransactionsPageContext()
|
||||
|
||||
async function clearFilter() {
|
||||
dateFilter.value = undefined
|
||||
await reloadTable()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" src="./styles/desktop-transactions.less"></style>
|
||||
@@ -0,0 +1,286 @@
|
||||
/* Premium desktop transactions page */
|
||||
|
||||
.dtxn {
|
||||
min-height: 100%;
|
||||
background: #f6f7f9;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.dtxn__container {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
padding: 28px 36px 40px;
|
||||
}
|
||||
|
||||
.dtxn-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.dtxn-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
|
||||
&__btn {
|
||||
padding: 8px 14px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #d1d5db;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: @App-primary-color;
|
||||
border-color: @App-primary-color;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dtxn-toggle {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
background: #f3f4f6;
|
||||
border-radius: 10px;
|
||||
|
||||
&__btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
|
||||
|
||||
&--active {
|
||||
background: #fff;
|
||||
color: #111827;
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dtxn-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
flex: 1 1 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
&__label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__product {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
&__select,
|
||||
&__date {
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
&__dates {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.dtxn-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dtxn-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto auto;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 16px 18px;
|
||||
border: 1px solid #f3f4f6;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
text-align: right;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #e5e7eb;
|
||||
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
&__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&--up {
|
||||
background: #ecfdf3;
|
||||
color: #027a48;
|
||||
}
|
||||
|
||||
&--down {
|
||||
background: #fef3f2;
|
||||
color: #b42318;
|
||||
}
|
||||
}
|
||||
|
||||
&__main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 12px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
&__amount {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
direction: ltr;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
|
||||
&--up {
|
||||
color: #027a48;
|
||||
}
|
||||
|
||||
&--down {
|
||||
color: #b42318;
|
||||
}
|
||||
}
|
||||
|
||||
&__status {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dtxn-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 56px 24px;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
border: 1px dashed #e5e7eb;
|
||||
|
||||
&__image {
|
||||
width: 140px;
|
||||
max-width: 40vw;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
&__text {
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
.dtxn-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
font-size: 13px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
color: #6b7280;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__value {
|
||||
color: #111827;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.dtxn-row {
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
|
||||
&__amount,
|
||||
&__status {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
&__amount {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
&__status {
|
||||
justify-self: end;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,213 +1,28 @@
|
||||
<template>
|
||||
<div >
|
||||
<!-- <teleport to="body">-->
|
||||
<div :style="{ backgroundImage: `url(${img})` }" class="fixed top-0 right-0 left-0 bottom-0"></div>
|
||||
<!-- </teleport>-->
|
||||
<div class="absolute inset-0 bg-cover bg-center opacity-60"></div>
|
||||
<div
|
||||
class="bg-white w-full transaction-list py-3 px-3 font-bold drop-shadow flex justify-between z-10"
|
||||
>
|
||||
<div>لیست تراکنش ها</div>
|
||||
<BasicIcon icon="vuesax-linear:filter" @click="showFilter = true" />
|
||||
</div>
|
||||
<!-- <a-spin :spinning="loading">-->
|
||||
<div
|
||||
ref="el"
|
||||
:class="loading ? 'h-screen' : ''"
|
||||
class="px-3 pt-16 pb-4 "
|
||||
style="max-height: calc(100vh - 50px)"
|
||||
>
|
||||
<!-- <div v-for="item in items">ss</div>-->
|
||||
<list-anim v-if="!loading">
|
||||
<Item v-for="item in items" :key="item.id" :item="item" />
|
||||
<div ref="observer" v-show="!loading" class="w-full opacity-0 relative">
|
||||
در حال بارگذاری
|
||||
</div>
|
||||
<TransactionSkeleton v-if="loadingMore" class="mt-1 mb-8" />
|
||||
</list-anim>
|
||||
<TransactionSkeleton v-for="i in 13" v-else :key="i" class="mt-1" />
|
||||
<div v-if="items.length <= 0" class="flex flex-col items-center justify-center h-80vh">
|
||||
<DotLottieVue
|
||||
class="w-11rem h-11rem"
|
||||
autoplay
|
||||
src="https://lottie.host/51d3315a-cf88-4c18-bb4d-a31511fd4325/KjSBzcMbi6.json"
|
||||
/>
|
||||
<div class="font-bold">تراکنشی موجود نیست</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- </a-spin>-->
|
||||
<BasicActionSheet v-model:show="showFilter">
|
||||
<div class="font-bold text-lg">فیلتر</div>
|
||||
<div class="pb-20 mt-5">
|
||||
<a-form-item class="w-full gap-2 !flex-col" label="تاریخ" name="password">
|
||||
<BaseDatePicker v-model:value="dateFilter" class="w-full" size="large" />
|
||||
</a-form-item>
|
||||
<BasicButton class="w-full mt-5" type="primary" @click="applyFilter"
|
||||
>اعمال فیلتر</BasicButton
|
||||
>
|
||||
<BasicButton class="w-full mt-2" @click="showFilter = false">بازگشت</BasicButton>
|
||||
</div>
|
||||
</BasicActionSheet>
|
||||
<div class="transactions-page-root">
|
||||
<ResponsiveViewPair>
|
||||
<template #mobile>
|
||||
<MobileTransactionsView />
|
||||
</template>
|
||||
<template #desktop>
|
||||
<DesktopDefaultTransactionsView />
|
||||
</template>
|
||||
</ResponsiveViewPair>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
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, 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, useIntersectionObserver, useScroll} from '@vueuse/core'
|
||||
import ListAnim from '@/components/ListAnim.vue'
|
||||
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
|
||||
import { DotLottieVue } from '@lottiefiles/dotlottie-vue'
|
||||
import img from "@/assets/img/user_ly1.svg";
|
||||
import ResponsiveViewPair from '@/layouts/components/ResponsiveViewPair.vue'
|
||||
import { useTransactionsPageProvider } from './composables/useTransactionsPage'
|
||||
import MobileTransactionsView from './mobile/MobileTransactionsView.vue'
|
||||
import DesktopDefaultTransactionsView from './desktop/DesktopDefaultTransactionsView.vue'
|
||||
|
||||
const pagination = ref<TableEngineInput>({ filters: [], limit: 7, page: 0 })
|
||||
const items = ref<Transaction[]>([])
|
||||
const tableOutput = ref<TableOutput<Transaction> | null>(null)
|
||||
const showFilter = ref<boolean>(false)
|
||||
const dateFilter = ref<string>()
|
||||
const observer = ref()
|
||||
// const { y } = useScroll(el)
|
||||
|
||||
async function initTable() {
|
||||
try {
|
||||
if (dateFilter.value) {
|
||||
pagination.value.filters = []
|
||||
pagination.value.filters.push({
|
||||
field: 'created_at',
|
||||
cr: 'and',
|
||||
type: 'date',
|
||||
op: 'eq',
|
||||
value: dateFilter.value
|
||||
})
|
||||
} else {
|
||||
pagination.value.filters = []
|
||||
}
|
||||
pagination.value.page === 0 ? (loading.value = true) : (loadingMore.value = true)
|
||||
tableOutput.value = await getTransactionTable(pagination.value)
|
||||
items.value.push(...tableOutput.value.rows)
|
||||
// console.log(tableOutput.value)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function applyFilter() {
|
||||
pagination.value.page = 0
|
||||
items.value = []
|
||||
await initTable()
|
||||
showFilter.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initTable()
|
||||
})
|
||||
|
||||
/*useInfiniteScroll(
|
||||
el,
|
||||
async () => {
|
||||
// load more
|
||||
// console.log('salamss')
|
||||
if (
|
||||
tableOutput.value &&
|
||||
(pagination.value.page + 1) * pagination.value.limit < tableOutput.value.query.total
|
||||
) {
|
||||
// console.log('gfgf')
|
||||
pagination.value.page++
|
||||
await initTable()
|
||||
}
|
||||
},
|
||||
{ distance: 12 }
|
||||
)*/
|
||||
|
||||
useIntersectionObserver(
|
||||
observer,
|
||||
async ([{isIntersecting}]) => {
|
||||
if ( isIntersecting &&
|
||||
tableOutput.value &&
|
||||
(pagination.value.page + 1) * pagination.value.limit < tableOutput.value.query.total
|
||||
) {
|
||||
// console.log('gfgf')
|
||||
pagination.value.page++
|
||||
await initTable()
|
||||
}
|
||||
},
|
||||
{
|
||||
root: null,
|
||||
rootMargin: '300px',
|
||||
threshold: 0.01
|
||||
}
|
||||
)
|
||||
|
||||
/*const items = [
|
||||
{
|
||||
isUp: true,
|
||||
hasTransition: true
|
||||
},
|
||||
{
|
||||
isUp: false,
|
||||
hasTransition: false
|
||||
},
|
||||
{
|
||||
isUp: true,
|
||||
hasTransition: false
|
||||
},
|
||||
{
|
||||
isUp: true,
|
||||
hasTransition: true
|
||||
},
|
||||
{
|
||||
isUp: false,
|
||||
hasTransition: false
|
||||
},
|
||||
{
|
||||
isUp: true,
|
||||
hasTransition: false
|
||||
},
|
||||
{
|
||||
isUp: true,
|
||||
hasTransition: true
|
||||
},
|
||||
{
|
||||
isUp: true,
|
||||
hasTransition: true
|
||||
},
|
||||
{
|
||||
isUp: false,
|
||||
hasTransition: false
|
||||
},
|
||||
{
|
||||
isUp: true,
|
||||
hasTransition: true
|
||||
},
|
||||
]*/
|
||||
useTransactionsPageProvider()
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.transaction-list {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
left: 0;
|
||||
@media screen and (min-width: 426px) {
|
||||
width: 426px;
|
||||
left: auto;
|
||||
right: auto;
|
||||
}
|
||||
<style scoped>
|
||||
.transactions-page-root {
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div>
|
||||
<div :style="{ backgroundImage: `url(${img})` }" class="fixed top-0 right-0 left-0 bottom-0"></div>
|
||||
<div class="absolute inset-0 bg-cover bg-center opacity-60"></div>
|
||||
<div
|
||||
class="bg-white w-full transaction-list py-3 px-3 font-bold drop-shadow flex justify-between z-10"
|
||||
>
|
||||
<div>لیست تراکنش ها</div>
|
||||
<BasicIcon icon="vuesax-linear:filter" @click="showFilter = true" />
|
||||
</div>
|
||||
<div
|
||||
ref="el"
|
||||
:class="loading ? 'h-screen' : ''"
|
||||
class="px-3 pt-16 pb-4"
|
||||
style="max-height: calc(100vh - 50px)"
|
||||
>
|
||||
<list-anim v-if="!loading">
|
||||
<Item v-for="item in items" :key="item.id" :item="item" />
|
||||
<div :ref="bindMobileObserver" v-show="!loading" class="w-full opacity-0 relative">
|
||||
در حال بارگذاری
|
||||
</div>
|
||||
<TransactionSkeleton v-if="loadingMore" class="mt-1 mb-8" />
|
||||
</list-anim>
|
||||
<TransactionSkeleton v-for="i in 13" v-else :key="i" class="mt-1" />
|
||||
<div v-if="items.length <= 0 && !loading" class="flex flex-col items-center justify-center h-80vh">
|
||||
<DotLottieVue
|
||||
class="w-11rem h-11rem"
|
||||
autoplay
|
||||
src="https://lottie.host/51d3315a-cf88-4c18-bb4d-a31511fd4325/KjSBzcMbi6.json"
|
||||
/>
|
||||
<div class="font-bold">{{ emptyText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<BasicActionSheet v-model:show="showFilter">
|
||||
<div class="font-bold text-lg">فیلتر</div>
|
||||
<div class="pb-20 mt-5">
|
||||
<a-form-item class="w-full gap-2 !flex-col" label="تاریخ" name="password">
|
||||
<BaseDatePicker v-model:value="dateFilter" class="w-full" size="large" />
|
||||
</a-form-item>
|
||||
<BasicButton class="w-full mt-5" type="primary" @click="applyFilter">اعمال فیلتر</BasicButton>
|
||||
<BasicButton class="w-full mt-2" @click="showFilter = false">بازگشت</BasicButton>
|
||||
</div>
|
||||
</BasicActionSheet>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Item from '../components/Item.vue'
|
||||
import { ref } from 'vue'
|
||||
import { loading, loadingMore } from '@/utils'
|
||||
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 ListAnim from '@/components/ListAnim.vue'
|
||||
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
|
||||
import { DotLottieVue } from '@lottiefiles/dotlottie-vue'
|
||||
import img from '@/assets/img/user_ly1.svg'
|
||||
import { useTransactionsPageContext } from '../composables/useTransactionsPage'
|
||||
|
||||
const el = ref<HTMLElement | null>(null)
|
||||
|
||||
const {
|
||||
items,
|
||||
dateFilter,
|
||||
emptyText,
|
||||
applyFilter,
|
||||
bindMobileObserver,
|
||||
showFilter,
|
||||
} = useTransactionsPageContext()
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.transaction-list {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
left: 0;
|
||||
@media screen and (min-width: 426px) {
|
||||
width: 426px;
|
||||
left: auto;
|
||||
right: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<a-form ref="formPassRef" :model="userPassForm" class="!mt-5" name="basic" @finish="submitPassForm">
|
||||
<a-form-item label="کلمه عبور جدید" :rules="passwordRules()" name="password">
|
||||
<BasicInput v-model:value="userPassForm.password" is-password place-holder="کلمه عبور " />
|
||||
<div
|
||||
:class="[guideTypePassword.color, userPassForm.password.length >= 6 ? 'block' : 'hidden']"
|
||||
class="text-[12px] mt-1"
|
||||
>
|
||||
{{ guideTypePassword.text }}
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="showSubmit">
|
||||
<BasicButton :disabled="loading" class="w-full py-2" 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 { loading } from '@/utils'
|
||||
import { useUserPageContext } from '../composables/useUserPage'
|
||||
|
||||
withDefaults(defineProps<{ showSubmit?: boolean }>(), { showSubmit: false })
|
||||
|
||||
const { userPassForm, formPassRef, passwordRules, guideTypePassword, submitPassForm } =
|
||||
useUserPageContext()
|
||||
</script>
|
||||
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<a-form ref="formRef" :model="userFormState" class="!mt-5" name="basic" @finish="ok">
|
||||
<a-form-item :rules="[isValidPersian()]" label="نام" name="fname">
|
||||
<BasicInput v-model:value="userFormState.fname" />
|
||||
</a-form-item>
|
||||
<a-form-item :rules="[isValidPersian()]" label="نام خانوادگی" name="lname">
|
||||
<BasicInput v-model:value="userFormState.lname" />
|
||||
</a-form-item>
|
||||
<a-form-item label="تاریخ تولد" name="birth_date">
|
||||
<BaseDatePicker v-model:value="userFormState.birth_date" class="w-full" input-read-only />
|
||||
</a-form-item>
|
||||
<a-form-item :rules="[requireNationalCode()]" label="شماره ملی" name="n_code">
|
||||
<BasicInput
|
||||
v-model:value="userFormState.n_code"
|
||||
type="number"
|
||||
:disabled="userFormState.is_legal == 1 || userFormState.is_legal == 2"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="نام شهر" name="city">
|
||||
<BasicInput v-model:value="userFormState.city" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="showSubmit">
|
||||
<BasicButton :disabled="loading" class="w-full py-2" 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 BaseDatePicker from '@/components/BasicPacks/BaseDatePicker'
|
||||
import { loading } from '@/utils'
|
||||
import { useUserPageContext } from '../composables/useUserPage'
|
||||
|
||||
withDefaults(defineProps<{ showSubmit?: boolean }>(), { showSubmit: false })
|
||||
|
||||
const { userFormState, formRef, isValidPersian, requireNationalCode, ok } = useUserPageContext()
|
||||
</script>
|
||||
@@ -0,0 +1,201 @@
|
||||
import { computed, inject, onMounted, provide, ref, type InjectionKey } from 'vue'
|
||||
import { loading } from '@/utils'
|
||||
import {
|
||||
guideTypePassword,
|
||||
isValidPersian,
|
||||
passwordRules,
|
||||
requireNationalCode,
|
||||
} from '@/utils/inputRules'
|
||||
import type { Password, User } from '@/models/login'
|
||||
import router from '@/router'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { updateMe, updatePass } from '@/api/me'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
|
||||
export interface UserMenuAction {
|
||||
id: string
|
||||
label: string
|
||||
icon: string
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export interface UserPageContext {
|
||||
userStore: ReturnType<typeof useUserStore>
|
||||
userFormState: ReturnType<typeof ref<User>>
|
||||
userPassForm: ReturnType<typeof ref<Password>>
|
||||
formRef: ReturnType<typeof ref<FormInstance | undefined>>
|
||||
formPassRef: ReturnType<typeof ref<FormInstance | undefined>>
|
||||
editPassVisible: ReturnType<typeof ref<boolean>>
|
||||
editPasswordVisible: ReturnType<typeof ref<boolean>>
|
||||
isLegal: ReturnType<typeof computed<{ title: string; color: string; icon: string }>>
|
||||
shareButtonLabel: ReturnType<typeof computed<string>>
|
||||
menuActions: ReturnType<typeof computed<UserMenuAction[]>>
|
||||
loading: typeof loading
|
||||
guideTypePassword: typeof guideTypePassword
|
||||
isValidPersian: typeof isValidPersian
|
||||
requireNationalCode: typeof requireNationalCode
|
||||
passwordRules: typeof passwordRules
|
||||
copyCode: () => void
|
||||
shareCode: () => void
|
||||
ok: () => void
|
||||
submitPassForm: () => void
|
||||
}
|
||||
|
||||
export const USER_PAGE_CONTEXT_KEY: InjectionKey<UserPageContext> = Symbol('userPageContext')
|
||||
|
||||
export function useUserPage() {
|
||||
const userFormState = ref<User>({
|
||||
fname: '',
|
||||
lname: '',
|
||||
mobile: undefined,
|
||||
birth_date: '',
|
||||
city: '',
|
||||
n_code: undefined,
|
||||
})
|
||||
const userPassForm = ref<Password>({ password: '' })
|
||||
const formRef = ref<FormInstance>()
|
||||
const formPassRef = ref<FormInstance>()
|
||||
const userStore = useUserStore()
|
||||
const editPassVisible = ref(false)
|
||||
const editPasswordVisible = ref(false)
|
||||
|
||||
const isLegal = computed(() => {
|
||||
const result = { title: '', color: '', icon: '' }
|
||||
switch (userFormState.value.is_legal) {
|
||||
case 0:
|
||||
result.title = 'احراز هویت نشده'
|
||||
result.color = 'reject'
|
||||
result.icon = 'vuesax-linear:close-circle'
|
||||
break
|
||||
case 1:
|
||||
case 2:
|
||||
result.title = 'احراز هویت شده'
|
||||
result.color = 'success'
|
||||
result.icon = 'vuesax-linear:verify'
|
||||
break
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
const shareButtonLabel = computed(() => 'اشتراک گذاری')
|
||||
|
||||
const menuActions = computed<UserMenuAction[]>(() => [
|
||||
{
|
||||
id: 'password',
|
||||
label: 'تغییر رمز عبور',
|
||||
icon: 'vuesax-linear:lock',
|
||||
onClick: () => { editPasswordVisible.value = true },
|
||||
},
|
||||
{
|
||||
id: 'about',
|
||||
label: 'درباره ما',
|
||||
icon: 'vuesax-linear:info-circle',
|
||||
onClick: () => router.push({ name: 'about' }),
|
||||
},
|
||||
{
|
||||
id: 'rules',
|
||||
label: 'قوانین و مقررات',
|
||||
icon: 'vuesax-linear:task',
|
||||
onClick: () => router.push({ name: 'rules' }),
|
||||
},
|
||||
])
|
||||
|
||||
function setForm() {
|
||||
if (userStore.userInfo) {
|
||||
userFormState.value = { ...userStore.userInfo }
|
||||
}
|
||||
}
|
||||
|
||||
function copyCode() {
|
||||
const val = userStore.userInfo?.referral_id
|
||||
if (val) {
|
||||
navigator.clipboard.writeText(val)
|
||||
message.info('کد رونوشت شد')
|
||||
}
|
||||
}
|
||||
|
||||
function shareCode() {
|
||||
const val = `⭐️الان طلا بخر؛ آخر ماه سودشو ببر!
|
||||
با خرید طلای آبشده در مقدارهای کم ولی مستمر، سود بیشتری ببر!
|
||||
|
||||
این دعوت نامه از طرف "${userStore.userInfo?.name}" عزیز برای شما ارسال شده تا شما هم از بازار طلای کشکول گلد جا نمونید.
|
||||
روی لینک زیر کلیک کنید و ثبت نام خود را تکمیل کنید.
|
||||
|
||||
kh.kashkolgold.ir/login?id=${userStore.userInfo?.referral_id}`
|
||||
navigator.share({ title: 'کشکول گلد', text: val })
|
||||
}
|
||||
|
||||
function ok() {
|
||||
formRef.value?.validateFields().then(async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
await updateMe(userFormState.value)
|
||||
await userStore.getMyInfo()
|
||||
editPassVisible.value = false
|
||||
message.success('اطلاعات شما با موفقیت به روز رسانی شد')
|
||||
location.reload()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function submitPassForm() {
|
||||
formPassRef.value?.validateFields().then(async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
await updatePass(userPassForm.value)
|
||||
await userStore.getMyInfo()
|
||||
message.success('پسورد شما با موفقیت به روز رسانی شد')
|
||||
editPasswordVisible.value = false
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setForm()
|
||||
if (userStore.userInfo?.is_legal === 0) {
|
||||
editPassVisible.value = true
|
||||
}
|
||||
})
|
||||
|
||||
const context: UserPageContext = {
|
||||
userStore,
|
||||
userFormState,
|
||||
userPassForm,
|
||||
formRef,
|
||||
formPassRef,
|
||||
editPassVisible,
|
||||
editPasswordVisible,
|
||||
isLegal,
|
||||
shareButtonLabel,
|
||||
menuActions,
|
||||
loading,
|
||||
guideTypePassword,
|
||||
isValidPersian,
|
||||
requireNationalCode,
|
||||
passwordRules,
|
||||
copyCode,
|
||||
shareCode,
|
||||
ok,
|
||||
submitPassForm,
|
||||
}
|
||||
|
||||
provide(USER_PAGE_CONTEXT_KEY, context)
|
||||
return context
|
||||
}
|
||||
|
||||
export function useUserPageContext() {
|
||||
const context = inject(USER_PAGE_CONTEXT_KEY)
|
||||
if (!context) {
|
||||
throw new Error('useUserPageContext must be used within useUserPage')
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div class="duser">
|
||||
<div class="duser__container">
|
||||
<header class="duser-topbar">
|
||||
<button type="button" class="duser-topbar__back" @click="router.back()">
|
||||
<BasicIcon icon="vuesax-linear:arrow-right-1" size="18" />
|
||||
<span>بازگشت</span>
|
||||
</button>
|
||||
<div class="duser-topbar__meta">
|
||||
<p class="duser-topbar__eyebrow">حساب کاربری</p>
|
||||
<h1 class="duser-topbar__title">اطلاعات حساب کاربری</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="duser-grid">
|
||||
<section class="duser-surface duser-profile">
|
||||
<div class="duser-profile__header">
|
||||
<div class="duser-profile__avatar">
|
||||
<BasicIcon icon="vuesax-linear:user" size="28" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="duser-profile__name">{{ userStore.userInfo?.name ?? 'کاربر' }}</h2>
|
||||
<p class="duser-profile__phone">{{ userStore.userInfo?.mobile }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="duser-profile__fields">
|
||||
<div class="duser-profile__field">
|
||||
<span class="duser-profile__label">نام و نام خانوادگی</span>
|
||||
<span class="duser-profile__value">{{ userStore.userInfo?.name }}</span>
|
||||
</div>
|
||||
<div class="duser-profile__field">
|
||||
<span class="duser-profile__label">شماره تلفن همراه</span>
|
||||
<span class="duser-profile__value">{{ userStore.userInfo?.mobile }}</span>
|
||||
</div>
|
||||
<div v-if="userStore.userInfo?.referral_id" class="duser-profile__field">
|
||||
<span class="duser-profile__label">کد معرف</span>
|
||||
<span class="duser-profile__value">{{ userStore.userInfo?.referral_id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="userStore.userInfo?.referral_id" class="duser-profile__referral-actions">
|
||||
<button type="button" class="duser-profile__btn" @click="shareCode">
|
||||
<BasicIcon icon="vuesax-linear:share" size="16" />
|
||||
<span>{{ shareButtonLabel }}</span>
|
||||
</button>
|
||||
<button type="button" class="duser-profile__btn" @click="copyCode">
|
||||
<BasicIcon icon="vuesax-linear:copy" size="16" />
|
||||
<span>رونوشت</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="duser-profile__btn duser-profile__btn--primary"
|
||||
@click="editPassVisible = true"
|
||||
>
|
||||
<BasicIcon icon="vuesax-linear:edit-2" size="16" />
|
||||
<span>ویرایش اطلاعات کاربری</span>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="duser-surface duser-actions">
|
||||
<h3 class="duser-actions__title">تنظیمات و خدمات</h3>
|
||||
<div class="duser-actions__list">
|
||||
<button
|
||||
v-for="action in menuActions"
|
||||
:key="action.id"
|
||||
type="button"
|
||||
class="duser-actions__item"
|
||||
@click="action.onClick"
|
||||
>
|
||||
<span class="duser-actions__icon">
|
||||
<BasicIcon :icon="action.icon" size="18" />
|
||||
</span>
|
||||
<span class="duser-actions__label">{{ action.label }}</span>
|
||||
<BasicIcon class="duser-actions__chevron" icon="vuesax-linear:arrow-left" size="16" />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import router from '@/router'
|
||||
import { useUserPageContext } from '../composables/useUserPage'
|
||||
|
||||
const {
|
||||
userStore,
|
||||
editPassVisible,
|
||||
shareButtonLabel,
|
||||
menuActions,
|
||||
copyCode,
|
||||
shareCode,
|
||||
} = useUserPageContext()
|
||||
</script>
|
||||
|
||||
<style lang="less" src="./styles/desktop-user.less"></style>
|
||||
@@ -0,0 +1,260 @@
|
||||
/* Premium desktop user account page */
|
||||
|
||||
.duser {
|
||||
min-height: 100%;
|
||||
background: #f6f7f9;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.duser__container {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 28px 36px 40px;
|
||||
}
|
||||
|
||||
.duser-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;
|
||||
}
|
||||
}
|
||||
|
||||
.duser-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(280px, 340px);
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.duser-surface {
|
||||
background: #ffffff;
|
||||
border-radius: 14px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.duser-profile {
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%);
|
||||
color: @App-primary-color;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
color: #111827;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
&__phone {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
direction: ltr;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&__fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
&__field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
direction: ltr;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&__referral-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e5e7eb;
|
||||
background: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f9fafb;
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
width: 100%;
|
||||
background: @App-primary-color;
|
||||
border-color: @App-primary-color;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.92;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.duser-actions {
|
||||
&__title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
&__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #f3f4f6;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
text-align: right;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f9fafb;
|
||||
border-color: #e5e7eb;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
flex-shrink: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
&__icon-image {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
object-fit: contain;
|
||||
transform: scale(2.2);
|
||||
}
|
||||
|
||||
&__label {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
&__chevron {
|
||||
color: #9ca3af;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.duser-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
+61
-493
@@ -1,212 +1,9 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div>
|
||||
<!-- <img :src="gr_bottom" alt="" class="absolute bottom-20">-->
|
||||
<img :src="img" alt="" class="mt-2 w-full absolute bottom-0" />
|
||||
</div>
|
||||
<div class="bg-white w-full py-3 px-3 font-bold drop-shadow flex">
|
||||
<div @click="router.back()" class="cursor-pointer">
|
||||
<BasicIcon icon="vuesax-linear:arrow-right-1" />
|
||||
</div>
|
||||
<div class="mr-2 items-center">اطلاعات حساب کاربری</div>
|
||||
</div>
|
||||
<div class="pt-16 pb-8 z-30 relative">
|
||||
<div
|
||||
class="border-gray-200 border-1 rounded-xl p-5 trans flex flex-col items-center mx-5 z-1000 relative bg-white shadow-lg"
|
||||
>
|
||||
<div class="font-bold w-full text-center flex justify-between">
|
||||
نام و نام خانوادگی:
|
||||
<div class="mr-2">{{ userStore.userInfo?.name }}</div>
|
||||
</div>
|
||||
<div class="font-bold w-full text-center flex justify-between">
|
||||
شماره تلفن همراه:
|
||||
<div class="mr-2">{{ userStore.userInfo?.mobile }}</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="userStore.userInfo?.referral_id"
|
||||
class="font-bold w-full text-center flex justify-between gap-2 items-center"
|
||||
>
|
||||
کد معرف:
|
||||
<div class="flex gap-2">
|
||||
<div class="mr-2">{{ userStore.userInfo?.referral_id }}</div>
|
||||
<!-- <BasicIcon icon="vuesax-linear:copy" color="#2563EB" class="cursor-pointer" size="18"
|
||||
@click.stop="copyCode()" />
|
||||
<!– vuesax-linear:hierarchy-3–>
|
||||
<BasicIcon icon="vuesax-linear:share" color="#2563EB" class="cursor-pointer" size="18"
|
||||
@click.stop="shareCode()" />-->
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex gap-2 mt-1">
|
||||
<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>
|
||||
<BasicButton
|
||||
class="mt-5 w-full"
|
||||
icon=""
|
||||
iconSize="18"
|
||||
size="large"
|
||||
type="primary"
|
||||
@click="editPassVisible = true"
|
||||
>
|
||||
<div class="flex">
|
||||
<BasicIcon icon="vuesax-linear:eye" class="absolute right-8" />
|
||||
<div>ویرایش اطلاعات کاربری</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
<div class="h-40vh overflow-y-auto mt-4">
|
||||
<BasicButton
|
||||
class="mt-2 w-full"
|
||||
icon=""
|
||||
iconSize="18"
|
||||
size="large"
|
||||
@click="editPasswordVisible = true"
|
||||
:key="10"
|
||||
>
|
||||
<div class="flex">
|
||||
<BasicIcon icon="vuesax-linear:lock" class="absolute right-8" />
|
||||
<div>تغییر رمز عبور</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
<!-- <BasicButton
|
||||
class="w-full mt-2"
|
||||
size="large"
|
||||
@click="$router.push({ name: 'address' })"
|
||||
>
|
||||
<div class="flex">
|
||||
<BasicIcon icon="vuesax-linear:map" class="absolute right-8" />
|
||||
<div>آدرس ها</div>
|
||||
</div>
|
||||
</BasicButton>-->
|
||||
<!-- <BasicButton-->
|
||||
<!-- class="w-full mt-2"-->
|
||||
<!-- size="large"-->
|
||||
<!-- @click="$router.push({ name: 'notifyTrigger' })"-->
|
||||
<!-- >-->
|
||||
<!-- <div class="flex">-->
|
||||
<!-- <BasicIcon icon="vuesax-linear:bill" class="absolute right-8" />-->
|
||||
<!-- <div>هشدار قیمت</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </BasicButton>-->
|
||||
<BasicButton
|
||||
class="mt-2 w-full"
|
||||
icon=""
|
||||
iconSize="18"
|
||||
size="large"
|
||||
@click="$router.push({ name: 'about' })"
|
||||
>
|
||||
<div class="flex">
|
||||
<BasicIcon icon="vuesax-linear:info-circle" class="absolute right-8" />
|
||||
<div>درباره ما</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
<!-- <BasicButton-->
|
||||
<!-- class="mt-2 w-full"-->
|
||||
<!-- icon=""-->
|
||||
<!-- iconSize="18"-->
|
||||
<!-- size="large"-->
|
||||
<!-- @click="$router.push({ name: 'physicalReceive' })"-->
|
||||
<!-- >-->
|
||||
<!-- <div class="flex">-->
|
||||
<!-- <BasicIcon icon="vuesax-linear:direct-inbox" class="absolute right-8" />-->
|
||||
<!-- <div>دریافت طلای فیزیکی</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </BasicButton>-->
|
||||
<BasicButton
|
||||
class="mt-2 w-full"
|
||||
icon=""
|
||||
iconSize="18"
|
||||
size="large"
|
||||
@click="$router.push({ name: 'rules' })"
|
||||
>
|
||||
<div class="flex">
|
||||
<BasicIcon icon="vuesax-linear:task" class="absolute right-8" />
|
||||
<div>قوانین و مقررات</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
<!-- <BasicButton
|
||||
class="mt-2 w-full"
|
||||
icon=""
|
||||
iconSize="18"
|
||||
size="large"
|
||||
@click="$router.push({ name: 'permissions' })"
|
||||
>
|
||||
<div class="flex">
|
||||
<BasicIcon icon="vuesax-linear:archive-book" class="absolute right-8" />
|
||||
<div>مجوز ها</div>
|
||||
</div>
|
||||
</BasicButton>-->
|
||||
<!-- <BasicButton-->
|
||||
<!-- class="mt-2 w-full"-->
|
||||
<!-- icon=""-->
|
||||
<!-- iconSize="18"-->
|
||||
<!-- size="large"-->
|
||||
<!-- @click="$router.push({ name: 'earnMoney' })"-->
|
||||
<!-- >-->
|
||||
<!-- <div class="flex">-->
|
||||
<!-- <BasicIcon icon="vuesax-linear:dollar-circle" class="absolute right-8" />-->
|
||||
<!-- <div>کسب درآمد</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </BasicButton>-->
|
||||
<BasicButton
|
||||
class="mt-2 w-full"
|
||||
icon=""
|
||||
iconSize="18"
|
||||
size="large"
|
||||
@click="showLogoutConfirm=!showLogoutConfirm"
|
||||
>
|
||||
<div class="flex">
|
||||
<Icon
|
||||
:color="y > 50 ? '' : ''"
|
||||
class="color-transition absolute right-8"
|
||||
icon="vuesax-linear:logout"
|
||||
@click="showLogoutConfirm = true"
|
||||
/>
|
||||
<div>خروج از حساب کاربری</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogVant v-model:show="showLogoutConfirm" :closeOnClickOverlay="false" :timeOut="-1">
|
||||
<!-- <template v-slot:img>
|
||||
<img :src="registerDelayImage" alt="" class="opacity-80">
|
||||
</template>-->
|
||||
<div class="user-page-root">
|
||||
<DesktopUserView v-show="isDesktop" />
|
||||
<MobileUserView v-show="!isDesktop" />
|
||||
|
||||
<template v-slot:title>
|
||||
<div class="flex flex-col items-center">
|
||||
<DotLottieVue
|
||||
class="w-11rem h-11rem"
|
||||
autoplay
|
||||
src="https://lottie.host/129cf3a1-df84-4758-a856-58c133ae6013/R5RqclXA8G.json"
|
||||
/>
|
||||
<div class="p-5 text-sm">آیا از خروج خود اطمینان دارید؟</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:button>
|
||||
<div class="text-center text-red flex items-center justify-center" @click="logout">
|
||||
<a-spin :spinning="logoutLoading" size="small"> خروج</a-spin>
|
||||
</div>
|
||||
<div class="text-center" @click="showLogoutConfirm = false">انصراف</div>
|
||||
</template>
|
||||
</DialogVant>
|
||||
<BasicActionSheet v-model:show="editPassVisible">
|
||||
<BasicActionSheet v-if="!isDesktop" v-model:show="editPassVisible">
|
||||
<div class="flex justify-center justify-between">
|
||||
<div class="font-bold text-lg">ویرایش اطلاعات</div>
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
@@ -215,314 +12,85 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="h60vh overflow-y-auto pb20">
|
||||
<a-form ref="formRef" :model="userFormState" class="!mt-5" name="basic" @finish="ok">
|
||||
<a-form-item :rules="[isValidPersian()]" label="نام" name="fname">
|
||||
<BasicInput v-model:value="userFormState.fname" />
|
||||
</a-form-item>
|
||||
<a-form-item :rules="[isValidPersian()]" label="نام خانوادگی" name="lname">
|
||||
<BasicInput v-model:value="userFormState.lname" />
|
||||
</a-form-item>
|
||||
<a-form-item label="تاریخ تولد" name="birth_date">
|
||||
<BaseDatePicker v-model:value="userFormState.birth_date" class="w-full" inputReadOnly />
|
||||
</a-form-item>
|
||||
<a-form-item :rules="[requireNationalCode()]" label="شماره ملی" name="n_code">
|
||||
<BasicInput v-model:value="userFormState.n_code" type="number" :disabled="userFormState.is_legal == 1 || userFormState.is_legal == 2" />
|
||||
</a-form-item>
|
||||
<a-form-item label="نام شهر" name="city">
|
||||
<BasicInput v-model:value="userFormState.city" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<div class="wrap-fix md:w-[395px] bottom-5 px-3">
|
||||
<BasicButton
|
||||
:disabled="loading"
|
||||
class="w-full py-2 mb-20"
|
||||
html-type="submit"
|
||||
type="primary"
|
||||
>
|
||||
<div>
|
||||
<a-spin v-if="loading" size="small" />
|
||||
<div v-else>ثبت</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<UserProfileForm show-submit />
|
||||
</div>
|
||||
</BasicActionSheet>
|
||||
<BasicActionSheet v-model:show="editPasswordVisible">
|
||||
|
||||
<BasicModal
|
||||
v-else
|
||||
v-model:visible="editPassVisible"
|
||||
title="ویرایش اطلاعات"
|
||||
width="520px"
|
||||
destroy-on-close
|
||||
:loading="loading"
|
||||
@ok="ok"
|
||||
>
|
||||
<div class="flex justify-end mb-4">
|
||||
<div class="flex flex-col items-center">
|
||||
<BasicIcon :icon="isLegal.icon" size="30" :class="isLegal.color" />
|
||||
<p class="text-[10px] mt-1" :class="isLegal.color">{{ isLegal.title }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<UserProfileForm />
|
||||
</BasicModal>
|
||||
|
||||
<BasicActionSheet v-if="!isDesktop" v-model:show="editPasswordVisible">
|
||||
<div class="font-bold text-lg">ویرایش رمزعبور</div>
|
||||
<div class="h30vh overflow-y-auto pb20">
|
||||
<a-form
|
||||
ref="formPassRef"
|
||||
:model="userPassForm"
|
||||
class="!mt-5"
|
||||
name="basic"
|
||||
@finish="submitPassForm"
|
||||
>
|
||||
<a-form-item label="کلمه عبور جدید" :rules="passwordRules()" name="password">
|
||||
<BasicInput v-model:value="userPassForm.password" isPassword place-holder="کلمه عبور " />
|
||||
<div
|
||||
:class="[
|
||||
guideTypePassword.color,
|
||||
userPassForm.password.length >= 6 ? 'block' : 'hidden'
|
||||
]"
|
||||
class="text-[12px] mt-1"
|
||||
>
|
||||
{{ guideTypePassword.text }}
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<div class="wrap-fix md:w-[395px] bottom-5 px-3">
|
||||
<BasicButton
|
||||
:disabled="loading"
|
||||
class="w-full py-2 mb-20"
|
||||
html-type="submit"
|
||||
type="primary"
|
||||
>
|
||||
<div>
|
||||
<a-spin v-if="loading" size="small" />
|
||||
<div v-else>ثبت</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<div class="h35vh overflow-y-auto">
|
||||
<UserPasswordForm show-submit />
|
||||
</div>
|
||||
</BasicActionSheet>
|
||||
|
||||
<BasicModal
|
||||
v-else
|
||||
v-model:visible="editPasswordVisible"
|
||||
title="ویرایش رمزعبور"
|
||||
width="480px"
|
||||
destroy-on-close
|
||||
:loading="loading"
|
||||
@ok="submitPassForm"
|
||||
>
|
||||
<UserPasswordForm />
|
||||
</BasicModal>
|
||||
</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, removeToken} from '@/utils'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
||||
import {
|
||||
lengthValidation,
|
||||
required,
|
||||
requireNationalCode,
|
||||
passwordRules,
|
||||
guideTypePassword,
|
||||
isValidPersian,
|
||||
} from '@/utils/inputRules'
|
||||
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
||||
import type { User, Password } 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, updatePass } from '@/api/me'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import BaseDatePicker from '@/components/BasicPacks/BaseDatePicker'
|
||||
import {DotLottieVue} from "@lottiefiles/dotlottie-vue";
|
||||
import DialogVant from "@/components/DialogVant.vue";
|
||||
import {useWalletStore} from "@/store/walletStore";
|
||||
import Icon from "@/components/BasicPacks/BasicIcon.vue";
|
||||
import {useWindowScroll} from "@vueuse/core/index";
|
||||
const userFormState = ref<User>({
|
||||
fname: '',
|
||||
lname: '',
|
||||
mobile: undefined,
|
||||
birth_date: '',
|
||||
city: '',
|
||||
n_code: undefined
|
||||
})
|
||||
const userPassForm = ref<Password>({
|
||||
password: ''
|
||||
})
|
||||
const formRef = ref<FormInstance>()
|
||||
const formPassRef = ref<FormInstance>()
|
||||
const userStore = useUserStore()
|
||||
const showLogoutConfirm = ref<boolean>()
|
||||
const walletStore=useWalletStore()
|
||||
const logoutLoading = ref<boolean>(false)
|
||||
const { x, y } = useWindowScroll()
|
||||
import BasicModal from '@/components/BasicPacks/BasicModal.vue'
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import { useUserPage } from './composables/useUserPage'
|
||||
import MobileUserView from './mobile/MobileUserView.vue'
|
||||
import DesktopUserView from './desktop/DesktopUserView.vue'
|
||||
import UserProfileForm from './components/UserProfileForm.vue'
|
||||
import UserPasswordForm from './components/UserPasswordForm.vue'
|
||||
|
||||
// const editPassVisible = ref<boolean>(false)
|
||||
const editPassVisible = ref<boolean>(false)
|
||||
const editPasswordVisible = ref<boolean>(false)
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
|
||||
async function logout() {
|
||||
logoutLoading.value = true
|
||||
// baseConfirm('آیا از حساب کاربری خارج می شوید؟',async () => {
|
||||
userStore.userInfo = undefined
|
||||
walletStore.walletInfo = undefined
|
||||
/* userStore.$reset()
|
||||
walletStore.$reset()*/
|
||||
removeToken()
|
||||
await router.push({ name: 'login' })
|
||||
logoutLoading.value = true
|
||||
window.location.reload()
|
||||
// console.log('salam')
|
||||
// })
|
||||
}
|
||||
function setForm() {
|
||||
userFormState.value = userStore.userInfo
|
||||
}
|
||||
|
||||
function copyCode() {
|
||||
// nextTick(() => {
|
||||
const val = userStore.userInfo?.referral_id
|
||||
navigator.clipboard.writeText(val)
|
||||
message.info('کد رونوشت شد')
|
||||
// })
|
||||
}
|
||||
const isLegal = computed(() => {
|
||||
const result = {
|
||||
title: '',
|
||||
color: '',
|
||||
icon: ''
|
||||
}
|
||||
switch (userFormState.value.is_legal) {
|
||||
case 0:
|
||||
result.title = 'احراز هویت نشده'
|
||||
result.color = 'reject'
|
||||
result.icon = 'vuesax-linear:close-circle'
|
||||
break
|
||||
case 1:
|
||||
result.title = 'احراز هویت شده'
|
||||
result.color = 'success'
|
||||
result.icon = 'vuesax-linear:verify'
|
||||
|
||||
break
|
||||
case 2:
|
||||
result.title = 'احراز هویت شده'
|
||||
result.color = 'success'
|
||||
result.icon = 'vuesax-linear:verify'
|
||||
|
||||
break
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
function shareCode() {
|
||||
// nextTick(() => {
|
||||
const val = `⭐️الان طلا بخر؛ آخر ماه سودشو ببر!
|
||||
با خرید طلای آبشده در مقدارهای کم ولی مستمر، سود بیشتری ببر!
|
||||
|
||||
این دعوت نامه از طرف "${userStore.userInfo?.name}" عزیز برای شما ارسال شده تا شما هم از بازار طلای کشکول گلد جا نمونید.
|
||||
روی لینک زیر کلیک کنید و ثبت نام خود را تکمیل کنید.
|
||||
|
||||
kh.kashkolgold.ir/login?id=${userStore.userInfo?.referral_id}`
|
||||
navigator.share({
|
||||
title: 'کشکول گلد',
|
||||
text: val
|
||||
})
|
||||
// message.info('کد رونوشت شد')
|
||||
// })
|
||||
}
|
||||
|
||||
const resetData = () => {
|
||||
formRef.value?.resetFields()
|
||||
userFormState.value = {
|
||||
fname: '',
|
||||
lname: '',
|
||||
mobile: undefined,
|
||||
birth_date: '',
|
||||
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('اطلاعات شما با موفقیت به روز رسانی شد')
|
||||
location.reload()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
function submitPassForm() {
|
||||
formPassRef.value?.validateFields().then(async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
await updatePass(userPassForm.value)
|
||||
await userStore.getMyInfo()
|
||||
editPassVisible.value = false
|
||||
message.success('پسورد شما با موفقیت به روز رسانی شد')
|
||||
editPasswordVisible.value = false
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
console.log(userStore.userInfo?.is_legal === 2)
|
||||
if (userStore.userInfo?.is_legal === 0) {
|
||||
editPassVisible.value = true
|
||||
}
|
||||
})
|
||||
const {
|
||||
editPassVisible,
|
||||
editPasswordVisible,
|
||||
isLegal,
|
||||
loading,
|
||||
ok,
|
||||
submitPassForm,
|
||||
} = useUserPage()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/*.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
.user-page-root {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}*/
|
||||
/*.trans{
|
||||
transition: height 1s;
|
||||
}*/
|
||||
.midLevel {
|
||||
color: orange;
|
||||
}
|
||||
.week,
|
||||
<style lang="less">
|
||||
.reject {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: green;
|
||||
}
|
||||
.background-image-container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.background-image-container img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
.relative {
|
||||
overflow-y: auto;
|
||||
max-height: 100vh;
|
||||
//scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.fixed {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 50; /* بالاتر از همه */
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div>
|
||||
<!-- <img :src="gr_bottom" alt="" class="absolute bottom-20">-->
|
||||
<img :src="img" alt="" class="mt-2 w-full absolute bottom-0" />
|
||||
</div>
|
||||
<div class="bg-white w-full py-3 px-3 font-bold drop-shadow flex">
|
||||
<div @click="router.back()" class="cursor-pointer">
|
||||
<BasicIcon icon="vuesax-linear:arrow-right-1" />
|
||||
</div>
|
||||
<div class="mr-2 items-center">اطلاعات حساب کاربری</div>
|
||||
</div>
|
||||
<div class="pt-16 pb-8 z-30 relative">
|
||||
<div
|
||||
class="border-gray-200 border-1 rounded-xl p-5 trans flex flex-col items-center mx-5 z-1000 relative bg-white shadow-lg"
|
||||
>
|
||||
<div class="font-bold w-full text-center flex justify-between">
|
||||
نام و نام خانوادگی:
|
||||
<div class="mr-2">{{ userStore.userInfo?.name }}</div>
|
||||
</div>
|
||||
<div class="font-bold w-full text-center flex justify-between">
|
||||
شماره تلفن همراه:
|
||||
<div class="mr-2">{{ userStore.userInfo?.mobile }}</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="userStore.userInfo?.referral_id"
|
||||
class="font-bold w-full text-center flex justify-between gap-2 items-center"
|
||||
>
|
||||
کد معرف:
|
||||
<div class="flex gap-2">
|
||||
<div class="mr-2">{{ userStore.userInfo?.referral_id }}</div>
|
||||
<!-- <BasicIcon icon="vuesax-linear:copy" color="#2563EB" class="cursor-pointer" size="18"
|
||||
@click.stop="copyCode()" />
|
||||
<!– vuesax-linear:hierarchy-3–>
|
||||
<BasicIcon icon="vuesax-linear:share" color="#2563EB" class="cursor-pointer" size="18"
|
||||
@click.stop="shareCode()" />-->
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex gap-2 mt-1">
|
||||
<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>
|
||||
<BasicButton
|
||||
class="mt-5 w-full"
|
||||
icon=""
|
||||
iconSize="18"
|
||||
size="large"
|
||||
type="primary"
|
||||
@click="editPassVisible = true"
|
||||
>
|
||||
<div class="flex">
|
||||
<BasicIcon icon="vuesax-linear:eye" class="absolute right-8" />
|
||||
<div>ویرایش اطلاعات کاربری</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
<div class="h-40vh overflow-y-auto mt-4">
|
||||
<BasicButton
|
||||
class="mt-2 w-full"
|
||||
icon=""
|
||||
iconSize="18"
|
||||
size="large"
|
||||
@click="editPasswordVisible = true"
|
||||
>
|
||||
<div class="flex">
|
||||
<BasicIcon icon="vuesax-linear:lock" class="absolute right-8" />
|
||||
<div>تغییر رمز عبور</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
<!-- <BasicButton
|
||||
class="w-full mt-2"
|
||||
size="large"
|
||||
@click="$router.push({ name: 'address' })"
|
||||
>
|
||||
<div class="flex">
|
||||
<BasicIcon icon="vuesax-linear:map" class="absolute right-8" />
|
||||
<div>آدرس ها</div>
|
||||
</div>
|
||||
</BasicButton>-->
|
||||
<!-- <BasicButton-->
|
||||
<!-- class="w-full mt-2"-->
|
||||
<!-- size="large"-->
|
||||
<!-- @click="$router.push({ name: 'notifyTrigger' })"-->
|
||||
<!-- >-->
|
||||
<!-- <div class="flex">-->
|
||||
<!-- <BasicIcon icon="vuesax-linear:bill" class="absolute right-8" />-->
|
||||
<!-- <div>هشدار قیمت</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </BasicButton>-->
|
||||
<BasicButton
|
||||
class="mt-2 w-full"
|
||||
icon=""
|
||||
iconSize="18"
|
||||
size="large"
|
||||
@click="$router.push({ name: 'about' })"
|
||||
>
|
||||
<div class="flex">
|
||||
<BasicIcon icon="vuesax-linear:info-circle" class="absolute right-8" />
|
||||
<div>درباره ما</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
<!-- <BasicButton-->
|
||||
<!-- class="mt-2 w-full"-->
|
||||
<!-- icon=""-->
|
||||
<!-- iconSize="18"-->
|
||||
<!-- size="large"-->
|
||||
<!-- @click="$router.push({ name: 'physicalReceive' })"-->
|
||||
<!-- >-->
|
||||
<!-- <div class="flex">-->
|
||||
<!-- <BasicIcon icon="vuesax-linear:direct-inbox" class="absolute right-8" />-->
|
||||
<!-- <div>دریافت طلای فیزیکی</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </BasicButton>-->
|
||||
<BasicButton
|
||||
class="mt-2 w-full"
|
||||
icon=""
|
||||
iconSize="18"
|
||||
size="large"
|
||||
@click="$router.push({ name: 'rules' })"
|
||||
>
|
||||
<div class="flex">
|
||||
<BasicIcon icon="vuesax-linear:task" class="absolute right-8" />
|
||||
<div>قوانین و مقررات</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
<!-- <BasicButton
|
||||
class="mt-2 w-full"
|
||||
icon=""
|
||||
iconSize="18"
|
||||
size="large"
|
||||
@click="$router.push({ name: 'permissions' })"
|
||||
>
|
||||
<div class="flex">
|
||||
<BasicIcon icon="vuesax-linear:archive-book" class="absolute right-8" />
|
||||
<div>مجوز ها</div>
|
||||
</div>
|
||||
</BasicButton>-->
|
||||
<!-- <BasicButton-->
|
||||
<!-- class="mt-2 w-full"-->
|
||||
<!-- icon=""-->
|
||||
<!-- iconSize="18"-->
|
||||
<!-- size="large"-->
|
||||
<!-- @click="$router.push({ name: 'earnMoney' })"-->
|
||||
<!-- >-->
|
||||
<!-- <div class="flex">-->
|
||||
<!-- <BasicIcon icon="vuesax-linear:dollar-circle" class="absolute right-8" />-->
|
||||
<!-- <div>کسب درآمد</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </BasicButton>-->
|
||||
<BasicButton
|
||||
class="mt-2 w-full"
|
||||
icon=""
|
||||
iconSize="18"
|
||||
size="large"
|
||||
@click="showLogoutConfirm=!showLogoutConfirm"
|
||||
>
|
||||
<div class="flex">
|
||||
<Icon
|
||||
:color="y > 50 ? '' : ''"
|
||||
class="color-transition absolute right-8"
|
||||
icon="vuesax-linear:logout"
|
||||
@click="showLogoutConfirm = true"
|
||||
/>
|
||||
<div>خروج از حساب کاربری</div>
|
||||
</div>
|
||||
</BasicButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogVant v-model:show="showLogoutConfirm" :closeOnClickOverlay="false" :timeOut="-1">
|
||||
<!-- <template v-slot:img>
|
||||
<img :src="registerDelayImage" alt="" class="opacity-80">
|
||||
</template>-->
|
||||
|
||||
<template v-slot:title>
|
||||
<div class="flex flex-col items-center">
|
||||
<DotLottieVue
|
||||
class="w-11rem h-11rem"
|
||||
autoplay
|
||||
src="https://lottie.host/129cf3a1-df84-4758-a856-58c133ae6013/R5RqclXA8G.json"
|
||||
/>
|
||||
<div class="p-5 text-sm">آیا از خروج خود اطمینان دارید؟</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:button>
|
||||
<div class="text-center text-red flex items-center justify-center" @click="logout">
|
||||
<a-spin :spinning="logoutLoading" size="small"> خروج</a-spin>
|
||||
</div>
|
||||
<div class="text-center" @click="showLogoutConfirm = false">انصراف</div>
|
||||
</template>
|
||||
</DialogVant>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import img from '@/assets/img/user_ly1.svg'
|
||||
import { loading, removeToken } from '@/utils'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import router from '@/router'
|
||||
import { DotLottieVue } from '@lottiefiles/dotlottie-vue'
|
||||
import DialogVant from '@/components/DialogVant.vue'
|
||||
import { useWalletStore } from '@/store/walletStore'
|
||||
import Icon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { useWindowScroll } from '@vueuse/core/index'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { useUserPageContext } from '../composables/useUserPage'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const walletStore = useWalletStore()
|
||||
const showLogoutConfirm = ref<boolean>()
|
||||
const logoutLoading = ref<boolean>(false)
|
||||
const { y } = useWindowScroll()
|
||||
|
||||
const { editPassVisible, editPasswordVisible, copyCode, shareCode } = useUserPageContext()
|
||||
|
||||
async function logout() {
|
||||
logoutLoading.value = true
|
||||
userStore.userInfo = undefined
|
||||
walletStore.walletInfo = undefined
|
||||
removeToken()
|
||||
await router.push({ name: 'login' })
|
||||
window.location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/*.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}*/
|
||||
/*.trans{
|
||||
transition: height 1s;
|
||||
}*/
|
||||
.midLevel {
|
||||
color: orange;
|
||||
}
|
||||
.week,
|
||||
.reject {
|
||||
color: red;
|
||||
}
|
||||
.success {
|
||||
color: green;
|
||||
}
|
||||
.background-image-container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.background-image-container img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
.relative {
|
||||
overflow-y: auto;
|
||||
max-height: 100vh;
|
||||
//scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.fixed {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 50; /* بالاتر از همه */
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user