From 2b27b1629657977b2e4c0ee2ddc1c25f24b4ac99 Mon Sep 17 00:00:00 2001 From: mohammad sedighi Date: Fri, 14 Aug 2026 21:05:11 +0330 Subject: [PATCH] fix persian number in input --- components.d.ts | 2 + src/components/BasicInput.vue | 98 ++++++++++--------- src/core/utils/convert.ts | 80 +++++++++++---- src/core/utils/usePrice.ts | 17 ++-- .../accounting/utils/accountingDocAmounts.ts | 5 +- .../configuration/SettingValueField.vue | 23 +---- .../menuPages/configuration/settingTypes.ts | 5 +- .../base/components/WalletChargeForm.vue | 27 ++--- .../customers/base/utils/walletChargeForm.ts | 13 ++- src/utils/productOrderLimit.ts | 13 +-- 10 files changed, 160 insertions(+), 123 deletions(-) diff --git a/components.d.ts b/components.d.ts index c637b35..9f43faa 100644 --- a/components.d.ts +++ b/components.d.ts @@ -94,6 +94,8 @@ declare module '@vue/runtime-core' { SmsCreditWidget: typeof import('./src/components/SmsCreditWidget.vue')['default'] SmsNegativeBalanceHost: typeof import('./src/components/SmsNegativeBalanceHost.vue')['default'] SmsNegativeBalanceModal: typeof import('./src/components/SmsNegativeBalanceModal.vue')['default'] + SubscriptionExpiryHost: typeof import('./src/components/SubscriptionExpiryHost.vue')['default'] + SubscriptionExpiryModal: typeof import('./src/components/SubscriptionExpiryModal.vue')['default'] TahesabRetryModal: typeof import('./src/components/TahesabRetryModal.vue')['default'] ThemeMenuSection: typeof import('./src/core/components/ThemeMenuSection.vue')['default'] ThemeToggle: typeof import('./src/core/components/ThemeToggle.vue')['default'] diff --git a/src/components/BasicInput.vue b/src/components/BasicInput.vue index b2df28b..2f2e191 100644 --- a/src/components/BasicInput.vue +++ b/src/components/BasicInput.vue @@ -36,8 +36,8 @@ import { usePrefix } from '@core/composable/usePrefix' import { computed } from 'vue' import { usePrice } from '@core/utils/usePrice' +import { sanitizeNumericString, parseNumericValue } from '@core/utils/convert' -//Props interface Prop { title?: String value?: String | Number @@ -53,45 +53,66 @@ interface Prop { step?: String | Number } -const addonBgColor = computed(() => props.addonBgColor) -const addonTextColor = computed(() => props.addonTextColor) -//emits -const localValue = computed({ - set(value) { - // console.log(value?.toString().replaceAll(',', '')) - props.type === 'price' - ? emits('update:value', value?.toString().replaceAll(',', '')) - : emits('update:value', value) - }, - get() { - let a = - props.type === 'price' - ? props.value != null && props.value !== '' - ? usePrice(props.value as number, true, '').trim() - : '' - : props.value - // console.log(props.value) - return a - } -}) - const props = withDefaults(defineProps(), { - isPassword: false + isPassword: false, }) const emits = defineEmits(['update:value', 'onchange']) +const addonBgColor = computed(() => props.addonBgColor) +const addonTextColor = computed(() => props.addonTextColor) + +const isNumericInput = computed(() => { + const t = String(props.type ?? '') + return t === 'price' || t === 'number' || t === 'tel' +}) + +function cleanNumeric(raw: unknown): string { + // قیمت و tel معمولاً بدون اعشار لازم؛ number ممکن است اعشار داشته باشد + const allowDecimal = String(props.type) === 'number' + return sanitizeNumericString(raw, { allowDecimal }) +} + +function formatPriceDisplay(raw: unknown): string { + const cleaned = sanitizeNumericString(raw, { allowDecimal: false }) + if (!cleaned) return '' + const num = parseNumericValue(cleaned) + if (!Number.isFinite(num)) return cleaned + return usePrice(num, true, '').trim() +} + +const localValue = computed({ + set(value) { + if (isNumericInput.value) { + emits('update:value', cleanNumeric(value)) + return + } + emits('update:value', value) + }, + get() { + if (props.type === 'price') { + if (props.value == null || props.value === '') return '' + return formatPriceDisplay(props.value) + } + if (isNumericInput.value) { + if (props.value == null || props.value === '') return '' + return cleanNumeric(props.value) + } + return props.value + }, +}) + const handlerChange = (e: any) => { - // emits('onchange', e.target.value) - props.type === 'price' - ? emits('update:value', e.target.value?.toString().replaceAll(',', '')) - : emits('update:value', e.target.value) - props.type === 'price' - ? emits('onchange', e.target.value?.toString().replaceAll(',', '')) - : emits('onchange', e.target.value) - // console.log(e.target.value) + const raw = e?.target?.value + if (isNumericInput.value) { + const cleaned = cleanNumeric(raw) + emits('update:value', cleaned) + emits('onchange', cleaned) + return + } + emits('update:value', raw) + emits('onchange', raw) } -//css style const { prefixCls } = usePrefix('input') @@ -99,10 +120,6 @@ const { prefixCls } = usePrefix('input') @prefix: ~'@{prefixCls}-input'; .@{prefix} { - /* .ant-input-group-addon:first-child { - !* border: #d9d9d9 1px solid; - border-radius: 0 6px 6px 0;*! - }*/ .ant-input-group { direction: ltr; } @@ -113,13 +130,6 @@ const { prefixCls } = usePrefix('input') border-bottom-left-radius: @border-radius-base; border-top-left-radius: @border-radius-base; } - /*.ant-input-group > .ant-input-rtl:first-child, - .ant-input-group-rtl .ant-input-group-addon:first-child { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - border-bottom-left-radius: @border-radius-base; - border-top-left-radius: @border-radius-base; - }*/ .ant-input-group > .ant-input:last-child, .ant-input-group-addon:last-child { border-radius: 0 @border-radius-base @border-radius-base 0; diff --git a/src/core/utils/convert.ts b/src/core/utils/convert.ts index b87f629..185d9df 100644 --- a/src/core/utils/convert.ts +++ b/src/core/utils/convert.ts @@ -1,22 +1,64 @@ -//const e2p = (s: string) => s.replace(/\d/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[+d]) -//const p2a = (s : string) => s.replace(/[۰-۹]/g, (d) => '٠١٢٣٤٥٦٧٨٩'['۰۱۲۳۴۵۶۷۸۹'.indexOf(d)]) -//const a2p = (s : string) => s.replace(/[٠-٩]/g, (d) => '۰۱۲۳۴۵۶۷۸۹'['٠١٢٣٤٥٦٧٨٩'.indexOf(d)]) -//const e2a = (s : string) => s.replace(/\d/g, (d) => '٠١٢٣٤٥٦٧٨٩'[+d]) -//_sample -//e2p('asdf1234') // asdf۱۲۳۴ -//e2a('asdf1234') // asdf١٢٣٤ -//p2e('asdf۱۲۳۴') // asdf1234 -//a2e('asdf١٢٣٤') // asdf1234 -//p2a('asdf۱۲۳۴') // asdf١٢٣٤ -//a2p('asdf١٢٣٤') // asdf۱۲۳۴ -//٠٩١٣٥٥١۷٤٢٥ -//۰۹۱۳۵۵۱۷۴۲۵ +/** + * تبدیل ارقام فارسی/عربی به لاتین (انگلیسی) و پاکسازی ورودی‌های عددی/قیمتی. + */ -const a2e = (s: string) => s.replace(/[٠-٩]/g, (d) => '٠١٢٣٤٥٦٧٨٩'.indexOf(d).toString()) -const p2e = (s: string) => s.replace(/[۰-۹]/g, (d) => '۰۱۲۳۴۵۶۷۸۹'.indexOf(d).toString()) +const PERSIAN_DIGITS = '۰۱۲۳۴۵۶۷۸۹' +const ARABIC_DIGITS = '٠١٢٣٤٥٦٧٨٩' -function persian2english(s: string) { - const temp = p2e(s) - return a2e(temp) +export function persian2english(s: string): string { + return String(s ?? '') + .replace(/[۰-۹]/g, (d) => String(PERSIAN_DIGITS.indexOf(d))) + .replace(/[٠-٩]/g, (d) => String(ARABIC_DIGITS.indexOf(d))) +} + +export type SanitizeNumericOptions = { + /** اجازهٔ نقطه اعشار (پیش‌فرض true) */ + allowDecimal?: boolean +} + +/** + * ارقام فارسی/عربی → لاتین، حذف جداکننده هزار، یکسان‌سازی ممیز. + * خروجی فقط شامل ارقام لاتین و (اختیاری) یک نقطه است. + */ +export function sanitizeNumericString( + raw: unknown, + options: SanitizeNumericOptions = {}, +): string { + const allowDecimal = options.allowDecimal !== false + let s = persian2english(String(raw ?? '').trim()) + .replace(/[٬,\s]/g, '') + .replace(/٫/g, '.') + .replace(/\//g, '.') + + if (!allowDecimal) { + return s.replace(/[^\d-]/g, '').replace(/(?!^)-/g, '') + } + + let out = '' + let hasDot = false + let hasSign = false + for (const ch of s) { + if (ch >= '0' && ch <= '9') { + out += ch + continue + } + if (ch === '.' && !hasDot) { + hasDot = true + out += ch + continue + } + if (ch === '-' && !hasSign && out.length === 0) { + hasSign = true + out += ch + } + } + return out +} + +/** پارس امن عدد از رشتهٔ فارسی/لاتین/با کاما */ +export function parseNumericValue(raw: unknown): number { + const cleaned = sanitizeNumericString(raw) + if (!cleaned || cleaned === '-' || cleaned === '.') return NaN + const n = Number(cleaned) + return Number.isFinite(n) ? n : NaN } -export { persian2english } diff --git a/src/core/utils/usePrice.ts b/src/core/utils/usePrice.ts index 4e89d78..597b4f4 100644 --- a/src/core/utils/usePrice.ts +++ b/src/core/utils/usePrice.ts @@ -1,15 +1,20 @@ -export function usePrice(price: number, digit = true, unit = 'ریال') { - let _price = price.toString() - if (digit) { - _price = _price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') +import { persian2english, parseNumericValue, sanitizeNumericString } from '@core/utils/convert' + +export function usePrice(price: number | string, digit = true, unit = 'ریال') { + const n = typeof price === 'number' ? price : parseNumericValue(price) + let _price = Number.isFinite(n) ? String(n) : persian2english(String(price ?? '')) + if (digit && Number.isFinite(n)) { + _price = _price.replace(/\B(?=(\d{3})+(?!\d))/g, ',') } return `${_price} ${unit}` } export function useTowDecimal(num: string | number): string | number { - const temp = num.toString().match(/^-?\d+(?:\.\d{0,2})?/) + const cleaned = sanitizeNumericString(num, { allowDecimal: true }) + const temp = cleaned.match(/^-?\d+(?:\.\d{0,2})?/) if (temp && temp.length > 0) return temp[0] - return Math.round(+num * 100) / 100 + const n = parseNumericValue(cleaned) + return Number.isFinite(n) ? Math.round(n * 100) / 100 : 0 } export function useFormat(num: string | number, unit = '') { diff --git a/src/pages/menuPages/accounting/utils/accountingDocAmounts.ts b/src/pages/menuPages/accounting/utils/accountingDocAmounts.ts index d938cf0..a23b51e 100644 --- a/src/pages/menuPages/accounting/utils/accountingDocAmounts.ts +++ b/src/pages/menuPages/accounting/utils/accountingDocAmounts.ts @@ -3,6 +3,8 @@ * منبع: ستون amount + JSON فیلد data (بدون ستون DB جدا). */ +import { parseNumericValue } from '@core/utils/convert' + export type AccountingSyncDocRecord = { amount?: number | string | null type?: string | null @@ -12,8 +14,7 @@ export type AccountingSyncDocRecord = { function parseNum(value: unknown): number { if (value == null || value === '') return NaN - const n = Number(String(value).replace(/,/g, '').replace('/', '.')) - return Number.isFinite(n) ? n : NaN + return parseNumericValue(value) } export function parseDocPayload(data?: string | null): Record | null { diff --git a/src/pages/menuPages/configuration/SettingValueField.vue b/src/pages/menuPages/configuration/SettingValueField.vue index 65dbae5..16fa230 100644 --- a/src/pages/menuPages/configuration/SettingValueField.vue +++ b/src/pages/menuPages/configuration/SettingValueField.vue @@ -91,7 +91,7 @@ :disabled="disabled" class="w-full" :formatter="(v: string) => `${v}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')" - :parser="(v: string) => v.replace(/,/g, '')" + :parser="(v: string) => sanitizeNumericString(v, { allowDecimal: false })" @update:value="onPriceChange" /> @@ -144,6 +144,7 @@ import { switchValueFromChecked, type SettingFieldType } from './settingTypes' +import { parseNumericValue, sanitizeNumericString } from '@core/utils/convert' import dayjs, { type Dayjs } from 'dayjs' import customParseFormat from 'dayjs/plugin/customParseFormat' @@ -234,7 +235,7 @@ function onTimeChange(val: Dayjs | string | null | undefined) { } const priceValue = computed(() => { - const n = parseFloat(String(props.modelValue || '').replace(/,/g, '')) + const n = parseNumericValue(props.modelValue || '') return Number.isFinite(n) ? n : undefined }) @@ -243,25 +244,11 @@ function onPriceChange(val: number | string | null) { emit('update:value', '') return } - emit('update:value', String(val)) -} - -function normalizePlainNumberInput(value: string): string { - const latin = value.replace(/[۰-۹]/g, (d) => String('۰۱۲۳۴۵۶۷۸۹'.indexOf(d))) - let out = '' - let hasDot = false - for (const ch of latin.replace(/,/g, '')) { - if (ch >= '0' && ch <= '9') out += ch - else if (ch === '.' && !hasDot) { - hasDot = true - out += ch - } - } - return out + emit('update:value', sanitizeNumericString(val, { allowDecimal: false })) } function onPlainNumberChange(val: string) { - emit('update:value', normalizePlainNumberInput(val ?? '')) + emit('update:value', sanitizeNumericString(val ?? '', { allowDecimal: true })) } diff --git a/src/pages/menuPages/configuration/settingTypes.ts b/src/pages/menuPages/configuration/settingTypes.ts index 26884de..d0abf7c 100644 --- a/src/pages/menuPages/configuration/settingTypes.ts +++ b/src/pages/menuPages/configuration/settingTypes.ts @@ -1,5 +1,6 @@ import type { Const } from '@/model/configuration' import { conditionalOrderEnabledSetting, isStoreApp, priceChannelAccessSetting, priceProviderSetting, showToUsersChatSetting } from '@/utils/config' +import { sanitizeNumericString } from '@core/utils/convert' export type SettingFieldType = | 'text' @@ -517,10 +518,10 @@ export function formatValueForSave( ) } if (type === 'price') { - return value === '' ? '0' : String(value).replace(/,/g, '') + return value === '' ? '0' : sanitizeNumericString(value, { allowDecimal: false }) } if (type === 'number') { - return String(value ?? '').replace(/,/g, '') + return sanitizeNumericString(value ?? '', { allowDecimal: true }) } if (type === 'time') { return normalizeTimeValue(value) diff --git a/src/pages/menuPages/customers/base/components/WalletChargeForm.vue b/src/pages/menuPages/customers/base/components/WalletChargeForm.vue index 28b7483..fbdad49 100644 --- a/src/pages/menuPages/customers/base/components/WalletChargeForm.vue +++ b/src/pages/menuPages/customers/base/components/WalletChargeForm.vue @@ -41,8 +41,8 @@ :place-holder="amountPlaceholder" @onchange="onAmountChange" /> -
- معادل {{ usePrice(Number(normalizedAmount) / 10, true, 'تومان') }} +
+ معادل {{ usePrice(normalizedAmount / 10, true, 'تومان') }}
@@ -58,6 +58,10 @@ import type { FormInstance } from 'ant-design-vue' import type { Charge } from '@/model/wallet' import { inputRules } from '@/utils/inputRules' import { usePrice } from '@core/utils/usePrice' +import { + normalizeAmountRaw, + parseAmount, +} from '@/pages/menuPages/customers/base/utils/walletChargeForm' const props = withDefaults( defineProps<{ @@ -124,35 +128,16 @@ const notePlaceholder = computed(() => isDischarge.value ? 'دلیل کسر موجودی' : 'دلیل افزایش موجودی', ) -/** ارقام فارسی/عربی → لاتین؛ جداکننده هزار حذف؛ ممیز یکسان */ -function normalizeAmountRaw(raw: string | number | undefined | null): string { - return String(raw ?? '') - .trim() - .replace(/[۰-۹]/g, (d) => String('۰۱۲۳۴۵۶۷۸۹'.indexOf(d))) - .replace(/[٠-٩]/g, (d) => String('٠١٢٣٤٥٦٧٨٩'.indexOf(d))) - .replace(/[٬,\s]/g, '') - .replace(/٫/g, '.') -} - function hasDecimalSeparator(raw: string): boolean { - // بعد از حذف جداکننده هزار، نقطه = اعشار return raw.includes('.') } -function parseAmount(raw: string | number | undefined | null): number { - const normalized = normalizeAmountRaw(raw) - if (!normalized) return 0 - const num = Number(normalized) - return Number.isFinite(num) ? Math.abs(num) : NaN -} - const normalizedAmount = computed(() => parseAmount(props.modelValue.amountAbs)) function onAmountChange(raw: string | number) { if (!requiresInteger.value) return const text = normalizeAmountRaw(raw) if (!hasDecimalSeparator(text)) return - // قطع اعشار — فقط بخش صحیح بماند const whole = text.split('.')[0] ?? '' props.modelValue.amountAbs = whole === '' ? undefined : whole } diff --git a/src/pages/menuPages/customers/base/utils/walletChargeForm.ts b/src/pages/menuPages/customers/base/utils/walletChargeForm.ts index 0a5124a..7a3d571 100644 --- a/src/pages/menuPages/customers/base/utils/walletChargeForm.ts +++ b/src/pages/menuPages/customers/base/utils/walletChargeForm.ts @@ -1,4 +1,15 @@ import type { Charge, WalletChargeDirection } from '@/model/wallet' +import { parseNumericValue, sanitizeNumericString } from '@core/utils/convert' + +/** ارقام فارسی/عربی → لاتین؛ جداکننده هزار حذف؛ ممیز یکسان */ +export function normalizeAmountRaw(raw: string | number | undefined | null): string { + return sanitizeNumericString(raw, { allowDecimal: true }) +} + +export function parseAmount(raw: string | number | undefined | null): number { + const n = parseNumericValue(raw) + return Number.isFinite(n) ? Math.abs(n) : 0 +} export function createEmptyChargeForm(customerId?: number): Charge { return { @@ -13,7 +24,7 @@ export function createEmptyChargeForm(customerId?: number): Charge { } export function buildChargePayload(form: Charge, productId?: number | null): Charge { - const amount = Math.abs(Number(form.amountAbs) || 0) + const amount = parseAmount(form.amountAbs) const signed = form.direction === 'discharge' ? -amount : amount const payload: Charge = { value: signed, diff --git a/src/utils/productOrderLimit.ts b/src/utils/productOrderLimit.ts index 5281bb5..329985b 100644 --- a/src/utils/productOrderLimit.ts +++ b/src/utils/productOrderLimit.ts @@ -1,11 +1,4 @@ -const PERSIAN_DIGITS = '۰۱۲۳۴۵۶۷۸۹' -const ARABIC_DIGITS = '٠١٢٣٤٥٦٧٨٩' - -function toAsciiDigits(value: string): string { - return value - .replace(/[۰-۹]/g, (d) => String(PERSIAN_DIGITS.indexOf(d))) - .replace(/[٠-٩]/g, (d) => String(ARABIC_DIGITS.indexOf(d))) -} +import { parseNumericValue, sanitizeNumericString } from '@core/utils/convert' /** پارس مبلغ محدودیت سفارش — ارقام فارسی/عربی و جداکننده هزارگان */ export function parseOrderLimitCashValue(value: unknown): number | undefined { @@ -13,12 +6,12 @@ export function parseOrderLimitCashValue(value: unknown): number | undefined { return undefined } - const raw = toAsciiDigits(String(value).trim().replace(/[,\s٬]/g, '')) + const raw = sanitizeNumericString(value, { allowDecimal: false }) if (raw === '') { return undefined } - const num = Number(raw) + const num = parseNumericValue(raw) if (!Number.isFinite(num) || num < 0) { return undefined }