Merge remote-tracking branch 'origin/boursecu' into boursecu
This commit is contained in:
@@ -1,15 +1,22 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { getAllPrices } from '@/api/home'
|
||||
import type { Product } from '@/models/products'
|
||||
import { getProductsCatalog, getProductsCatalogRef } from '@/composables/useProductsCatalog'
|
||||
import { getProductBuyPrice, resolvePriceOffsetRatio } from '@/utils/productPrice'
|
||||
import { isKatodMesGroup, isMaftolMesGroup, isShamshMesGroup } from '@/utils/productDealMethod'
|
||||
import { getTradeBuyPrice, getTradeSellPrice, lastPrice } from '@/utils/price'
|
||||
import { getTradeBuyPrice, getTradeSellPrice } from '@/utils/price'
|
||||
|
||||
export type ChartMetalType = 'katod' | 'maftol' | 'shamsh'
|
||||
|
||||
type ChartPoint = [number, number]
|
||||
|
||||
/**
|
||||
* مارکآپ ثابت روی قیمت بورس کالا برای نمودار:
|
||||
* کاتد +۳٪ ، مفتول +۶٪ ، شمش +۱۲٪
|
||||
*/
|
||||
export const CHART_METAL_MARKUP_RATIO: Record<ChartMetalType, number> = {
|
||||
katod: 0.03,
|
||||
maftol: 0.06,
|
||||
shamsh: 0.12,
|
||||
}
|
||||
|
||||
/** Module-level chart state — one dataset shared by mobile and desktop PriceChart instances. */
|
||||
const priceChartSeries = ref<{ name: string; data: ChartPoint[] }[]>([
|
||||
{ name: 'قیمت', data: [] },
|
||||
@@ -20,65 +27,18 @@ const chartLoading = ref(false)
|
||||
const selectedChartMetal = ref<ChartMetalType>('katod')
|
||||
const catalogProducts = getProductsCatalogRef()
|
||||
|
||||
function resolveChartProducts(products: Product[]) {
|
||||
return {
|
||||
katod: findKatodProduct(products),
|
||||
maftol: findMaftolProduct(products),
|
||||
shamsh: findShamshProduct(products),
|
||||
}
|
||||
}
|
||||
const maftolChartRatio = computed(() => CHART_METAL_MARKUP_RATIO.maftol)
|
||||
const shamshChartRatio = computed(() => CHART_METAL_MARKUP_RATIO.shamsh)
|
||||
|
||||
/** نسبت مفتول به کاتد — reactive به قیمت لحظهای و کاتالوگ محصولات */
|
||||
const maftolChartRatio = computed(() => {
|
||||
void lastPrice.value?.price_last
|
||||
|
||||
const { katod, maftol } = resolveChartProducts(catalogProducts.value)
|
||||
return deriveMetalChartRatio(maftol, katod)
|
||||
})
|
||||
|
||||
/** نسبت شمش مس به کاتد */
|
||||
const shamshChartRatio = computed(() => {
|
||||
void lastPrice.value?.price_last
|
||||
|
||||
const { katod, shamsh } = resolveChartProducts(catalogProducts.value)
|
||||
return deriveMetalChartRatio(shamsh, katod)
|
||||
})
|
||||
|
||||
const selectedMetalChartRatio = computed(() => {
|
||||
if (selectedChartMetal.value === 'maftol') {
|
||||
return maftolChartRatio.value
|
||||
}
|
||||
if (selectedChartMetal.value === 'shamsh') {
|
||||
return shamshChartRatio.value
|
||||
}
|
||||
return 0
|
||||
})
|
||||
const selectedMetalChartRatio = computed(
|
||||
() => CHART_METAL_MARKUP_RATIO[selectedChartMetal.value],
|
||||
)
|
||||
|
||||
let chartFetchPromise: Promise<void> | null = null
|
||||
let chartInitialized = false
|
||||
let metalOffsetPromise: Promise<void> | null = null
|
||||
let metalOffsetLoaded = false
|
||||
|
||||
function findKatodProduct(products: Product[]) {
|
||||
return (
|
||||
products.find((product) => isKatodMesGroup(product.group_type))
|
||||
?? products.find((product) => (product.title ?? '').includes('کاتد'))
|
||||
)
|
||||
}
|
||||
|
||||
function findMaftolProduct(products: Product[]) {
|
||||
return (
|
||||
products.find((product) => isMaftolMesGroup(product.group_type))
|
||||
?? products.find((product) => (product.title ?? '').includes('مفتول'))
|
||||
)
|
||||
}
|
||||
|
||||
function findShamshProduct(products: Product[]) {
|
||||
return products.find((product) =>
|
||||
isShamshMesGroup(product.group_type, product.title)
|
||||
)
|
||||
}
|
||||
|
||||
function applyChartRatio(basePrice: number, ratio: number) {
|
||||
if (!ratio) {
|
||||
return basePrice
|
||||
@@ -87,7 +47,7 @@ function applyChartRatio(basePrice: number, ratio: number) {
|
||||
return Math.round(basePrice + basePrice * ratio)
|
||||
}
|
||||
|
||||
/** بالاترین/پایینترین امروز — برای boursecu با نسبت مفتول/شمش نسبت به کاتد */
|
||||
/** بالاترین/پایینترین امروز — همان مارکآپ ثابت فلز روی قیمت بورس کالا */
|
||||
export function resolveMetalDayRangePrices(options: {
|
||||
price: { up?: { buy?: number; sell?: number }; buy?: number; sell?: number; price_last?: number } | null | undefined
|
||||
metal: ChartMetalType
|
||||
@@ -95,44 +55,14 @@ export function resolveMetalDayRangePrices(options: {
|
||||
}) {
|
||||
const baseHigh = options.price?.up?.buy ?? getTradeBuyPrice(options.price as Parameters<typeof getTradeBuyPrice>[0])
|
||||
const baseLow = options.price?.up?.sell ?? getTradeSellPrice(options.price as Parameters<typeof getTradeSellPrice>[0])
|
||||
|
||||
if (options.metal === 'katod') {
|
||||
return { high: baseHigh, low: baseLow }
|
||||
}
|
||||
const ratio = options.metalRatio || CHART_METAL_MARKUP_RATIO[options.metal]
|
||||
|
||||
return {
|
||||
high: applyChartRatio(baseHigh, options.metalRatio),
|
||||
low: applyChartRatio(baseLow, options.metalRatio),
|
||||
high: applyChartRatio(baseHigh, ratio),
|
||||
low: applyChartRatio(baseLow, ratio),
|
||||
}
|
||||
}
|
||||
|
||||
function deriveMetalChartRatio(metalProduct?: Product, katodProduct?: Product) {
|
||||
const metalOffset = resolvePriceOffsetRatio(metalProduct?.price_offset_percent)
|
||||
if (metalOffset) {
|
||||
return metalOffset
|
||||
}
|
||||
|
||||
if (!metalProduct) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const liveBuy = getTradeBuyPrice(lastPrice.value)
|
||||
const katodLive = katodProduct ? getProductBuyPrice(katodProduct) : liveBuy
|
||||
const metalLive = getProductBuyPrice(metalProduct)
|
||||
|
||||
const katodStatic = Number(katodProduct?.price ?? 0) + Number(katodProduct?.price_profit ?? 0)
|
||||
const metalStatic = Number(metalProduct.price ?? 0) + Number(metalProduct.price_profit ?? 0)
|
||||
|
||||
const referencePrice = katodLive || liveBuy || katodStatic
|
||||
const derivedMetalPrice = metalLive || metalStatic
|
||||
|
||||
if (!referencePrice || !derivedMetalPrice || referencePrice === derivedMetalPrice) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return (derivedMetalPrice - referencePrice) / referencePrice
|
||||
}
|
||||
|
||||
function normalizeChartTimestamp(timestamp: number) {
|
||||
return timestamp < 1_000_000_000_000 ? timestamp * 1000 : timestamp
|
||||
}
|
||||
@@ -165,15 +95,9 @@ function buildChartPoints(items: Array<{ timestamp: number; value: number }>): C
|
||||
|
||||
function syncChartSeries() {
|
||||
const ratio = selectedMetalChartRatio.value
|
||||
const data =
|
||||
selectedChartMetal.value !== 'katod'
|
||||
? rawChartPoints.value.map(
|
||||
([timestamp, value]) =>
|
||||
[timestamp, applyChartRatio(value, ratio)] as ChartPoint
|
||||
)
|
||||
: rawChartPoints.value
|
||||
|
||||
priceChartSeries.value[0].data = data
|
||||
priceChartSeries.value[0].data = rawChartPoints.value.map(
|
||||
([timestamp, value]) => [timestamp, applyChartRatio(value, ratio)] as ChartPoint,
|
||||
)
|
||||
}
|
||||
|
||||
watch([selectedChartMetal, maftolChartRatio, shamshChartRatio], () => {
|
||||
@@ -242,10 +166,7 @@ function updateChartData(timeline: string) {
|
||||
|
||||
function setChartMetal(metal: ChartMetalType) {
|
||||
selectedChartMetal.value = metal
|
||||
|
||||
if (metal !== 'katod' && !catalogProducts.value.length) {
|
||||
void loadMetalChartOffsets(true)
|
||||
}
|
||||
syncChartSeries()
|
||||
}
|
||||
|
||||
export function usePriceChart() {
|
||||
|
||||
@@ -118,31 +118,12 @@
|
||||
name="token"
|
||||
style="position: relative"
|
||||
>
|
||||
<OtpInput
|
||||
<SmsOtpField
|
||||
ref="refOtpInput"
|
||||
:is-input-num="true"
|
||||
:num-inputs="4"
|
||||
:should-auto-focus="true"
|
||||
autocomplete="one-time-code"
|
||||
:class="variant === 'desktop' ? 'dauth-otp justify-center' : 'mt-5 justify-center'"
|
||||
input-classes="otp-input"
|
||||
separator=""
|
||||
style="direction: ltr"
|
||||
:value="userFormState.token || ''"
|
||||
@on-change="handleOnChange"
|
||||
@on-complete="handleOnComplete"
|
||||
/>
|
||||
<!-- برای WebOTP / پیشنهاد کیبورد: autocomplete باید روی input واقعی باشد -->
|
||||
<input
|
||||
class="web-otp-assist"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
autocomplete="one-time-code"
|
||||
maxlength="4"
|
||||
tabindex="-1"
|
||||
aria-hidden="true"
|
||||
:value="userFormState.token || ''"
|
||||
@input="onWebOtpAssistInput"
|
||||
v-model="userFormState.token"
|
||||
:class="variant === 'desktop' ? 'dauth-otp' : undefined"
|
||||
@change="handleOnChange"
|
||||
@complete="handleOnComplete"
|
||||
/>
|
||||
</AFormItem>
|
||||
|
||||
@@ -333,8 +314,8 @@ import { mobileValidation, required } from '@/utils/inputRules'
|
||||
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import { authenticateWithFingerprint, getToken, loading, removeToken, setToken, URLUserType } from '@/utils'
|
||||
import {computed, reactive, ref, inject, unref, nextTick, onBeforeUnmount} from 'vue'
|
||||
import OtpInput from 'vue3-otp-input'
|
||||
import {computed, reactive, ref, inject, unref} from 'vue'
|
||||
import SmsOtpField from '@/components/SmsOtpField.vue'
|
||||
import type { Login } from '@/models/login'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
@@ -357,8 +338,8 @@ import {
|
||||
isTWA
|
||||
} from "@/utils/config";
|
||||
import BasicActionSheet from "@/components/BasicPacks/BasicActionSheet.vue";
|
||||
import { normalizeAuthFormFields, persianToEnglishDigits } from '@/utils/persianDigits'
|
||||
import { listenForSmsOtp } from '@/utils/webOtp'
|
||||
import { normalizeAuthFormFields } from '@/utils/persianDigits'
|
||||
import { beginSmsOtpCapture, clearPendingSmsOtp } from '@/utils/webOtp'
|
||||
import {getConfig} from "@/api/config";
|
||||
import shareAppleIcon from '@/assets/img/share-apple.png'
|
||||
import addToHomeScreenIcon from '@/assets/img/Add-to-homeScreen.png'
|
||||
@@ -390,8 +371,7 @@ const walletStore = useWalletStore()
|
||||
const setting=useSettingsStore()
|
||||
const data = reactive(['ورود با پیامک', 'ورود با کلمه عبور'])
|
||||
const value = ref(data[0])
|
||||
const refOtpInput = ref<InstanceType<typeof OtpInput> | null>(null)
|
||||
let otpAbort: AbortController | null = null
|
||||
const refOtpInput = ref<InstanceType<typeof SmsOtpField> | null>(null)
|
||||
|
||||
const userFormState = reactive<Login>({
|
||||
provider: 'customer'
|
||||
@@ -402,18 +382,16 @@ onFinish(() => {
|
||||
enableSendAgainCode.value = true
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
otpAbort?.abort()
|
||||
otpAbort = null
|
||||
})
|
||||
|
||||
async function reSendCode() {
|
||||
userFormState.token = undefined
|
||||
;(refOtpInput.value as { clearInput?: () => void } | null)?.clearInput?.()
|
||||
clearPendingSmsOtp()
|
||||
refOtpInput.value?.clearInput?.()
|
||||
enableSendAgainCode.value = false
|
||||
normalizeAuthFormFields(userFormState)
|
||||
beginSmsOtpCapture()
|
||||
await loginCustomer(userFormState)
|
||||
getcode()
|
||||
refOtpInput.value?.startWebOtp?.()
|
||||
refOtpInput.value?.focusInput?.()
|
||||
reset()
|
||||
}
|
||||
|
||||
@@ -489,10 +467,11 @@ function phoneVerification() {
|
||||
userFormState.token = undefined
|
||||
userFormState.password = undefined
|
||||
userFormState.provider = 'customer'
|
||||
// مهم: قبل از ارسال SMS گوش بده تا دیالوگ Allow به listener زنده وصل باشد
|
||||
beginSmsOtpCapture()
|
||||
const { id } = await loginCustomer(userFormState)
|
||||
userFormState.id = id
|
||||
stepTwoVerify.value = true
|
||||
getcode()
|
||||
reset()
|
||||
} else {
|
||||
const { access_token } = await verifyToken(userFormState)
|
||||
@@ -510,37 +489,6 @@ function phoneVerification() {
|
||||
}
|
||||
})
|
||||
}
|
||||
const getcode = () => {
|
||||
otpAbort?.abort()
|
||||
otpAbort = listenForSmsOtp((code) => {
|
||||
void applySmsOtpCode(code)
|
||||
})
|
||||
}
|
||||
|
||||
async function applySmsOtpCode(code: string) {
|
||||
userFormState.token = code
|
||||
await nextTick()
|
||||
const input = refOtpInput.value as { fillInput?: (v: string) => void } | null
|
||||
if (input?.fillInput) {
|
||||
try {
|
||||
input.fillInput(code)
|
||||
return
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
}
|
||||
await phoneVerification()
|
||||
}
|
||||
|
||||
function onWebOtpAssistInput(event: Event) {
|
||||
const raw = (event.target as HTMLInputElement | null)?.value ?? ''
|
||||
const code = persianToEnglishDigits(raw).replace(/\D/g, '').slice(0, 4)
|
||||
if (code.length === 4) {
|
||||
void applySmsOtpCode(code)
|
||||
} else {
|
||||
userFormState.token = code
|
||||
}
|
||||
}
|
||||
|
||||
const goToEducate = () => {
|
||||
router.push({ name: "educate" })
|
||||
@@ -570,18 +518,7 @@ async function proceedWithInstall() {
|
||||
transform: translate(100%, 0);
|
||||
}
|
||||
|
||||
/* Must stay in DOM (not display:none) so Chrome/Safari can autofill OTP */
|
||||
.web-otp-assist {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
.error-token {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import OtpInput from 'vue3-otp-input'
|
||||
import type SmsOtpField from '@/components/SmsOtpField.vue'
|
||||
import type { Login } from '@/models/login'
|
||||
import {
|
||||
authenticateWithFingerprint,
|
||||
@@ -11,14 +11,13 @@ import {
|
||||
} from '@/utils'
|
||||
import useCountdownTimer from '@/utils/useCountdownTimer'
|
||||
import { loginCustomer, verifyToken } from '@/api/login'
|
||||
import router from '@/router'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { useWalletStore } from '@/store/walletStore'
|
||||
import { useSettingsStore } from '@/store/settingsStore'
|
||||
import { setTruth } from '@/app/authBoot'
|
||||
import { completeLoginNavigation } from '@/layouts/composables/useMobileScrollReset'
|
||||
import { normalizeAuthFormFields } from '@/utils/persianDigits'
|
||||
import { listenForSmsOtp } from '@/utils/webOtp'
|
||||
import { beginSmsOtpCapture, clearPendingSmsOtp } from '@/utils/webOtp'
|
||||
|
||||
export function useLoginForm() {
|
||||
const { time, onFinish, reset } = useCountdownTimer('00:02:00', 'mm:ss')
|
||||
@@ -27,11 +26,10 @@ export function useLoginForm() {
|
||||
const formRef = ref<FormInstance>()
|
||||
const userStore = useUserStore()
|
||||
const walletStore = useWalletStore()
|
||||
const refOtpInput = ref<InstanceType<typeof OtpInput> | null>(null)
|
||||
const refOtpInput = ref<InstanceType<typeof SmsOtpField> | null>(null)
|
||||
const isSMS = ref(true)
|
||||
const isRegister = ref<boolean | undefined>()
|
||||
const showPWAInstallGuide = ref(false)
|
||||
let otpAbort: AbortController | null = null
|
||||
|
||||
const userFormState = reactive<Login>({
|
||||
provider: 'customer',
|
||||
@@ -42,17 +40,19 @@ export function useLoginForm() {
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
otpAbort?.abort()
|
||||
otpAbort = null
|
||||
// SmsOtpField cleans its own listener
|
||||
})
|
||||
|
||||
async function reSendCode() {
|
||||
userFormState.token = undefined
|
||||
;(refOtpInput.value as { clearInput?: () => void } | null)?.clearInput?.()
|
||||
clearPendingSmsOtp()
|
||||
refOtpInput.value?.clearInput?.()
|
||||
enableSendAgainCode.value = false
|
||||
normalizeAuthFormFields(userFormState)
|
||||
beginSmsOtpCapture()
|
||||
await loginCustomer(userFormState)
|
||||
getcode()
|
||||
refOtpInput.value?.startWebOtp?.()
|
||||
refOtpInput.value?.focusInput?.()
|
||||
reset()
|
||||
}
|
||||
|
||||
@@ -92,31 +92,8 @@ export function useLoginForm() {
|
||||
await completeLoginNavigation()
|
||||
}
|
||||
|
||||
async function applySmsOtpCode(code: string) {
|
||||
userFormState.token = code
|
||||
await nextTick()
|
||||
const input = refOtpInput.value as { fillInput?: (v: string) => void } | null
|
||||
if (input?.fillInput) {
|
||||
try {
|
||||
input.fillInput(code)
|
||||
// fillInput → on-complete → handleOnComplete → phoneVerification
|
||||
return
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
}
|
||||
await phoneVerification()
|
||||
}
|
||||
|
||||
function getcode() {
|
||||
otpAbort?.abort()
|
||||
otpAbort = listenForSmsOtp((code) => {
|
||||
void applySmsOtpCode(code)
|
||||
})
|
||||
}
|
||||
|
||||
function phoneVerification() {
|
||||
formRef.value?.validateFields().then(async () => {
|
||||
return formRef.value?.validateFields().then(async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
normalizeAuthFormFields(userFormState)
|
||||
@@ -125,10 +102,10 @@ export function useLoginForm() {
|
||||
userFormState.token = undefined
|
||||
userFormState.password = undefined
|
||||
userFormState.provider = 'customer'
|
||||
beginSmsOtpCapture()
|
||||
const { id } = await loginCustomer(userFormState)
|
||||
userFormState.id = id
|
||||
stepTwoVerify.value = true
|
||||
getcode()
|
||||
reset()
|
||||
} else {
|
||||
const { access_token } = await verifyToken(userFormState)
|
||||
|
||||
@@ -3,15 +3,13 @@ import { mobileValidation, required } from '@/utils/inputRules'
|
||||
import BasicInput from '@/components/BasicPacks/BasicInput.vue'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import OtpInput from 'vue3-otp-input'
|
||||
import SmsOtpField from '@/components/SmsOtpField.vue'
|
||||
import { useConfigStore } from '@/store/configStore'
|
||||
import { useLoginPageContext } from '../../composables/useLoginPage'
|
||||
import DesktopPwaGuideModal from './DesktopPwaGuideModal.vue'
|
||||
import router from '@/router'
|
||||
import {isTWA} from "@/utils/config";
|
||||
import {useUserStore} from "@/store/userStore";
|
||||
import {useSettingsStore} from "@/store/settingsStore";
|
||||
import { persianToEnglishDigits } from '@/utils/persianDigits'
|
||||
|
||||
const emit = defineEmits<{
|
||||
register: [val: boolean]
|
||||
@@ -42,17 +40,6 @@ const {
|
||||
proceedWithInstall,
|
||||
} = useLoginPageContext().loginForm
|
||||
|
||||
function onWebOtpAssistInput(event: Event) {
|
||||
const raw = (event.target as HTMLInputElement | null)?.value ?? ''
|
||||
const code = persianToEnglishDigits(raw).replace(/\D/g, '').slice(0, 4)
|
||||
if (code.length === 4) {
|
||||
userFormState.token = code
|
||||
handleOnComplete(code)
|
||||
} else {
|
||||
handleOnChange(code)
|
||||
}
|
||||
}
|
||||
|
||||
function goToEducate() {
|
||||
router.push({ name: 'educate' })
|
||||
}
|
||||
@@ -137,30 +124,12 @@ function goToAboutUs() {
|
||||
name="token"
|
||||
style="position: relative"
|
||||
>
|
||||
<OtpInput
|
||||
<SmsOtpField
|
||||
ref="refOtpInput"
|
||||
:is-input-num="true"
|
||||
:num-inputs="4"
|
||||
:should-auto-focus="true"
|
||||
autocomplete="one-time-code"
|
||||
class="dauth-otp justify-center"
|
||||
input-classes="otp-input"
|
||||
separator=""
|
||||
style="direction: ltr"
|
||||
:value="userFormState.token || ''"
|
||||
@on-change="handleOnChange"
|
||||
@on-complete="handleOnComplete"
|
||||
/>
|
||||
<input
|
||||
class="web-otp-assist"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
autocomplete="one-time-code"
|
||||
maxlength="4"
|
||||
tabindex="-1"
|
||||
aria-hidden="true"
|
||||
:value="userFormState.token || ''"
|
||||
@input="onWebOtpAssistInput"
|
||||
v-model="userFormState.token"
|
||||
class="dauth-otp"
|
||||
@change="handleOnChange"
|
||||
@complete="handleOnComplete"
|
||||
/>
|
||||
</AFormItem>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user