1405/04/27

-responive bugs
This commit is contained in:
2026-07-18 15:46:38 +03:30
parent c2a355f2de
commit 37d86ba36e
39 changed files with 1910 additions and 404 deletions
+19 -35
View File
@@ -33,57 +33,41 @@
</div>
</div>
<a-divider class="!my-3" />
<BasicActionSheet v-model:show="infoVisible">
<BasicActionSheet v-if="!isDesktop" v-model:show="infoVisible">
<div class="font-bold text-lg">{{ props.item.title }}</div>
<div class="pb-20 mt-5">
<div class="flex justify-between mb-3">
<div class="text-black">مقدار {{ props.title }} مبادله شده:</div>
<div
:class="item.gold >= 0 ? 'text-green-600' : 'text-red-600'"
class="flex text-xs items-center"
>
<div class="text-base ml-1 number-ltr">{{ formatGoldAmount(item.gold) }}</div>
{{ item.unit }}
</div>
</div>
<div class="flex justify-between mb-3">
<div class="text-black">موجودی {{ props.title }}:</div>
<div class="flex text-xs text-yellow-700 items-center">
<div class="text-base ml-1">{{ formatGoldAmount(item.balance_gold) }}</div>
{{ item.unit }}
</div>
</div>
<div class="flex justify-between mb-3">
<div class="text-black">وضعیت:</div>
<div class="flex text-xs items-center">
<ATag :color="item.active ? 'blue' : 'red'" class="!py-1 !px-6 !ml-0">
{{ item.active ? 'موفق' : 'نا موفق' }}
</ATag>
</div>
</div>
<div class="flex justify-between mb-3">
<div class="text-black">تاریخ:</div>
<div>{{ toJalaliNotime(item.expire_at) }}</div>
</div>
<div class="flex justify-between mb-3">
<div class="text-black">زمان:</div>
<div>{{ toTime(item.expire_at) }}</div>
</div>
<TransactionDetailContent :item="item" :title="props.title" />
<BasicButton class="w-full mt-5" @click="infoVisible = false">بازگشت</BasicButton>
</div>
</BasicActionSheet>
<BasicModal
v-else
v-model:visible="infoVisible"
:title="props.item.title"
width="440px"
custom-footer
>
<TransactionDetailContent class="mt-2" :item="item" :title="props.title" />
<template #footer>
<BasicButton class="w-full" @click.stop="infoVisible = false">بازگشت</BasicButton>
</template>
</BasicModal>
</div>
</template>
<script lang="ts" setup>
import Icon from '@/components/BasicPacks/BasicIcon.vue'
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 { formatGoldAmount } from '@/utils/usePrice'
import { toJalaliNotime, toTime } from '@/utils/useDateTime'
import { toJalaliNotime } from '@/utils/useDateTime'
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
import TransactionDetailContent from './TransactionDetailContent.vue'
const isDesktop = useDesktopBreakpoint()
const infoVisible = ref<boolean>(false)
interface Prop {
@@ -0,0 +1,48 @@
<template>
<div>
<div class="flex justify-between mb-3">
<div class="text-black">مقدار {{ title }} مبادله شده:</div>
<div
:class="item.gold >= 0 ? 'text-green-600' : 'text-red-600'"
class="flex text-xs items-center"
>
<div class="text-base ml-1 number-ltr">{{ formatGoldAmount(item.gold) }}</div>
{{ item.unit }}
</div>
</div>
<div class="flex justify-between mb-3">
<div class="text-black">موجودی {{ title }}:</div>
<div class="flex text-xs text-yellow-700 items-center">
<div class="text-base ml-1">{{ formatGoldAmount(item.balance_gold) }}</div>
{{ item.unit }}
</div>
</div>
<div class="flex justify-between mb-3">
<div class="text-black">وضعیت:</div>
<div class="flex text-xs items-center">
<ATag :color="item.active ? 'blue' : 'red'" class="!py-1 !px-6 !ml-0">
{{ item.active ? 'موفق' : 'نا موفق' }}
</ATag>
</div>
</div>
<div class="flex justify-between mb-3">
<div class="text-black">تاریخ:</div>
<div>{{ toJalaliNotime(item.expire_at) }}</div>
</div>
<div class="flex justify-between mb-3">
<div class="text-black">زمان:</div>
<div>{{ toTime(item.expire_at) }}</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { Transaction } from '@/models/transaction'
import { formatGoldAmount } from '@/utils/usePrice'
import { toJalaliNotime, toTime } from '@/utils/useDateTime'
defineProps<{
item: Transaction
title?: string
}>()
</script>
@@ -0,0 +1,126 @@
/* Desktop standalone gold trade page */
.gold-page {
height: 100%;
min-height: 0;
width: 100%;
max-width: 100%;
box-sizing: border-box;
overflow-x: hidden;
}
.gold-embed {
max-height: min(70vh, 35rem);
overflow-x: hidden;
overflow-y: auto;
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.gold-form-card {
min-height: 0;
}
.gold-fee-box {
margin-top: 1rem;
}
.dgold {
min-height: 100%;
background: #f6f7f9;
color: #111827;
padding: 28px 36px 40px;
}
.dgold-topbar {
display: flex;
align-items: flex-start;
gap: 16px;
max-width: 720px;
margin: 0 auto 24px;
padding-bottom: 24px;
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
&__back {
display: inline-flex;
align-items: center;
gap: 8px;
border: none;
background: transparent;
color: #6b7280;
font-size: 13px;
font-weight: 500;
font-family: inherit;
cursor: pointer;
padding: 8px 0;
flex-shrink: 0;
&:hover {
color: #111827;
}
}
&__meta {
flex: 1;
min-width: 0;
}
&__eyebrow {
font-size: 12px;
font-weight: 500;
color: #9ca3af;
margin: 0 0 4px;
}
&__title {
font-size: 26px;
font-weight: 600;
letter-spacing: -0.03em;
color: #111827;
margin: 0;
line-height: 1.2;
}
}
.dgold__panel {
max-width: 720px;
margin: 0 auto;
background: #fff;
border: 1px solid rgba(15, 23, 42, 0.06);
border-radius: 16px;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
overflow-y: auto;
max-height: calc(100vh - 180px);
padding: 0 24px 16px;
box-sizing: border-box;
}
.dgold__panel .mb-25vh {
margin-bottom: 2rem !important;
}
.dgold__panel .gold-fee-box {
position: static !important;
margin-top: 1rem;
}
@media screen and (max-width: 900px) {
.dgold {
padding: 16px;
}
.dgold-topbar {
gap: 12px;
margin-bottom: 16px;
padding-bottom: 16px;
}
.dgold-topbar__title {
font-size: 20px;
}
.dgold__panel {
max-height: none;
}
}
+310 -196
View File
@@ -1,31 +1,54 @@
<template>
<div class="h-35rem overflow-y-auto px-5">
<div class=" !overflow-y-scroll bg-white py-3 text-primary font-bold flex justify-evenly">
<div :class="rootClass">
<header v-if="isStandaloneDesktop" class="dgold-topbar">
<button type="button" class="dgold-topbar__back" @click="router.back()">
<BasicIcon icon="vuesax-linear:arrow-right-1" size="18" />
بازگشت
</button>
<div class="dgold-topbar__meta">
<p class="dgold-topbar__eyebrow">معامله</p>
<h1 class="dgold-topbar__title">خرید و فروش {{ tradeProductLabel }}</h1>
</div>
</header>
<MobilePageHeader
v-else-if="isStandaloneMobile"
:title="`خرید و فروش ${tradeProductLabel}`"
show-back
/>
<div :class="panelClass">
<div
:class="[
'!overflow-y-scroll bg-white py-3 text-primary font-bold flex justify-evenly',
isStandaloneMobile ? 'mpage-subheader wrap-fix' : '',
]"
>
<!-- <div @click="isBuy = false" class="w-full text-center" :class="!isBuy?'text-primary':'text-gray-400'">خرید طلا</div>
<div @click="isBuy = true" class="w-full text-center" :class="isBuy?'text-primary':'text-gray-400'">فروش طلا</div>-->
<div class="flex justify-evenly w-full px-2 h-11 items-center bg-gray-50 relative rounded ">
<div :class="isBuy?'text-primary':'text-gray-400'" class="w-full text-center z-2"
@click.stop="setIsBuy(true) ">
خرید طلا {{ props?.ProductItem?.title }}
<div :class="isBuy?'text-tertiary':'text-gray-400'" class="w-full text-center z-2"
@click.stop="setIsBuy(true) "
:style="!isProductBuyOpen(props?.ProductItem) ? 'pointer-events: none; opacity: 0.5;' : ''">
خرید {{ tradeProductLabel }}
</div>
<div :class="!isBuy?'text-primary':'text-gray-400'" class="w-full text-center z-2"
@click.stop="setIsBuy(false) ">
فروش طلا {{ props?.ProductItem?.title }}
<div :class="!isBuy?'text-tertiary':'text-gray-400'" class="w-full text-center z-2"
@click.stop="setIsBuy(false) "
:style="!isProductSellOpen(props?.ProductItem) ? 'pointer-events: none; opacity: 0.5;' : ''">
فروش {{ tradeProductLabel }}
</div>
<!-- <div ref="bganim" class="absolute bg-white h-8 w-1/2 rounded-lg " :style="`transform: translateX(${isbuy? '0':'-100%'}); left: ${isbuy? '0':'100%'}`"></div>-->
<div ref="bganim" :style="useStyle()"
class="absolute bg-white h-8 w-1/2 rounded-lg animate right"></div>
</div>
</div>
<div v-if="userStore" class=" mt-5 mb-25vh pb-2">
<div v-if="userStore" :class="contentClass">
<a-spin :spinning="!price">
<div v-if="!loading" :class="!isBuy? ' bg-red-100' : ' bg-green-100'"
class="w-full relative rounded-lg h-[88px] flex flex-col justify-center background-color-transition">
<img :src="img1" alt="" class="h-[88px] absolute">
<img :src="img1" alt="" class="h-[88px] absolute inset-0 w-full object-cover">
<div class="w-full items-center flex flex-col">
<div class="text-gray-700 font-bold">{{ !isBuy ? 'قیمت فروش' : 'قیمت خرید' }}</div>
<div v-if="price" class="flex gap-1 mt-3 text-xs items-center">
<div :class="!isBuy ? 'text-red-700' : 'text-green-700'" class="font-bold text-2xl color-transition">
<div :class="!isBuy? 'text-red-700' : 'text-green-700'" class="font-bold text-2xl color-transition">
{{ usePriceWithoutType(goldPriceValue) }}
</div>
ریال
@@ -37,50 +60,58 @@
</div>
</a-spin>
<a-form :model="form" class="!mt-5" name="basic" @finish="okConfirm()">
<div class="border-gray-200 border-1 rounded-xl p-5 h-58 relative">
<bg_main/>
<div class="border-gray-200 border-1 rounded-xl p-5 gold-form-card relative">
<bg_main />
<div class="z-100 relative">
<a-form-item>
<BasicInput v-model:value="form.goldCash" addon-before="ریال" addon-bg-color="#a72b77"
<BasicInput v-model:value="form.goldCash" addon-before="ریال" :addon-bg-color="colors.primary"
addon-text-color="white"
place-holder="ارزش کل"
size="large"
type="price" @onchange="calculateGold" />
</a-form-item>
<a-form-item :rules="[justNumber()]">
<BasicInput v-model:value="form.goldValue" addon-before="گرم" addon-bg-color="#a72b77"
<BasicInput v-model:value="form.goldValue" :addon-before="weightUnit" :addon-bg-color="colors.primary"
addon-text-color="white"
place-holder="مقدار طلا"
:place-holder="amountFieldLabel"
size="large" type="text" inputmode="decimal" @onchange="calculateCash" />
</a-form-item>
<div v-if="cashValidationMessage" class="text-red-700 text-xs font-bold mt-2">
{{ cashValidationMessage }}
</div>
<!-- {{walletStore?.walletInfo?.gold}}-->
<Transition mode="out-in" name="fade">
<div v-if="!isBuy" class="font-bold text-xs">
<div> {{ `موجودی کیف طلا: ${formatGoldAmount(walletStore?.walletInfo?.gold ?? 0)} گرم` }}</div>
<div>{{ productWalletBalanceText }}</div>
<!-- <div v-if="walletStore?.walletInfo?.gold_block !== 0">
{{ `موجودی قابل برداشت طلا: ${walletStore?.walletInfo?.gold_block ?? 0} گرم` }}
</div>-->
</div>
<div v-else class="font-bold text-xs">
{{ `موجودی کیف پول: ${usePriceWithoutType(walletStore?.walletInfo?.cash ?? 0)} ریال` }}
{{ formatCashWalletBalanceText(usePriceWithoutType(cashWalletBalance), props?.ProductItem?.group_type) }}
</div>
</Transition>
<Transition mode="out-in" name="fade">
<a-form-item v-if="walletStore?.walletInfo" class="!mt-3 ">
<!-- {{ isBuy }}-->
<a-slider v-model:value="form.percentWallet"
:disabled="isBuy? walletStore?.walletInfo?.cash <= 0: false"
:max="100"
:min="0"
:step="10"
:tip-formatter="null"
class="!my-1"
@change="percentChange" />
<div
class="app-touch-slider"
@touchstart.stop
@pointerdown.stop
>
<a-slider v-model:value="form.percentWallet"
:disabled="isBuy? cashWalletBalance <= 0: false"
:max="100"
:min="0"
:step="10"
:tip-formatter="null"
class="!my-1"
@change="percentChange" />
</div>
</a-form-item>
</Transition>
</div>
</div>
<GoldLoading v-model:show="infoVisible" />
<a-form-item>
<BasicButton
v-if="walletStore"
@@ -94,31 +125,28 @@
<div v-else>{{ !isBuy ? 'فروش' : 'خرید' }}</div>
</div>
</BasicButton>
<div v-if="!isBuy && sellGoldDebit > (walletStore?.walletInfo?.gold ?? 0)"
<div v-if="!isBuy && sellGoldDebit > productWalletBalance"
class="text-red-700 text-xs font-bold mt-2">
مقدار گرم طلای وارد شده جهت فروش ({{ form.goldValue }}گرم) بیشتر از مقدار موجودی شما
({{ walletStore?.walletInfo?.gold }}گرم) می باشد.
<span v-if="feeDeductFromGold"> (با احتساب {{ feeInGrams }} گرم کارمزد، مجموعاً {{ sellGoldDebit }} گرم کسر می‌شود)</span>
مقدار {{ weightUnit }} وارد شده جهت فروش ({{ form.goldValue }}{{ weightUnit }}) بیشتر از مقدار موجودی شما
({{ productWalletBalance }}{{ weightUnit }}) می باشد.
<span v-if="feeDeductsFromGold"> (با احتساب {{ feeInGrams }}{{ weightUnit }} کارمزد، مجموعاً {{ sellGoldDebit }}{{ weightUnit }} کسر می‌شود)</span>
</div>
<!-- <div>{{form?.goldValue? true: false}}</div>-->
<div v-if="isBuy? ((form?.goldValue || form.goldValue === 0)? form?.goldCash < 500000: false) : false"
<div v-if="isBuy ? (payablePrice > cashWalletBalance) : false"
class="text-red-700 text-xs font-bold mt-2">
حداقل مقدار طلا جهت خرید 500,000 ریال می باشد.
موجودی کیف پول ناکافی می باشد.
</div>
</a-form-item>
<div class=" w-full bottom-18 px-6 rounded-xl py-1 border-1 bg-white">
<!-- <BasicInfoItem is-line title="کارمزد" :value="parseFloat(((form.goldValue?? 0)*0.005).toFixed(3))" />-->
<!-- <BasicInfoItem is-line title="کارمزد" :value="parseFloat(((form.goldValue?? 0)*0.005).toFixed(3))" />-->
<div class="w-full px-4 rounded-xl py-2 border-1 bg-white gold-fee-box">
<div class="flex justify-between items-center">
<div>کارمزد:</div>
<div class="flex gap-2 items-center">
<div class="">{{ feeInGrams }}</div>
<!-- <div class="font-bold text-2xl">{{ 0 }}</div>-->
<div>
گرم
</div>
<div>کارمزد ({{ feeDeductsFromGold ? `از طلا` : `از ریال` }}):</div>
<div class="flex gap-2 items-center font-bold">
<div>{{ feeDisplayText }}</div>
</div>
</div>
<div class="text-xs text-gray-600 mt-1">
{{ feeFormHint }}
</div>
</div>
</a-form>
<!-- <div ref="el" :class="loading? 'h-screen': ''"
@@ -132,56 +160,59 @@
</div>-->
</div>
<BasicActionSheet v-model:show="confirmAction">
<BasicActionSheet v-if="!isStandaloneDesktop" v-model:show="confirmAction">
<div class="font-bold text-lg"></div>
<div class="w-full rounded-lg bg-gray-50 p-3">
<div class="flex justify-between ">
<div>وزن:</div>
<div class="flex items-center">
<div class="font-bold">{{ displayTradeGoldAmount }}</div>
<div class="text-xs mr-1">گرم</div>
</div>
</div>
<div class="flex justify-between mt-3">
<div>مظنه:</div>
<div class="flex items-center">
<div class="font-bold">{{ usePriceWithoutType(Math.round(goldPriceValue)) }}</div>
<div class="text-xs mr-1">ریال</div>
</div>
</div>
<div class="flex justify-between mt-3">
<div>کارمزد:</div>
<div class="flex items-center gap-2">
<div class="font-bold text-xl">{{ feeInGrams }}</div>
<div class="text-xs mr-1">گرم</div>
</div>
</div>
<div >(معادل {{ equivalentPrice }} ریال)</div>
<div v-if="isBuy && feeDeductFromGold" class="flex justify-between mt-3">
<div>طلا دریافتی:</div>
<div class="flex items-center">
<div class="font-bold">{{ displayTradeGoldAmount }}</div>
<div class="text-xs mr-1">گرم (دقیق — کارمزد از ریال)</div>
</div>
</div>
<div v-if="!isBuy && feeDeductFromGold" class="flex justify-between mt-3">
<div>کسر از کیف طلا:</div>
<div class="flex items-center">
<div class="font-bold">{{ sellGoldDebit }}</div>
<div class="text-xs mr-1">گرم</div>
</div>
</div>
<div class="flex justify-between mt-3">
<div>{{ isBuy ? 'قیمت قابل پرداخت:' : 'مبلغ دریافتی:' }}</div>
<div class="flex items-center">
<div class="font-bold">{{ usePriceWithoutType(payablePrice.toFixed(0)) }}</div>
<div class="text-xs mr-1">ریال</div>
</div>
</div>
</div>
<BasicButton class="mb-20 mt-5 w-full" type="primary" @click="ok(); confirmAction= false">ثبت نهایی</BasicButton>
<TradeFeeConfirmSummary
:is-buy="isBuy"
:input-lock="inputLock"
:display-trade-gold-amount="displayTradeGoldAmount"
:weight-unit="weightUnit"
:unit-price-rial="goldPriceValue"
:fee-in-grams="feeInGrams"
:equivalent-price="equivalentPrice"
:payable-price="payablePrice"
:order-gold-amount="orderGoldAmount"
:fee-in-rial="feeInRial"
:sell-gold-debit="sellGoldDebit"
/>
<BasicButton
class="mb-20 mt-5 w-full"
type="primary"
:disabled="loading"
@click="ok"
>
ثبت نهایی
</BasicButton>
</BasicActionSheet>
<BasicModal
v-else
v-model:visible="confirmAction"
title="تأیید سفارش"
width="480px"
destroy-on-close
custom-footer
>
<TradeFeeConfirmSummary
:is-buy="isBuy"
:input-lock="inputLock"
:display-trade-gold-amount="displayTradeGoldAmount"
:weight-unit="weightUnit"
:unit-price-rial="goldPriceValue"
:fee-in-grams="feeInGrams"
:equivalent-price="equivalentPrice"
:payable-price="payablePrice"
:order-gold-amount="orderGoldAmount"
:fee-in-rial="feeInRial"
:sell-gold-debit="sellGoldDebit"
/>
<BasicButton class="mt-5 w-full" type="primary" :disabled="loading" @click="ok">
ثبت نهایی
</BasicButton>
<template #footer>
<div></div>
</template>
</BasicModal>
<GoldLoading v-model:show="infoVisible" />
<van-dialog v-model:show="noCompleteVisible" class="completeButtonStyle" confirm-button-text="تکمیل اطلاعات"
show-confirm-button @confirm="router.push({name: 'user',query:{showEdit: true}})">
<div class="flex flex-col items-center p-2">
@@ -194,14 +225,15 @@
</div>
</div>
</van-dialog>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import img1 from '@/assets/img/home_ly2.svg'
import { usePrice, usePriceWithoutType, formatGoldAmount } from '@/utils/usePrice'
import { usePrice, usePriceWithoutType } from '@/utils/usePrice'
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
import type { Gold } from '@/models/gold'
import { isIphone, loading, loadingMore } from '@/utils'
@@ -211,9 +243,14 @@ import { useWalletStore } from '@/store/walletStore'
import { createOrder } from '@/api/order'
import { message } from 'ant-design-vue'
import type { Price } from '@/models/home'
import { getLastPrice } from '@/api/home'
import Icon from '@/components/BasicPacks/BasicIcon.vue'
import { useUserStore } from '@/store/userStore'
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
import BasicModal from '@/components/BasicPacks/BasicModal.vue'
import TradeFeeConfirmSummary from '@/components/TradeFeeConfirmSummary.vue'
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
import { justNumber } from '@/utils/inputRules'
import GoldSkeleton from '@/views/base/gold/components/GoldSkeleton.vue'
import GoldLoading from '@/components/GoldLoading.vue'
@@ -226,10 +263,11 @@ import type { Transaction } from '@/models/transaction'
import type { TableEngineInput, TableOutput } from '@/models/table'
import { useInfiniteScroll } from '@vueuse/core'
import { getTransactionTable } from '@/api/transaction'
import { getLastPrices, getTradeBuyPrice, getTradeSellPrice, lastPrice, setIntervalGetAllPrice, stopPriceInterval } from '@/utils/price'
import { isStoreClosed, getStoreClosedMessage } from '@/utils/storeHours'
import { getProductBuyPrice, getProductSellPrice } from '@/utils/productPrice'
import { getLastPrices, getTradeBuyPrice, getTradeSellPrice, lastPrice } from '@/utils/price'
import type { Product } from '@/models/products'
import { useFundUserStore } from '@/store/fundStore'
import { colors } from '@/themeConfig'
import {
calcFeeInGrams,
calcGramFromLockedRial,
@@ -237,20 +275,46 @@ import {
calcPayablePriceRial,
calcRialFromLockedGram,
calcSellGoldDebit,
calcSellTargetCash,
calcTradeTargetCash,
feeDeductFromGold,
buildTradeConfirmBreakdown,
formatTradeGramDisplay,
formatTradeGramInputValue,
loadServiceFeeFromSettings,
normalizeOrderGoldAmount,
serviceFeePercentage,
type TradeInputLock,
} from '@/utils/serviceFee'
import Bg_main from '@/components/bg_main.vue'
import { isStoreClosed, getStoreClosedMessage } from '@/utils/storeHours'
import {
isProductBuyOpen,
isProductSellOpen,
isProductTradeOpen,
productTradeClosedMessage,
resolveProductTradeIsBuy,
} from '@/utils/productTradeOpen'
import { formatCashWalletBalanceText, getWeightUnit } from '@/utils/productDealMethod'
import { formatProductWalletBalanceText, getCashWalletBalance, getProductInventoryQuantity } from '@/utils/productAssets'
import { useWalletAssets } from '@/composables/useWalletAssets'
import { usesQuantityTrade } from '@/utils/productCategories'
import { useSettingsStore } from '@/store/settingsStore'
import { storeToRefs } from 'pinia'
import {
productCashValidationMessage,
productMaxOrderCash,
productMinOrderCash
} from '@/utils/productOrderLimit'
const infoVisible = ref(false)
const walletStore = useWalletStore()
const userStore = useUserStore()
const settingsStore = useSettingsStore()
const { meltedGoldLabel } = storeToRefs(settingsStore)
const weightUnit = computed(() => getWeightUnit())
const fundStore = useFundUserStore()
const { assetsSummary, loadWalletAssets } = useWalletAssets()
const cashWalletBalance = computed(() =>
getCashWalletBalance(walletStore.walletInfo, userStore.userInfo?.wallets)
)
const confirmAction = ref<boolean>(false)
const noCompleteVisible = ref<boolean>(false)
const buyItems = ref<Transaction[]>([])
@@ -264,9 +328,47 @@ interface Prop {
}
const props = withDefaults(defineProps<Prop>(), {})
const isDesktop = useDesktopBreakpoint()
const isStandaloneDesktop = computed(() => isDesktop.value && !props.ProductItem)
const isStandaloneMobile = computed(() => !isDesktop.value && !props.ProductItem)
const rootClass = computed(() => {
if (isStandaloneDesktop.value) return 'dgold'
if (props.ProductItem) return 'gold-embed'
return 'gold-page'
})
const panelClass = computed(() => (isStandaloneDesktop.value ? 'dgold__panel' : ''))
const contentClass = computed(() => {
if (isStandaloneDesktop.value) return 'mt-5 mb-25vh pb-2'
if (isStandaloneMobile.value) {
return 'px-3 mpage-content mpage-content--with-subheader pb-[calc(var(--app-bottom-chrome-height)+1rem)]'
}
return 'mt-5 px-3 pb-4'
})
const isBuy = ref<boolean>(true)
const inputLock = ref<TradeInputLock>(null)
const tradeProductLabel = computed(() => {
const title = props.ProductItem?.title?.trim()
return title || meltedGoldLabel.value
})
const amountFieldLabel = computed(() => `مقدار ${tradeProductLabel.value}`)
const productWalletBalance = computed(() =>
getProductInventoryQuantity(
assetsSummary.value?.inventory_cards ?? [],
props.ProductItem?.id,
walletStore?.walletInfo?.gold
)
)
const productWalletBalanceText = computed(() =>
formatProductWalletBalanceText(
props.ProductItem,
assetsSummary.value?.inventory_cards ?? [],
walletStore?.walletInfo?.gold
)
)
const buyPagination = ref<TableEngineInput>({
filters: [{
field: 'type',
@@ -286,9 +388,13 @@ const sellPagination = ref<TableEngineInput>({
}], limit: 6, page: 0
})
const unitPrice = computed(() =>
isBuy.value ? getTradeBuyPrice(lastPrice.value) : getTradeSellPrice(lastPrice.value)
)
const unitPrice = computed(() => {
const product = props.ProductItem
if (usesQuantityTrade(product)) {
return isBuy.value ? getProductBuyPrice(product!) : getProductSellPrice(product!)
}
return isBuy.value ? getTradeBuyPrice(lastPrice.value) : getTradeSellPrice(lastPrice.value)
})
const orderGoldAmount = computed(() =>
calcOrderGoldAmount(
form.value?.goldValue,
@@ -310,12 +416,39 @@ const payablePrice = computed(() =>
inputLock.value === 'rial' ? form.value?.goldCash : undefined
)
)
const sellGoldDebit = computed(() => calcSellGoldDebit(orderGoldAmount.value, feeInGrams.value))
const sellGoldDebit = computed(() =>
calcSellGoldDebit(orderGoldAmount.value, feeInGrams.value, inputLock.value)
)
const feeDeductsFromGold = computed(() => inputLock.value === 'rial')
const feeDisplayText = computed(() =>
feeDeductsFromGold.value
? `${feeInGrams.value} ${weightUnit.value}`
: `${usePriceWithoutType(Math.round(feeInRial.value))} ریال`
)
const displayTradeGoldAmount = computed(() =>
formatTradeGramDisplay(orderGoldAmount.value)
)
const feeFormHint = computed(() =>
buildTradeConfirmBreakdown({
isBuy: isBuy.value,
inputLock: inputLock.value,
orderGoldAmount: orderGoldAmount.value,
feeInGrams: feeInGrams.value,
feeInRial: feeInRial.value,
payablePrice: payablePrice.value,
unitPriceRial: unitPrice.value,
sellGoldDebit: sellGoldDebit.value,
}).formHint
)
const minOrderCash = computed(() => productMinOrderCash(props.ProductItem, isBuy.value))
const maxOrderCash = computed(() => productMaxOrderCash(props.ProductItem))
const cashValidationMessage = computed(() =>
productCashValidationMessage(props.ProductItem, form.value?.goldCash, isBuy.value, usePriceWithoutType)
)
const priceUnavailable = computed(() => {
const unit = unitPrice.value
return !unit || Number.isNaN(unit) || unit <= 0
@@ -324,13 +457,20 @@ const priceUnavailable = computed(() => {
const disableTransaction = computed(() =>
loading.value ||
isStoreClosed() ||
!isProductTradeOpen(props.ProductItem, isBuy.value) ||
form.value?.goldValue === undefined ||
form.value?.goldValue === '' ||
form.value?.goldCash === '' ||
form.value?.goldCash === undefined ||
(isBuy.value ? (form.value?.goldCash ?? 0) < 500000 : (form.value?.goldValue ?? 0) < 0.001) ||
(!isBuy.value && sellGoldDebit.value > (walletStore?.walletInfo?.gold ?? 0)) ||
(isBuy.value && payablePrice.value > (walletStore?.walletInfo?.cash ?? 0)) ||
(isBuy.value
? (form.value?.goldCash ?? 0) < minOrderCash.value
: (productMinOrderCash(props.ProductItem, false) > 0 || productMaxOrderCash(props.ProductItem) != null
? ((form.value?.goldCash ?? 0) < minOrderCash.value || (maxOrderCash.value != null && (form.value?.goldCash ?? 0) > maxOrderCash.value))
: (form.value?.goldValue ?? 0) < 0.001)) ||
(maxOrderCash.value != null && (form.value?.goldCash ?? 0) > maxOrderCash.value) ||
cashValidationMessage.value !== '' ||
(!isBuy.value && sellGoldDebit.value > productWalletBalance.value) ||
(isBuy.value && payablePrice.value > cashWalletBalance.value) ||
priceUnavailable.value)
function refreshTable() {
@@ -424,90 +564,40 @@ async function initSellTable() {
// }
onMounted(async () => {
switch (router.currentRoute.value.query.state) {
case 'sell':
isBuy.value = false
break
case 'buy':
isBuy.value = true
break
}
isBuy.value = resolveProductTradeIsBuy(
props?.ProductItem,
router.currentRoute.value.query.state?.toString(),
)
await initPrice()
await loadServiceFeeFromSettings()
/* await initSellTable()
await initBuyTable()*/
})
watch(
() => [props?.ProductItem?.id, props?.ProductItem?.open_buy, props?.ProductItem?.open_sell],
() => {
isBuy.value = resolveProductTradeIsBuy(props?.ProductItem, isBuy.value ? 'buy' : 'sell')
},
)
const form = ref<Gold>({
goldCash: undefined,
goldValue: undefined,
percentWallet: undefined
percentWallet: 0,
})
const allowedAsciiCodeFloat = [46, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57]
const allowedAsciiCodeInt = [48, 49, 50, 51, 52, 53, 54, 55, 56, 57]
// Function to convert Persian/Farsi numbers to English
function convertPersianToEnglish(str: string): string {
const persianNumbers = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹']
const englishNumbers = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
const persianDecimal = '٫'
const englishDecimal = '.'
let result = str
// Replace Persian numbers with English numbers
for (let i = 0; i < persianNumbers.length; i++) {
result = result.replace(new RegExp(persianNumbers[i], 'g'), englishNumbers[i])
}
// Replace Persian decimal separator with English decimal separator
result = result.replace(new RegExp(persianDecimal, 'g'), englishDecimal)
return result
}
// Handler for Persian number input
function handlePersianNumberInput(event: Event) {
const target = event.target as HTMLInputElement
if (target && target.value) {
const convertedValue = convertPersianToEnglish(target.value)
if (convertedValue !== target.value) {
form.value.goldValue = convertedValue as any
}
}
}
watch(form.value, () => {
if (form.value.goldValue) {
const goldValueStr = form.value.goldValue.toString()
// Convert Persian numbers to English
const convertedValue = convertPersianToEnglish(goldValueStr)
if (convertedValue !== goldValueStr) {
const numericValue = parseFloat(convertedValue)
form.value.goldValue = isNaN(numericValue) ? null : numericValue
if (!isNaN(numericValue)) {
calculateCash(numericValue)
}
return
}
const lastCharGoldStr: string = goldValueStr
const lastCharGoldStr: string = form.value.goldValue.toString()
if (!allowedAsciiCodeFloat.includes(lastCharGoldStr.slice(-1).charCodeAt(0))) {
form.value.goldValue = isNaN(parseFloat(lastCharGoldStr.slice(0, -1))) ? null : parseFloat(lastCharGoldStr.slice(0, -1))
calculateCash(form.value.goldValue)
}
}
if (form.value.goldCash) {
const goldCashStr = form.value.goldCash.toString()
// Convert Persian numbers to English
const convertedValue = convertPersianToEnglish(goldCashStr)
if (convertedValue !== goldCashStr) {
const numericValue = parseFloat(convertedValue.replaceAll(',', ''))
form.value.goldCash = isNaN(numericValue) ? null : numericValue
if (!isNaN(numericValue)) {
calculateGold(numericValue)
}
return
}
const lastCharCashStr: string = goldCashStr
const lastCharCashStr: string = form.value.goldCash.toString()
if (!allowedAsciiCodeInt.includes(lastCharCashStr.slice(-1).charCodeAt(0))) {
form.value.goldCash = isNaN(parseFloat(lastCharCashStr.slice(0, -1))) ? null : parseFloat(lastCharCashStr.slice(0, -1))
calculateGold(form.value.goldCash)
@@ -517,24 +607,23 @@ watch(form.value, () => {
})
onMounted(() => {
void loadWalletAssets()
console.log(userStore.userInfo?.is_legal === 2)
if (userStore.userInfo?.is_legal === 0) {
noCompleteVisible.value = true
}
})
onUnmounted(() => {
stopPriceInterval()
})
function percentChange(val: number) {
if (val !== 0) {
if (isBuy.value) {
form.value.goldCash = unitCashCalculator.value * val
calculateGold(form.value.goldCash)
} else {
form.value.goldValue = formatTradeGramDisplay(parseFloat(String(unitGoldCalculator.value * val)))
calculateCash(form.value.goldValue)
form.value.goldValue = formatTradeGramInputValue(parseFloat(String(unitGoldCalculator.value * val)))
if (form.value.goldValue != null) {
calculateCash(Number(form.value.goldValue))
}
}
} else {
form.value.goldValue = undefined
@@ -543,13 +632,10 @@ function percentChange(val: number) {
}
}
const unitGoldCalculator = computed(() => {
// console.log(walletStore?.walletInfo?.gold / 100)
return (walletStore?.walletInfo?.gold ?? 0) / 100
})
const unitGoldCalculator = computed(() => productWalletBalance.value / 100)
const unitCashCalculator = computed(() => {
return (walletStore?.walletInfo?.cash ?? 0) / 100
return cashWalletBalance.value / 100
})
function syncFormWithLatestPrice() {
@@ -571,13 +657,13 @@ function syncFormWithLatestPrice() {
}
watch(
() => [unitPrice.value, isBuy.value],
() => [unitPrice.value, isBuy.value, props.ProductItem?.id],
() => {
syncFormWithLatestPrice()
}
)
watch([feeDeductFromGold, () => serviceFeePercentage.value], () => {
watch([() => serviceFeePercentage.value, inputLock], () => {
syncFormWithLatestPrice()
})
@@ -601,12 +687,12 @@ function calculateGold(val: number) {
if (!unit || Number.isNaN(unit)) return
if (val) {
form.value.goldValue = formatTradeGramDisplay(calcGramFromLockedRial(val, unit, isBuy.value))
form.value.goldValue = formatTradeGramInputValue(calcGramFromLockedRial(val, unit, isBuy.value))
} else if (val.toString() === '') {
form.value.goldValue = undefined
inputLock.value = null
} else {
form.value.goldValue = 0
form.value.goldValue = undefined
}
}
@@ -623,7 +709,10 @@ function useStyle() {
else return ' right: 98%; transform: translate(100%, 0);'
}
function setIsBuy(val: boolean) {
if (val && !isProductBuyOpen(props?.ProductItem)) return
if (!val && !isProductSellOpen(props?.ProductItem)) return
isBuy.value = val
form.value.goldValue = undefined
form.value.goldCash = undefined
@@ -642,6 +731,7 @@ const formatter = (value: number) => {
return `${value}%`
}
const price = computed((): Price => <Price>lastPrice.value ?? 0)
const goldPriceValue = computed(() => unitPrice.value)
@@ -649,9 +739,7 @@ const goldPriceValue = computed(() => unitPrice.value)
async function initPrice() {
try {
await getLastPrices()
await setIntervalGetAllPrice()
loading.value = true
price.value = lastPrice.value
} catch (e) {
console.log(e)
} finally {
@@ -664,38 +752,51 @@ function okConfirm() {
message.warning(getStoreClosedMessage())
return
}
if (!isProductTradeOpen(props.ProductItem, isBuy.value)) {
message.warning(productTradeClosedMessage(isBuy.value))
return
}
if (cashValidationMessage.value) {
message.warning(cashValidationMessage.value)
return
}
confirmAction.value = true
}
async function ok() {
if (loading.value) {
return
}
if (isStoreClosed()) {
message.warning(getStoreClosedMessage())
return
}
if (!isProductTradeOpen(props.ProductItem, isBuy.value)) {
message.warning(productTradeClosedMessage(isBuy.value))
return
}
confirmAction.value = false
try {
loading.value = true
await nextTick()
infoVisible.value = true
await createOrder({
direction: isBuy.value ? 1 : 2,
price: Math.round(unitPrice.value),
amount: normalizeOrderGoldAmount(orderGoldAmount.value),
target_cash: calcTradeTargetCash(form.value.goldCash, inputLock.value)
?? calcSellTargetCash(
form.value.goldCash,
form.value.goldValue,
unitPrice.value,
inputLock.value
),
// ریال قفل → مبلغ دقیق به سرور می‌رود (کارمزد از طلا). گرم قفل → target_cash خالی، وزن دقیق معامله می‌شود.
target_cash: calcTradeTargetCash(form.value.goldCash, inputLock.value),
abshode_id: props.ProductItem?.id,
})
await walletStore.getMyWallet()
await loadWalletAssets(true)
await router.push({ name: 'transactions' })
form.value = {
goldValue: undefined,
goldCash: undefined,
percentWallet: undefined
percentWallet: 0,
}
inputLock.value = null
// refreshTable()
message.success('سفارش شما با موفقیت انجام شد')
await fundStore.getMyFund()
} catch (e) {
@@ -706,11 +807,24 @@ async function ok() {
infoVisible.value = false
}
}
</script>
<style lang="less">
/*.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}*/
/*.completeButtonStyle{
background: blue;
}*/
.completeButtonStyle .van-dialog__confirm .van-button__content {
background-color: #a72b77;
background: @App-primary-color;
color: white;
}
+60 -43
View File
@@ -380,55 +380,28 @@
<div class="text-center" @click="showLogoutConfirm = false">انصراف</div>
</template>
</DialogVant>
<BasicActionSheet v-model:show="wholeBalanceVisible">
<BasicActionSheet v-if="!isDesktop" v-model:show="wholeBalanceVisible">
<div class="pb-20">
<div>
<div class="w-full text-center font-bold">موجودی ها</div>
<div class="h-30vh overflow-y-auto">
<div class="mt-5">
<BasicInfoItem
class="flex justify-between"
title="مجموعه کل دارایی طلایی"
is-line
:value="balanceLabel"
/>
<a-divider class="!my-1" />
<BasicInfoItem
v-if="countBasedInventory > 0"
class="flex justify-between"
title="تعداد محصولات"
is-line
:value="`${countBasedInventory} عدد`"
/>
<a-divider v-if="countBasedInventory > 0" class="!my-1" />
<BasicInfoItem
class="flex justify-between"
title="دارایی کیف پول ریالی"
is-line
:value="usePrice(assetsSummary?.cash_wallet ?? walletStore.walletInfo?.cash ?? 0)"
/>
<a-divider class="!my-1" />
<BasicInfoItem
class="flex justify-between"
title="دارایی کیف پول طلایی"
is-line
:value="(walletStore.walletInfo?.count_gold_wallet ?? 0) + ' گرم'"
/>
<a-divider class="!my-1" />
<!-- <BasicInfoItem class="flex justify-between" title="دارایی کیف پول(به گرم)" is-line
:value="((walletStore.walletInfo?.cash?? 0) / (goldVal?? 0)).toFixed(2)+ ' گرم'" />
<a-divider class="!my-1" />-->
<BasicInfoItem
class="flex justify-between"
title="ارزش کل دارایی شما"
is-line
:value="usePrice(totalRialDisplay)"
/>
</div>
<WholeBalanceContent />
</div>
</div>
</div>
</BasicActionSheet>
<BasicModal
v-else
v-model:visible="wholeBalanceVisible"
title="موجودی ها"
width="480px"
custom-footer
>
<WholeBalanceContent />
<template #footer>
<div></div>
</template>
</BasicModal>
<BasicModal
v-model:visible="openModal"
custom-footer
@@ -489,7 +462,7 @@ import type { PInventory, Product } from '@/models/products'
import { useWalletAssets } from '@/composables/useWalletAssets'
import { resolveAssetsRialValue } from '@/utils/productAssets'
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
import { message, Modal } from 'ant-design-vue'
import { message, Modal, Divider } from 'ant-design-vue'
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import BasicModal from '@/components/BasicPacks/BasicModal.vue'
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
@@ -543,6 +516,50 @@ const totalRialDisplay = computed(() =>
)
)
/** محتوای مشترک «موجودی‌ها» برای اکشن‌شیت موبایل و مودال دسکتاپ */
const WholeBalanceContent = () => (
<div class="mt-5">
<BasicInfoItem
class="flex justify-between"
title="مجموعه کل دارایی طلایی"
is-line
value={balanceLabel.value}
/>
<Divider class="!my-1" />
{countBasedInventory.value > 0 && (
<>
<BasicInfoItem
class="flex justify-between"
title="تعداد محصولات"
is-line
value={`${countBasedInventory.value} عدد`}
/>
<Divider class="!my-1" />
</>
)}
<BasicInfoItem
class="flex justify-between"
title="دارایی کیف پول ریالی"
is-line
value={usePrice(assetsSummary.value?.cash_wallet ?? walletStore.walletInfo?.cash ?? 0)}
/>
<Divider class="!my-1" />
<BasicInfoItem
class="flex justify-between"
title="دارایی کیف پول طلایی"
is-line
value={(walletStore.walletInfo?.count_gold_wallet ?? 0) + ' گرم'}
/>
<Divider class="!my-1" />
<BasicInfoItem
class="flex justify-between"
title="ارزش کل دارایی شما"
is-line
value={usePrice(totalRialDisplay.value)}
/>
</div>
)
onUnmounted(() => {
stopPriceInterval()
})
@@ -844,7 +861,7 @@ onMounted(async () => {
update()
const hasSeenModal = localStorage.getItem('hasSeenIntroModal')
if (!hasSeenModal) {
openModal.value = true
// openModal.value = true
}
} catch (e) {
console.log(e)
@@ -1,7 +1,7 @@
<template>
<div class="h-120 flex flex-col">
<div class="text-center">انتخاب آدرس</div>
<div class="overflow-y-scroll h-85">
<div class="flex flex-col" :class="hideTitle ? 'max-h-[60vh]' : 'h-120'">
<div v-if="!hideTitle" class="text-center">انتخاب آدرس</div>
<div class="overflow-y-scroll" :class="hideTitle ? 'flex-1 min-h-0 max-h-[48vh]' : 'h-85'">
<ARadioGroup v-model:value="selectedAddressID">
<div class="flex flex-col">
<div
@@ -33,9 +33,13 @@ import type { Address } from '@/models/address'
interface Props {
addresses?: Address[]
selectedAddressId?: number
/** وقتی داخل مودال دسکتاپ است، عنوان تکراری نمایش داده نشود */
hideTitle?: boolean
}
const props = defineProps<Props>()
const props = withDefaults(defineProps<Props>(), {
hideTitle: false,
})
const emit = defineEmits<{
addAddress: []
@@ -51,25 +51,42 @@
</div>
</div>
<BasicActionSheet v-model:show="showAddress">
<BasicModal
v-model:visible="showAddress"
title="انتخاب آدرس"
width="520px"
custom-footer
>
<PhysicalReceiveAddressPicker
v-model:selected-address-id="selectedAddressID"
:addresses="address"
hide-title
@add-address="addAddressVisible = true; showAddress = false"
@submit="handelsubmit"
/>
</BasicActionSheet>
<template #footer>
<div></div>
</template>
</BasicModal>
<BasicActionSheet v-model:show="addAddressVisible">
<BasicModal
v-model:visible="addAddressVisible"
title="افزودن آدرس"
width="520px"
custom-footer
>
<New_EditAddress v-if="addAddressVisible" :visible="addAddressVisible" @onOk="handleAddress" />
</BasicActionSheet>
<template #footer>
<div></div>
</template>
</BasicModal>
</div>
</template>
<script lang="ts" setup>
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
import BasicSelect from '@/components/BasicPacks/BasicSelect.vue'
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
import BasicModal from '@/components/BasicPacks/BasicModal.vue'
import DesktopPageTopbar from '@/layouts/components/DesktopPageTopbar.vue'
import New_EditAddress from '@/views/base/address/components/New_EditAddress.vue'
import PhysicalReceiveCartRow from '../components/PhysicalReceiveCartRow.vue'
+14 -1
View File
@@ -55,9 +55,22 @@ const { isDesktop } = usePhysicalReceiveShell()
.physical-receive-page-root--desktop {
height: auto;
min-height: 0;
min-height: 100%;
overflow: visible;
}
/* در دسکتاپ ارتفاع تا انتهای صفحه کشیده شود */
.physical-receive-page-root--desktop > div,
.physical-receive-page-root--desktop > div > div {
flex: 1 1 auto;
display: flex;
flex-direction: column;
min-height: 0;
}
.physical-receive-page-root--desktop > div > div > :deep(*) {
flex: 1 1 auto;
}
</style>
<style lang="less">
@@ -11,7 +11,7 @@
هنوز درخواستی ثبت نشده است.
</div>
</div>
<Teleport to="body">
<Teleport v-if="!isDesktop" to="body">
<div class="app-dock-add-bar" @click="openAdd()">
<BasicIcon icon="vuesax-linear:add" />
<div>افزودن درخواست جدید</div>
@@ -24,6 +24,8 @@
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import RequestItem from '../components/RequestItem.vue'
import { usePhysicalReceiveListContext } from '../composables/usePhysicalReceiveList'
import { usePhysicalReceiveShellContext } from '../composables/usePhysicalReceiveShell'
const { requestItems, openAdd } = usePhysicalReceiveListContext()
const { isDesktop } = usePhysicalReceiveShellContext()
</script>
@@ -20,7 +20,7 @@
</div>
</div>
<Teleport to="body">
<Teleport v-if="!isDesktop" to="body">
<div
v-if="!addTriggerVisible"
class="app-dock-add-bar"
@@ -40,6 +40,9 @@ import ListAnim from '@/components/ListAnim.vue'
import TransactionSkeleton from '@/components/TransactionSkeleton.vue'
import Item from '../components/Item.vue'
import { usePriceTriggerPageContext } from '../composables/usePriceTriggerPage'
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
const isDesktop = useDesktopBreakpoint()
const {
items,
@@ -0,0 +1,116 @@
<template>
<div class="recovery-sites-list" :class="`recovery-sites-list--${variant}`">
<p v-if="loading" class="recovery-sites-list__empty">در حال بارگذاری سایت‌ها...</p>
<p v-else-if="isEmpty" class="recovery-sites-list__empty">
سایتی برای نمایش ثبت نشده است.
</p>
<button
v-for="site in sites"
:key="site.id"
type="button"
class="recovery-sites-list__item"
@click="openSite(site.url)"
>
<span class="recovery-sites-list__icon">
<BasicIcon icon="vuesax-linear:global" :size="variant === 'desktop' ? 20 : 18" />
</span>
<span class="recovery-sites-list__content">
<span class="recovery-sites-list__name">{{ site.name }}</span>
<span class="recovery-sites-list__url" dir="ltr">{{ site.url }}</span>
</span>
<BasicIcon
class="recovery-sites-list__chevron"
icon="vuesax-linear:export"
:size="variant === 'desktop' ? 18 : 16"
/>
</button>
</div>
</template>
<script lang="ts" setup>
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import { useRecoverySitesPageContext } from '../composables/useRecoverySitesPage'
withDefaults(defineProps<{ variant?: 'mobile' | 'desktop' }>(), { variant: 'mobile' })
const { sites, loading, isEmpty, openSite } = useRecoverySitesPageContext()
</script>
<style scoped>
.recovery-sites-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.recovery-sites-list__empty {
margin: 0;
color: var(--app-text-secondary, #6b7280);
font-size: 0.875rem;
line-height: 1.8;
text-align: center;
}
.recovery-sites-list__item {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
border: 1px solid rgba(var(--app-gold-rgb, 212, 175, 55), 0.16);
border-radius: 0.875rem;
background: #fff;
padding: 0.875rem 1rem;
text-align: right;
cursor: pointer;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.recovery-sites-list__item:hover {
border-color: rgba(var(--app-gold-rgb, 212, 175, 55), 0.35);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.06);
}
.recovery-sites-list__icon {
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border-radius: 999px;
background: rgba(var(--app-gold-rgb, 212, 175, 55), 0.12);
color: var(--app-primary, #b8860b);
flex-shrink: 0;
}
.recovery-sites-list__content {
display: flex;
flex: 1;
flex-direction: column;
gap: 0.25rem;
min-width: 0;
}
.recovery-sites-list__name {
color: var(--app-text-primary, #111827);
font-size: 0.9375rem;
font-weight: 700;
}
.recovery-sites-list__url {
overflow: hidden;
color: var(--app-text-secondary, #6b7280);
font-size: 0.75rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.recovery-sites-list__chevron {
color: var(--app-text-secondary, #6b7280);
flex-shrink: 0;
}
.recovery-sites-list--desktop .recovery-sites-list__item {
padding: 1rem 1.125rem;
}
</style>
@@ -0,0 +1,73 @@
import {
computed,
inject,
onMounted,
provide,
ref,
type ComputedRef,
type InjectionKey,
type Ref,
} from 'vue'
import {
fetchTrackingSettings,
resolveTrackingSystems,
type TrackingSystemSite,
} from '@/utils/trackingSystemsSettings'
export interface RecoverySitesPageContext {
sites: Ref<TrackingSystemSite[]>
loading: Ref<boolean>
isEmpty: ComputedRef<boolean>
openSite: (url: string) => void
}
export const RECOVERY_SITES_PAGE_CONTEXT_KEY: InjectionKey<RecoverySitesPageContext> =
Symbol('recoverySitesPageContext')
function normalizeExternalUrl(url: string) {
const trimmed = url.trim()
if (!trimmed) return ''
if (/^https?:\/\//i.test(trimmed)) return trimmed
return `https://${trimmed.replace(/^\/\//, '')}`
}
export function useRecoverySitesPage() {
const sites = ref<TrackingSystemSite[]>([])
const loading = ref(false)
const isEmpty = computed(() => !loading.value && sites.value.length === 0)
async function loadSites() {
loading.value = true
try {
sites.value = resolveTrackingSystems(await fetchTrackingSettings(true))
} catch (error) {
console.log(error)
sites.value = []
} finally {
loading.value = false
}
}
onMounted(() => {
void loadSites()
})
function openSite(url: string) {
const normalized = normalizeExternalUrl(url)
if (normalized) window.open(normalized, '_blank', 'noopener,noreferrer')
}
const context: RecoverySitesPageContext = { sites, loading, isEmpty, openSite }
provide(RECOVERY_SITES_PAGE_CONTEXT_KEY, context)
return context
}
export function useRecoverySitesPageContext() {
const context = inject(RECOVERY_SITES_PAGE_CONTEXT_KEY)
if (!context) {
throw new Error(
'useRecoverySitesPageContext must be used within a page that calls useRecoverySitesPage()',
)
}
return context
}
@@ -0,0 +1,76 @@
<template>
<div class="drecovery">
<div class="drecovery__container">
<DesktopPageTopbar eyebrow="خدمات" title="سایت‌های ری‌گیری" show-back />
<section class="drecovery-surface">
<div class="drecovery-main__intro">
<div class="drecovery-main__logo">
<BasicIcon icon="vuesax-linear:global" size="28" />
</div>
<div>
<h2 class="drecovery-main__heading">سایت‌های ری‌گیری</h2>
<p class="drecovery-main__tagline">دسترسی به سایت‌های مرتبط با ری‌گیری</p>
</div>
</div>
<RecoverySitesList variant="desktop" />
</section>
</div>
</div>
</template>
<script lang="ts" setup>
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import DesktopPageTopbar from '@/layouts/components/DesktopPageTopbar.vue'
import RecoverySitesList from '../components/RecoverySitesList.vue'
</script>
<style scoped>
.drecovery {
min-height: 100%;
height: 100%;
padding: 1.5rem 1.75rem 2rem;
}
.drecovery__container {
max-width: 960px;
margin: 0 auto;
}
.drecovery-surface {
border: 1px solid rgba(var(--app-gold-rgb, 212, 175, 55), 0.18);
border-radius: 1rem;
background: var(--app-surface, #fff);
padding: 1.5rem;
}
.drecovery-main__intro {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.drecovery-main__logo {
display: flex;
align-items: center;
justify-content: center;
width: 3.5rem;
height: 3.5rem;
border-radius: 999px;
background: rgba(var(--app-gold-rgb, 212, 175, 55), 0.12);
color: var(--app-primary, #b8860b);
}
.drecovery-main__heading {
margin: 0 0 0.25rem;
font-size: 1.125rem;
}
.drecovery-main__tagline {
margin: 0;
color: var(--app-text-secondary, #6b7280);
font-size: 0.875rem;
}
</style>
+26
View File
@@ -0,0 +1,26 @@
<template>
<div class="recovery-sites-page-root">
<DesktopRecoverySitesView v-show="isDesktop" />
<MobileRecoverySitesView v-show="!isDesktop" />
</div>
</template>
<script lang="ts" setup>
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
import { useRecoverySitesPage } from './composables/useRecoverySitesPage'
import DesktopRecoverySitesView from './desktop/DesktopRecoverySitesView.vue'
import MobileRecoverySitesView from './mobile/MobileRecoverySitesView.vue'
const isDesktop = useDesktopBreakpoint()
useRecoverySitesPage()
</script>
<style scoped>
.recovery-sites-page-root {
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 100%;
height: 100%;
}
</style>
@@ -0,0 +1,35 @@
<template>
<div class="mobile-recovery-sites mobile-info-page">
<div class="mobile-info-page__bg" aria-hidden="true">
<img :src="img" alt="" />
</div>
<MobilePageHeader title="سایت‌های ری‌گیری" show-back />
<div class="mobile-recovery-sites__main mobile-info-page__main">
<div class="mobile-recovery-sites__card">
<RecoverySitesList variant="mobile" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import img from '@/assets/img/user_ly1.svg'
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
import RecoverySitesList from '../components/RecoverySitesList.vue'
</script>
<style scoped>
.mobile-recovery-sites__main {
padding-bottom: calc(var(--app-bottom-chrome-height, 108px) + 1.5rem);
}
.mobile-recovery-sites__card {
border: 1px solid rgba(var(--app-gold-rgb, 212, 175, 55), 0.16);
border-radius: 0.875rem;
background: #fff;
padding: 1rem;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
}
</style>
+42 -26
View File
@@ -12,12 +12,14 @@ import type { FormInstance } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import { updateMe, updatePass } from '@/api/me'
import { useUserStore } from '@/store/userStore'
import { useTrackingSystemsFeature } from '@/composables/useTrackingSystemsFeature'
export interface UserMenuAction {
id: string
label: string
icon: string
onClick: () => void
visible?: boolean
}
export interface UserPageContext {
@@ -59,6 +61,7 @@ export function useUserPage() {
const userStore = useUserStore()
const editPassVisible = ref(false)
const editPasswordVisible = ref(false)
const { trackingSystemsEnabled } = useTrackingSystemsFeature()
const isLegal = computed(() => {
const result = { title: '', color: '', icon: '' }
@@ -80,32 +83,45 @@ export function useUserPage() {
const shareButtonLabel = computed(() => 'اشتراک گذاری')
const menuActions = computed<UserMenuAction[]>(() => [
{
id: 'password',
label: 'تغییر رمز عبور',
icon: 'vuesax-linear:lock',
onClick: () => { editPasswordVisible.value = true },
},
{
id: 'notify',
label: 'هشدار قیمت',
icon: 'vuesax-linear:bill',
onClick: () => router.push({ name: 'notifyTrigger' }),
},
{
id: 'about',
label: 'درباره ما',
icon: 'vuesax-linear:info-circle',
onClick: () => router.push({ name: 'about' }),
},
{
id: 'rules',
label: 'قوانین و مقررات',
icon: 'vuesax-linear:task',
onClick: () => router.push({ name: 'rules' }),
},
])
const menuActions = computed<UserMenuAction[]>(() =>
[
{
id: 'password',
label: 'تغییر رمز عبور',
icon: 'vuesax-linear:lock',
onClick: () => { editPasswordVisible.value = true },
visible: true,
},
{
id: 'notify',
label: 'هشدار قیمت',
icon: 'vuesax-linear:bill',
onClick: () => router.push({ name: 'notifyTrigger' }),
visible: true,
},
{
id: 'about',
label: 'درباره ما',
icon: 'vuesax-linear:info-circle',
onClick: () => router.push({ name: 'about' }),
visible: true,
},
{
id: 'recoverySites',
label: 'سایت‌های ری‌گیری',
icon: 'vuesax-linear:global',
onClick: () => router.push({ name: 'recoverySites' }),
visible: trackingSystemsEnabled.value,
},
{
id: 'rules',
label: 'قوانین و مقررات',
icon: 'vuesax-linear:task',
onClick: () => router.push({ name: 'rules' }),
visible: true,
},
].filter((action) => action.visible !== false),
)
function setForm() {
if (userStore.userInfo) {
@@ -108,6 +108,19 @@
<div>درباره ما</div>
</div>
</BasicButton>
<BasicButton
v-if="trackingSystemsEnabled"
class="mt-2 w-full"
icon=""
iconSize="18"
size="large"
@click="$router.push({ name: 'recoverySites' })"
>
<div class="flex">
<BasicIcon icon="vuesax-linear:global" class="absolute right-8" />
<div>سایت‌های ری‌گیری</div>
</div>
</BasicButton>
<!-- <BasicButton-->
<!-- class="mt-2 w-full"-->
<!-- icon=""-->
@@ -214,6 +227,7 @@ import { useWalletStore } from '@/store/walletStore'
import Icon from '@/components/BasicPacks/BasicIcon.vue'
import { useWindowScroll } from '@vueuse/core/index'
import { onMounted, ref } from 'vue'
import { useTrackingSystemsFeature } from '@/composables/useTrackingSystemsFeature'
import { useUserStore } from '@/store/userStore'
import { useUserPageContext } from '../composables/useUserPage'
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
@@ -223,6 +237,7 @@ const walletStore = useWalletStore()
const showLogoutConfirm = ref<boolean>()
const logoutLoading = ref<boolean>(false)
const { y } = useWindowScroll()
const { trackingSystemsEnabled } = useTrackingSystemsFeature()
const { editPassVisible, editPasswordVisible, copyCode, shareCode } = useUserPageContext()