|
|
@@ -1,9 +1,12 @@
|
|
|
<script setup lang="ts">
|
|
|
-import { computed, ref } from 'vue'
|
|
|
+import { computed, ref, watchEffect } from 'vue'
|
|
|
+import { showToast } from 'vant'
|
|
|
import type { NativeSafeArea } from '~/types/native'
|
|
|
-import { callNativeScheme } from '~/utils/helpers'
|
|
|
+import { callNativeScheme, idrFormat } from '~/utils/helpers'
|
|
|
import { walletApi } from '~/api/wallet'
|
|
|
+import type { WalletWithdrawInfoVo } from '~/types/api'
|
|
|
import beanPng from '~/assets/images/common/bean.png'
|
|
|
+import { useBaseConstsStore } from '~/stores/baseConsts'
|
|
|
|
|
|
definePageMeta({
|
|
|
auth: true,
|
|
|
@@ -16,8 +19,9 @@ const router = useRouter()
|
|
|
|
|
|
// Check auth status on mount
|
|
|
const checkingAuth = ref(true)
|
|
|
+const loadingInfo = ref(true)
|
|
|
|
|
|
-const checkAuthStatus = async () => {
|
|
|
+const checkAuthStatus = async (): Promise<boolean> => {
|
|
|
try {
|
|
|
checkingAuth.value = true
|
|
|
const status = await walletApi.getWithdrawRealNameAuthStatus()
|
|
|
@@ -25,13 +29,15 @@ const checkAuthStatus = async () => {
|
|
|
// If both are not approved (state=2), redirect to auth page
|
|
|
if (!status || status.idCardState !== 2 || status.bankCardState !== 2) {
|
|
|
await router.replace('/wallet/withdraw/auth')
|
|
|
- return
|
|
|
+ return false
|
|
|
}
|
|
|
+ return true
|
|
|
}
|
|
|
catch (error) {
|
|
|
console.error('Failed to check auth status:', error)
|
|
|
// Redirect to auth page on error
|
|
|
await router.replace('/wallet/withdraw/auth')
|
|
|
+ return false
|
|
|
}
|
|
|
finally {
|
|
|
checkingAuth.value = false
|
|
|
@@ -39,19 +45,89 @@ const checkAuthStatus = async () => {
|
|
|
}
|
|
|
|
|
|
onMounted(() => {
|
|
|
- checkAuthStatus()
|
|
|
+ (async () => {
|
|
|
+ const ok = await checkAuthStatus()
|
|
|
+ if (!ok) return
|
|
|
+
|
|
|
+ try {
|
|
|
+ loadingInfo.value = true
|
|
|
+ const info = await walletApi.getWithdrawInfo()
|
|
|
+ applyWithdrawInfo(info)
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ console.error('Failed to fetch withdraw info:', error)
|
|
|
+ showToast({ message: 'Failed to fetch withdraw info' })
|
|
|
+ }
|
|
|
+ finally {
|
|
|
+ loadingInfo.value = false
|
|
|
+ }
|
|
|
+ })()
|
|
|
})
|
|
|
|
|
|
-// Static demo data based on Figma. Replace with real wallet data later.
|
|
|
-const balanceBeans = ref(20000)
|
|
|
-const balanceCurrency = ref('IDR')
|
|
|
-const balanceCurrencyAmount = ref('20000.000')
|
|
|
+const withdrawInfo = ref<WalletWithdrawInfoVo | null>(null)
|
|
|
+
|
|
|
+const balanceBeans = ref(0)
|
|
|
+const balanceCurrencyAmount = computed(() => {
|
|
|
+ if (!Number.isFinite(balanceBeans.value) || balanceBeans.value <= 0) return idrFormat(0)
|
|
|
+ return idrFormat(balanceBeans.value * exchangeRate.value)
|
|
|
+})
|
|
|
|
|
|
const fromBeans = ref<string>('')
|
|
|
|
|
|
const exchangeRate = ref<number>(1)
|
|
|
const minimumWithdrawalAmount = ref<number>(0)
|
|
|
const withdrawalFeePercent = ref<number>(0)
|
|
|
+const withdrawalFeeAmount = ref<number>(0)
|
|
|
+const withdrawFeeType = ref<0 | 1>(1)
|
|
|
+
|
|
|
+const baseConstsStore = useBaseConstsStore()
|
|
|
+
|
|
|
+const beanToIdrExchange = computed(() => {
|
|
|
+ const list = baseConstsStore.config?.commonCoinExchangeConsts ?? []
|
|
|
+ const item = list.find((it) => {
|
|
|
+ const obj = it as Record<string, number>
|
|
|
+ return typeof obj.bean === 'number' && typeof obj.IDR === 'number'
|
|
|
+ }) as Record<string, number> | undefined
|
|
|
+
|
|
|
+ const bean = item?.bean ?? 1
|
|
|
+ const idr = item?.IDR ?? 1
|
|
|
+ const rate = bean > 0 ? idr / bean : 1
|
|
|
+
|
|
|
+ return { bean, idr, rate }
|
|
|
+})
|
|
|
+
|
|
|
+watchEffect(() => {
|
|
|
+ const next = beanToIdrExchange.value.rate
|
|
|
+ if (Number.isFinite(next) && next > 0) {
|
|
|
+ exchangeRate.value = next
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const canInputBeans = computed(() => {
|
|
|
+ const max = balanceBeans.value
|
|
|
+ const min = minimumWithdrawalAmount.value
|
|
|
+ return Number.isFinite(max) && Number.isFinite(min) && max > 0 && max >= min
|
|
|
+})
|
|
|
+
|
|
|
+const beansPlaceholder = computed(() => {
|
|
|
+ return canInputBeans.value ? 'Please fill in' : '不满足最低提现金额'
|
|
|
+})
|
|
|
+
|
|
|
+const applyWithdrawInfo = (info: WalletWithdrawInfoVo) => {
|
|
|
+ withdrawInfo.value = info
|
|
|
+
|
|
|
+ balanceBeans.value = Number(info.availableBeanAmount) || 0
|
|
|
+ minimumWithdrawalAmount.value = Number(info.withdrawMinBeanAmount) || 0
|
|
|
+
|
|
|
+ withdrawFeeType.value = (info.config?.feeType ?? 1) as 0 | 1
|
|
|
+ withdrawalFeeAmount.value = Number(info.config?.feeAmount) || 0
|
|
|
+ withdrawalFeePercent.value = Number(info.config?.feeRate) || 0
|
|
|
+
|
|
|
+ // If balance is below minimum, clear any existing input
|
|
|
+ if (!canInputBeans.value) {
|
|
|
+ fromBeans.value = ''
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
const nativeSafeArea = useState<NativeSafeArea>('native-safe-area')
|
|
|
const containerStyle = computed<Record<string, string>>(() => ({ paddingTop: `${nativeSafeArea.value.top}px` }))
|
|
|
@@ -60,12 +136,17 @@ const bottomBarStyle = computed<Record<string, string>>(() => ({ paddingBottom:
|
|
|
|
|
|
const toCurrencyAmount = computed(() => {
|
|
|
const n = Number(fromBeans.value)
|
|
|
- if (!fromBeans.value || !Number.isFinite(n) || n <= 0) return '0.000'
|
|
|
- return (n * exchangeRate.value).toFixed(3)
|
|
|
+ if (!fromBeans.value || !Number.isFinite(n) || n <= 0) return idrFormat(0)
|
|
|
+ return idrFormat(n * exchangeRate.value)
|
|
|
})
|
|
|
|
|
|
const tipText = computed(() => {
|
|
|
- return `Exchange rate from Gami Beans to IDR: ${exchangeRate.value},Mininum withdrawal amount:${minimumWithdrawalAmount.value},withdrawal fee: ${withdrawalFeePercent.value}%.`
|
|
|
+ const minBeans = minimumWithdrawalAmount.value
|
|
|
+ const feeText = withdrawFeeType.value === 0
|
|
|
+ ? `withdrawal fee: ${idrFormat(withdrawalFeeAmount.value, { withSymbol: true })}`
|
|
|
+ : `withdrawal fee: ${withdrawalFeePercent.value}%`
|
|
|
+ const { bean, idr } = beanToIdrExchange.value
|
|
|
+ return `Exchange rate from Beans to IDR: ${bean}:${idr}, Minimum withdrawal amount: ${minBeans} Beans, ${feeText}.`
|
|
|
})
|
|
|
|
|
|
const onBack = () => {
|
|
|
@@ -73,8 +154,61 @@ const onBack = () => {
|
|
|
}
|
|
|
|
|
|
const onWithdrawAll = () => {
|
|
|
+ if (!canInputBeans.value) return
|
|
|
fromBeans.value = String(balanceBeans.value)
|
|
|
}
|
|
|
+
|
|
|
+const normalizeBeansInput = () => {
|
|
|
+ if (!canInputBeans.value) {
|
|
|
+ fromBeans.value = ''
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const raw = fromBeans.value
|
|
|
+ const n = Number(raw)
|
|
|
+ if (!raw || !Number.isFinite(n)) {
|
|
|
+ fromBeans.value = ''
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const max = Math.floor(balanceBeans.value)
|
|
|
+ const min = Math.floor(minimumWithdrawalAmount.value)
|
|
|
+
|
|
|
+ let v = Math.floor(n)
|
|
|
+ if (v < min) v = min
|
|
|
+ if (v > max) v = max
|
|
|
+
|
|
|
+ fromBeans.value = String(v)
|
|
|
+}
|
|
|
+
|
|
|
+const validateBeforeSubmit = (): boolean => {
|
|
|
+ if (!canInputBeans.value) {
|
|
|
+ showToast({ message: '不满足最低提现金额' })
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ normalizeBeansInput()
|
|
|
+ const v = Number(fromBeans.value)
|
|
|
+ const min = minimumWithdrawalAmount.value
|
|
|
+ const max = balanceBeans.value
|
|
|
+
|
|
|
+ if (!fromBeans.value || !Number.isFinite(v) || v <= 0) {
|
|
|
+ showToast({ message: 'Please fill in' })
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if (v < min || v > max) {
|
|
|
+ // Should have been normalized, but keep a safe guard
|
|
|
+ showToast({ message: 'Invalid amount' })
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+const onSubmit = () => {
|
|
|
+ if (!validateBeforeSubmit()) return
|
|
|
+ // Submit API is not provided in the spec; only do local validation for now.
|
|
|
+}
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
@@ -90,7 +224,7 @@ const onWithdrawAll = () => {
|
|
|
|
|
|
<!-- Loading state -->
|
|
|
<div
|
|
|
- v-if="checkingAuth"
|
|
|
+ v-if="checkingAuth || loadingInfo"
|
|
|
class="withdraw-apply-loading"
|
|
|
>
|
|
|
<p>{{ t('common.loading') }}</p>
|
|
|
@@ -117,7 +251,7 @@ const onWithdrawAll = () => {
|
|
|
|
|
|
<div class="withdraw-apply-balance__approx">
|
|
|
<span class="withdraw-apply-balance__approx-sign">≈</span>
|
|
|
- <span class="withdraw-apply-balance__approx-currency">{{ balanceCurrency }}</span>
|
|
|
+ <span class="withdraw-apply-balance__approx-currency">Rp</span>
|
|
|
<span class="withdraw-apply-balance__approx-amount">{{ balanceCurrencyAmount }}</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -150,7 +284,9 @@ const onWithdrawAll = () => {
|
|
|
type="number"
|
|
|
inputmode="numeric"
|
|
|
autocomplete="off"
|
|
|
- placeholder="Please fill in"
|
|
|
+ :disabled="!canInputBeans"
|
|
|
+ :placeholder="beansPlaceholder"
|
|
|
+ @blur="normalizeBeansInput"
|
|
|
>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -165,7 +301,7 @@ const onWithdrawAll = () => {
|
|
|
</p>
|
|
|
|
|
|
<div class="withdraw-apply-card__to">
|
|
|
- <span class="withdraw-apply-card__to-currency">IDR</span>
|
|
|
+ <span class="withdraw-apply-card__to-currency">Rp</span>
|
|
|
<span class="withdraw-apply-card__to-amount">{{ toCurrencyAmount }}</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -184,6 +320,8 @@ const onWithdrawAll = () => {
|
|
|
<button
|
|
|
type="button"
|
|
|
class="withdraw-apply-bottom__btn"
|
|
|
+ :disabled="!canInputBeans"
|
|
|
+ @click="onSubmit"
|
|
|
>
|
|
|
With Draw
|
|
|
</button>
|
|
|
@@ -395,5 +533,9 @@ const onWithdrawAll = () => {
|
|
|
font-weight: 600;
|
|
|
font-size: 16px;
|
|
|
line-height: 17px;
|
|
|
+
|
|
|
+ &:disabled {
|
|
|
+ opacity: 0.5;
|
|
|
+ }
|
|
|
}
|
|
|
</style>
|