Split desktop and mobile layouts with shared page data so APIs fetch once per context.
Add layout shells, desktop views, module-level page caches, and UI fixes for auth, home, wallet, transactions, address, and related pages. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<div class="boursecu-transactions pb-[calc(var(--app-bottom-chrome-height)+1rem)]">
|
||||
<div class="px-4 pt-4 pb-2">
|
||||
<h1 class="text-xl font-bold text-gray-900">گزارشها</h1>
|
||||
</div>
|
||||
|
||||
<div class="boursecu-transactions__tabs px-4">
|
||||
<div class="flex gap-5 overflow-x-auto whitespace-nowrap pb-1">
|
||||
<button
|
||||
v-for="tab in BOURSECU_TRANSACTION_TABS"
|
||||
:key="tab.id"
|
||||
type="button"
|
||||
class="boursecu-transactions__tab shrink-0"
|
||||
:class="{ 'boursecu-transactions__tab--active': activeTabId === tab.id }"
|
||||
@click="setActiveTab(tab.id)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab.showTradeToggle" class="px-4 mt-4 flex justify-end">
|
||||
<div class="boursecu-transactions__trade-toggle">
|
||||
<button
|
||||
type="button"
|
||||
class="boursecu-transactions__trade-toggle-item"
|
||||
:class="{ 'boursecu-transactions__trade-toggle-item--active': tradeSide === 'buy' }"
|
||||
@click="setTradeSide('buy')"
|
||||
>
|
||||
خرید
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="boursecu-transactions__trade-toggle-item"
|
||||
:class="{ 'boursecu-transactions__trade-toggle-item--active': tradeSide === 'sell' }"
|
||||
@click="setTradeSide('sell')"
|
||||
>
|
||||
فروش
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 mt-4 flex items-center gap-2 flex-wrap">
|
||||
<div class="flex items-center gap-1 text-sm text-gray-600 shrink-0">
|
||||
<BasicIcon icon="vuesax-linear:filter" size="16" />
|
||||
<span>فیلترها:</span>
|
||||
</div>
|
||||
<a-select
|
||||
v-model:value="statusFilter"
|
||||
allow-clear
|
||||
class="boursecu-transactions__filter-select min-w-[7rem]"
|
||||
placeholder="وضعیت"
|
||||
size="large"
|
||||
@change="applyInlineFilters"
|
||||
>
|
||||
<a-select-option value="active">فعال</a-select-option>
|
||||
<a-select-option value="inactive">غیرفعال</a-select-option>
|
||||
</a-select>
|
||||
<BaseDatePicker
|
||||
v-model:value="dateFilter"
|
||||
class="boursecu-transactions__filter-date min-w-[7rem]"
|
||||
placeholder="تاریخ"
|
||||
size="large"
|
||||
@onchange="applyInlineFilters"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div ref="listRef" :class="loading ? 'min-h-[50vh]' : ''" class="px-4 mt-5">
|
||||
<list-anim v-if="!loading">
|
||||
<Item v-for="item in items" :key="item.id" :item="item" />
|
||||
<div :ref="bindMobileObserver" class="w-full opacity-0 relative">
|
||||
در حال بارگذاری
|
||||
</div>
|
||||
<TransactionSkeleton v-if="loadingMore" class="mt-1 mb-8" />
|
||||
</list-anim>
|
||||
|
||||
<template v-else>
|
||||
<TransactionSkeleton v-for="i in 8" :key="i" class="mt-1" />
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-if="!loading && items.length === 0"
|
||||
class="boursecu-transactions__empty"
|
||||
>
|
||||
<img :src="emptyImage" alt="" class="boursecu-transactions__empty-image" />
|
||||
<p class="boursecu-transactions__empty-text">{{ emptyText }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { loading, loadingMore } from '@/utils'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import BaseDatePicker from '@/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue'
|
||||
import ListAnim from '@/components/ListAnim.vue'
|
||||
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
|
||||
import emptyImage from '@/assets/img/cash_empty.svg'
|
||||
import Item from './components/Item.vue'
|
||||
import { BOURSECU_TRANSACTION_TABS } from './config/boursecuTabs'
|
||||
import { useTransactionsPageContext } from './composables/useTransactionsPage'
|
||||
|
||||
const listRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const {
|
||||
items,
|
||||
dateFilter,
|
||||
statusFilter,
|
||||
activeTabId,
|
||||
tradeSide,
|
||||
activeTab,
|
||||
emptyText,
|
||||
setActiveTab,
|
||||
setTradeSide,
|
||||
applyInlineFilters,
|
||||
bindMobileObserver,
|
||||
} = useTransactionsPageContext()
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.boursecu-transactions {
|
||||
background: #fff;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.boursecu-transactions__tab {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0 0 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #8b9490;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: color 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.boursecu-transactions__tab--active {
|
||||
color: #1f2937;
|
||||
border-bottom-color: #e8b923;
|
||||
}
|
||||
|
||||
.boursecu-transactions__trade-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 3px;
|
||||
border-radius: 10px;
|
||||
background: #eceff1;
|
||||
}
|
||||
|
||||
.boursecu-transactions__trade-toggle-item {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
min-width: 5.5rem;
|
||||
padding: 0.45rem 1rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: #6b7280;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.boursecu-transactions__trade-toggle-item--active {
|
||||
background: #fff;
|
||||
border-color: #d4e3de;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.boursecu-transactions__filter-select,
|
||||
.boursecu-transactions__filter-date {
|
||||
flex: 1;
|
||||
max-width: 9rem;
|
||||
}
|
||||
|
||||
.boursecu-transactions__empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 1rem 4rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.boursecu-transactions__empty-image {
|
||||
width: 11rem;
|
||||
max-width: 60vw;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.boursecu-transactions__empty-text {
|
||||
margin-top: 1rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<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-10">
|
||||
<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 { ref } from 'vue'
|
||||
import { loading, loadingMore } from '@/utils'
|
||||
import Item from './components/Item.vue'
|
||||
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 { useTransactionsPageContext } from './composables/useTransactionsPage'
|
||||
|
||||
const el = ref<HTMLElement | null>(null)
|
||||
|
||||
const {
|
||||
items,
|
||||
showFilter,
|
||||
dateFilter,
|
||||
emptyText,
|
||||
applyFilter,
|
||||
bindMobileObserver,
|
||||
} = 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>
|
||||
@@ -1,24 +1,70 @@
|
||||
<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">{{ props.item.title }}</p>
|
||||
<div class="dtxn-row__meta">
|
||||
<span>{{ toJalaliOutOfUTC(props.item.expire_at) }}</span>
|
||||
<span :class="props.item.gold >= 0 ? 'text-green-600' : 'text-red-600'">
|
||||
{{ props.item.gold }} {{ props.item.unit }}
|
||||
</span>
|
||||
<span v-if="isGold">
|
||||
قیمت معامله:
|
||||
{{ usePriceWithoutType(parseInt((Math.abs(props.item.cash) / Math.abs(props.item.gold)).toFixed(0))) }}
|
||||
ریال
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="dtxn-row__amount"
|
||||
:class="props.item.cash > 0 ? 'dtxn-row__amount--up' : 'dtxn-row__amount--down'"
|
||||
>
|
||||
{{ usePriceWithoutType(props.item.cash ?? 0) }} ریال
|
||||
</div>
|
||||
<div class="dtxn-row__status">
|
||||
<ATag color="blue" class="!py-1 !px-4 !ml-0 !text-xs">
|
||||
{{ props.item.order?.sts_str ?? props.item.withdraw?.sts_str }}
|
||||
</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">
|
||||
<div>{{ item.title }}</div>
|
||||
<div>{{ toJalaliOutOfUTC(item.expire_at) }}</div>
|
||||
<div>{{ props.item.title }}</div>
|
||||
<div>{{ toJalaliOutOfUTC(props.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">{{ item.gold }}
|
||||
<div>{{item.unit}}</div>
|
||||
<div
|
||||
:class="props.item.gold >= 0 ? 'text-green-600' : 'text-red-600'"
|
||||
class="text-gray-500 text-xs number-ltr"
|
||||
>
|
||||
{{ props.item.gold }}
|
||||
<div>{{ props.item.unit }}</div>
|
||||
</div>
|
||||
<div 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="props.item.cash > 0 ? 'text-green-600' : 'text-red-600'" class="text-base">
|
||||
{{ usePriceWithoutType(props.item.cash ?? 0) }}
|
||||
</div>
|
||||
ریال
|
||||
</div>
|
||||
@@ -28,8 +74,9 @@
|
||||
<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">{{ typeof parseInt((Math.abs(item.cash) / Math.abs(item.gold)).toFixed(0)) }}</div>-->
|
||||
<div class="text-base">{{ usePriceWithoutType(parseInt((Math.abs(item.cash) / Math.abs(item.gold)).toFixed(0))) }}</div>
|
||||
<div class="text-base">
|
||||
{{ usePriceWithoutType(parseInt((Math.abs(props.item.cash) / Math.abs(props.item.gold)).toFixed(0))) }}
|
||||
</div>
|
||||
ریال
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,149 +84,63 @@
|
||||
<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 }}
|
||||
{{ props.item.order?.sts_str ?? props.item.withdraw?.sts_str }}
|
||||
</ATag>
|
||||
</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="mb-8rem 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>
|
||||
<div v-if="item.order" class="flex justify-between mb-3">
|
||||
<div class="text-primary">قیمت هر گرم {{ getTradePriceUnitLabel() }} حین معامله:</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 : getMeltedGoldTradeLabel() }} مبادله شده:</div>
|
||||
<div :class="item.gold >= 0? 'text-green-600' : 'text-red-600'" class="flex flex-col items-end text-xs number-ltr">
|
||||
<div class="flex items-center">
|
||||
<div class="text-base ml-1">{{ item.gold }}</div>
|
||||
{{ item.unit }}
|
||||
</div>
|
||||
<div
|
||||
v-if="item.wallet?.product && getProductTotalWeightLabel(Math.abs(item.gold ?? 0), item.wallet.product)"
|
||||
class="text-gray-500"
|
||||
>
|
||||
{{ getProductTotalWeightLabel(Math.abs(item.gold ?? 0), item.wallet.product) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mb-3">
|
||||
<div class="text-primary">{{ getCashWalletLabel(item.wallet?.product?.group_type) }}:</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 flex-col items-end text-xs text-yellow-700">
|
||||
<div class="flex items-center">
|
||||
<div class="text-base ml-1">{{ item.balance_gold }}</div>
|
||||
{{ getProductQuantityUnit(item.wallet.product) }}
|
||||
</div>
|
||||
<div v-if="getProductUnitWeightLabel(item.wallet.product)" class="text-gray-500">
|
||||
هر عدد {{ getProductUnitWeightLabel(item.wallet.product) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mb-3" v-else>
|
||||
<div class="text-primary">{{ getGoldWalletLabel(item.wallet?.product?.group_type) }}:</div>
|
||||
<div class="flex text-xs text-yellow-700 items-center">
|
||||
<div class="text-base ml-1">{{ item.balance_gold }}</div>
|
||||
{{ getWeightUnit() }}
|
||||
</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">{{ props.item.title }}</div>
|
||||
<TransactionDetailPanel :item="props.item" variant="mobile" />
|
||||
<BasicButton class="w-full mt-5" @click="infoVisible = false">بازگشت</BasicButton>
|
||||
</BasicActionSheet>
|
||||
|
||||
<BasicModal
|
||||
v-else
|
||||
v-model:visible="infoVisible"
|
||||
:title="props.item.title"
|
||||
width="520px"
|
||||
custom-footer
|
||||
destroy-on-close
|
||||
>
|
||||
<TransactionDetailPanel :item="props.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 { usePriceWithoutType } from '../../../../utils/usePrice'
|
||||
import { usePriceWithoutType } from '@/utils/usePrice'
|
||||
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
|
||||
import BasicModal from '@/components/BasicPacks/BasicModal.vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import type { Transaction } from '@/models/transaction'
|
||||
import { getCashWalletLabel, getGoldWalletLabel, getMeltedGoldTradeLabel, getProductQuantityUnit, getProductTotalWeightLabel, getProductUnitWeightLabel, getTradePriceUnitLabel, getWeightUnit } from '@/utils/productDealMethod'
|
||||
import { toJalaliNotime, toJalaliOutOfUTC, toTime } from '../../../../utils/useDateTime'
|
||||
import { toJalaliOutOfUTC } from '@/utils/useDateTime'
|
||||
import TransactionDetailPanel from './TransactionDetailPanel.vue'
|
||||
|
||||
const infoVisible = ref<boolean>(false)
|
||||
const infoVisible = ref(false)
|
||||
|
||||
interface Prop {
|
||||
item: Transaction
|
||||
}
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
item: Transaction
|
||||
variant?: 'mobile' | 'desktop'
|
||||
}>(),
|
||||
{ variant: 'mobile' },
|
||||
)
|
||||
|
||||
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 isGold = computed(() => props.item.type === 1 || props.item.type === 2 || props.item.type === 10)
|
||||
/*const title = computed(() => {
|
||||
if (isUp.value) {
|
||||
if (isGold.value) {
|
||||
return 'واریز ریالی'
|
||||
} else {
|
||||
return 'خرید طلا'
|
||||
}
|
||||
} else {
|
||||
if (isGold.value) {
|
||||
return 'برداشت ریالی'
|
||||
} else {
|
||||
return 'فروش طلا'
|
||||
}
|
||||
}
|
||||
})*/
|
||||
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,
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
<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>
|
||||
<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'">
|
||||
قیمت هر گرم {{ getTradePriceUnitLabel() }} حین معامله:
|
||||
</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 : getMeltedGoldTradeLabel() }} مبادله شده:
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
item.gold >= 0 ? 'text-green-600' : 'text-red-600',
|
||||
'flex flex-col items-end text-xs number-ltr',
|
||||
variant === 'desktop' ? 'dtxn-detail__value' : '',
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="text-base ml-1">{{ item.gold }}</div>
|
||||
{{ item.unit }}
|
||||
</div>
|
||||
<div
|
||||
v-if="item.wallet?.product && getProductTotalWeightLabel(Math.abs(item.gold ?? 0), item.wallet.product)"
|
||||
class="text-gray-500"
|
||||
>
|
||||
{{ getProductTotalWeightLabel(Math.abs(item.gold ?? 0), item.wallet.product) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__row' : 'flex justify-between mb-3'">
|
||||
<div :class="variant === 'desktop' ? 'dtxn-detail__label' : 'text-primary'">
|
||||
{{ getCashWalletLabel(item.wallet?.product?.group_type) }}:
|
||||
</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 flex-col items-end text-xs text-yellow-700">
|
||||
<div class="flex items-center">
|
||||
<div class="text-base ml-1">{{ item.balance_gold }}</div>
|
||||
{{ getProductQuantityUnit(item.wallet.product) }}
|
||||
</div>
|
||||
<div v-if="getProductUnitWeightLabel(item.wallet.product)" class="text-gray-500">
|
||||
هر عدد {{ getProductUnitWeightLabel(item.wallet.product) }}
|
||||
</div>
|
||||
</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'">
|
||||
{{ getGoldWalletLabel(item.wallet?.product?.group_type) }}:
|
||||
</div>
|
||||
<div class="flex text-xs text-yellow-700 items-center">
|
||||
<div class="text-base ml-1">{{ item.balance_gold }}</div>
|
||||
{{ getWeightUnit() }}
|
||||
</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 { usePriceWithoutType } from '@/utils/usePrice'
|
||||
import type { Transaction } from '@/models/transaction'
|
||||
import {
|
||||
getCashWalletLabel,
|
||||
getGoldWalletLabel,
|
||||
getMeltedGoldTradeLabel,
|
||||
getProductQuantityUnit,
|
||||
getProductTotalWeightLabel,
|
||||
getProductUnitWeightLabel,
|
||||
getTradePriceUnitLabel,
|
||||
getWeightUnit,
|
||||
} from '@/utils/productDealMethod'
|
||||
import { toJalaliNotime, toTime } from '@/utils/useDateTime'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
item: Transaction
|
||||
variant?: 'mobile' | 'desktop'
|
||||
}>(),
|
||||
{ variant: 'mobile' },
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,188 @@
|
||||
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 {
|
||||
BOURSECU_TRANSACTION_TABS,
|
||||
type BoursecuTradeSide,
|
||||
type BoursecuTransactionTab,
|
||||
type BoursecuTransactionTabId,
|
||||
} from '../config/boursecuTabs'
|
||||
import {
|
||||
getActiveBoursecuTab,
|
||||
initTransactionsPageData,
|
||||
loadMoreTransactions,
|
||||
reloadTransactionsTable,
|
||||
resetTransactionsPagination,
|
||||
transactionActiveTabId,
|
||||
transactionDateFilter,
|
||||
transactionItems,
|
||||
transactionPagination,
|
||||
transactionStatusFilter,
|
||||
transactionTableOutput,
|
||||
transactionTradeSide,
|
||||
transactionsBoursecuMode,
|
||||
} from './useTransactionsPageData'
|
||||
|
||||
export interface TransactionsPageContext {
|
||||
pagination: typeof transactionPagination
|
||||
items: typeof transactionItems
|
||||
tableOutput: typeof transactionTableOutput
|
||||
showFilter: ReturnType<typeof ref<boolean>>
|
||||
dateFilter: typeof transactionDateFilter
|
||||
statusFilter: typeof transactionStatusFilter
|
||||
mobileObserverEl: ReturnType<typeof ref<HTMLElement | null>>
|
||||
desktopObserverEl: ReturnType<typeof ref<HTMLElement | null>>
|
||||
activeTabId: typeof transactionActiveTabId
|
||||
tradeSide: typeof transactionTradeSide
|
||||
activeTab: ReturnType<typeof computed<BoursecuTransactionTab>>
|
||||
emptyText: ReturnType<typeof computed<string>>
|
||||
resetPagination: typeof resetTransactionsPagination
|
||||
initTable: () => Promise<void>
|
||||
reloadTable: typeof reloadTransactionsTable
|
||||
applyFilter: () => Promise<void>
|
||||
setActiveTab: (tabId: BoursecuTransactionTabId) => Promise<void>
|
||||
setTradeSide: (side: BoursecuTradeSide) => Promise<void>
|
||||
applyInlineFilters: () => Promise<void>
|
||||
bindMobileObserver: (el: Element | ComponentPublicInstance | null) => void
|
||||
bindDesktopObserver: (el: Element | ComponentPublicInstance | null) => void
|
||||
}
|
||||
|
||||
export const TRANSACTIONS_PAGE_KEY: InjectionKey<TransactionsPageContext> =
|
||||
Symbol('transactionsPage')
|
||||
|
||||
export function useTransactionsPageProvider(options?: { boursecu?: boolean }) {
|
||||
const isBoursecu = options?.boursecu ?? false
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
const showFilter = ref(false)
|
||||
const mobileObserverEl = ref<HTMLElement | null>(null)
|
||||
const desktopObserverEl = ref<HTMLElement | null>(null)
|
||||
|
||||
const activeObserverEl = computed(() =>
|
||||
isDesktop.value ? desktopObserverEl.value : mobileObserverEl.value,
|
||||
)
|
||||
|
||||
const activeTab = computed<BoursecuTransactionTab>(
|
||||
() =>
|
||||
BOURSECU_TRANSACTION_TABS.find((tab) => tab.id === transactionActiveTabId.value) ??
|
||||
BOURSECU_TRANSACTION_TABS[0],
|
||||
)
|
||||
|
||||
const emptyText = computed(() => {
|
||||
if (!transactionsBoursecuMode.value) {
|
||||
return 'تراکنشی موجود نیست'
|
||||
}
|
||||
|
||||
if (activeTab.value.id === 'trades' && transactionTradeSide.value === 'sell') {
|
||||
return 'معامله فروشی برای شما به ثبت نرسیده است'
|
||||
}
|
||||
|
||||
return activeTab.value.emptyText
|
||||
})
|
||||
|
||||
async function initTable() {
|
||||
await reloadTransactionsTable()
|
||||
}
|
||||
|
||||
async function applyFilter() {
|
||||
await reloadTransactionsTable()
|
||||
showFilter.value = false
|
||||
}
|
||||
|
||||
async function setActiveTab(tabId: BoursecuTransactionTabId) {
|
||||
if (transactionActiveTabId.value === tabId) {
|
||||
return
|
||||
}
|
||||
|
||||
transactionActiveTabId.value = tabId
|
||||
await reloadTransactionsTable()
|
||||
}
|
||||
|
||||
async function setTradeSide(side: BoursecuTradeSide) {
|
||||
if (transactionTradeSide.value === side) {
|
||||
return
|
||||
}
|
||||
|
||||
transactionTradeSide.value = side
|
||||
if (getActiveBoursecuTab().showTradeToggle) {
|
||||
await reloadTransactionsTable()
|
||||
}
|
||||
}
|
||||
|
||||
async function applyInlineFilters() {
|
||||
await reloadTransactionsTable()
|
||||
}
|
||||
|
||||
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) {
|
||||
await loadMoreTransactions()
|
||||
}
|
||||
},
|
||||
{
|
||||
root: null,
|
||||
rootMargin: '300px',
|
||||
threshold: 0.01,
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
await initTransactionsPageData(isBoursecu)
|
||||
})
|
||||
|
||||
const context: TransactionsPageContext = {
|
||||
pagination: transactionPagination,
|
||||
items: transactionItems,
|
||||
tableOutput: transactionTableOutput,
|
||||
showFilter,
|
||||
dateFilter: transactionDateFilter,
|
||||
statusFilter: transactionStatusFilter,
|
||||
mobileObserverEl,
|
||||
desktopObserverEl,
|
||||
activeTabId: transactionActiveTabId,
|
||||
tradeSide: transactionTradeSide,
|
||||
activeTab,
|
||||
emptyText,
|
||||
resetPagination: resetTransactionsPagination,
|
||||
initTable,
|
||||
reloadTable: reloadTransactionsTable,
|
||||
applyFilter,
|
||||
setActiveTab,
|
||||
setTradeSide,
|
||||
applyInlineFilters,
|
||||
bindMobileObserver,
|
||||
bindDesktopObserver,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/** @deprecated Use useTransactionsPageProvider in index + useTransactionsPageContext in views. */
|
||||
export function useTransactionsPage(options?: { boursecu?: boolean }) {
|
||||
return useTransactionsPageProvider(options)
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import { ref } from 'vue'
|
||||
import { getTransactionTable, getTransactionTable2 } from '@/api/transaction'
|
||||
import type { TableEngineInput, TableEngineOutput } from '@/models/table'
|
||||
import type { Transaction } from '@/models/transaction'
|
||||
import { loading, loadingMore } from '@/utils'
|
||||
import {
|
||||
BOURSECU_TRANSACTION_TABS,
|
||||
buildBoursecuTransactionPagination,
|
||||
type BoursecuTradeSide,
|
||||
type BoursecuTransactionTab,
|
||||
type BoursecuTransactionTabId,
|
||||
} from '../config/boursecuTabs'
|
||||
|
||||
/** Shared transactions datasets — mobile/desktop views read this module state only. */
|
||||
let initPromise: Promise<void> | null = null
|
||||
const initialized = ref(false)
|
||||
|
||||
export const transactionsBoursecuMode = ref(false)
|
||||
export const transactionPagination = ref<TableEngineInput>({ filters: [], limit: 7, page: 0 })
|
||||
export const transactionItems = ref<Transaction[]>([])
|
||||
export const transactionTableOutput = ref<TableEngineOutput<Transaction> | null>(null)
|
||||
export const transactionDateFilter = ref<string>()
|
||||
export const transactionStatusFilter = ref<string>()
|
||||
export const transactionActiveTabId = ref<BoursecuTransactionTabId>('trades')
|
||||
export const transactionTradeSide = ref<BoursecuTradeSide>('buy')
|
||||
|
||||
export function isTransactionsPageDataInitialized() {
|
||||
return initialized.value
|
||||
}
|
||||
|
||||
export function getActiveBoursecuTab(): BoursecuTransactionTab {
|
||||
return (
|
||||
BOURSECU_TRANSACTION_TABS.find((tab) => tab.id === transactionActiveTabId.value) ??
|
||||
BOURSECU_TRANSACTION_TABS[0]
|
||||
)
|
||||
}
|
||||
|
||||
export function resetTransactionsPagination() {
|
||||
if (transactionsBoursecuMode.value) {
|
||||
transactionPagination.value = buildBoursecuTransactionPagination(
|
||||
getActiveBoursecuTab(),
|
||||
transactionTradeSide.value,
|
||||
transactionDateFilter.value,
|
||||
transactionStatusFilter.value,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
transactionPagination.value = { filters: [], limit: 7, page: 0 }
|
||||
if (transactionDateFilter.value) {
|
||||
transactionPagination.value.filters = [
|
||||
{
|
||||
field: 'created_at',
|
||||
cr: 'and',
|
||||
type: 'date',
|
||||
op: 'eq',
|
||||
value: transactionDateFilter.value,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchTransactionsPage(append: boolean) {
|
||||
const isFirstPage = transactionPagination.value.page === 0 && !append
|
||||
|
||||
if (isFirstPage) {
|
||||
loading.value = true
|
||||
} else {
|
||||
loadingMore.value = true
|
||||
}
|
||||
|
||||
try {
|
||||
const activeTab = getActiveBoursecuTab()
|
||||
const fetcher =
|
||||
transactionsBoursecuMode.value && activeTab.fetchMode === 'deposit'
|
||||
? getTransactionTable2
|
||||
: getTransactionTable
|
||||
|
||||
transactionTableOutput.value = await fetcher(transactionPagination.value)
|
||||
const rows = transactionTableOutput.value.rows ?? []
|
||||
|
||||
if (isFirstPage) {
|
||||
transactionItems.value = [...rows]
|
||||
} else {
|
||||
transactionItems.value.push(...rows)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching transactions:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
export async function reloadTransactionsTable() {
|
||||
transactionPagination.value.page = 0
|
||||
transactionItems.value = []
|
||||
resetTransactionsPagination()
|
||||
await fetchTransactionsPage(false)
|
||||
}
|
||||
|
||||
export async function loadMoreTransactions() {
|
||||
if (
|
||||
!transactionTableOutput.value
|
||||
|| (transactionPagination.value.page + 1) * transactionPagination.value.limit
|
||||
>= transactionTableOutput.value.query.total
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
transactionPagination.value.page++
|
||||
await fetchTransactionsPage(true)
|
||||
}
|
||||
|
||||
export async function initTransactionsPageData(boursecu: boolean, force = false) {
|
||||
if (!force && initialized.value && transactionsBoursecuMode.value === boursecu) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!force && initPromise) {
|
||||
return initPromise
|
||||
}
|
||||
|
||||
initPromise = (async () => {
|
||||
try {
|
||||
if (force) {
|
||||
resetTransactionsPageData()
|
||||
}
|
||||
|
||||
transactionsBoursecuMode.value = boursecu
|
||||
transactionPagination.value.page = 0
|
||||
transactionItems.value = []
|
||||
resetTransactionsPagination()
|
||||
await fetchTransactionsPage(false)
|
||||
initialized.value = true
|
||||
} catch (error) {
|
||||
console.error('Transactions page initialization error:', error)
|
||||
} finally {
|
||||
initPromise = null
|
||||
}
|
||||
})()
|
||||
|
||||
return initPromise
|
||||
}
|
||||
|
||||
export function resetTransactionsPageData() {
|
||||
initialized.value = false
|
||||
initPromise = null
|
||||
transactionsBoursecuMode.value = false
|
||||
transactionPagination.value = { filters: [], limit: 7, page: 0 }
|
||||
transactionItems.value = []
|
||||
transactionTableOutput.value = null
|
||||
transactionDateFilter.value = undefined
|
||||
transactionStatusFilter.value = undefined
|
||||
transactionActiveTabId.value = 'trades'
|
||||
transactionTradeSide.value = 'buy'
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { TableEngineInput } from '@/models/table'
|
||||
|
||||
export type BoursecuTransactionTabId =
|
||||
| 'trades'
|
||||
| 'deposit'
|
||||
| 'withdrawal'
|
||||
| 'physical'
|
||||
|
||||
export type BoursecuTradeSide = 'buy' | 'sell'
|
||||
|
||||
export type BoursecuTransactionFetchMode = 'transaction' | 'deposit'
|
||||
|
||||
export interface BoursecuTransactionTab {
|
||||
id: BoursecuTransactionTabId
|
||||
label: string
|
||||
emptyText: string
|
||||
showTradeToggle?: boolean
|
||||
fetchMode?: BoursecuTransactionFetchMode
|
||||
typeFilters?: number[]
|
||||
}
|
||||
|
||||
export const BOURSECU_TRANSACTION_TABS: BoursecuTransactionTab[] = [
|
||||
{
|
||||
id: 'trades',
|
||||
label: 'همه',
|
||||
emptyText: 'معاملهای برای شما به ثبت نرسیده است',
|
||||
showTradeToggle: true,
|
||||
typeFilters: [1, 2],
|
||||
},
|
||||
{
|
||||
id: 'deposit',
|
||||
label: 'واریز',
|
||||
emptyText: 'واریزی برای شما به ثبت نرسیده است',
|
||||
fetchMode: 'deposit',
|
||||
},
|
||||
{
|
||||
id: 'withdrawal',
|
||||
label: 'برداشت',
|
||||
emptyText: 'برداشتی برای شما به ثبت نرسیده است',
|
||||
typeFilters: [4],
|
||||
},
|
||||
{
|
||||
id: 'physical',
|
||||
label: 'تحویل فیزیکی',
|
||||
emptyText: 'درخواست تحویل فیزیکی برای شما ثبت نشده است',
|
||||
typeFilters: [9],
|
||||
},
|
||||
]
|
||||
|
||||
const TRADE_TYPE_BY_SIDE: Record<BoursecuTradeSide, number> = {
|
||||
buy: 1,
|
||||
sell: 2,
|
||||
}
|
||||
|
||||
export function buildBoursecuTransactionPagination(
|
||||
tab: BoursecuTransactionTab,
|
||||
tradeSide: BoursecuTradeSide,
|
||||
dateFilter?: string,
|
||||
statusFilter?: string
|
||||
): TableEngineInput {
|
||||
const filters: TableEngineInput['filters'] = []
|
||||
|
||||
if (tab.fetchMode !== 'deposit' && tab.typeFilters?.length) {
|
||||
const typeValue =
|
||||
tab.showTradeToggle && tab.typeFilters.includes(TRADE_TYPE_BY_SIDE[tradeSide])
|
||||
? TRADE_TYPE_BY_SIDE[tradeSide]
|
||||
: tab.typeFilters[0]
|
||||
|
||||
filters.push({
|
||||
field: 'type',
|
||||
cr: 'and',
|
||||
type: 'number',
|
||||
op: 'eq',
|
||||
value: typeValue,
|
||||
})
|
||||
}
|
||||
|
||||
if (dateFilter) {
|
||||
filters.push({
|
||||
field: 'created_at',
|
||||
cr: 'and',
|
||||
type: 'date',
|
||||
op: 'eq',
|
||||
value: dateFilter,
|
||||
})
|
||||
}
|
||||
|
||||
if (statusFilter === 'active' || statusFilter === 'inactive') {
|
||||
filters.push({
|
||||
field: 'active',
|
||||
cr: 'and',
|
||||
type: 'boolean',
|
||||
op: 'eq',
|
||||
value: statusFilter === 'active',
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
page: 0,
|
||||
limit: 7,
|
||||
filters,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div class="dtxn">
|
||||
<div class="dtxn__container">
|
||||
<header class="dtxn-topbar">
|
||||
<p class="dtxn-topbar__eyebrow">گزارشها</p>
|
||||
<h1 class="dtxn-topbar__title">تراکنشها</h1>
|
||||
</header>
|
||||
|
||||
<div class="dtxn-toolbar">
|
||||
<div class="dtxn-tabs">
|
||||
<button
|
||||
v-for="tab in BOURSECU_TRANSACTION_TABS"
|
||||
:key="tab.id"
|
||||
type="button"
|
||||
class="dtxn-tabs__btn"
|
||||
:class="{ 'dtxn-tabs__btn--active': activeTabId === tab.id }"
|
||||
@click="setActiveTab(tab.id)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab.showTradeToggle" class="dtxn-toggle">
|
||||
<button
|
||||
type="button"
|
||||
class="dtxn-toggle__btn"
|
||||
:class="{ 'dtxn-toggle__btn--active': tradeSide === 'buy' }"
|
||||
@click="setTradeSide('buy')"
|
||||
>
|
||||
خرید
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="dtxn-toggle__btn"
|
||||
:class="{ 'dtxn-toggle__btn--active': tradeSide === 'sell' }"
|
||||
@click="setTradeSide('sell')"
|
||||
>
|
||||
فروش
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dtxn-filters">
|
||||
<div class="dtxn-filters__label">
|
||||
<BasicIcon icon="vuesax-linear:filter" size="16" />
|
||||
<span>فیلترها:</span>
|
||||
</div>
|
||||
<a-select
|
||||
v-model:value="statusFilter"
|
||||
allow-clear
|
||||
class="dtxn-filters__select"
|
||||
placeholder="وضعیت"
|
||||
size="large"
|
||||
@change="applyInlineFilters"
|
||||
>
|
||||
<a-select-option value="active">فعال</a-select-option>
|
||||
<a-select-option value="inactive">غیرفعال</a-select-option>
|
||||
</a-select>
|
||||
<BaseDatePicker
|
||||
v-model:value="dateFilter"
|
||||
class="dtxn-filters__date"
|
||||
placeholder="تاریخ"
|
||||
size="large"
|
||||
@onchange="applyInlineFilters"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div ref="listRef" :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="!loading && items.length === 0" class="dtxn-empty">
|
||||
<img :src="emptyImage" alt="" class="dtxn-empty__image" />
|
||||
<p class="dtxn-empty__text">{{ emptyText }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { loading, loadingMore } from '@/utils'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import BaseDatePicker from '@/components/BasicPacks/BaseDatePicker/src/BaseDatePicker.vue'
|
||||
import ListAnim from '@/components/ListAnim.vue'
|
||||
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
|
||||
import emptyImage from '@/assets/img/cash_empty.svg'
|
||||
import Item from '../components/Item.vue'
|
||||
import { BOURSECU_TRANSACTION_TABS } from '../config/boursecuTabs'
|
||||
import { useTransactionsPageContext } from '../composables/useTransactionsPage'
|
||||
|
||||
const listRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const {
|
||||
items,
|
||||
dateFilter,
|
||||
statusFilter,
|
||||
activeTabId,
|
||||
tradeSide,
|
||||
activeTab,
|
||||
emptyText,
|
||||
setActiveTab,
|
||||
setTradeSide,
|
||||
applyInlineFilters,
|
||||
bindDesktopObserver,
|
||||
} = useTransactionsPageContext()
|
||||
</script>
|
||||
|
||||
<style lang="less" src="./styles/desktop-transactions.less"></style>
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="dtxn">
|
||||
<div class="dtxn__container">
|
||||
<header class="dtxn-topbar">
|
||||
<p class="dtxn-topbar__eyebrow">گزارشها</p>
|
||||
<h1 class="dtxn-topbar__title">لیست تراکنشها</h1>
|
||||
</header>
|
||||
|
||||
<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 ref="el" :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 { ref } from 'vue'
|
||||
import { loading, loadingMore } from '@/utils'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.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 el = ref<HTMLElement | null>(null)
|
||||
|
||||
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-topbar {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
|
||||
|
||||
&__eyebrow {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #9ca3af;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.03em;
|
||||
color: #111827;
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
&__label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
&__select,
|
||||
&__date {
|
||||
min-width: 140px;
|
||||
}
|
||||
}
|
||||
|
||||
.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,41 @@
|
||||
<template>
|
||||
<div >
|
||||
<!-- <teleport to="body">-->
|
||||
<!-- <div 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-10 "
|
||||
|
||||
>
|
||||
<!-- <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 v-if="isBoursecu">
|
||||
<template #mobile>
|
||||
<BoursecuTransactionsPage />
|
||||
</template>
|
||||
<template #desktop>
|
||||
<DesktopBoursecuTransactionsView />
|
||||
</template>
|
||||
</ResponsiveViewPair>
|
||||
<ResponsiveViewPair v-else>
|
||||
<template #mobile>
|
||||
<DefaultTransactionsPage />
|
||||
</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 { isBoursecuProject } from '@/utils/config'
|
||||
import { useTransactionsPageProvider } from './composables/useTransactionsPage'
|
||||
import BoursecuTransactionsPage from './BoursecuTransactionsPage.vue'
|
||||
import DefaultTransactionsPage from './DefaultTransactionsPage.vue'
|
||||
import DesktopBoursecuTransactionsView from './desktop/DesktopBoursecuTransactionsView.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)
|
||||
const isBoursecu = isBoursecuProject()
|
||||
|
||||
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({ boursecu: isBoursecu })
|
||||
</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: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user