feat: add required kyc blocking modal and fix copper asset units to kilogram
This commit is contained in:
Vendored
+1
@@ -68,6 +68,7 @@ declare module 'vue' {
|
||||
PwaInstallGuideModal: typeof import('./src/components/pwa/PwaInstallGuideModal.vue')['default']
|
||||
PwaInstallStepIllustration: typeof import('./src/components/pwa/PwaInstallStepIllustration.vue')['default']
|
||||
RangePicker: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/range-picker.vue')['default']
|
||||
RequiredKycModal: typeof import('./src/components/RequiredKycModal.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SelectInRange: typeof import('./src/components/BasicPacks/BaseDatePicker/src/datePick/demo/select-in-range.vue')['default']
|
||||
|
||||
+6
-1
@@ -30,9 +30,10 @@
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</router-view>
|
||||
</div>
|
||||
<RequiredKycModal v-if="!showSplash && !isTvBoardRoute && !isLoginRoute" />
|
||||
</AConfigProvider>
|
||||
<div
|
||||
v-if="showSupportButton && !showSplash && route.name !== 'support' && route.name !== 'login' && !isTvBoardRoute"
|
||||
v-if="showSupportButton && !showSplash && route.name !== 'support' && route.name !== 'login' && !isTvBoardRoute && !isKycRequired"
|
||||
id="support-button"
|
||||
ref="supportButtonEl"
|
||||
class="fixed z-[5100] cursor-grab select-none rounded-full"
|
||||
@@ -132,6 +133,7 @@ import { usePwaInstall } from '@/composables/usePwaInstall'
|
||||
import PwaInstallBanner from '@/components/pwa/PwaInstallBanner.vue'
|
||||
import PwaInstallGuideModal from '@/components/pwa/PwaInstallGuideModal.vue'
|
||||
import GoldDayCalculatorModal from '@/components/GoldDayCalculatorModal.vue'
|
||||
import RequiredKycModal from '@/components/RequiredKycModal.vue'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { useSettingsStore } from '@/store/settingsStore'
|
||||
import {
|
||||
@@ -299,6 +301,9 @@ watch(
|
||||
)
|
||||
|
||||
const userStore = useUserStore()
|
||||
const isKycRequired = computed(
|
||||
() => Boolean(getToken()) && (userStore.userInfo?.is_legal === 0 || !userStore.userInfo?.is_legal),
|
||||
)
|
||||
const settingsStore = useSettingsStore()
|
||||
const showLoginCreditExtras = computed(() => {
|
||||
if (!isLoginRoute.value) return false
|
||||
|
||||
@@ -82,6 +82,16 @@ export function setupRouterGuards() {
|
||||
return goToLogin(next)
|
||||
}
|
||||
|
||||
// کاربر احراز هویت نشده نباید بتواند به صفحات داخلی دسترسی پیدا کند
|
||||
if (
|
||||
userStore.userInfo &&
|
||||
(userStore.userInfo.is_legal === 0 || !userStore.userInfo.is_legal) &&
|
||||
!isPublicRoute(to) &&
|
||||
to.name !== 'home'
|
||||
) {
|
||||
return goToHome(next)
|
||||
}
|
||||
|
||||
if (to.name === 'notifyTrigger') {
|
||||
const { useSettingsStore } = await import('@/store/settingsStore')
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="isModalVisible"
|
||||
:closable="false"
|
||||
:mask-closable="false"
|
||||
:keyboard="false"
|
||||
:footer="null"
|
||||
:centered="true"
|
||||
:z-index="5500"
|
||||
:width="modalWidth"
|
||||
class="required-kyc-modal"
|
||||
wrap-class-name="required-kyc-modal-wrap"
|
||||
>
|
||||
<div class="kyc-modal-content text-right" dir="rtl">
|
||||
<!-- State 1: Need to enter/edit KYC information -->
|
||||
<template v-if="!isPendingApproval || isEditing">
|
||||
<div class="text-center mb-5">
|
||||
<div class="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-amber-50 text-amber-700 border border-amber-200 mb-3 shadow-sm">
|
||||
<BasicIcon icon="vuesax-bold:security-user" size="32" color="#b45309" />
|
||||
</div>
|
||||
<h2 class="text-lg font-bold text-slate-800 mb-1">
|
||||
احراز هویت الزامی است
|
||||
</h2>
|
||||
<p class="text-xs text-slate-500 leading-6 max-w-sm mx-auto">
|
||||
کاربر گرامی، جهت حفظ امنیت حساب و امکان انجام معاملات مس و واریز/برداشت وجه، لازم است مشخصات هویتی خود را ثبت نمایید.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formState"
|
||||
layout="vertical"
|
||||
class="kyc-form space-y-2"
|
||||
@finish="onSubmit"
|
||||
>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-3 gap-y-0">
|
||||
<a-form-item
|
||||
label="نام"
|
||||
name="fname"
|
||||
:rules="[required('نام'), isValidPersian()]"
|
||||
>
|
||||
<BasicInput
|
||||
v-model:value="formState.fname"
|
||||
placeholder="مثال: علی"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="نام خانوادگی"
|
||||
name="lname"
|
||||
:rules="[required('نام خانوادگی'), isValidPersian()]"
|
||||
>
|
||||
<BasicInput
|
||||
v-model:value="formState.lname"
|
||||
placeholder="مثال: محمدی"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
<a-form-item
|
||||
label="کد ملی (۱۰ رقم)"
|
||||
name="n_code"
|
||||
:rules="[requireNationalCode()]"
|
||||
>
|
||||
<BasicInput
|
||||
v-model:value="formState.n_code"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
maxlength="10"
|
||||
placeholder="مثال: ۰۰۱۲۳۴۵۶۷۸"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-3 gap-y-0">
|
||||
<a-form-item
|
||||
label="تاریخ تولد"
|
||||
name="birth_date"
|
||||
:rules="[required('تاریخ تولد')]"
|
||||
>
|
||||
<BaseDatePicker
|
||||
v-model:value="formState.birth_date"
|
||||
class="w-full"
|
||||
input-read-only
|
||||
placeholder="انتخاب تاریخ تولد"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="شهر"
|
||||
name="city"
|
||||
:rules="[required('شهر')]"
|
||||
>
|
||||
<BasicInput
|
||||
v-model:value="formState.city"
|
||||
placeholder="مثال: تهران"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 flex flex-col gap-2.5">
|
||||
<BasicButton
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
:disabled="loading"
|
||||
class="w-full h-11 text-sm font-semibold !rounded-xl"
|
||||
>
|
||||
<a-spin v-if="loading" size="small" />
|
||||
<span v-else>ثبت و استعلام هویت</span>
|
||||
</BasicButton>
|
||||
|
||||
<div class="flex items-center justify-between gap-2 pt-1">
|
||||
<button
|
||||
v-if="isEditing && isPendingApproval"
|
||||
type="button"
|
||||
class="text-xs text-slate-500 hover:text-slate-700 py-1 px-2 cursor-pointer transition-colors"
|
||||
@click="isEditing = false"
|
||||
>
|
||||
انصراف از ویرایش
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs text-red-600 hover:text-red-700 flex items-center gap-1 py-1 px-2 mr-auto cursor-pointer transition-colors"
|
||||
@click="onLogout"
|
||||
>
|
||||
<BasicIcon icon="vuesax-linear:logout" size="14" color="#dc2626" />
|
||||
<span>خروج از حساب کاربری</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<!-- State 2: Already submitted, waiting for Admin review -->
|
||||
<template v-else>
|
||||
<div class="text-center mb-5">
|
||||
<div class="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-amber-50 text-amber-700 border border-amber-200 mb-3 shadow-sm">
|
||||
<BasicIcon icon="vuesax-bold:clock" size="32" color="#d97706" />
|
||||
</div>
|
||||
<h2 class="text-lg font-bold text-slate-800 mb-1">
|
||||
در انتظار تأیید اطلاعات هویتی
|
||||
</h2>
|
||||
<p class="text-xs text-slate-600 leading-6 max-w-sm mx-auto">
|
||||
اطلاعات شما با موفقیت در سامانه ثبت شده و در صف بررسی و تایید کارشناسان مدیریت قرار دارد. پس از تایید هویت، دسترسی به تمام بخشها فعال خواهد شد.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-50 rounded-xl p-3.5 border border-slate-200 text-xs text-slate-700 space-y-2 mb-5">
|
||||
<div class="flex justify-between items-center py-1 border-b border-slate-200/60">
|
||||
<span class="text-slate-500">نام و نام خانوادگی:</span>
|
||||
<span class="font-medium text-slate-800">{{ formState.fname }} {{ formState.lname }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center py-1 border-b border-slate-200/60">
|
||||
<span class="text-slate-500">کد ملی:</span>
|
||||
<span class="font-medium text-slate-800 tracking-wider">{{ formState.n_code }}</span>
|
||||
</div>
|
||||
<div v-if="formState.birth_date" class="flex justify-between items-center py-1 border-b border-slate-200/60">
|
||||
<span class="text-slate-500">تاریخ تولد:</span>
|
||||
<span class="font-medium text-slate-800">{{ formState.birth_date }}</span>
|
||||
</div>
|
||||
<div v-if="formState.city" class="flex justify-between items-center py-1">
|
||||
<span class="text-slate-500">شهر:</span>
|
||||
<span class="font-medium text-slate-800">{{ formState.city }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<BasicButton
|
||||
type="primary"
|
||||
:disabled="checkingStatus"
|
||||
class="w-full h-11 text-sm font-semibold !rounded-xl"
|
||||
@click="onCheckStatus"
|
||||
>
|
||||
<a-spin v-if="checkingStatus" size="small" />
|
||||
<span v-else>بررسی مجدد وضعیت تأیید</span>
|
||||
</BasicButton>
|
||||
|
||||
<div class="flex items-center justify-between gap-2 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs text-slate-600 hover:text-slate-800 flex items-center gap-1 py-1 px-2 cursor-pointer transition-colors"
|
||||
@click="isEditing = true"
|
||||
>
|
||||
<BasicIcon icon="vuesax-linear:edit-2" size="14" color="#475569" />
|
||||
<span>ویرایش مجدد اطلاعات</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs text-red-600 hover:text-red-700 flex items-center gap-1 py-1 px-2 mr-auto cursor-pointer transition-colors"
|
||||
@click="onLogout"
|
||||
>
|
||||
<BasicIcon icon="vuesax-linear:logout" size="14" color="#dc2626" />
|
||||
<span>خروج از حساب</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
||||
import BaseDatePicker from '@/components/BasicPacks/BaseDatePicker'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { updateMe } from '@/api/me'
|
||||
import { getToken } from '@/utils'
|
||||
import { isValidPersian, requireNationalCode, required } from '@/utils/inputRules'
|
||||
import { normalizeAuthFormFields } from '@/utils/persianDigits'
|
||||
import { performUserLogout } from '@/utils/authSession'
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const loading = ref(false)
|
||||
const checkingStatus = ref(false)
|
||||
const isEditing = ref(false)
|
||||
|
||||
const formState = reactive({
|
||||
fname: '',
|
||||
lname: '',
|
||||
n_code: '',
|
||||
birth_date: '',
|
||||
city: '',
|
||||
})
|
||||
|
||||
const modalWidth = computed(() => (isDesktop.value ? '460px' : '92vw'))
|
||||
|
||||
const isModalVisible = computed(() => {
|
||||
if (!getToken()) return false
|
||||
if (route.name === 'login' || route.name === 'tvMonitor' || route.name === 'aboutSoftware') {
|
||||
return false
|
||||
}
|
||||
const info = userStore.userInfo
|
||||
if (!info) return false
|
||||
// 0 indicates unverified. 1 and 2 indicate verified customer.
|
||||
return info.is_legal === 0 || !info.is_legal
|
||||
})
|
||||
|
||||
const isPendingApproval = computed(() => {
|
||||
const info = userStore.userInfo
|
||||
if (!info) return false
|
||||
// If national code is set and user is still unverified (is_legal === 0)
|
||||
return Boolean(info.n_code && info.is_legal === 0)
|
||||
})
|
||||
|
||||
function populateFormFromUser() {
|
||||
const info = userStore.userInfo
|
||||
if (!info) return
|
||||
formState.fname = info.fname || ''
|
||||
formState.lname = info.lname || ''
|
||||
formState.n_code = info.n_code || ''
|
||||
formState.birth_date = info.birth_date || ''
|
||||
formState.city = info.city || ''
|
||||
}
|
||||
|
||||
watch(
|
||||
() => userStore.userInfo,
|
||||
() => {
|
||||
populateFormFromUser()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
async function onSubmit() {
|
||||
try {
|
||||
await formRef.value?.validateFields()
|
||||
loading.value = true
|
||||
normalizeAuthFormFields(formState)
|
||||
|
||||
const payload: any = {
|
||||
...userStore.userInfo,
|
||||
fname: formState.fname,
|
||||
lname: formState.lname,
|
||||
n_code: formState.n_code,
|
||||
birth_date: formState.birth_date,
|
||||
city: formState.city,
|
||||
}
|
||||
|
||||
const res: any = await updateMe(payload)
|
||||
await userStore.getMyInfo()
|
||||
|
||||
const currentLegal = userStore.userInfo?.is_legal
|
||||
if (currentLegal === 1 || currentLegal === 2) {
|
||||
message.success('احراز هویت شما با موفقیت انجام و تأیید شد.')
|
||||
isEditing.value = false
|
||||
} else {
|
||||
isEditing.value = false
|
||||
message.info(res?.message || 'اطلاعات هویتی شما ثبت شد و در انتظار تأیید مدیریت قرار گرفت.')
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMsg = error?.response?.data?.message || error?.message
|
||||
if (errorMsg) {
|
||||
message.error(errorMsg)
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onCheckStatus() {
|
||||
try {
|
||||
checkingStatus.value = true
|
||||
await userStore.getMyInfo()
|
||||
const currentLegal = userStore.userInfo?.is_legal
|
||||
if (currentLegal === 1 || currentLegal === 2) {
|
||||
message.success('حساب کاربری شما تأیید شد!')
|
||||
} else {
|
||||
message.info('اطلاعات شما همچنان در صف بررسی و تایید مدیریت است.')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('خطا در بررسی وضعیت. لطفاً مجدداً تلاش کنید.')
|
||||
} finally {
|
||||
checkingStatus.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onLogout() {
|
||||
await performUserLogout()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.kyc-modal-content {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-label > label) {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #334155;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
import router from '@/router'
|
||||
import { removeToken } from '@/utils'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { useWalletStore } from '@/store/walletStore'
|
||||
import { useWalletAssets } from '@/composables/useWalletAssets'
|
||||
import { resetHomePageData } from '@/views/base/home/composables/useHomePageData'
|
||||
import { resetWalletPageData } from '@/views/base/wallet/composables/useWalletPageData'
|
||||
import { resetAddressPageData } from '@/views/base/address/composables/useAddressPageData'
|
||||
import { resetTransactionsPageData } from '@/views/base/transactions/composables/useTransactionsPageData'
|
||||
import { resetProductsPageData } from '@/views/base/products/composables/useProductsPageData'
|
||||
|
||||
export async function performUserLogout() {
|
||||
try {
|
||||
const userStore = useUserStore()
|
||||
const walletStore = useWalletStore()
|
||||
userStore.userInfo = undefined
|
||||
walletStore.walletInfo = undefined
|
||||
try {
|
||||
const { clearWalletAssets } = useWalletAssets()
|
||||
clearWalletAssets()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try { resetHomePageData() } catch {}
|
||||
try { resetWalletPageData() } catch {}
|
||||
try { resetAddressPageData() } catch {}
|
||||
try { resetTransactionsPageData() } catch {}
|
||||
try { resetProductsPageData() } catch {}
|
||||
} catch (e) {
|
||||
console.error('Logout cleanup error:', e)
|
||||
}
|
||||
|
||||
removeToken()
|
||||
|
||||
try {
|
||||
await router.push({ name: 'login' })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ interface BankDefinition {
|
||||
aliases: string[]
|
||||
}
|
||||
|
||||
const bankLogoModules = import.meta.glob<string>('@/assets/img/banks/*.{png,svg}', {
|
||||
const bankLogoModules = import.meta.glob<string>('../assets/img/banks/*.{png,svg}', {
|
||||
eager: true,
|
||||
import: 'default',
|
||||
})
|
||||
|
||||
@@ -186,6 +186,14 @@ function sumBoursecuHomeProductInventories(
|
||||
return sum
|
||||
}
|
||||
|
||||
export function resolveBoursecuCopperUnit(unit?: string | null): string {
|
||||
const trimmed = (unit ?? '').trim()
|
||||
if (!trimmed || trimmed === 'گرم' || trimmed === 'gram' || trimmed === 'g') {
|
||||
return 'کیلوگرم'
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
function findMetalTotalById(metalTotals: WalletMetalTotal[], metalId: number) {
|
||||
return metalTotals.find((metal) => metal.metal_id === metalId)
|
||||
}
|
||||
@@ -195,7 +203,8 @@ function formatBoursecuMetalRowValue(metal: WalletMetalTotal): string {
|
||||
if (metal.metal_id === METAL_GOLD) {
|
||||
return `${amount} عدد`
|
||||
}
|
||||
return `${amount}${metal.unit ?? ''}`
|
||||
const unit = metal.metal_id === METAL_COPPER ? resolveBoursecuCopperUnit(metal.unit) : (metal.unit ?? 'کیلوگرم')
|
||||
return `${amount} ${unit}`
|
||||
}
|
||||
|
||||
function formatRialNumber(value: number): string {
|
||||
@@ -237,8 +246,11 @@ function buildBoursecuCopperPopupLines(summary: {
|
||||
const metalTotals = summary.metal_totals ?? []
|
||||
const copperMetal = findMetalTotalById(metalTotals, METAL_COPPER)
|
||||
const shamshMetal = findMetalTotalById(metalTotals, METAL_GOLD)
|
||||
const weightUnit = copperMetal?.unit || 'کیلوگرم'
|
||||
const totalWeight = sumBoursecuHomeProductInventories(inventoryCards, countProducts)
|
||||
const weightUnit = resolveBoursecuCopperUnit(copperMetal?.unit)
|
||||
const totalWeight =
|
||||
copperMetal != null && Number(copperMetal.total_weight ?? 0) > 0
|
||||
? Number(copperMetal.total_weight)
|
||||
: sumBoursecuHomeProductInventories(inventoryCards, countProducts)
|
||||
|
||||
const katodCard = findBoursecuProductCard(inventoryCards, countProducts, isBoursecuKatodCard)
|
||||
const maftolCard = findBoursecuProductCard(inventoryCards, countProducts, isBoursecuMaftolCard)
|
||||
@@ -319,10 +331,10 @@ export function buildBoursecuHomeAssetRows(
|
||||
const countProducts = summary.count_products ?? []
|
||||
const metalTotals = summary.metal_totals ?? []
|
||||
const copperMetal = findMetalTotalById(metalTotals, METAL_COPPER)
|
||||
const weightUnit = copperMetal?.unit || 'کیلوگرم'
|
||||
const weightUnit = resolveBoursecuCopperUnit(copperMetal?.unit)
|
||||
const totalCopperWeight =
|
||||
copperMetal != null
|
||||
? Number(copperMetal.total_weight ?? 0)
|
||||
copperMetal != null && Number(copperMetal.total_weight ?? 0) > 0
|
||||
? Number(copperMetal.total_weight)
|
||||
: sumBoursecuHomeProductInventories(inventoryCards, countProducts)
|
||||
|
||||
const cash = Number(summary.cash_wallet ?? 0)
|
||||
@@ -387,7 +399,7 @@ export function resolveSummaryMetalWeight(
|
||||
|
||||
/** متن توضیح مودال دارایی فلزی — بر اساس metal_totals سرور، بدون هاردکد طلا */
|
||||
export function formatMetalAssetsInfoText(metal: WalletMetalTotal): string {
|
||||
const unit = metal.unit || 'گرم'
|
||||
const unit = metal.metal_id === METAL_COPPER ? resolveBoursecuCopperUnit(metal.unit) : (metal.unit || 'کیلوگرم')
|
||||
return `${metal.label}: مجموع کیف محصولات (${metal.product_weight} ${unit}) + موجودی کیف اصلی (${metal.melted_weight} ${unit})`
|
||||
}
|
||||
|
||||
@@ -510,7 +522,7 @@ export function computeMetalRialValue(
|
||||
const metalId = Number(metal.metal_id ?? 0)
|
||||
const unitPrice =
|
||||
livePricesByMetal?.[metalId] ??
|
||||
(metalId === METAL_GOLD ? liveGoldPrice : 0)
|
||||
(metalId === METAL_COPPER || metalId === METAL_GOLD ? liveGoldPrice : 0)
|
||||
|
||||
return sum + weight * unitPrice
|
||||
}, 0)
|
||||
@@ -534,7 +546,7 @@ export function formatInventoryCardValue(card: {
|
||||
display_type?: string
|
||||
}): string {
|
||||
const quantity = Number(card.quantity ?? 0)
|
||||
const unit = card.unit ?? (card.display_type === 'count' ? 'عدد' : 'گرم')
|
||||
const unit = card.unit ?? (card.display_type === 'count' ? 'عدد' : 'کیلوگرم')
|
||||
const formatted =
|
||||
card.display_type === 'count'
|
||||
? usePriceWithoutType(quantity, false)
|
||||
|
||||
@@ -6,8 +6,11 @@ export function isKilogramUnit(unit?: string | null) {
|
||||
return value === TRADE_WEIGHT_UNIT_KILOGRAM || value === 'kilogram' || value === 'kg'
|
||||
}
|
||||
|
||||
/** واحد نمایش از ستینگ — خالی یا نامعتبر = گرم */
|
||||
/** واحد نمایش از ستینگ — در بورس مس پیشفرض همیشه کیلوگرم است */
|
||||
export function resolveTradeWeightUnit(settingValue?: string | number | null) {
|
||||
if (!settingValue) {
|
||||
return TRADE_WEIGHT_UNIT_KILOGRAM
|
||||
}
|
||||
return isKilogramUnit(settingValue?.toString()) ? TRADE_WEIGHT_UNIT_KILOGRAM : TRADE_WEIGHT_UNIT_GRAM
|
||||
}
|
||||
|
||||
|
||||
@@ -175,8 +175,10 @@ import {
|
||||
formatPortfolioProfitPercent,
|
||||
formatPortfolioProfitText,
|
||||
resolveAssetsRialValue,
|
||||
resolveBoursecuCopperUnit,
|
||||
resolveMetalAssetsDisplayLabel,
|
||||
} from '@/utils/productAssets'
|
||||
import { formatWeightAmount } from '@/utils/usePrice'
|
||||
import type { WalletMetalTotal } from '@/models/walletAssets'
|
||||
import { useAssetsPortfolioChart } from './composables/useAssetsPortfolioChart'
|
||||
import assetsCopperBanner from '@/assets/img/assets-copper-banner.webp'
|
||||
@@ -265,10 +267,13 @@ const profitClass = computed(() => {
|
||||
})
|
||||
|
||||
const summaryItems = computed(() => {
|
||||
const items = metalTotals.value.map((metal) => ({
|
||||
label: assetsMetalLabel(metal),
|
||||
value: `${metal.total_weight} ${metal.unit}`,
|
||||
}))
|
||||
const items = metalTotals.value.map((metal) => {
|
||||
const unit = metal.metal_id === 3 ? resolveBoursecuCopperUnit(metal.unit) : (metal.unit || 'کیلوگرم')
|
||||
return {
|
||||
label: assetsMetalLabel(metal),
|
||||
value: `${formatWeightAmount(Number(metal.total_weight ?? 0))} ${unit}`,
|
||||
}
|
||||
})
|
||||
items.push(
|
||||
{ label: 'کیف پول ریالی', value: usePrice(cashWallet.value) },
|
||||
{ label: 'ارزش ریالی مس', value: usePrice(metalRialValue.value) },
|
||||
@@ -287,10 +292,11 @@ const assetRows = computed(() => {
|
||||
const profit = portfolioInsight.value?.unrealized_profit_rial
|
||||
const changeClass = profit == null || profit === 0 ? '' : profit > 0 ? 'is-profit' : 'is-loss'
|
||||
const rows = metalTotals.value.flatMap((metal) => {
|
||||
const unit = metal.metal_id === 3 ? resolveBoursecuCopperUnit(metal.unit) : (metal.unit || 'کیلوگرم')
|
||||
const head = {
|
||||
key: `metal-${metal.metal_id}`,
|
||||
name: assetsMetalLabel(metal),
|
||||
balance: `${metal.total_weight} ${metal.unit}`,
|
||||
balance: `${formatWeightAmount(Number(metal.total_weight ?? 0))} ${unit}`,
|
||||
rial: usePrice(metalRialValue.value),
|
||||
change: pct || '—',
|
||||
changeClass,
|
||||
|
||||
@@ -62,6 +62,7 @@ import {
|
||||
formatPortfolioProfitPercent,
|
||||
formatPortfolioProfitText,
|
||||
resolveAssetsRialValue,
|
||||
resolveBoursecuCopperUnit,
|
||||
} from '@/utils/productAssets'
|
||||
import { formatWeightAmount, usePriceWithoutType } from '@/utils/usePrice'
|
||||
import { getTradeBuyPrice, lastPrice } from '@/utils/price'
|
||||
@@ -76,7 +77,7 @@ const props = withDefaults(
|
||||
const { assetsSummary } = useWalletAssets()
|
||||
|
||||
const portfolio = computed(() => assetsSummary.value?.portfolio)
|
||||
const weightUnit = computed(() => assetsSummary.value?.meta?.weight_unit || 'گرم')
|
||||
const weightUnit = computed(() => resolveBoursecuCopperUnit(assetsSummary.value?.meta?.weight_unit))
|
||||
|
||||
const totalRial = computed(() =>
|
||||
Number(portfolio.value?.total_rial_value ?? assetsSummary.value?.total_rial_value ?? 0)
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
formatPortfolioProfitLabel,
|
||||
formatPortfolioProfitPercent,
|
||||
formatPortfolioProfitText,
|
||||
resolveBoursecuCopperUnit,
|
||||
} from '@/utils/productAssets'
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -55,7 +56,7 @@ const props = withDefaults(
|
||||
const { assetsSummary } = useWalletAssets()
|
||||
|
||||
const portfolio = computed(() => assetsSummary.value?.portfolio)
|
||||
const weightUnit = computed(() => assetsSummary.value?.meta?.weight_unit || 'گرم')
|
||||
const weightUnit = computed(() => resolveBoursecuCopperUnit(assetsSummary.value?.meta?.weight_unit))
|
||||
|
||||
const hasProfitInsight = computed(() => portfolio.value?.unrealized_profit_rial != null)
|
||||
const profitTone = computed(() => formatPortfolioProfitLabel(portfolio.value?.unrealized_profit_rial))
|
||||
|
||||
@@ -20,6 +20,7 @@ import { getBoursecuActionIconSrc } from '@/utils/config'
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import { getReferralShareText, getReferralShareTitle, shareReferralText } from '@/utils/referralShare'
|
||||
import { normalizeAuthFormFields } from '@/utils/persianDigits'
|
||||
import { performUserLogout } from '@/utils/authSession'
|
||||
import { useSettingsStore } from '@/store/settingsStore'
|
||||
import { useTrackingSystemsFeature } from '@/composables/useTrackingSystemsFeature'
|
||||
import { openGoldDayCalculator } from '@/composables/useGoldDayCalculator'
|
||||
@@ -300,18 +301,8 @@ export function useUserPage() {
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
userStore.userInfo = undefined
|
||||
walletStore.walletInfo = undefined
|
||||
clearWalletAssets()
|
||||
resetHomePageData()
|
||||
resetWalletPageData()
|
||||
resetAddressPageData()
|
||||
resetTransactionsPageData()
|
||||
resetProductsPageData()
|
||||
removeToken()
|
||||
showLogoutConfirm.value = false
|
||||
await router.push({ name: 'login' })
|
||||
window.location.reload()
|
||||
await performUserLogout()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -68,11 +68,11 @@
|
||||
<div class="text-black">مقدار {{ getMetalTypeTradeLabel() }} مبادله شده:</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>
|
||||
گرم
|
||||
{{ getWeightUnit() }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="item.order" class="flex justify-between mb-3">
|
||||
<div class="text-black">قیمت هر گرم {{ getTradePriceUnitLabel() }} حین معامله:</div>
|
||||
<div class="text-black">قیمت هر {{ getWeightUnit() }} {{ getTradePriceUnitLabel() }} حین معامله:</div>
|
||||
<div class="flex text-xs items-center number-ltr">
|
||||
<div class="text-base ml-1">{{ usePriceWithoutType(item.order?.price?? 0) }}</div>
|
||||
ریال
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="settingsStore.isReferralFieldVisible"
|
||||
v-if="settingsStore.isReferralFieldVisible || disableForm"
|
||||
:rules="referralIdRules"
|
||||
:class="variant === 'desktop' ? '' : 'home_appear_fade_in_down'"
|
||||
label="کد معرف"
|
||||
|
||||
@@ -56,7 +56,9 @@
|
||||
درباره مس
|
||||
</button>
|
||||
<span class="dauth-form-links__sep" aria-hidden="true">|</span>
|
||||
<span class="dauth-form-links__text">پشتیبانی</span>
|
||||
<button type="button" class="dauth-form-links__link" @click="onSupportClick">
|
||||
پشتیبانی
|
||||
</button>
|
||||
</nav>
|
||||
<div class="dauth-page-footer__status">
|
||||
<span class="dauth-page-footer__status-icon" aria-hidden="true">
|
||||
|
||||
@@ -100,7 +100,7 @@ function handleSuccessDismiss() {
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
v-if="settingsStore.isReferralFieldVisible"
|
||||
v-if="settingsStore.isReferralFieldVisible || disableForm"
|
||||
:rules="referralIdRules"
|
||||
label="کد معرف"
|
||||
name="referral_id"
|
||||
|
||||
@@ -1191,18 +1191,19 @@
|
||||
/* ─── موبایل (<768): لوگو + کارت + پشتیبانی، وسطچین عمودی ─── */
|
||||
@media (max-width: 767px) {
|
||||
.dauth.dauth--boursecu {
|
||||
/* 100vh آیفون پشت نوار سافاری است؛ اسکرول داخل 100dvh تا دکمهٔ ثبتنام برسد */
|
||||
height: 100dvh;
|
||||
max-height: 100dvh;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
/* 100vh آیفون پشت نوار سافاری است؛ اسکرول روان در ارتفاع واقعی بدون قفل شدن */
|
||||
height: auto !important;
|
||||
min-height: 100dvh !important;
|
||||
max-height: none !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
touch-action: pan-y !important;
|
||||
overscroll-behavior-y: auto !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
justify-content: safe center;
|
||||
padding: 12px 0 calc(24px + env(safe-area-inset-bottom, 0px));
|
||||
padding: 16px 0 calc(36px + env(safe-area-inset-bottom, 0px));
|
||||
|
||||
.dauth-page-logo {
|
||||
position: relative;
|
||||
@@ -1367,14 +1368,18 @@
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* شناور پایین روی دکمهٔ ثبتنام مینشیند؛ زیر کارت بیاید */
|
||||
/* دکمه شناور پشتیبانی در موبایل: زیر کارت و بالاتر از لینکهای فوتر نمایش داده شود و شناور روی فرم نیفتد */
|
||||
.dauth-form-support--fab {
|
||||
position: static !important;
|
||||
display: inline-flex !important;
|
||||
top: auto !important;
|
||||
left: auto !important;
|
||||
right: auto !important;
|
||||
bottom: auto !important;
|
||||
width: auto !important;
|
||||
align-self: center;
|
||||
margin: 4px 0 0;
|
||||
margin: 14px auto 0 !important;
|
||||
z-index: 10 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,16 @@ onUnmounted(() => {
|
||||
/* موبایل لاگین: تمامعرض — بدون فریم ۴۲۶ و حاشیه سفید دوطرف */
|
||||
@media screen and (max-width: 767.98px) {
|
||||
html.login-page-active {
|
||||
html,
|
||||
body {
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
height: auto !important;
|
||||
min-height: 100dvh !important;
|
||||
overscroll-behavior-y: auto !important;
|
||||
}
|
||||
|
||||
body {
|
||||
display: block !important;
|
||||
justify-content: unset !important;
|
||||
@@ -71,6 +81,9 @@ onUnmounted(() => {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
background: #0b192e !important;
|
||||
height: auto !important;
|
||||
min-height: 100dvh !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.desktopConfig,
|
||||
@@ -90,12 +103,12 @@ onUnmounted(() => {
|
||||
.app-main,
|
||||
.login-page-root,
|
||||
.dauth.dauth--boursecu {
|
||||
min-height: 0 !important;
|
||||
height: 100dvh !important;
|
||||
max-height: 100dvh !important;
|
||||
min-height: 100dvh !important;
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
background: #0b192e !important;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user