Merge feature/responsive-layout-refactor into modular for desktop layouts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-15 17:02:11 +03:30
co-authored by Cursor
235 changed files with 18805 additions and 5910 deletions
+9 -2
View File
@@ -1,5 +1,9 @@
<template>
<div v-if="item" class="bg-white flex flex-col items-center p-3 rounded-xl">
<div
v-if="item"
class="bg-white flex flex-col items-center p-3 rounded-xl"
:class="{ 'dfund-card': variant === 'desktop' }"
>
<img :src="displayImage" class="w-[87px] h-[87px] object-contain" alt="" @error="onImageError">
<div class="mt-3">{{ item.title ?? '-' }}</div>
<div class="mt-1 flex flex-col items-center gap-0.5 text-center">
@@ -21,9 +25,12 @@ import { getProductDisplayImage } from '@/utils/productAssets'
interface Prop {
item: WalletInventoryCard
variant?: 'mobile' | 'desktop'
}
const props = defineProps<Prop>()
const props = withDefaults(defineProps<Prop>(), {
variant: 'mobile',
})
const imageSrc = ref('')
const displayImage = computed(() => imageSrc.value || resolveImage())
@@ -0,0 +1,120 @@
import { computed, inject, onMounted, provide, type InjectionKey, type Ref } from 'vue'
import router from '@/router'
import { loading } from '@/utils'
import { usePriceWithoutType } from '@/utils/usePrice'
import { useConfigStore } from '@/store/configStore'
import { useWalletAssets } from '@/composables/useWalletAssets'
import type { WalletMetalTotal } from '@/models/walletAssets'
import { resolveSummaryMetalWeight, fundPageInventoryCards } from '@/utils/productAssets'
import { useWalletStore } from '@/store/walletStore'
import { getLastPrices } from '@/utils/price'
import {
getProjectFundEmptyImage,
getProjectFundEmptyText,
getProjectFundHeaderBackground,
isBazretalaProject,
} from '@/utils/config'
import img from '@/assets/img/user_ly1.svg'
import empty from '@/assets/img/cash_empty.svg'
export interface FundPageContext {
isDesktop: Ref<boolean>
nameProject: string
meta: ReturnType<typeof computed>
inventoryCards: ReturnType<typeof computed>
metalTotals: ReturnType<typeof computed>
primaryMetalTotal: ReturnType<typeof computed>
cashWallet: ReturnType<typeof computed>
totalRialValue: ReturnType<typeof computed>
countBasedInventory: ReturnType<typeof computed>
fundHeaderBackground: string
fundEmptyImage: string
fundEmptyText: string
isBazretalaProject: typeof isBazretalaProject
formatMetalTotal: (metal: WalletMetalTotal) => string
openFundInfo: (productId: number) => void
goBack: () => void
}
export const FUND_PAGE_KEY: InjectionKey<FundPageContext> = Symbol('fundPage')
export function useFundPage(isDesktop: Ref<boolean>) {
const nameProject = useConfigStore().config.name
const { assetsSummary, loadWalletAssets } = useWalletAssets()
const walletStore = useWalletStore()
const fundHeaderBackground = getProjectFundHeaderBackground(img)
const fundEmptyImage = getProjectFundEmptyImage(empty)
const fundEmptyText = getProjectFundEmptyText()
const meta = computed(() => assetsSummary.value?.meta)
const inventoryCards = computed(() =>
fundPageInventoryCards(assetsSummary.value?.inventory_cards ?? [])
)
const metalTotals = computed(() => assetsSummary.value?.metal_totals ?? [])
const primaryMetalTotal = computed(() => metalTotals.value[0])
const cashWallet = computed(() => assetsSummary.value?.cash_wallet ?? 0)
const totalRialValue = computed(() => assetsSummary.value?.total_rial_value ?? 0)
const countBasedInventory = computed(() =>
(assetsSummary.value?.count_products ?? []).reduce(
(sum, item) => sum + Number(item.quantity ?? 0),
0
)
)
function formatMetalTotal(metal: WalletMetalTotal) {
return `${resolveSummaryMetalWeight(metal, inventoryCards.value)}${metal.unit}`
}
function openFundInfo(productId: number) {
router.push({ name: 'fund_info', params: { id: productId } })
}
function goBack() {
router.back()
}
onMounted(async () => {
try {
loading.value = true
const tasks = [loadWalletAssets(true), getLastPrices()]
if (isBazretalaProject()) {
tasks.push(walletStore.getMyWallet())
}
await Promise.all(tasks)
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
})
const context: FundPageContext = {
isDesktop,
nameProject,
meta,
inventoryCards,
metalTotals,
primaryMetalTotal,
cashWallet,
totalRialValue,
countBasedInventory,
fundHeaderBackground,
fundEmptyImage,
fundEmptyText,
isBazretalaProject,
formatMetalTotal,
openFundInfo,
goBack,
}
provide(FUND_PAGE_KEY, context)
return context
}
export function useFundPageContext() {
const context = inject(FUND_PAGE_KEY)
if (!context) {
throw new Error('useFundPageContext must be used within useFundPage')
}
return context
}
@@ -0,0 +1,138 @@
<template>
<div class="dfund">
<div class="dfund__container">
<header class="dfund-topbar">
<button type="button" class="dfund-topbar__back" @click="goBack()">
<BasicIcon icon="vuesax-linear:arrow-right-1" size="18" />
بازگشت
</button>
<div class="dfund-topbar__meta">
<p class="dfund-topbar__eyebrow">دارایی</p>
<h1 class="dfund-topbar__title">دارایی‌ها</h1>
<p class="dfund-topbar__subtitle">نمای کلی موجودی محصولات و کیف پول</p>
</div>
</header>
<section
class="dfund-summary"
:class="{ 'dfund-summary--bazretala': isBazretalaProject() }"
>
<img
:src="fundHeaderBackground"
alt=""
class="dfund-summary__bg"
:class="{ 'dfund-summary__bg--cover': isBazretalaProject() }"
/>
<div class="dfund-summary__content">
<h2 class="dfund-summary__title">موجودی دارایی‌ها</h2>
<div v-if="meta?.has_count_based_products" class="dfund-summary__row">
<span>محصولات</span>
<strong>{{ countBasedInventory }} عدد</strong>
</div>
<template v-if="isBazretalaProject()">
<div class="dfund-summary__row">
<span>دارایی نقره</span>
<strong>{{ walletStore.walletInfo?.count_silver ?? 0 }} گرم</strong>
</div>
<div class="dfund-summary__row">
<span>دارایی طلایی</span>
<strong>{{ walletStore.walletInfo?.count_gold_wallet ?? 0 }} گرم</strong>
</div>
</template>
<template v-else-if="meta?.single_metal && primaryMetalTotal">
<div class="dfund-summary__row">
<span>{{ primaryMetalTotal.label }}</span>
<strong>{{ formatMetalTotal(primaryMetalTotal) }}</strong>
</div>
</template>
<template v-else>
<div
v-for="metal in metalTotals"
:key="`metal-${metal.metal_id}`"
class="dfund-summary__row"
>
<span>{{ metal.label }}</span>
<strong>{{ formatMetalTotal(metal) }}</strong>
</div>
</template>
<div
v-if="meta?.has_count_based_products && !isBazretalaProject()"
class="dfund-summary__row"
>
<span>مجموع موجودی</span>
<strong>{{ countBasedInventory }} عدد</strong>
</div>
<div class="dfund-summary__row">
<span>دارایی ریالی</span>
<strong class="number-ltr">{{ usePriceWithoutType(cashWallet) }} ریال</strong>
</div>
<div class="dfund-summary__row dfund-summary__row--highlight">
<span>معادل ریالی مجموع</span>
<strong class="number-ltr">{{ usePriceWithoutType(totalRialValue) }} ریال</strong>
</div>
</div>
</section>
<section class="dfund-inventory">
<div class="dfund-inventory__header">
<h3 class="dfund-inventory__title">لیست دارایی‌ها</h3>
<span class="dfund-inventory__count">{{ inventoryCards.length }} مورد</span>
</div>
<div
v-if="inventoryCards.length === 0 && !loading"
class="dfund-inventory__empty"
>
<img :src="fundEmptyImage" alt="" class="dfund-inventory__empty-img" />
<p v-if="nameProject !== 'abaeigold'">{{ fundEmptyText }}</p>
</div>
<a-spin :spinning="loading">
<div v-if="inventoryCards.length" class="dfund-inventory__grid">
<button
v-for="item in inventoryCards"
:key="item.product_id"
type="button"
class="dfund-inventory__card"
@click="openFundInfo(item.product_id)"
>
<Item :item="item" variant="desktop" />
</button>
</div>
</a-spin>
</section>
</div>
</div>
</template>
<script lang="ts" setup>
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import { loading } from '@/utils'
import { usePriceWithoutType } from '@/utils/usePrice'
import { useWalletStore } from '@/store/walletStore'
import Item from '../components/Item.vue'
import { useFundPageContext } from '../composables/useFundPage'
const walletStore = useWalletStore()
const {
nameProject,
meta,
inventoryCards,
metalTotals,
primaryMetalTotal,
cashWallet,
totalRialValue,
countBasedInventory,
fundHeaderBackground,
fundEmptyImage,
fundEmptyText,
isBazretalaProject,
formatMetalTotal,
openFundInfo,
goBack,
} = useFundPageContext()
</script>
@@ -0,0 +1,495 @@
/* Premium desktop fund page */
.dfund {
min-height: 100%;
background: #f6f7f9;
color: #111827;
}
.dfund__container {
max-width: 1180px;
margin: 0 auto;
padding: 28px 36px 40px;
}
.dfund-topbar {
display: flex;
align-items: flex-start;
gap: 16px;
margin-bottom: 24px;
padding-bottom: 24px;
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
&__back {
display: inline-flex;
align-items: center;
gap: 8px;
border: none;
background: transparent;
color: #6b7280;
font-size: 13px;
font-weight: 500;
font-family: inherit;
cursor: pointer;
padding: 8px 0;
flex-shrink: 0;
&:hover {
color: #111827;
}
}
&__meta {
flex: 1;
min-width: 0;
}
&__eyebrow {
font-size: 12px;
font-weight: 500;
color: #9ca3af;
margin: 0 0 4px;
}
&__title {
font-size: 26px;
font-weight: 600;
letter-spacing: -0.03em;
color: #111827;
margin: 0;
line-height: 1.2;
}
&__subtitle {
font-size: 14px;
color: #6b7280;
margin: 6px 0 0;
line-height: 1.6;
}
}
.dfund-summary {
position: relative;
overflow: hidden;
border-radius: 16px;
margin-bottom: 24px;
background: @App-primary-color;
color: #fff;
&--bazretala {
background: #000;
}
&__bg {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
opacity: 0.08;
pointer-events: none;
&--cover {
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0.35;
}
}
&__content {
position: relative;
z-index: 1;
padding: 24px 28px;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px 24px;
}
&__title {
grid-column: 1 / -1;
font-size: 18px;
font-weight: 600;
margin: 0 0 4px;
color: #fff;
}
&__row {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 13px;
span {
opacity: 0.85;
}
strong {
font-size: 16px;
font-weight: 700;
}
&--highlight {
grid-column: 1 / -1;
padding-top: 8px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
}
}
.dfund-surface {
background: #fff;
border: 1px solid rgba(15, 23, 42, 0.06);
border-radius: 16px;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
}
.dfund-inventory {
background: #fff;
border: 1px solid rgba(15, 23, 42, 0.06);
border-radius: 16px;
padding: 24px;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
&__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 20px;
}
&__title {
font-size: 16px;
font-weight: 600;
margin: 0;
color: #111827;
}
&__count {
font-size: 13px;
color: #9ca3af;
}
&__grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 16px;
}
&__card {
border: none;
background: transparent;
padding: 0;
cursor: pointer;
font-family: inherit;
text-align: inherit;
width: 100%;
&:hover .dfund-card {
border-color: rgba(15, 23, 42, 0.12);
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.08);
}
}
&__empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 48px 24px;
text-align: center;
color: #6b7280;
}
&__empty-img {
width: 160px;
max-width: 100%;
}
}
.dfund-card {
width: 100%;
border: 1px solid rgba(15, 23, 42, 0.08) !important;
border-radius: 14px !important;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
@media (max-width: 1024px) {
.dfund-summary__content {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.dfund-inventory__grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 768px) {
.dfund-inventory__grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
/* Fund info desktop */
.dfund-info {
min-height: 100%;
background: #f6f7f9;
color: #111827;
}
.dfund-info__container {
max-width: 1180px;
margin: 0 auto;
padding: 28px 36px 40px;
}
.dfund-info-topbar {
display: flex;
align-items: flex-start;
gap: 16px;
margin-bottom: 24px;
padding-bottom: 24px;
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
&__back {
display: inline-flex;
align-items: center;
gap: 8px;
border: none;
background: transparent;
color: #6b7280;
font-size: 13px;
font-weight: 500;
font-family: inherit;
cursor: pointer;
padding: 8px 0;
flex-shrink: 0;
&:hover {
color: #111827;
}
}
&__meta {
flex: 1;
min-width: 0;
}
&__eyebrow {
font-size: 12px;
font-weight: 500;
color: #9ca3af;
margin: 0 0 4px;
}
&__title {
font-size: 26px;
font-weight: 600;
letter-spacing: -0.03em;
color: #111827;
margin: 0;
line-height: 1.2;
}
}
.dfund-info__layout {
display: grid;
grid-template-columns: 280px minmax(0, 1fr);
gap: 20px;
align-items: start;
}
.dfund-info__hero {
padding: 28px 24px;
text-align: center;
}
.dfund-info__image {
width: 120px;
height: 120px;
margin: 0 auto 16px;
border-radius: 16px;
background: #e0f2fe;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
img {
max-width: 80%;
max-height: 80%;
object-fit: contain;
}
}
.dfund-info__balance {
display: flex;
align-items: baseline;
justify-content: center;
gap: 6px;
}
.dfund-info__balance-value {
font-size: 28px;
font-weight: 700;
color: @App-primary-color;
}
.dfund-info__balance-unit {
font-size: 14px;
color: #6b7280;
}
.dfund-info__balance-label {
margin: 8px 0 0;
font-size: 13px;
color: #9ca3af;
}
.dfund-info__transactions {
padding: 24px;
}
.dfund-info__transactions-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
}
.dfund-info__transactions-title {
font-size: 16px;
font-weight: 600;
margin: 0;
}
.dfund-info__transactions-count {
font-size: 13px;
color: #9ca3af;
}
.dfund-info__transactions-list {
max-height: 520px;
overflow-y: auto;
}
/* Fund new desktop */
.dfund-new {
min-height: 100%;
background: #f6f7f9;
color: #111827;
}
.dfund-new__container {
max-width: 640px;
margin: 0 auto;
padding: 28px 36px 40px;
}
.dfund-new-topbar {
display: flex;
align-items: flex-start;
gap: 16px;
margin-bottom: 24px;
padding-bottom: 24px;
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
&__back {
display: inline-flex;
align-items: center;
gap: 8px;
border: none;
background: transparent;
color: #6b7280;
font-size: 13px;
font-weight: 500;
font-family: inherit;
cursor: pointer;
padding: 8px 0;
flex-shrink: 0;
&:hover {
color: #111827;
}
}
&__meta {
flex: 1;
min-width: 0;
}
&__eyebrow {
font-size: 12px;
font-weight: 500;
color: #9ca3af;
margin: 0 0 4px;
}
&__title {
font-size: 26px;
font-weight: 600;
letter-spacing: -0.03em;
color: #111827;
margin: 0;
line-height: 1.2;
}
}
.dfund-new__card {
padding: 32px 28px;
}
.dfund-new__preview {
display: flex;
justify-content: center;
margin-bottom: 24px;
}
.dfund-new__preview-img {
width: 120px;
height: 120px;
object-fit: contain;
border-radius: 16px;
background: #e0f2fe;
padding: 16px;
}
.dfund-new__label {
display: block;
font-size: 13px;
font-weight: 600;
color: #374151;
margin-bottom: 8px;
}
.dfund-new__tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 20px 0 28px;
}
.dfund-new__tag {
padding: 8px 14px;
border-radius: 999px;
border: 1px solid rgba(15, 23, 42, 0.1);
background: #f3f4f6;
color: #4b5563;
font-size: 12px;
font-family: inherit;
cursor: pointer;
&--active {
background: @App-primary-color;
border-color: @App-primary-color;
color: #fff;
}
}
.dfund-new__submit {
width: 100%;
}
+12 -162
View File
@@ -1,173 +1,23 @@
<template>
<div>
<div class="bg-white w-full py-3 px-3 text-primary font-bold drop-shadow fixed md:absolute top-0 flex h-12 z-20">
<div @click="router.back()">
<BasicIcon icon="vuesax-linear:arrow-right-1"/>
</div>
<div class="mr-2">دارایی ها</div>
</div>
<div
class="w-screen md:w-full h-[70vh] fixed md:absolute overflow-hidden"
:class="isBazretalaProject() ? 'bazretala-fund-header' : 'text-tertiary bg-primary'"
>
<img
:src="fundHeaderBackground"
alt=""
:class="isBazretalaProject()
? 'pointer-events-none z-0 absolute inset-0 w-full object-cover object-center'
: 'pointer-events-none z-0 w-full absolute bottom-0 left-0 opacity-5'"
/>
<div
class="pt-15 flex flex-col relative z-10"
:class="isBazretalaProject() ? 'text-white' : 'text-tertiary'"
>
<div class="font-bold mr-5 z-20">موجودی دارایی ها</div>
<div v-if="meta?.has_count_based_products" class="flex mt-4 justify-center mt-2 w-full z-20">
محصولات:
<div class="text-l mr-4 font-bold">{{ countBasedInventory }} عدد</div>
</div>
<template v-if="isBazretalaProject()">
<div class="flex justify-center mt-2 w-full z-20">
دارایی نقره:
<div class="text-l mr-4 font-bold">{{ walletStore.walletInfo?.count_silver ?? 0 }}گرم</div>
</div>
<div class="flex justify-center mt-2 w-full z-20">
دارایی طلایی:
<div class="text-l mr-4 font-bold">{{ walletStore.walletInfo?.count_gold_wallet ?? 0 }}گرم</div>
</div>
</template>
<template v-else-if="meta?.single_metal && primaryMetalTotal">
<div class="flex justify-center mt-2 w-full z-20">
{{ primaryMetalTotal.label }}:
<div class="text-l mr-4 font-bold">{{ formatMetalTotal(primaryMetalTotal) }}</div>
</div>
</template>
<template v-else>
<div
v-for="metal in metalTotals"
:key="`metal-${metal.metal_id}`"
class="flex justify-center mt-2 w-full z-20"
>
{{ metal.label }}:
<div class="text-l mr-4 font-bold">{{ formatMetalTotal(metal) }}</div>
</div>
</template>
<div v-if="meta?.has_count_based_products && !isBazretalaProject()" class="flex justify-center mt-2 w-full z-20">
مجموع موجودی:
<div class="text-l mr-4 font-bold">{{ countBasedInventory }} عدد</div>
</div>
<div class="flex justify-center mt-2 w-full z-20">
دارایی ریالی:
<div class="text-l mr-4 font-bold">{{ usePriceWithoutType(cashWallet) }}ریال</div>
</div>
<div class="flex justify-center mt-2 w-full z-20">
مجموع دارایی ریالی:
<div class="text-l mr-4 font-bold">{{ usePriceWithoutType(cashWallet + metalRialValue) }}ریال</div>
</div>
</div>
</div>
<div class="pt-[20rem] ">
<div class="bg-gray-100 w-screen md:w-full z-20 relative rounded-t-3xl h-[calc(100vh-20rem-70px)] overflow-y-auto pb-15"
style="box-shadow: 0px -2px 4px -2px #0000001A;">
<div v-if="inventoryCards.length === 0 && !loading" class="w-full h-full flex justify-center items-center">
<div>
<img
:src="fundEmptyImage"
alt=""
:class="isBazretalaProject() ? 'w-48 max-w-[11rem]' : 'w-70'"
>
<div v-if="nameProject !== 'abaeigold'" class="text-center text-lg">{{ fundEmptyText }}</div>
</div>
</div>
<a-spin :spinning="loading">
<div :class="loading ? 'h-[40vh]' : ''" class="grid grid-cols-2 gap-3 p-5">
<Item
v-for="item in inventoryCards"
:key="item.product_id"
:item="item"
@click="router.push({ name: 'fund_info', params: { id: item.product_id } })"
/>
</div>
</a-spin>
</div>
</div>
<div class="fund-page-root">
<DesktopFundView v-show="isDesktop" />
<MobileFundView v-show="!isDesktop" />
</div>
</template>
<script lang="ts" setup>
import router from '@/router'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import img from '@/assets/img/user_ly1.svg'
import empty from '@/assets/img/cash_empty.svg'
import Item from './components/Item.vue'
import { onMounted, computed } from 'vue'
import { loading } from '@/utils'
import { usePriceWithoutType } from '@/utils/usePrice'
import { useConfigStore } from '@/store/configStore'
import { useWalletAssets } from '@/composables/useWalletAssets'
import type { WalletMetalTotal } from '@/models/walletAssets'
import { resolveSummaryMetalWeight, fundPageInventoryCards, resolveAssetsRialValue } from '@/utils/productAssets'
import { useWalletStore } from '@/store/walletStore'
import { getTradeBuyPrice, lastPrice } from '@/utils/price'
import {
getProjectFundEmptyImage,
getProjectFundEmptyText,
getProjectFundHeaderBackground,
isBazretalaProject,
} from '@/utils/config'
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
import { useFundPage } from './composables/useFundPage'
import DesktopFundView from './desktop/DesktopFundView.vue'
import MobileFundView from './mobile/MobileFundView.vue'
const nameProject = useConfigStore().config.name
const { assetsSummary, loadWalletAssets } = useWalletAssets()
const walletStore = useWalletStore()
const fundHeaderBackground = getProjectFundHeaderBackground(img)
const fundEmptyImage = getProjectFundEmptyImage(empty)
const fundEmptyText = getProjectFundEmptyText()
const meta = computed(() => assetsSummary.value?.meta)
const inventoryCards = computed(() => fundPageInventoryCards(assetsSummary.value?.inventory_cards ?? []))
const metalTotals = computed(() => assetsSummary.value?.metal_totals ?? [])
const primaryMetalTotal = computed(() => metalTotals.value[0])
const cashWallet = computed(() => assetsSummary.value?.cash_wallet ?? 0)
const metalRialValue = computed(() =>
resolveAssetsRialValue(assetsSummary.value, getTradeBuyPrice(lastPrice.value))
)
const countBasedInventory = computed(() =>
(assetsSummary.value?.count_products ?? []).reduce((sum, item) => sum + Number(item.quantity ?? 0), 0)
)
onMounted(async () => {
try {
loading.value = true
const tasks = [loadWalletAssets(true), getLastPrices()]
if (isBazretalaProject()) {
tasks.push(walletStore.getMyWallet())
}
await Promise.all(tasks)
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
})
function formatMetalTotal(metal: WalletMetalTotal) {
return `${resolveSummaryMetalWeight(metal, inventoryCards.value)}${metal.unit}`
}
const isDesktop = useDesktopBreakpoint()
useFundPage(isDesktop)
</script>
<style scoped>
.bazretala-fund-header {
background-color: #000;
}
.bazretala-fund-header::after {
content: '';
position: absolute;
inset: 0;
z-index: 1;
background: rgba(0, 0, 0, 0.45);
pointer-events: none;
.fund-page-root {
min-height: 100%;
height: 100%;
}
</style>
@@ -0,0 +1,48 @@
<template>
<div :class="variant === 'desktop' ? 'dfund-info-edit' : ''">
<div v-if="variant !== 'desktop'" class="font-bold text-lg">گنج {{ info?.title }}</div>
<BasicButton
:class="variant === 'desktop' ? 'mt-4 w-full' : 'mt-12 w-full'"
icon="vuesax-linear:edit"
icon-size="16"
@click="$router.push({ name: 'fund_add', query: { id: infoId } })"
>
<div class="flex w-full justify-right">ویرایش</div>
</BasicButton>
<BasicButton
:loading="loading"
:class="variant === 'desktop' ? 'mt-2 w-full !text-red-600' : 'mb-10 mt-2 w-full !text-red-600'"
icon="vuesax-linear:trash"
icon-size="16"
@click="deleteFundConfirm"
>
<div class="flex w-full justify-right">حذف</div>
</BasicButton>
<BasicButton
v-if="variant !== 'desktop'"
class="mb-20 mt-2 w-full"
type="primary"
@click="transactionShow = false"
>
بازگشت
</BasicButton>
</div>
</template>
<script lang="ts" setup>
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import { loading } from '@/utils'
import { useFundInfoPageContext } from '../composables/useFundInfoPage'
interface Props {
variant?: 'mobile' | 'desktop'
}
withDefaults(defineProps<Props>(), {
variant: 'mobile',
})
const { info, infoId, transactionShow, deleteFundConfirm } = useFundInfoPageContext()
</script>
@@ -0,0 +1,82 @@
<template>
<div :class="variant === 'desktop' ? 'dfund-info-form' : ''">
<div v-if="variant !== 'desktop'" class="font-bold text-lg">
{{ isDeposit ? 'واریز' : 'برداشت' }}
</div>
<a-form
ref="formRef"
:model="userFormState"
:class="variant === 'desktop' ? '!mt-0' : '!mt-5'"
name="basic"
@finish="transaction()"
>
<a-form-item :rules="[required()]" label="مقدار گرم طلا" name="value" class="!block">
<BasicInput
v-model:value="userFormState.value"
addon-before="گرم"
place-holder="گرم"
type="text"
inputmode="decimal"
size="large"
/>
<div v-if="isDeposit" class="mt-2 text-xs font-bold leading-5">
<div>{{ formatGoldWalletBalanceText(walletStore?.walletInfo?.gold ?? 0) }}</div>
<div v-if="info?.wallet?.gold !== 0">
{{ `موجودی قابل برداشت گنج: ${info?.wallet?.gold ?? 0} گرم` }}
</div>
</div>
<div v-else class="mt-2 text-xs font-bold">
موجودی گنج {{ info?.title }}: {{ info?.wallet?.gold }} گرم
</div>
</a-form-item>
<a-form-item>
<BasicButton
:disabled="transButtonDisable"
class="w-full py-2 mt-2"
html-type="submit"
type="primary"
>
<div>
<a-spin v-if="loading" size="small" />
<div v-else>{{ isDeposit ? 'واریز' : 'برداشت' }}</div>
</div>
</BasicButton>
<BasicButton
v-if="variant !== 'desktop'"
class="mb-20 mt-2 w-full"
@click="transactionShow = false"
>
بازگشت
</BasicButton>
</a-form-item>
</a-form>
</div>
</template>
<script lang="ts" setup>
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
import { loading } from '@/utils'
import { required } from '@/utils/inputRules'
import { formatGoldWalletBalanceText } from '@/utils/productDealMethod'
import { useFundInfoPageContext } from '../composables/useFundInfoPage'
interface Props {
variant?: 'mobile' | 'desktop'
}
withDefaults(defineProps<Props>(), {
variant: 'mobile',
})
const {
info,
isDeposit,
formRef,
userFormState,
transButtonDisable,
walletStore,
transactionShow,
transaction,
} = useFundInfoPageContext()
</script>
@@ -0,0 +1,185 @@
import { computed, inject, onMounted, provide, reactive, ref, watch, type InjectionKey } from 'vue'
import router from '@/router'
import { loading } from '@/utils'
import { deleteFund, fundDeposit, fundWithdraw, getFundById, getFundTransactionTable } from '@/api/fund'
import type { FundTransaction } from '@/models/fund'
import type { TableInput, TableOutput } from '@/models/table'
import type { Transaction } from '@/models/transaction'
import type { FormInstance } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import { useWalletStore } from '@/store/walletStore'
import { useFundUserStore } from '@/store/fundStore'
import { getOneProductWallet } from '@/api/products'
import type { Product } from '@/models/products'
import homeImg from '@/assets/img/cash_home.svg'
import giftImg from '@/assets/img/cash_gift.svg'
import circulationImg from '@/assets/img/cash_circulation.svg'
import loveImg from '@/assets/img/cash_love.svg'
import repairsImg from '@/assets/img/cash_repairs.svg'
import savingImg from '@/assets/img/cash_saving.svg'
export const FUND_INFO_PAGE_KEY: InjectionKey<ReturnType<typeof useFundInfoPage>> =
Symbol('fundInfoPage')
export function useFundInfoPage() {
const info = ref<Product>()
const items = ref<Transaction[]>([])
const tableOutput = ref<TableOutput<Transaction> | null>(null)
const pagination = ref<TableInput>({})
const transactionShow = ref<boolean>()
const editShow = ref<boolean>()
const showDeleteConfirm = ref<boolean>()
const isDeposit = ref<boolean>()
const formRef = ref<FormInstance>()
const fundStore = useFundUserStore()
const walletStore = useWalletStore()
const infoId: number = parseInt(String(router.currentRoute.value.params.id))
const userFormState = reactive<FundTransaction>({
value: undefined,
fund_id: infoId,
})
watch(transactionShow, (n) => (!n ? (userFormState.value = undefined) : ''))
const transButtonDisable = computed(() => {
if (!loading.value) {
if (userFormState.value !== undefined) {
if (isDeposit.value) {
return userFormState.value > walletStore?.walletInfo?.gold
}
return userFormState.value > (info.value?.wallet?.gold ?? 0)
}
return true
}
return true
})
const itemImg = computed(() => {
switch (info.value?.title) {
case 'خرید لوازم منزل':
return homeImg
case 'هدیه':
return giftImg
case 'گردش':
return circulationImg
case 'ازدواج':
return loveImg
case 'تعمیرات':
return repairsImg
case 'پس انداز':
return savingImg
default:
return savingImg
}
})
function deleteFundConfirm() {
if (info.value?.wallet === undefined || info.value?.wallet?.gold === 0) {
showDeleteConfirm.value = true
} else {
message.error('ابتدا گنج خود را خالی کنید')
editShow.value = false
}
}
async function deleteFundFunction() {
try {
loading.value = true
showDeleteConfirm.value = false
await deleteFund(infoId)
await fundStore.getMyFund()
router.back()
message.success('گنج با موفقیت حذف شد')
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
}
async function initInfo() {
try {
loading.value = true
info.value = await getOneProductWallet(infoId)
await initTable()
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
}
async function initTable() {
if (info.value?.wallet) {
try {
loading.value = true
items.value = []
Object.assign(pagination.value, { wallet_id: info.value?.wallet?.id })
tableOutput.value = await getFundTransactionTable(pagination.value)
items.value.push(...tableOutput.value.rows)
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
}
}
async function transaction() {
formRef.value?.validateFields().then(async () => {
try {
loading.value = true
userFormState.fund_id = infoId
isDeposit.value ? await fundDeposit(userFormState) : await fundWithdraw(userFormState)
await initInfo()
await fundStore.getMyFund()
await walletStore.getMyWallet()
message.success(`با موفقیت${isDeposit.value ? ' واریز' : ' برداشت'} شد`)
transactionShow.value = false
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
})
}
function goBack() {
router.back()
}
onMounted(() => {
initInfo()
})
const context = {
info,
items,
transactionShow,
editShow,
showDeleteConfirm,
isDeposit,
formRef,
userFormState,
transButtonDisable,
itemImg,
walletStore,
deleteFundConfirm,
deleteFundFunction,
transaction,
goBack,
infoId,
}
provide(FUND_INFO_PAGE_KEY, context)
return context
}
export function useFundInfoPageContext() {
const context = inject(FUND_INFO_PAGE_KEY)
if (!context) {
throw new Error('useFundInfoPageContext must be used within useFundInfoPage')
}
return context
}
@@ -0,0 +1,78 @@
<template>
<div class="dfund-info">
<div class="dfund-info__container">
<header class="dfund-info-topbar">
<button type="button" class="dfund-info-topbar__back" @click="goBack()">
<BasicIcon icon="vuesax-linear:arrow-right-1" size="18" />
بازگشت
</button>
<div class="dfund-info-topbar__meta">
<p class="dfund-info-topbar__eyebrow">جزئیات دارایی</p>
<h1 class="dfund-info-topbar__title">{{ info?.title ?? '—' }}</h1>
</div>
</header>
<div class="dfund-info__layout">
<section class="dfund-info__hero dfund-surface">
<a-spin :spinning="loading">
<div class="dfund-info__image">
<img v-if="info?.group_type == 'گوی'" :src="goyImg" alt="" />
<img v-if="info?.group_type == 'مکعب'" :src="mokaabImg" alt="" />
<img v-if="info?.group_type == 'سکه'" :src="sekkeImg" alt="" />
<img v-if="info?.group_type == 'مس'" :src="mesImg" alt="" />
<img v-if="info?.group_type == 'نقره'" :src="noghrehImg" alt="" />
<img
v-if="info?.group_type == 'طلا' || info?.group_type == 'آبشده' || info?.group_type == null"
:src="defaultImg"
alt=""
/>
</div>
</a-spin>
<div class="dfund-info__balance">
<span class="dfund-info__balance-value">{{ info?.wallet?.gold ?? 0 }}</span>
<span class="dfund-info__balance-unit">{{ info?.unit }}</span>
</div>
<p class="dfund-info__balance-label">موجودی گنج {{ info?.title }}</p>
</section>
<section class="dfund-info__transactions dfund-surface">
<div class="dfund-info__transactions-header">
<h2 class="dfund-info__transactions-title">تراکنش‌ها</h2>
<span class="dfund-info__transactions-count">{{ items.length }} مورد</span>
</div>
<div class="dfund-info__transactions-list">
<list-anim v-if="!loading">
<Item
v-for="item in items"
:key="item.id"
:item="item"
:title="info?.title"
/>
</list-anim>
<TransactionSkeleton v-for="i in 6" v-else :key="i" class="mt-1" />
<empty v-if="items.length === 0 && !loading" text="هیچ تراکنشی یافت نشد" />
</div>
</section>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import { loading } from '@/utils'
import ListAnim from '@/components/ListAnim.vue'
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
import Empty from '@/components/empty.vue'
import Item from '../components/Item.vue'
import goyImg from '@/assets/img/goyImg.svg'
import mokaabImg from '@/assets/img/mokaabImg.svg'
import sekkeImg from '@/assets/img/sekkeImg.svg'
import mesImg from '@/assets/img/mesImg.jpg'
import noghrehImg from '@/assets/img/noghrehImg.webp'
import defaultImg from '@/assets/img/defaultAbshodeh.svg'
import { useFundInfoPageContext } from '../composables/useFundInfoPage'
const { info, items, goBack } = useFundInfoPageContext()
</script>
+60 -286
View File
@@ -1,305 +1,79 @@
<template>
<div>
<div class="bg-white wrap-fix py-3 px-3 text-primary font-bold drop-shadow flex h-12 z-20 justify-between">
<div class="flex ">
<div @click="router.back()" class="cursor-pointer">
<BasicIcon icon="vuesax-linear:arrow-right-1" />
</div>
<div class="mr-2">{{ info?.title }}</div>
</div>
<!-- <div @click="editShow = true">
<BasicIcon icon="vuesax-linear:setting-4" />
</div>-->
</div>
<div class="pt-20 flex flex-col justify-center items-center">
<a-spin :spinning="loading">
<div class="bg-sky-100 w-[120px] aspect-square flex justify-center rounded-xl">
<!-- <img v-if="!loading" :src="itemImg" alt="">-->
<img v-if="info?.group_type=='گوی'" :src="goyImg">
<img v-if="info?.group_type=='مکعب'" :src="mokaabImg">
<img v-if="info?.group_type=='سکه'" :src="sekkeImg">
<img v-if="info?.group_type=='مس'" :src="mesImg">
<img v-if="info?.group_type=='نقره'" :src="noghrehImg">
<img v-if="info?.group_type=='طلا' || info?.group_type=='آبشده' || info?.group_type== null" :src="defaultImg">
</div>
</a-spin>
<div class="mt-2 font-bold flex text-xs items-center text-gray-500">
<div class="ml-1 text-base text-primary">{{ info?.wallet?.gold?? 0 }}</div>
{{info?.unit}}
</div>
<div class="text-gray-400">موجودی گنج {{ info?.title }}</div>
<div class="w-full p-5">
<!-- <div class="w-full flex gap-3">
<BasicButton class="w-full !bg-[#F3F0E9] !border-0 !rounded" icon="vuesax-linear:add-square" icon-size="18"
@click="()=> {transactionShow = true; isDeposit= false}">برداشت
</BasicButton>
<BasicButton class="w-full !bg-[#F3F0E9] !border-0 !rounded" icon="vuesax-linear:arrow-up" icon-size="18"
@click="()=> {transactionShow = true; isDeposit= true}">واریز
</BasicButton>
</div>-->
<div class=" font-bold mt-5">تراکنش ها</div>
<!-- <a-spin :spinning="loading">-->
<div class="mt-5 pb-17 overflow-y-scroll h-[54vh]">
<list-anim v-if="!loading">
<Item v-for="item in items" :key="item.id" :item="item" :title="info?.title"/>
</list-anim>
<TransactionSkeleton v-for="i in 8" v-else :key="i" class="mt-1" />
<empty v-if="items.length === 0 && !loading" text="هیچ تراکنشی یافت نشد" />
</div>
<!-- </a-spin>-->
</div>
</div>
<BasicActionSheet v-model:show="transactionShow">
<div class="font-bold text-lg">{{ isDeposit ? 'واریز' : 'برداشت' }}</div>
<a-form ref="formRef" :model="userFormState" class="!mt-5" name="basic" @finish="transaction()">
<a-form-item :rules="[required()]" label="مقدار گرم طلا" name="value" class="!block">
<BasicInput v-model:value="userFormState.value"
addon-before="گرم" place-holder="گرم"
type="text" inputmode="decimal"
size="large" />
<div v-if="isDeposit" class="mt-2 text-xs font-bold leading-5">
<div>{{ formatGoldWalletBalanceText(walletStore?.walletInfo?.gold ?? 0) }}</div>
<div v-if="info?.wallet?.gold !== 0">
{{ `موجودی قابل برداشت گنج: ${info?.wallet?.gold ?? 0} گرم` }}
</div>
</div>
<div v-else class="mt-2 text-xs font-bold">موجودی گنج {{ info.title }}: {{ info?.wallet?.gold }} گرم
</div>
</a-form-item>
<a-form-item>
<BasicButton
:disabled="transButtonDisable"
class=" w-full py-2 mt-2"
html-type="submit"
type="primary"
>
<div>
<a-spin v-if="loading" size="small" />
<div v-else>{{ isDeposit ? 'واریز' : 'برداشت' }}</div>
</div>
</BasicButton>
<BasicButton class="mb-20 mt-2 w-full" @click="transactionShow = false">بازگشت</BasicButton>
</a-form-item>
</a-form>
<div class="fund-info-page-root">
<DesktopFundInfoView v-show="isDesktop" />
<MobileFundInfoView v-show="!isDesktop" />
<BasicActionSheet v-if="!isDesktop" v-model:show="transactionShow">
<FundTransactionForm />
</BasicActionSheet>
<BasicActionSheet v-model:show="editShow">
<div class="font-bold text-lg">گنج {{ info?.title }}</div>
<BasicModal
v-else
v-model:visible="transactionShow"
:title="isDeposit ? 'واریز' : 'برداشت'"
width="480px"
destroy-on-close
custom-footer
>
<FundTransactionForm variant="desktop" />
<template #footer>
<div></div>
</template>
</BasicModal>
<BasicButton class="mt-12 w-full" icon="vuesax-linear:edit"
icon-size="16" @click="$router.push({name: 'fund_add', query:{id:infoId}})">
<div class="flex w-full justify-right">ویرایش</div>
</BasicButton>
<BasicButton :loading="loading" class="mb-10 mt-2 w-full !text-red-600"
icon="vuesax-linear:trash"
icon-size="16"
@click="deleteFundConfirm">
<div class="flex w-full justify-right">حذف</div>
</BasicButton>
<BasicButton class="mb-20 mt-2 w-full" type="primary" @click="transactionShow = false">بازگشت</BasicButton>
<BasicActionSheet v-if="!isDesktop" v-model:show="editShow">
<FundEditActions />
</BasicActionSheet>
<DialogVant v-model:show="showDeleteConfirm" :closeOnClickOverlay="true" :timeOut="-1">
<!-- <template v-slot:img>
<img :src="registerDelayImage" alt="" class="opacity-80">
</template>-->
<template v-slot:title>
<div class="p-5 text-sm">
آیا از حذف گنج خود اطمینان دارید؟
</div>
<BasicModal
v-else
v-model:visible="editShow"
:title="`گنج ${info?.title ?? ''}`"
width="420px"
destroy-on-close
custom-footer
>
<FundEditActions variant="desktop" />
<template #footer>
<div></div>
</template>
</BasicModal>
<template v-slot:button>
<div class="text-center text-red" @click="deleteFundFunction">
حذف
</div>
<div class="text-center" @click="showDeleteConfirm = false">
انصراف
</div>
<DialogVant v-model:show="showDeleteConfirm" :close-on-click-overlay="true" :time-out="-1">
<template #title>
<div class="p-5 text-sm">آیا از حذف گنج خود اطمینان دارید؟</div>
</template>
<template #button>
<div class="text-center text-red" @click="deleteFundFunction">حذف</div>
<div class="text-center" @click="showDeleteConfirm = false">انصراف</div>
</template>
</DialogVant>
</div>
</template>
<script lang="ts" setup>
import router from '@/router'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import Item from './components/Item.vue'
import homeImg from '@/assets/img/cash_home.svg'
import giftImg from '@/assets/img/cash_gift.svg'
import circulationImg from '@/assets/img/cash_circulation.svg'
import loveImg from '@/assets/img/cash_love.svg'
import repairsImg from '@/assets/img/cash_repairs.svg'
import savingImg from '@/assets/img/cash_saving.svg'
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import { loading } from '@/utils'
import { deleteFund, fundDeposit, fundWithdraw, getFundById, getFundTransactionTable } from '@/api/fund'
import type { Fund, FundTransaction } from '@/models/fund'
import type { TableInput, TableOutput } from '@/models/table'
import type { Transaction } from '@/models/transaction'
import Empty from '@/components/empty.vue'
import { required } from '@/utils/inputRules'
import type { FormInstance } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
import { formatGoldWalletBalanceText } from '@/utils/productDealMethod'
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
import { useWalletStore } from '@/store/walletStore'
import ListAnim from '@/components/ListAnim.vue'
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
import BasicModal from '@/components/BasicPacks/BasicModal.vue'
import DialogVant from '@/components/DialogVant.vue'
import { useFundUserStore } from '@/store/fundStore'
import { getOneProductWallet } from '@/api/products'
import goyImg from '@/assets/img/goyImg.svg'
import mokaabImg from '@/assets/img/mokaabImg.svg'
import sekkeImg from "@/assets/img/sekkeImg.svg"
import mesImg from "@/assets/img/mesImg.jpg"
import noghrehImg from "@/assets/img/noghrehImg.webp"
import defaultImg from '@/assets/img/defaultAbshodeh.svg'
import type { Product } from '@/models/products'
const info = ref<Product>()
const items = ref<Transaction[]>([])
const tableOutput = ref<TableOutput<Transaction> | null>(null)
const pagination = ref<TableInput>({/*limit: 100, page: 0*/ })
const transactionShow = ref<boolean>()
const editShow = ref<boolean>()
const showDeleteConfirm = ref<boolean>()
const isDeposit = ref<boolean>()
const formRef = ref<FormInstance>()
let fundStore = useFundUserStore()
let walletStore = useWalletStore()
onMounted(() => {
initInfo()
// initTable()
})
function deleteFundConfirm() {
if (info.value?.wallet === undefined || info.value?.wallet?.gold === 0) {
showDeleteConfirm.value = true
} else {
message.error('ابتدا گنج خود را خالی کنید')
editShow.value = false
}
}
async function deleteFundFunction() {
try {
loading.value = true
showDeleteConfirm.value = false
await deleteFund(infoId)
await fundStore.getMyFund()
router.back()
message.success('گنج با موفقیت حذف شد')
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
}
const infoId: number = parseInt(<string>router.currentRoute.value.params.id)
const userFormState = reactive<FundTransaction>({
value: undefined,
fund_id: infoId
})
watch(transactionShow, (n) => !n ? userFormState.value = undefined : '')
const transButtonDisable = computed(() => {
if (!loading.value) {
if (userFormState.value !== undefined) {
if (isDeposit.value) {
return userFormState.value > walletStore?.walletInfo?.gold
} else {
return userFormState.value > info.value?.wallet?.gold
}
// return isDeposit.value ? userFormState.value > walletStore?.walletInfo?.gold : userFormState.value > info.value?.wallet?.gold
} else return true
} else {
return true
}
}
)
async function initInfo() {
try {
loading.value = true
info.value = await getOneProductWallet(infoId)
//pagination.value.page=0
await initTable()
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
}
async function initTable() {
if (info.value?.wallet)
try {
loading.value = true
items.value = []
Object.assign(pagination.value, { wallet_id: info.value?.wallet?.id })
// pagination.value.fund_id = infoId
tableOutput.value = await getFundTransactionTable(pagination.value)
items.value.push(...tableOutput.value.rows)
// console.log(tableOutput.value)
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
}
async function transaction() {
formRef.value?.validateFields().then(async () => {
try {
loading.value = true
userFormState.fund_id = infoId
isDeposit.value ? await fundDeposit(userFormState) : await fundWithdraw(userFormState)
await initInfo()
// await initTable()
await fundStore.getMyFund()
await walletStore.getMyWallet()
fundStore = useWalletStore()
walletStore = useWalletStore()
message.success(` با موفقیت${isDeposit.value ? ' واریز' : ' برداشت'} شد`)
transactionShow.value = false
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
})
}
const itemImg = computed(() => {
switch (info.value?.title) {
case 'خرید لوازم منزل':
return homeImg
case 'هدیه':
return giftImg
case 'گردش':
return circulationImg
case 'ازدواج':
return loveImg
case 'تعمیرات':
return repairsImg
case 'پس انداز':
return savingImg
default:
return savingImg
}
})
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
import { useFundInfoPage } from './composables/useFundInfoPage'
import DesktopFundInfoView from './desktop/DesktopFundInfoView.vue'
import MobileFundInfoView from './mobile/MobileFundInfoView.vue'
import FundTransactionForm from './components/FundTransactionForm.vue'
import FundEditActions from './components/FundEditActions.vue'
const isDesktop = useDesktopBreakpoint()
const {
info,
transactionShow,
editShow,
showDeleteConfirm,
isDeposit,
deleteFundFunction,
} = useFundInfoPage()
</script>
<style scoped>
.fund-info-page-root {
min-height: 100%;
height: 100%;
}
</style>
@@ -0,0 +1,63 @@
<template>
<div>
<div
class="bg-white wrap-fix py-3 px-3 text-primary font-bold drop-shadow flex h-12 z-20 justify-between"
>
<div class="flex">
<div class="cursor-pointer" @click="goBack()">
<BasicIcon icon="vuesax-linear:arrow-right-1" />
</div>
<div class="mr-2">{{ info?.title }}</div>
</div>
</div>
<div class="pt-20 flex flex-col justify-center items-center">
<a-spin :spinning="loading">
<div class="bg-sky-100 w-[120px] aspect-square flex justify-center rounded-xl">
<img v-if="info?.group_type == 'گوی'" :src="goyImg" alt="" />
<img v-if="info?.group_type == 'مکعب'" :src="mokaabImg" alt="" />
<img v-if="info?.group_type == 'سکه'" :src="sekkeImg" alt="" />
<img v-if="info?.group_type == 'مس'" :src="mesImg" alt="" />
<img v-if="info?.group_type == 'نقره'" :src="noghrehImg" alt="" />
<img
v-if="info?.group_type == 'طلا' || info?.group_type == 'آبشده' || info?.group_type == null"
:src="defaultImg"
alt=""
/>
</div>
</a-spin>
<div class="mt-2 font-bold flex text-xs items-center text-gray-500">
<div class="ml-1 text-base text-primary">{{ info?.wallet?.gold ?? 0 }}</div>
{{ info?.unit }}
</div>
<div class="text-gray-400">موجودی گنج {{ info?.title }}</div>
<div class="w-full p-5">
<div class="font-bold mt-5">تراکنش ها</div>
<div class="mt-5 pb-17 overflow-y-scroll h-[54vh]">
<list-anim v-if="!loading">
<Item v-for="item in items" :key="item.id" :item="item" :title="info?.title" />
</list-anim>
<TransactionSkeleton v-for="i in 8" v-else :key="i" class="mt-1" />
<empty v-if="items.length === 0 && !loading" text="هیچ تراکنشی یافت نشد" />
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import { loading } from '@/utils'
import ListAnim from '@/components/ListAnim.vue'
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
import Empty from '@/components/empty.vue'
import Item from '../components/Item.vue'
import goyImg from '@/assets/img/goyImg.svg'
import mokaabImg from '@/assets/img/mokaabImg.svg'
import sekkeImg from '@/assets/img/sekkeImg.svg'
import mesImg from '@/assets/img/mesImg.jpg'
import noghrehImg from '@/assets/img/noghrehImg.webp'
import defaultImg from '@/assets/img/defaultAbshodeh.svg'
import { useFundInfoPageContext } from '../composables/useFundInfoPage'
const { info, items, goBack } = useFundInfoPageContext()
</script>
@@ -0,0 +1,161 @@
<template>
<div>
<div
class="bg-white w-full py-3 px-3 text-primary font-bold drop-shadow fixed md:absolute top-0 flex h-12 z-20"
>
<div @click="goBack()">
<BasicIcon icon="vuesax-linear:arrow-right-1" />
</div>
<div class="mr-2">دارایی ها</div>
</div>
<div
class="w-screen md:w-full h-[70vh] fixed md:absolute overflow-hidden"
:class="isBazretalaProject() ? 'bazretala-fund-header' : 'text-tertiary bg-primary'"
>
<img
:src="fundHeaderBackground"
alt=""
:class="
isBazretalaProject()
? 'pointer-events-none z-0 absolute inset-0 w-full object-cover object-center'
: 'pointer-events-none z-0 w-full absolute bottom-0 left-0 opacity-5'
"
/>
<div
class="pt-15 flex flex-col relative z-10"
:class="isBazretalaProject() ? 'text-white' : 'text-tertiary'"
>
<div class="font-bold mr-5 z-20">موجودی دارایی ها</div>
<div
v-if="meta?.has_count_based_products"
class="flex mt-4 justify-center mt-2 w-full z-20"
>
محصولات:
<div class="text-l mr-4 font-bold">{{ countBasedInventory }} عدد</div>
</div>
<template v-if="isBazretalaProject()">
<div class="flex justify-center mt-2 w-full z-20">
دارایی نقره:
<div class="text-l mr-4 font-bold">
{{ walletStore.walletInfo?.count_silver ?? 0 }}گرم
</div>
</div>
<div class="flex justify-center mt-2 w-full z-20">
دارایی طلایی:
<div class="text-l mr-4 font-bold">
{{ walletStore.walletInfo?.count_gold_wallet ?? 0 }}گرم
</div>
</div>
</template>
<template v-else-if="meta?.single_metal && primaryMetalTotal">
<div class="flex justify-center mt-2 w-full z-20">
{{ primaryMetalTotal.label }}:
<div class="text-l mr-4 font-bold">{{ formatMetalTotal(primaryMetalTotal) }}</div>
</div>
</template>
<template v-else>
<div
v-for="metal in metalTotals"
:key="`metal-${metal.metal_id}`"
class="flex justify-center mt-2 w-full z-20"
>
{{ metal.label }}:
<div class="text-l mr-4 font-bold">{{ formatMetalTotal(metal) }}</div>
</div>
</template>
<div
v-if="meta?.has_count_based_products && !isBazretalaProject()"
class="flex justify-center mt-2 w-full z-20"
>
مجموع موجودی:
<div class="text-l mr-4 font-bold">{{ countBasedInventory }} عدد</div>
</div>
<div class="flex justify-center mt-2 w-full z-20">
دارایی ریالی:
<div class="text-l mr-4 font-bold">{{ usePriceWithoutType(cashWallet) }}ریال</div>
</div>
<div class="flex justify-center mt-2 w-full z-20">
معادل ریالی مجموع دارایی ها:
<div class="text-l mr-4 font-bold">{{ usePriceWithoutType(totalRialValue) }}ریال</div>
</div>
</div>
</div>
<div class="pt-[20rem]">
<div
class="bg-gray-100 w-screen md:w-full z-20 relative rounded-t-3xl h-[calc(100vh-20rem-70px)] overflow-y-auto pb-15"
style="box-shadow: 0px -2px 4px -2px #0000001a"
>
<div
v-if="inventoryCards.length === 0 && !loading"
class="w-full h-full flex justify-center items-center"
>
<div>
<img
:src="fundEmptyImage"
alt=""
:class="isBazretalaProject() ? 'w-48 max-w-[11rem]' : 'w-70'"
/>
<div v-if="nameProject !== 'abaeigold'" class="text-center text-lg">
{{ fundEmptyText }}
</div>
</div>
</div>
<a-spin :spinning="loading">
<div :class="loading ? 'h-[40vh]' : ''" class="grid grid-cols-2 gap-3 p-5">
<Item
v-for="item in inventoryCards"
:key="item.product_id"
:item="item"
@click="openFundInfo(item.product_id)"
/>
</div>
</a-spin>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import { loading } from '@/utils'
import { usePriceWithoutType } from '@/utils/usePrice'
import { useWalletStore } from '@/store/walletStore'
import Item from '../components/Item.vue'
import { useFundPageContext } from '../composables/useFundPage'
const walletStore = useWalletStore()
const {
nameProject,
meta,
inventoryCards,
metalTotals,
primaryMetalTotal,
cashWallet,
totalRialValue,
countBasedInventory,
fundHeaderBackground,
fundEmptyImage,
fundEmptyText,
isBazretalaProject,
formatMetalTotal,
openFundInfo,
goBack,
} = useFundPageContext()
</script>
<style scoped>
.bazretala-fund-header {
background-color: #000;
}
.bazretala-fund-header::after {
content: '';
position: absolute;
inset: 0;
z-index: 1;
background: rgba(0, 0, 0, 0.45);
pointer-events: none;
}
</style>
@@ -0,0 +1,117 @@
import { computed, inject, onMounted, provide, ref, type InjectionKey } from 'vue'
import router from '@/router'
import { loading } from '@/utils'
import { usePrefix } from '@/utils/usePrefix'
import { createFund, editFund, getFundById } from '@/api/fund'
import { message } from 'ant-design-vue/es'
import { useFundUserStore } from '@/store/fundStore'
import type { Fund } from '@/models/fund'
import homeImg from '@/assets/img/cash_home.svg'
import giftImg from '@/assets/img/cash_gift.svg'
import circulationImg from '@/assets/img/cash_circulation.svg'
import loveImg from '@/assets/img/cash_love.svg'
import repairsImg from '@/assets/img/cash_repairs.svg'
import savingImg from '@/assets/img/cash_saving.svg'
export const FUND_NEW_PAGE_KEY: InjectionKey<ReturnType<typeof useFundNewPage>> =
Symbol('fundNewPage')
const types = [
{ name: 'خرید لوازم منزل' },
{ name: 'پس انداز' },
{ name: 'گردش' },
{ name: 'ازدواج' },
{ name: 'تعمیرات' },
{ name: 'هدیه' },
]
export function useFundNewPage() {
const fundStore = useFundUserStore()
const edit_id = computed(() => parseInt(router.currentRoute.value.query.id as string))
const name = ref<string>('پس انداز')
const { prefixCls } = usePrefix('input_name')
const info = ref<Fund>()
const itemImg = computed(() => {
switch (name.value) {
case 'خرید لوازم منزل':
return homeImg
case 'هدیه':
return giftImg
case 'گردش':
return circulationImg
case 'ازدواج':
return loveImg
case 'تعمیرات':
return repairsImg
case 'پس انداز':
return savingImg
default:
return savingImg
}
})
async function getEditInfo() {
try {
loading.value = true
info.value = await getFundById(edit_id.value)
name.value = info.value?.title ?? ''
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
}
async function ok() {
try {
loading.value = true
if (!name.value) {
message.error('ابتدا نام گنج را وارد کنید')
return
}
edit_id.value
? await editFund(edit_id.value, { title: name.value })
: await createFund({ title: name.value })
await fundStore.getMyFund()
message.success(`گنج با موفقیت ${edit_id.value ? 'ویرایش' : 'ساخته'} شد`)
router.back()
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
}
function goBack() {
router.back()
}
onMounted(() => {
if (edit_id.value) {
getEditInfo()
}
})
const context = {
types,
edit_id,
name,
prefixCls,
info,
itemImg,
ok,
goBack,
}
provide(FUND_NEW_PAGE_KEY, context)
return context
}
export function useFundNewPageContext() {
const context = inject(FUND_NEW_PAGE_KEY)
if (!context) {
throw new Error('useFundNewPageContext must be used within useFundNewPage')
}
return context
}
@@ -0,0 +1,81 @@
<template>
<div class="dfund-new">
<div class="dfund-new__container">
<header class="dfund-new-topbar">
<button type="button" class="dfund-new-topbar__back" @click="goBack()">
<BasicIcon icon="vuesax-linear:arrow-right-1" size="18" />
بازگشت
</button>
<div class="dfund-new-topbar__meta">
<p class="dfund-new-topbar__eyebrow">گنج</p>
<h1 class="dfund-new-topbar__title">
{{ edit_id ? `ویرایش گنج ${name}` : 'صندوق جدید' }}
</h1>
</div>
</header>
<a-spin :spinning="loading">
<section class="dfund-new__card dfund-surface">
<div class="dfund-new__preview">
<img :src="itemImg" alt="" class="dfund-new__preview-img" />
</div>
<div class="dfund-new__field" :class="[prefixCls]">
<label class="dfund-new__label">عنوان گنج</label>
<van-field
v-model="name"
label=""
placeholder="عنوان را وارد کنید"
input-align="center"
:show-word-limit="true"
maxlength="25"
rows="1"
size="small"
/>
</div>
<div class="dfund-new__tags">
<button
v-for="item in types"
:key="item.name"
type="button"
class="dfund-new__tag"
:class="{ 'dfund-new__tag--active': item.name === name }"
@click="name = item.name"
>
{{ item.name }}
</button>
</div>
<BasicButton class="dfund-new__submit" type="primary" :disabled="loading" @click="ok()">
<a-spin v-if="loading" size="small" />
<span v-else>{{ edit_id ? 'ویرایش گنج' : 'ساخت گنج' }}</span>
</BasicButton>
</section>
</a-spin>
</div>
</div>
</template>
<script setup lang="ts">
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import { loading } from '@/utils'
import { useFundNewPageContext } from '../composables/useFundNewPage'
const { types, edit_id, name, prefixCls, itemImg, ok, goBack } = useFundNewPageContext()
</script>
<style lang="less">
@prefix: ~'@{prefixCls}-input_name';
.@{prefix} {
.van-field__value {
@apply flex;
}
.van-field__body {
@apply w-full mr-5;
}
}
</style>
+14 -185
View File
@@ -1,194 +1,23 @@
<template>
<div>
<div class="bg-white wrap-fix py-3 px-3 text-primary font-bold drop-shadow flex h-12 z-20 justify-between">
<div class="flex ">
<div @click="router.back()" class="cursor-pointer">
<BasicIcon icon="vuesax-linear:arrow-right-1"/>
</div>
<div class="mr-2">{{edit_id? `ویرایش گنج ${name}`: 'صندوق جدید'}}</div>
</div>
<div @click="router.back()">
<!-- <BasicIcon icon="vuesax-linear:setting-4"/>-->
</div>
</div>
<a-spin :spinning="loading">
<div class="pt-20 flex flex-col justify-center items-center">
<div class="bg-sky-100 w-[120px] aspect-square flex justify-center rounded-xl"><img :src="itemImg" alt=""></div>
<!-- <div class="mt-1 text-xs">تغییر عکس</div>-->
<div class="flex mt-3 w-full relative px-5" :class="[prefixCls]">
<!-- <div class="text-primary font-bold mx-auto items-center">پس انداز</div>
<div class="absolute left-5 text-gray-400 text-xs">7/25</div>-->
<van-field
v-model="name"
label=""
class="font-bold"
placeholder="عنوان را وارد کنید"
input-align="center"
:show-word-limit="true"
maxlength="25"
rows="1"
size="small"
/>
</div>
<div class="flex overflow-x-scroll gap-2 whitespace-nowrap max-w-[320px] mt-2 px-3">
<!-- <BasicTags
:items="types"
color="green"
:has-add="false"
/>-->
<!-- <BasicButton v-for="item in types" size="small">{{item.name}}</BasicButton>-->
<div v-for="item in types"
class="border-1 px-3 py-2 flex-nowrap rounded text-xs color-transition background-color-transition"
:key="item.name"
:class="item.name === name? 'bg-primary text-white':'bg-gray-200 text-gray-600'"
@click="name = item.name">
{{ item.name }}
</div>
</div>
<!-- <div class="mt-2 font-bold flex text-xs items-center text-gray-500">
<div class="ml-1 text-base text-primary">{{ router.currentRoute.value.query.amount }}</div>
گرم
</div>
<div class="text-gray-400">موجودی صندوق {{itemName}}</div>
<div class="w-full p-5">
<div class="w-full flex gap-3">
<BasicButton class="w-full !bg-light" icon="vuesax-linear:add-square" icon-size="18">برداشت</BasicButton>
<BasicButton class="w-full !bg-light" icon="vuesax-linear:arrow-up" icon-size="18">واریز</BasicButton>
</div>
<div class="text-lg font-bold mt-5">تراکنش ها</div>
<div class="mt-5 pb-17">
<Item v-for="item in items" :is-up="item.isUp" :has-transition="item.hasTransition" :key="item"/>
</div>
</div>-->
<div class="wrap-fix flex justify-center bottom-23 px-5 z-200" @click="ok()">
<BasicButton class="rounded w-full px-5 py-2 text-center" type="primary" :disabled="loading">
<a-spin v-if="loading" size="small"/>
<div v-else>{{edit_id? 'ویرایش گنج':'ساخت گنج'}}</div>
</BasicButton>
</div>
</div>
</a-spin>
<div class="fund-new-page-root">
<DesktopFundNewView v-show="isDesktop" />
<MobileFundNewView v-show="!isDesktop" />
</div>
</template>
<script setup lang="ts">
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
import { useFundNewPage } from './composables/useFundNewPage'
import DesktopFundNewView from './desktop/DesktopFundNewView.vue'
import MobileFundNewView from './mobile/MobileFundNewView.vue'
import router from "@/router";
import {computed, onMounted, ref} from "vue";
import homeImg from "@/assets/img/cash_home.svg";
import giftImg from "@/assets/img/cash_gift.svg";
import circulationImg from "@/assets/img/cash_circulation.svg";
import loveImg from "@/assets/img/cash_love.svg";
import repairsImg from "@/assets/img/cash_repairs.svg";
import savingImg from "@/assets/img/cash_saving.svg";
import {usePrefix} from "@/utils/usePrefix";
import {loading} from "@/utils";
import {createFund, editFund, getFundById} from "@/api/fund";
import {message} from "ant-design-vue/es";
import {useFundUserStore} from "@/store/fundStore";
import BasicButton from "@/components/BasicPacks/BasicButton.vue";
import type {Fund} from "@/models/fund";
const fundStore = useFundUserStore()
const types = [
{
name: 'خرید لوازم منزل'
},
{
name: 'پس انداز'
},
{
name: 'گردش'
},
{
name: 'ازدواج'
},
{
name: 'تعمیرات'
},
{
name: 'هدیه'
},
]
const edit_id = computed(() => {
return parseInt(router.currentRoute.value.query.id as string)
})
const name = ref<string>('پس انداز')
const {prefixCls} = usePrefix('input_name')
const info = ref<Fund>()
async function getEditInfo() {
try {
loading.value = true
info.value = await getFundById(edit_id.value)
name.value = info.value?.title?? ''
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
}
onMounted(() => {
if (edit_id.value) {
getEditInfo()
}
})
const itemImg = computed(() => {
switch (name.value) {
case 'خرید لوازم منزل':
return homeImg
case 'هدیه':
return giftImg
case 'گردش':
return circulationImg
case 'ازدواج':
return loveImg
case 'تعمیرات':
return repairsImg
case 'پس انداز':
return savingImg
default:
return savingImg
}
})
async function ok() {
try {
loading.value = true
// console.log(name.value)
name.value ? edit_id.value? await editFund(edit_id.value,{title: name.value}) :await createFund({title: name.value}) : () => {
message.error('ابتدا نام گنج را وارد کنید')
return
}
await fundStore.getMyFund()
message.success(`گنج با موفقیت ${edit_id.value? 'ویرایش':'ساخته'} شد`)
router.back()
// name.value? message.warn('ss'): message.error('ابتدا نام صندوق را وارد کنید')
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
}
const isDesktop = useDesktopBreakpoint()
useFundNewPage()
</script>
<style lang="less">
@prefix: ~'@{prefixCls}-input_name';
.@{prefix} {
.van-field__value {
@apply flex;
}
.van-field__body {
@apply w-full mr-5;
}
<style scoped>
.fund-new-page-root {
min-height: 100%;
height: 100%;
}
</style>
</style>
@@ -0,0 +1,74 @@
<template>
<div>
<div
class="bg-white wrap-fix py-3 px-3 text-primary font-bold drop-shadow flex h-12 z-20 justify-between"
>
<div class="flex">
<div class="cursor-pointer" @click="goBack()">
<BasicIcon icon="vuesax-linear:arrow-right-1" />
</div>
<div class="mr-2">{{ edit_id ? `ویرایش گنج ${name}` : 'صندوق جدید' }}</div>
</div>
</div>
<a-spin :spinning="loading">
<div class="pt-20 flex flex-col justify-center items-center">
<div class="bg-sky-100 w-[120px] aspect-square flex justify-center rounded-xl">
<img :src="itemImg" alt="" />
</div>
<div class="flex mt-3 w-full relative px-5" :class="[prefixCls]">
<van-field
v-model="name"
label=""
class="font-bold"
placeholder="عنوان را وارد کنید"
input-align="center"
:show-word-limit="true"
maxlength="25"
rows="1"
size="small"
/>
</div>
<div class="flex overflow-x-scroll gap-2 whitespace-nowrap max-w-[320px] mt-2 px-3">
<div
v-for="item in types"
:key="item.name"
class="border-1 px-3 py-2 flex-nowrap rounded text-xs color-transition background-color-transition"
:class="item.name === name ? 'bg-primary text-white' : 'bg-gray-200 text-gray-600'"
@click="name = item.name"
>
{{ item.name }}
</div>
</div>
<div class="wrap-fix flex justify-center bottom-23 px-5 z-200" @click="ok()">
<BasicButton class="rounded w-full px-5 py-2 text-center" type="primary" :disabled="loading">
<a-spin v-if="loading" size="small" />
<div v-else>{{ edit_id ? 'ویرایش گنج' : 'ساخت گنج' }}</div>
</BasicButton>
</div>
</div>
</a-spin>
</div>
</template>
<script setup lang="ts">
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import { loading } from '@/utils'
import { useFundNewPageContext } from '../composables/useFundNewPage'
const { types, edit_id, name, prefixCls, itemImg, ok, goBack } = useFundNewPageContext()
</script>
<style lang="less">
@prefix: ~'@{prefixCls}-input_name';
.@{prefix} {
.van-field__value {
@apply flex;
}
.van-field__body {
@apply w-full mr-5;
}
}
</style>