Files
CRM_Montzar/apps/web/src/components/MoneyInput.vue
T
MahdiandCursor 4abab55a6b feat(web): migrate CRM screens to Ant Design Vue with central theme
Adopt ant-design-vue and a shared antdTheme for leads, pipeline, invoices, and shared form controls under strict single-page governance.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-18 19:39:59 +03:30

138 lines
3.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { nextTick, ref, watch } from 'vue';
import { Input as AInput } from 'ant-design-vue';
import {
caretIndexForDigitCount,
countDigitChars,
formatGroupedInput,
parseMoneyNumber,
} from '@/lib/money';
const model = defineModel<string | number | null>({ default: '' });
const props = withDefaults(
defineProps<{
id?: string;
placeholder?: string;
required?: boolean;
disabled?: boolean;
readonly?: boolean;
/** حداکثر رقم اعشار؛ پیش‌فرض ۲ */
decimals?: number;
/** اگر true باشد مقدار عددی emit می‌شود (نه رشته) */
asNumber?: boolean;
dir?: 'ltr' | 'rtl';
}>(),
{
required: false,
disabled: false,
readonly: false,
decimals: 2,
asNumber: false,
dir: 'ltr',
},
);
const text = ref('');
const focused = ref(false);
const inputRef = ref<{ input?: HTMLInputElement } | HTMLInputElement | null>(null);
function nativeInput(): HTMLInputElement | null {
const el = inputRef.value;
if (!el) return null;
if (el instanceof HTMLInputElement) return el;
return el.input ?? null;
}
function readModelNumber(): number | null {
if (model.value === '' || model.value == null) return null;
const n = typeof model.value === 'number' ? model.value : Number(model.value);
return Number.isFinite(n) ? n : null;
}
function writeModel(n: number | null) {
if (props.asNumber || typeof model.value === 'number') {
model.value = n == null ? (props.asNumber ? 0 : null) : n;
return;
}
model.value = n == null ? '' : String(n);
}
function syncDisplayFromModel() {
if (focused.value) return;
const n = readModelNumber();
text.value = n == null ? '' : formatGroupedInput(n, props.decimals);
}
watch(model, syncDisplayFromModel, { immediate: true });
watch(
() => props.decimals,
() => syncDisplayFromModel(),
);
function onFocus() {
focused.value = true;
}
function onBlur() {
focused.value = false;
const n = parseMoneyNumber(text.value, props.decimals);
writeModel(n);
text.value = n == null ? '' : formatGroupedInput(n, props.decimals);
}
async function onInput(event: Event) {
const el = event.target as HTMLInputElement;
const raw = el.value;
const caret = el.selectionStart ?? raw.length;
const digitsBefore = countDigitChars(raw.slice(0, caret));
const endsWithSep = /[.\u066B]$/.test(normalizeSep(raw));
const n = parseMoneyNumber(raw, props.decimals);
if (n == null) {
const cleaned = raw.replace(/[^\d۰-۹٠-٩.\u066B٬,،\s-]/g, '');
text.value = cleaned;
writeModel(null);
return;
}
let display = formatGroupedInput(n, props.decimals);
if (endsWithSep && props.decimals > 0 && Number.isInteger(n)) {
display += '٫';
}
text.value = display;
writeModel(n);
await nextTick();
const input = nativeInput();
if (!input) return;
const nextCaret = caretIndexForDigitCount(display, digitsBefore);
input.setSelectionRange(nextCaret, nextCaret);
}
function normalizeSep(raw: string): string {
return raw.replace(/٫/g, '.');
}
</script>
<template>
<a-input
:id="id"
ref="inputRef"
type="text"
inputmode="decimal"
autocomplete="off"
:dir="dir"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:value="text"
style="width: 100%; font-variant-numeric: tabular-nums"
@focus="onFocus"
@blur="onBlur"
@input="onInput"
/>
</template>