2026-11-05 17:17:29 +03:30
|
|
|
|
<template>
|
|
|
|
|
|
<a-modal
|
2026-11-10 11:48:28 +03:30
|
|
|
|
:open="requiresKyc"
|
2026-11-05 17:17:29 +03:30
|
|
|
|
:closable="false"
|
|
|
|
|
|
:mask-closable="false"
|
|
|
|
|
|
:keyboard="false"
|
|
|
|
|
|
:footer="null"
|
|
|
|
|
|
:centered="true"
|
2026-11-10 11:48:28 +03:30
|
|
|
|
:z-index="APP_Z_MODAL"
|
2026-11-05 17:17:29 +03:30
|
|
|
|
:width="modalWidth"
|
2026-11-10 11:48:28 +03:30
|
|
|
|
:get-container="getModalContainer"
|
2026-11-05 17:17:29 +03:30
|
|
|
|
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>
|
2026-11-10 11:48:28 +03:30
|
|
|
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
2026-11-05 17:17:29 +03:30
|
|
|
|
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'
|
2026-11-10 11:48:28 +03:30
|
|
|
|
import { isCustomerIdentityVerified } from '@/utils/customerIdentity'
|
|
|
|
|
|
import { APP_Z_MODAL, getModalContainer } from '@/utils/zIndex'
|
2026-11-05 17:17:29 +03:30
|
|
|
|
|
|
|
|
|
|
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'))
|
|
|
|
|
|
|
2026-11-10 11:48:28 +03:30
|
|
|
|
const requiresKyc = computed(() => {
|
2026-11-05 17:17:29 +03:30
|
|
|
|
if (!getToken()) return false
|
|
|
|
|
|
if (route.name === 'login' || route.name === 'tvMonitor' || route.name === 'aboutSoftware') {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
const info = userStore.userInfo
|
|
|
|
|
|
if (!info) return false
|
2026-11-10 11:48:28 +03:30
|
|
|
|
return !isCustomerIdentityVerified(info.is_legal)
|
2026-11-05 17:17:29 +03:30
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const isPendingApproval = computed(() => {
|
|
|
|
|
|
const info = userStore.userInfo
|
|
|
|
|
|
if (!info) return false
|
2026-11-10 11:48:28 +03:30
|
|
|
|
return Boolean(
|
|
|
|
|
|
String(info.n_code ?? '').trim() && !isCustomerIdentityVerified(info.is_legal),
|
|
|
|
|
|
)
|
2026-11-05 17:17:29 +03:30
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
2026-11-10 11:48:28 +03:30
|
|
|
|
if (isCustomerIdentityVerified(userStore.userInfo?.is_legal)) {
|
2026-11-05 17:17:29 +03:30
|
|
|
|
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()
|
2026-11-10 11:48:28 +03:30
|
|
|
|
if (isCustomerIdentityVerified(userStore.userInfo?.is_legal)) {
|
2026-11-05 17:17:29 +03:30
|
|
|
|
message.success('حساب کاربری شما تأیید شد!')
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.info('اطلاعات شما همچنان در صف بررسی و تایید مدیریت است.')
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('خطا در بررسی وضعیت. لطفاً مجدداً تلاش کنید.')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
checkingStatus.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function onLogout() {
|
|
|
|
|
|
await performUserLogout()
|
|
|
|
|
|
}
|
2026-11-10 11:48:28 +03:30
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
if (getToken() && !userStore.userInfo) {
|
|
|
|
|
|
void userStore.getMyInfo()
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-11-05 17:17:29 +03:30
|
|
|
|
</script>
|
|
|
|
|
|
|
2026-11-10 11:48:28 +03:30
|
|
|
|
<style lang="less">
|
|
|
|
|
|
.required-kyc-modal-wrap {
|
|
|
|
|
|
.ant-modal-content {
|
|
|
|
|
|
color: #1e293b;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
|
2026-11-05 17:17:29 +03:30
|
|
|
|
<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>
|