1405/02/24-Optimization home index.vue

This commit is contained in:
2026-05-14 13:44:43 +03:30
parent e3b4182f6c
commit 1b13aa2a7d
19 changed files with 2143 additions and 1082 deletions
+5
View File
@@ -14,12 +14,16 @@ declare module 'vue' {
AInput: typeof import('ant-design-vue/es')['Input']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
AModal: typeof import('ant-design-vue/es')['Modal']
ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASlider: typeof import('ant-design-vue/es')['Slider']
ASpin: typeof import('ant-design-vue/es')['Spin']
ASwitch: typeof import('ant-design-vue/es')['Switch']
ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
BaseDatePicker: typeof import('./src/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue')['default']
Basic: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/basic.vue')['default']
@@ -63,5 +67,6 @@ declare module 'vue' {
TransactionSkeleton: typeof import('./src/components/TransactionSkeleton.vue')['default']
VanActionSheet: typeof import('vant/es')['ActionSheet']
VanDialog: typeof import('vant/es')['Dialog']
VanField: typeof import('vant/es')['Field']
}
}
@@ -0,0 +1,32 @@
<template>
<div
class="text-center w-full flex-flex-col items-center home_appear_fade_in_up"
style="text-align: -webkit-center"
@click="$emit('click')"
>
<div
class="rounded-full w-[50px] h-[50px] bg-primary flex items-center justify-center"
style="border: solid 1px #0A1B37"
>
<Icon class="text-tertiary" :icon="action.icon" />
</div>
<div class="text-primary">{{ action.label }}</div>
</div>
</template>
<script setup lang="ts">
import Icon from '@/components/BasicPacks/BasicIcon.vue'
defineProps<{
action: {
name: string
icon: string
label: string
handler: () => void
}
}>()
defineEmits<{
click: []
}>()
</script>
@@ -0,0 +1,40 @@
<template>
<div
:class="y > 50 ? 'bg-white shadow opacity-95' : ''"
class="px-4 py-2 top-0 z-5 wrap-fix background-color-transition"
>
<div class="flex justify-between my-2">
<div class="flex items-center gap-2" @click.stop="router.push({ name: 'user' })">
<div
class="z-100000 rounded-full border-1"
:class="y > 50 ? 'text-primary border-primary' : 'text-primary border-primary'"
>
<Icon icon="vuesax-linear:user" size="20" />
</div>
<div :class="y > 50 ? 'text-primary' : 'text-primary'" class="color-transition font-bold">
{{ userStore.userInfo?.name }}
</div>
</div>
<Icon
:color="y > 50 ? '#0A1B37' : '#0A1B37'"
class="color-transition"
icon="vuesax-linear:logout"
@click="$emit('logout')"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { useWindowScroll } from '@vueuse/core'
import { useUserStore } from '@/store/userStore'
import router from '@/router'
import Icon from '@/components/BasicPacks/BasicIcon.vue'
const { y } = useWindowScroll()
const userStore = useUserStore()
defineEmits<{
logout: []
}>()
</script>
@@ -0,0 +1,13 @@
<template>
<div class="flex gap-1">
<div>{{ label }}</div>
<div class="font-bold">{{ value }}</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
label: string
value: string
}>()
</script>
@@ -0,0 +1,31 @@
<template>
<div class="bg-primary rounded-lg p-2 py-3 mt-2">
<div class="flex justify-center text-tertiary w-full z-20">
محصولات:
<div class="text-l mr-2 font-bold">{{ userStore?.userInfo?.wallet?.count_product ?? 0 }} عدد</div>
</div>
<div class="flex mt-2 justify-center text-tertiary w-full z-20">
دارایی طلایی:
<div class="text-l mr-2 font-bold">{{ (userStore?.userInfo?.wallet?.count_gold ?? 0).toFixed(3) }} گرم</div>
<BasicIcon @click="$emit('openGoldModal')" icon="vuesax-linear:info-circle" size="18" color="tertiary" />
</div>
<div class="flex mt-2 justify-center text-tertiary w-full z-20">
دارایی ریالی:
<div class="text-l mr-2 font-bold">{{ usePriceWithoutType(userStore?.userInfo?.wallet?.count_cash ?? 0) }} ریال</div>
<BasicIcon @click="$emit('openRialModal')" icon="vuesax-linear:info-circle" size="18" color="tertiary" />
</div>
</div>
</template>
<script setup lang="ts">
import { useUserStore } from '@/store/userStore'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import {usePriceWithoutType} from "../../../../utils/usePrice";
const userStore = useUserStore()
defineEmits<{
openGoldModal: []
openRialModal: []
}>()
</script>
@@ -0,0 +1,84 @@
<template>
<BasicActionSheet v-model:show="visible">
<div class="pb-20">
<div>
<div class="w-full text-center font-bold">موجودی ها</div>
<div class="h-30vh overflow-y-auto">
<div class="mt-5">
<BasicInfoItem
class="flex justify-between"
title="مجموعه کل دارایی طلایی"
is-line
:info="`در این قسمت مجموع کل وزن طلای آبشده (${(walletInfo?.gold ?? 0)?.toFixed(3)}) + ارزش دارایی ها (${(walletInfo?.treasure_sum ?? 0)?.toFixed(3)} ) را مشاهده میفرمایید.`"
infoHead="مجموع کل دارایی طلایی"
:value="parseFloat((walletInfo?.gold_sum ?? 0).toFixed(2)) + ' گرم'"
/>
<a-divider class="!my-1" />
<BasicInfoItem
class="flex justify-between"
title="دارایی کیف پول ریالی"
is-line
:value="usePrice(walletInfo?.cash ?? 0)"
/>
<a-divider class="!my-1" />
<BasicInfoItem
class="flex justify-between"
title="دارایی کیف پول طلایی"
is-line
:value="(walletInfo?.count_gold_wallet ?? 0) + ' گرم'"
/>
<a-divider v-if="nameProject === 'goldsun'" class="!my-1" />
<BasicInfoItem
v-if="nameProject === 'goldsun'"
class="flex justify-between"
title="دارایی کیف پول نقره"
is-line
:value="(walletInfo?.count_silver ?? 0) + ' گرم'"
/>
<a-divider v-if="nameProject === 'goldsun'" class="!my-1" />
<BasicInfoItem
v-if="nameProject === 'goldsun'"
class="flex justify-between"
title="دارایی کیف پول مس"
is-line
:value="(walletInfo?.count_copper ?? 0) + ' گرم'"
/>
<a-divider class="!my-1" />
<BasicInfoItem
class="flex justify-between"
title="معادل ریالی مجموع دارایی ها"
is-line
:value="usePrice(walletInfo?.count_cash_total ?? 0)"
/>
</div>
</div>
</div>
</div>
</BasicActionSheet>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
import BasicInfoItem from '@/components/BasicPacks/BasicInfoItem.vue'
import { useConfigStore } from '@/store/configStore'
import {usePrice} from "@/utils/usePrice";
const props = defineProps<{
show: boolean
walletInfo: any
goldVal: number
}>()
const emit = defineEmits<{
'update:show': [value: boolean]
}>()
const config = useConfigStore()
const nameProject = config.config.name
const visible = computed({
get: () => props.show,
set: (val) => emit('update:show', val)
})
</script>
@@ -0,0 +1,59 @@
<template>
<div class="w-full relative mt-2 flex flex-col justify-center home_appear_fade_in overflow-hidden rounded-lg">
<div class="w-full aspect-[2.8/1] min-h-[10rem] max-h-[14rem]">
<swiper
:modules="bannerSwiperModules"
:slides-per-view="1"
:space-between="6"
:autoplay="{ delay: 4000, disableOnInteraction: false }"
:pagination="{ clickable: true }"
class="banner-swiper h-full w-full rounded-lg"
>
<swiper-slide v-for="(banner, index) in banners" :key="index" class="!h-full">
<img
v-if="banner.show"
class="banner-img w-full h-full object-cover rounded-lg block"
:src="banner.icon"
:alt="banner.caption || ''"
:style="(banner.link || banner.typeClick === 'share') ? 'cursor: pointer;' : ''"
@click="$emit('bannerClick', banner)"
/>
</swiper-slide>
</swiper>
</div>
</div>
</template>
<script setup lang="ts">
import { Swiper, SwiperSlide } from 'swiper/vue'
import { Autoplay, Pagination } from 'swiper/modules'
import 'swiper/swiper-bundle.css'
import 'swiper/css/pagination'
const bannerSwiperModules = [Autoplay, Pagination]
defineProps<{
banners: any[]
}>()
defineEmits<{
bannerClick: [banner: any]
}>()
</script>
<style scoped>
.banner-swiper {
--swiper-pagination-bottom: 0.75rem;
}
.banner-swiper .swiper-pagination-bullet {
width: 0.5rem;
height: 0.5rem;
background: rgba(0, 0, 0, 0.35);
opacity: 1;
}
.banner-swiper .swiper-pagination-bullet-active {
background: #fff;
}
</style>
@@ -0,0 +1,61 @@
<template>
<div class="w-full relative mt-2 border-gray-200 border-1 rounded-lg py-3 flex flex-col justify-center home_appear_fade_in">
<div class="w-full items-center flex justify-center">
<div class="text-gray-600 text-[12px] ml-2">قیمت خرید از {{ config.config.name_fa }}</div>
<div v-if="price" class="flex gap-1 text-xs items-center">
<div
v-if="percentPriceChanged() !== 0"
class="flex flex-row-reverse rounded-lg justify-between items-center gap-2 px-3"
:class="percentPriceChanged() >= 0 ? 'bg-green-100 text-green-600' : 'bg-red-100 text-red-600'"
>
<div dir="ltr">{{ useTowDecimal(percentPriceChanged()) }}%</div>
<div>
<Icon
:size="15"
:icon="percentPriceChanged() < 0 ? 'vuesax-linear:received' : 'vuesax-linear:send'"
:color="percentPriceChanged() < 0 ? 'red-600' : 'green-600'"
/>
</div>
</div>
<Vue3Odometer
:value="displayGoldVal"
class="o-text !text-xl font-bold"
style="font-family: iran-sans; direction: ltr"
/>
ریال
</div>
</div>
</div>
</template>
<script setup lang="ts">
import Icon from '@/components/BasicPacks/BasicIcon.vue'
import { useConfigStore } from '@/store/configStore'
import {lastPrice, percentPriceChanged, price} from '@/utils/price'
import Vue3Odometer from 'vue3-odometer'
import 'odometer/themes/odometer-theme-default.css'
import {useTowDecimal} from "../../../../utils/usePrice";
import {computed} from "vue";
const config = useConfigStore()
// محاسبه درصد تغییرات به صورت reactive
const percentChange = computed(() => {
if (! lastPrice?.value?.price_last) return 0
return percentPriceChanged()
})
// بررسی بسته بودن بازار
const isMarketClosed = computed(() => {
return lastPrice?.value?.goldir === 0 && lastPrice?.value?.price_last === 0
})
// مقدار نمایشی برای ادومتر (با مدیریت خطا)
const displayGoldVal = computed(() => {
if (isMarketClosed.value) return 0
return lastPrice?.value?.price_last|| 0
})
// انیمیشن fade-in
</script>
@@ -0,0 +1,41 @@
<template>
<DialogVant v-model:show="visible" :closeOnClickOverlay="false" :timeOut="-1">
<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="$emit('confirm')">
<a-spin :spinning="false" size="small">خروج</a-spin>
</div>
<div class="text-center" @click="visible = false">انصراف</div>
</template>
</DialogVant>
</template>
<script setup lang="ts">
import DialogVant from '@/components/DialogVant.vue'
import { DotLottieVue } from '@lottiefiles/dotlottie-vue'
import {computed} from "vue";
const props = defineProps<{
show: boolean
}>()
const emit = defineEmits<{
'update:show': [value: boolean]
confirm: []
}>()
const visible = computed({
get: () => props.show,
set: (val) => emit('update:show', val)
})
</script>
@@ -0,0 +1,13 @@
<template>
<div
v-if="message"
class="bg-red-50 text-red-600 text-center font-500 py-4 px-2 mt-2 rounded-lg"
v-html="message"
/>
</template>
<script setup lang="ts">
defineProps<{
message: string
}>()
</script>
@@ -0,0 +1,104 @@
<template>
<div class="bg-gray-50 w-full flex justify-evenly rounded-lg py-4 mt-4 h-[15rem]">
<div class="w-full text-center pt-2">
<a-radio-group v-model:value="selectedValue" button-style="solid">
<a-radio-button value="DAILY" @click="handleUpdate('DAILY')">24 ساعت اخیر</a-radio-button>
<a-radio-button value="WEEKLY" @click="handleUpdate('WEEKLY')">هفته گذشته</a-radio-button>
<a-radio-button value="MONTHLY" @click="handleUpdate('MONTHLY')">ماه گذشته</a-radio-button>
</a-radio-group>
<div id="chart-timeline">
<apexchart
type="area"
height="200"
:options="chartOptions"
:series="props.priceChartSeries"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useConfigStore } from '@/store/configStore'
import {usePrice} from "@/utils/usePrice";
const props = defineProps<{
priceChartSeries: any[]
}>()
const emit = defineEmits<{
updateData: [timeline: string]
}>()
const config = useConfigStore()
const selectedValue = ref('DAILY')
const toPersianNumber = (num: number) => {
return num.toString().replace(/\d/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[d])
}
const chartOptions = computed(() => ({
chart: {
id: 'نوسانات قیمت',
type: 'area',
height: 200,
toolbar: { show: false },
zoom: { autoScaleYaxis: true }
},
grid: {
padding: { top: 0, right: 0, bottom: 0, left: 0 },
yaxis: { lines: { show: false } }
},
stroke: { width: 2 },
dataLabels: {
enabled: false,
// formatter: (val) => toPersianNumber(val),
style: {
fontFamily: '\'IranSans\', sans-serif'
}
},
yaxis: {
tickAmount: 8,
show: false,
labels: {
formatter: (val: number) => toPersianNumber(usePrice(val)),
style: { fontSize: '13px', fontFamily: "'IranSans', sans-serif" }
}
},
xaxis: {
show: false,
type: 'datetime',
axisTicks: { show: false },
tooltip: { enabled: false },
labels: { show: false }
},
tooltip: {
x: {
formatter: (value: number) => {
const date = new Date(value)
return `${date.toLocaleDateString('fa-IR')} - ${date.toLocaleTimeString('fa-IR', {
hour: '2-digit',
minute: '2-digit'
})}`
}
},
formatter: (val: number) => toPersianNumber(usePrice(val)),
style: { fontFamily: "'IranSans', sans-serif" }
},
colors: [config.config.color],
fill: {
type: 'gradient',
gradient: {
shadeIntensity: 1,
opacityFrom: 0.7,
opacityTo: 0.9,
stops: [0, 100]
}
}
}))
const handleUpdate = (timeline: string) => {
emit('updateData', timeline)
}
</script>
@@ -0,0 +1,34 @@
<template>
<div class="w-full flex justify-evenly rounded-lg py-2 home_appear_fade_in mt-6 pb-45">
<div class="text-center w-full">
<div>بالاترین قیمت امروز</div>
<div v-if="price" class="flex flex-col items-center mt-3">
<div class="text-green-600 flex gap-1 text-xs items-center">
<div class="text-base">{{ usePriceWithoutType(price?.buy ?? 0) }}</div>
ریال
</div>
</div>
</div>
<a-divider class="bg-gray-200 !h-auto !my-2" type="vertical" />
<div class="text-center w-full">
<div>پایین ترین قیمت امروز</div>
<div class="flex w-full mt-3 justify-evenly">
<div v-if="price" class="flex flex-col items-center">
<div class="text-red-600 flex gap-1 text-xs items-center">
<div class="text-base">{{ usePriceWithoutType(price?.sell ?? 0) }}</div>
ریال
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {usePriceWithoutType} from "../../../../utils/usePrice";
defineProps<{
price: any
}>()
</script>
@@ -0,0 +1,63 @@
<template>
<div class="bg-primary rounded-lg p-2 py-1 mt-2">
<div class="p-2">
<div
v-if="userStore.userInfo?.referral_id"
class="text-tertiary font-bold w-full text-center flex justify-between gap-2 items-center"
>
کد معرف:
<div class="flex gap-2">
<div class="mr-2">{{ userStore.userInfo?.referral_id }}</div>
</div>
</div>
<div class="w-full flex gap-2 mt-1">
<BasicButton
icon="vuesax-linear:share"
size="small"
class="flex-1 !bg-primary !border-tertiary !text-tertiary"
icon-size="18"
@click.stop="shareCode"
>
دعوت از دوستان
</BasicButton>
<BasicButton
icon="vuesax-linear:copy"
size="small"
class="flex-1 !bg-primary !border-tertiary !text-tertiary"
icon-size="18"
@click.stop="copyCode"
>
رونوشت
</BasicButton>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useUserStore } from '@/store/userStore'
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import { message } from 'ant-design-vue'
const userStore = useUserStore()
const shareCode = () => {
const text = `⭐️الان طلا بخر؛ آخر ماه سودشو ببر!
با خرید طلای آبشده در مقدارهای کم ولی مستمر، سود بیشتری ببر!
این دعوت نامه از طرف "${userStore.userInfo?.name}" عزیز برای شما ارسال شده تا شما هم از بازار طلا جا نمونید.
روی لینک زیر کلیک کنید و ثبت نام خود را تکمیل کنید.
${window.location.hostname}/login?id=${userStore.userInfo?.referral_id}`
navigator.share({ title: 'طلا', text })
}
const copyCode = () => {
const code = userStore.userInfo?.referral_id
if (code) {
navigator.clipboard.writeText(code)
message.info('کد رونوشت شد')
}
}
</script>
@@ -0,0 +1,78 @@
<template>
<BasicModal v-model:visible="visible" :custom-footer="true">
<div class="p-2">
<div
v-if="referralId"
class="text-primary font-bold w-full text-center flex justify-between gap-2 items-center"
>
کد معرف:
<div class="flex gap-2">
<div class="mr-2">{{ referralId }}</div>
</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>
</div>
</BasicModal>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import BasicModal from '@/components/BasicPacks/BasicModal.vue'
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import { message } from 'ant-design-vue'
import { useUserStore } from '@/store/userStore'
const props = defineProps<{
show: boolean
referralId: string
}>()
const emit = defineEmits<{
'update:show': [value: boolean]
}>()
const userStore = useUserStore()
const visible = computed({
get: () => props.show,
set: (val) => emit('update:show', val)
})
const shareCode = () => {
const text = `⭐️الان طلا بخر؛ آخر ماه سودشو ببر!
با خرید طلای آبشده در مقدارهای کم ولی مستمر، سود بیشتری ببر!
این دعوت نامه از طرف "${userStore.userInfo?.name}" عزیز برای شما ارسال شده تا شما هم از بازار طلا جا نمونید.
روی لینک زیر کلیک کنید و ثبت نام خود را تکمیل کنید.
${window.location.hostname}/login?id=${props.referralId}`
navigator.share({ title: 'طلا', text })
}
const copyCode = () => {
if (props.referralId) {
navigator.clipboard.writeText(props.referralId)
message.info('کد رونوشت شد')
}
}
</script>
@@ -0,0 +1,37 @@
import { ref } from 'vue'
import { getConstByKey } from '@/api/const'
export function useBanners() {
const banners = ref([])
const loading = ref(false)
const loadBanners = async () => {
try {
loading.value = true
const response = await getConstByKey('header_banner')
if (!response) return
let list = []
if (typeof response === 'string') {
list = JSON.parse(response)
} else if (typeof response === 'object' && !Array.isArray(response) && 'value' in response) {
list = typeof response.value === 'string' ? JSON.parse(response.value) : response.value
} else if (Array.isArray(response) && response.length > 0) {
const first = response[0]
if (first.value) {
list = typeof first.value === 'string' ? JSON.parse(first.value) : first.value
} else if (first.icon) {
list = response
}
}
banners.value = Array.isArray(list) ? list.filter(b => b.show === true) : []
} catch (error) {
console.error('Error loading banners:', error)
} finally {
loading.value = false
}
}
return { banners, loading, loadBanners }
}
@@ -0,0 +1,33 @@
import { ref } from 'vue'
import { getAllPrices } from '@/api/home'
export function usePriceChart() {
const priceChartSeries = ref([{ name: 'قیمت', data: [] }])
const selection = ref('DAILY')
const chartLoading = ref(false)
const toPersianNumber = (num: number) => {
return num.toString().replace(/\d/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[d])
}
const fetchPrices = async () => {
try {
chartLoading.value = true
const response = await getAllPrices()
const data = response.data?.prices
const selectedData = data[selection.value] || []
priceChartSeries.value[0].data = selectedData.map((item: any) => [item.timestamp, item.value - 1])
} catch (error) {
console.error('Error fetching prices:', error)
} finally {
chartLoading.value = false
}
}
const updateChartData = (timeline: string) => {
selection.value = timeline
fetchPrices()
}
return { priceChartSeries, chartLoading, updateChartData, fetchPrices }
}
@@ -0,0 +1,22 @@
import { ref } from 'vue'
import { useFundUserStore } from '@/store/fundStore'
export function useUserFunds() {
const funds = ref([])
const loading = ref(false)
const fundStore = useFundUserStore()
const loadFunds = async () => {
try {
loading.value = true
const result = await fundStore.getMyFund()
funds.value = result?.rows || []
} catch (error) {
console.error('Error loading funds:', error)
} finally {
loading.value = false
}
}
return { funds, loading, loadFunds }
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff