feat(forms): автоподстановка банков и улучшенная обработка телефона

- Step1Phone: добавлена обработка вставки телефона с автоматической очисткой от +7 и обрезкой до 10 цифр
- Step3Payment: заменён Select на AutoComplete для выбора банка с автоподстановкой
- generateConfirmationFormHTML: заменён select на input с datalist для автоподстановки банков в форме подтверждения
- Добавлены скрытые поля bank_id для сохранения ID банка отдельно от названия
- Добавлены файлы для проверки заявки 226564ce

Улучшения UX:
- Пользователь может вводить название банка вместо прокрутки длинного списка
- Автоматическая фильтрация списка банков при вводе
- Предупреждение при обрезке номера телефона при вставке
This commit is contained in:
Fedor
2025-12-02 17:12:25 +03:00
parent ee1c4af5c3
commit b7197e0da5
13 changed files with 476 additions and 58 deletions

View File

@@ -278,6 +278,25 @@ export default function Step1Phone({
maxLength={10}
size="large"
style={{ flex: 1 }}
onPaste={(e) => {
// Обработка вставки: очищаем от +7, пробелов и других символов
e.preventDefault();
const pastedText = (e.clipboardData || (window as any).clipboardData).getData('text');
// Убираем все нецифровые символы
let cleanText = pastedText.replace(/\D/g, '');
// Если начинается с 7 или 8, убираем первую цифру (код страны)
if (cleanText.length === 11 && (cleanText.startsWith('7') || cleanText.startsWith('8'))) {
cleanText = cleanText.substring(1);
}
// Оставляем только первые 10 цифр
cleanText = cleanText.substring(0, 10);
// Устанавливаем очищенное значение
form.setFieldValue('phone', cleanText);
// Показываем предупреждение, если номер был обрезан
if (pastedText.replace(/\D/g, '').length > 10) {
message.warning('Номер автоматически обрезан до 10 цифр');
}
}}
/>
</Space.Compact>
</Form.Item>

View File

@@ -1,12 +1,10 @@
import { useState, useEffect } from 'react';
import { Form, Input, Button, Select, message, Space, Divider } from 'antd';
import { Form, Input, Button, AutoComplete, message, Space, Divider } from 'antd';
import { PhoneOutlined, SafetyOutlined, QrcodeOutlined, MailOutlined, CopyOutlined } from '@ant-design/icons';
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8200';
const NSPK_BANKS_API = 'http://212.193.27.93/api/payouts/dictionaries/nspk-banks';
const { Option } = Select;
interface Bank {
bankid: string;
bankname: string;
@@ -61,15 +59,34 @@ export default function Step3Payment({
setBanks(banksData);
addDebugEvent?.('banks', 'success', `✅ Загружено ${banksData.length} банков`, { count: banksData.length });
// Если есть сохранённый bankName, но нет bankId - пытаемся найти по названию
if (formData.bankName && !formData.bankId) {
// Если есть сохранённый bankName или bankId - восстанавливаем значения
if (formData.bankName) {
const foundBank = banksData.find(b =>
b.bankname.toLowerCase() === formData.bankName.toLowerCase() ||
b.bankname.toLowerCase().includes(formData.bankName.toLowerCase())
);
if (foundBank) {
updateFormData({ bankId: foundBank.bankid });
form.setFieldsValue({ bankId: foundBank.bankid });
updateFormData({
bankId: foundBank.bankid,
bankName: foundBank.bankname
});
form.setFieldsValue({
bankId: foundBank.bankid,
bankName: foundBank.bankname
});
}
} else if (formData.bankId) {
// Если есть только bankId, находим по ID
const foundBank = banksData.find(b => b.bankid === formData.bankId);
if (foundBank) {
updateFormData({
bankId: foundBank.bankid,
bankName: foundBank.bankname
});
form.setFieldsValue({
bankId: foundBank.bankid,
bankName: foundBank.bankname
});
}
}
} catch (error: any) {
@@ -189,12 +206,15 @@ export default function Step3Payment({
}
};
// Инициализация формы с bankId если есть
// Инициализация формы с bankId и bankName если есть
useEffect(() => {
if (formData.bankId) {
form.setFieldsValue({ bankId: formData.bankId });
if (formData.bankId || formData.bankName) {
form.setFieldsValue({
bankId: formData.bankId,
bankName: formData.bankName
});
}
}, [formData.bankId, form]);
}, [formData.bankId, formData.bankName, form]);
return (
<Form
@@ -202,7 +222,8 @@ export default function Step3Payment({
layout="vertical"
initialValues={{
...formData,
bankId: formData.bankId || formData.bankName, // Fallback на bankName для совместимости
bankId: formData.bankId,
bankName: formData.bankName,
}}
style={{ marginTop: 24 }}
>
@@ -377,40 +398,78 @@ export default function Step3Payment({
</div>
</Form.Item>
{/* Скрытое поле для bankId */}
<Form.Item name="bankId" hidden>
<Input />
</Form.Item>
<Form.Item
label="Выберите ваш банк"
name="bankId"
rules={[{ required: true, message: 'Выберите банк для получения выплаты' }]}
>
<Select
placeholder={banksLoading ? "Загрузка списка банков..." : "Выберите банк"}
size="large"
showSearch
loading={banksLoading}
notFoundContent={banksLoading ? "Загрузка..." : "Банк не найден"}
filterOption={(input: string, option: any) => {
const label = option?.label || option?.children;
if (typeof label === 'string') {
return label.toLowerCase().includes(input.toLowerCase());
label="Банк для получения выплаты"
name="bankName"
rules={[
{ required: true, message: 'Выберите банк для получения выплаты' },
{
validator: (_, value) => {
if (!value) {
return Promise.resolve();
}
const foundBank = banks.find(b =>
b.bankname.toLowerCase() === value.toLowerCase()
);
if (!foundBank) {
return Promise.reject(new Error('Выберите банк из списка'));
}
return Promise.resolve();
}
return false;
}
]}
>
<AutoComplete
placeholder={banksLoading ? "Загрузка списка банков..." : "Начните вводить название банка"}
size="large"
loading={banksLoading}
notFoundContent={banksLoading ? "Загрузка..." : "Банк не найден. Попробуйте ввести другое название"}
options={banks.map((bank) => ({
value: bank.bankname,
label: bank.bankname,
}))}
filterOption={(inputValue, option) => {
if (!option?.label) return false;
return option.label.toLowerCase().includes(inputValue.toLowerCase());
}}
onChange={(value) => {
const selectedBank = banks.find(b => b.bankid === value);
onSelect={(value) => {
// При выборе из списка находим банк и сохраняем оба поля
const selectedBank = banks.find(b => b.bankname === value);
if (selectedBank) {
updateFormData({
bankId: selectedBank.bankid,
bankName: selectedBank.bankname
});
// Устанавливаем bankId в скрытое поле
form.setFieldsValue({ bankId: selectedBank.bankid });
}
}}
>
{banks.map((bank) => (
<Option key={bank.bankid} value={bank.bankid} label={bank.bankname}>
{bank.bankname}
</Option>
))}
</Select>
onChange={(value) => {
// При вводе текста ищем точное совпадение по названию
if (typeof value === 'string') {
const foundBank = banks.find(b =>
b.bankname.toLowerCase() === value.toLowerCase()
);
if (foundBank) {
updateFormData({
bankId: foundBank.bankid,
bankName: foundBank.bankname
});
form.setFieldsValue({ bankId: foundBank.bankid });
} else if (value === '') {
// Если поле очищено, очищаем и bankId
updateFormData({ bankId: undefined, bankName: undefined });
form.setFieldsValue({ bankId: undefined });
}
}
}}
style={{ width: '100%' }}
/>
</Form.Item>
<Form.Item>

View File

@@ -775,10 +775,16 @@ export function generateConfirmationFormHTML(data: any): string {
function createBankSelect(root, key, value) {
var id = 'field_' + root + '_' + key + '_' + Math.random().toString(36).slice(2);
var selectHtml = '<select class="inline-field bind bank-select" data-root="' + esc(root) + '" data-key="' + esc(key) + '" id="' + id + '">';
selectHtml += '<option value="">Загрузка списка банков...</option>';
selectHtml += '</select>';
return selectHtml;
var datalistId = 'bank-datalist-' + id;
// Создаём input с datalist для автоподстановки
var inputHtml = '<input type="text" class="inline-field bind bank-select" data-root="' + esc(root) + '" data-key="' + esc(key) + '" id="' + id + '" list="' + datalistId + '" placeholder="Начните вводить название банка" autocomplete="off" />';
inputHtml += '<datalist id="' + datalistId + '" class="bank-datalist">';
inputHtml += '<option value="">Загрузка списка банков...</option>';
inputHtml += '</datalist>';
// Скрытое поле для bank_id
var hiddenId = id + '_id';
inputHtml += '<input type="hidden" class="bank-id-field" data-root="' + esc(root) + '" data-key="bank_id" id="' + hiddenId + '" />';
return inputHtml;
}
function createCheckbox(root, key, checked, labelText, required) {
@@ -1240,6 +1246,19 @@ export function generateConfirmationFormHTML(data: any): string {
var fields = document.querySelectorAll('.bind');
console.log('Found fields:', fields.length);
// Обработка скрытых полей bank_id
var bankIdFields = document.querySelectorAll('.bank-id-field');
Array.prototype.forEach.call(bankIdFields, function(field) {
field.addEventListener('change', function() {
var root = this.getAttribute('data-root');
var value = this.value;
if (root === 'user') {
state.user = state.user || {};
state.user.bank_id = value;
}
});
});
// ✅ Устанавливаем начальный стиль для всех полей и форматируем телефоны
Array.prototype.forEach.call(fields, function(field) {
var key = field.getAttribute('data-key');
@@ -1335,6 +1354,12 @@ export function generateConfirmationFormHTML(data: any): string {
// Обновляем состояние
if (root === 'user') {
state.user = state.user || {};
// Для bank_id не сохраняем название банка, только ID из скрытого поля
if (key === 'bank_id' && this.classList.contains('bank-select')) {
// Это текстовое поле для названия банка - не сохраняем в state
// bank_id будет сохранён из скрытого поля
return;
}
state.user[key] = value;
// Обновляем телефон в СБП
@@ -1437,8 +1462,8 @@ export function generateConfirmationFormHTML(data: any): string {
// Загрузка списка банков СБП
function loadBanks() {
var bankSelects = document.querySelectorAll('.bank-select');
if (bankSelects.length === 0) {
var bankInputs = document.querySelectorAll('.bank-select');
if (bankInputs.length === 0) {
console.log('Bank select fields not found');
return;
}
@@ -1458,32 +1483,109 @@ export function generateConfirmationFormHTML(data: any): string {
return a.bankname.localeCompare(b.bankname, 'ru');
});
// Заполняем все bank-select элементы
Array.prototype.forEach.call(bankSelects, function(select) {
var currentValue = select.getAttribute('data-selected') || state.user?.bank_id || '';
select.innerHTML = '<option value="">Выберите банк</option>';
// Сохраняем список банков глобально для поиска
window.__banksList = banks;
// Заполняем все datalist элементы
Array.prototype.forEach.call(bankInputs, function(input) {
var datalistId = input.getAttribute('list');
var datalist = document.getElementById(datalistId);
var hiddenId = input.id + '_id';
var hiddenField = document.getElementById(hiddenId);
var currentBankId = state.user?.bank_id || '';
var currentBankName = '';
if (!datalist) {
console.error('Datalist not found for input:', input.id);
return;
}
// Очищаем datalist
datalist.innerHTML = '';
// Заполняем datalist опциями
banks.forEach(function(bank) {
var option = document.createElement('option');
option.value = bank.bankid;
option.textContent = bank.bankname;
if (bank.bankid === currentValue) {
option.selected = true;
option.value = bank.bankname;
option.setAttribute('data-bank-id', bank.bankid);
datalist.appendChild(option);
// Если это текущий банк, устанавливаем значение
if (bank.bankid === currentBankId) {
currentBankName = bank.bankname;
}
select.appendChild(option);
});
// Если выбран банк, обновляем стиль
if (currentValue && select.value) {
select.classList.add('filled');
updateFieldStyle(select);
// Устанавливаем текущее значение если есть
if (currentBankName) {
input.value = currentBankName;
if (hiddenField) {
hiddenField.value = currentBankId;
}
input.classList.add('filled');
updateFieldStyle(input);
}
// Обработчик изменения для поиска банка по названию
input.addEventListener('input', function() {
var inputValue = this.value.trim();
var foundBank = null;
// Ищем точное совпадение
if (inputValue) {
foundBank = banks.find(function(b) {
return b.bankname.toLowerCase() === inputValue.toLowerCase();
});
}
if (foundBank) {
// Найден банк - сохраняем ID
if (hiddenField) {
hiddenField.value = foundBank.bankid;
}
state.user = state.user || {};
state.user.bank_id = foundBank.bankid;
this.classList.add('filled');
} else {
// Банк не найден - очищаем ID
if (hiddenField) {
hiddenField.value = '';
}
state.user = state.user || {};
state.user.bank_id = '';
this.classList.remove('filled');
}
updateFieldStyle(this);
updateSubmitButton();
});
// Обработчик выбора из списка
input.addEventListener('change', function() {
var inputValue = this.value.trim();
var foundBank = banks.find(function(b) {
return b.bankname.toLowerCase() === inputValue.toLowerCase();
});
if (foundBank) {
if (hiddenField) {
hiddenField.value = foundBank.bankid;
}
state.user = state.user || {};
state.user.bank_id = foundBank.bankid;
this.classList.add('filled');
updateFieldStyle(this);
}
});
});
})
.catch(function(error) {
console.error('Error loading banks:', error);
Array.prototype.forEach.call(bankSelects, function(select) {
select.innerHTML = '<option value="">Ошибка загрузки банков. Обновите страницу.</option>';
Array.prototype.forEach.call(bankInputs, function(input) {
var datalistId = input.getAttribute('list');
var datalist = document.getElementById(datalistId);
if (datalist) {
datalist.innerHTML = '<option value="">Ошибка загрузки банков. Обновите страницу.</option>';
}
});
});
}
@@ -1552,6 +1654,17 @@ export function generateConfirmationFormHTML(data: any): string {
return;
}
// Собираем bank_id из скрытых полей перед отправкой
var bankIdFields = document.querySelectorAll('.bank-id-field');
Array.prototype.forEach.call(bankIdFields, function(field) {
var root = field.getAttribute('data-root');
var bankId = field.value;
if (root === 'user' && bankId) {
state.user = state.user || {};
state.user.bank_id = bankId;
}
});
window.parent.postMessage({
type: 'claim_confirmed',
data: {