From 2150792d2084453039656c1df21a62c208dd0a04 Mon Sep 17 00:00:00 2001 From: Michael Chus Date: Wed, 21 Jan 2026 04:02:18 +0300 Subject: [PATCH] =?UTF-8?q?refactor:=20=D0=BF=D0=B5=D1=80=D0=B5=D1=85?= =?UTF-8?q?=D0=BE=D0=B4=20=D0=BD=D0=B0=20=D1=87=D0=B5=D0=BA=D0=B1=D0=BE?= =?UTF-8?q?=D0=BA=D1=81=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D1=8B=D0=B1?= =?UTF-8?q?=D0=BE=D1=80=D0=B0=20=D1=81=D1=82=D1=80=D0=BE=D0=BA=20=D0=B8=20?= =?UTF-8?q?=D1=83=D0=BF=D1=80=D0=BE=D1=89=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B8?= =?UTF-8?q?=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлены чекбоксы для множественного выбора строк - Убрана кнопка "Сохранить строку" (сохранение по Enter) - Изменена логика кнопки "Вставить": добавляет строку над выбранной или внизу - Убрана подсказка про Ctrl+Click - Упрощен код выбора строк (rowClick теперь не нужен) - Очищено визуальное оформление --- public/app.js | 163 ++++++++++++++++++---------------------------- public/index.html | 53 +++++++-------- 2 files changed, 91 insertions(+), 125 deletions(-) diff --git a/public/app.js b/public/app.js index 12f120b..38e1b16 100644 --- a/public/app.js +++ b/public/app.js @@ -110,12 +110,25 @@ async function selectTable(schema, tableName) { `/api/table/meta?schema=${encodeURIComponent(schema)}&table=${encodeURIComponent(tableName)}` ); - const columns = currentMeta.columns.map(col => ({ - title: col.COLUMN_NAME, - field: col.COLUMN_NAME, - editor: "input", - headerFilter: "input" - })); + // ✅ Добавляем столбец с чекбоксами в начало + const columns = [ + { + formatter: "rowSelection", + titleFormatter: "rowSelection", + hozAlign: "center", + headerSort: false, + width: 40, + cellClick: function(e, cell) { + cell.getRow().toggleSelect(); + } + }, + ...currentMeta.columns.map(col => ({ + title: col.COLUMN_NAME, + field: col.COLUMN_NAME, + editor: "input", + headerFilter: "input" + })) + ]; if (table) { table.destroy(); @@ -123,7 +136,7 @@ async function selectTable(schema, tableName) { } table = new Tabulator("#table", { - selectableRows: true, // ✅ Множественное выделение (вместо selectableRows: 1) + selectableRows: true, columns: columns, layout: "fitColumns", resizableColumnFit: true, @@ -187,17 +200,6 @@ async function selectTable(schema, tableName) { }; }, - rowClick: function(e, row) { - // ✅ Ctrl/Cmd + Click для множественного выбора - if (e.ctrlKey || e.metaKey) { - row.toggleSelect(); - } else { - // Обычный клик - снимаем выделение с других и выделяем текущую - table.deselectRow(); - row.toggleSelect(); - } - }, - cellEdited: function(cell) { const row = cell.getRow(); lastEditedRow = row.getData(); @@ -222,6 +224,7 @@ async function selectTable(schema, tableName) { } }); + // ✅ Сохранение по Enter enterHandler = async function(e) { if (e.key === 'Enter' && lastEditedRow && currentSchema && currentTable) { e.preventDefault(); @@ -252,10 +255,7 @@ async function selectTable(schema, tableName) { document.addEventListener('keydown', enterHandler); } - - - -// CRUD кнопки +// ✅ ВСТАВИТЬ - добавляет строку НАД выбранной или внизу document.getElementById('btnInsert').addEventListener('click', async () => { if (!currentSchema || !currentTable || !currentMeta) { alert('Сначала выберите таблицу'); @@ -300,17 +300,13 @@ document.getElementById('btnInsert').addEventListener('click', async () => { } }); - // ✅ Если есть обязательные FK - показываем модальное окно для выбора const requiredFKs = fkFields.filter(f => f.required); if (requiredFKs.length > 0) { try { const fkValues = await promptForForeignKeys(requiredFKs); if (!fkValues) { - // Пользователь отменил return; } - - // Добавляем выбранные FK значения Object.assign(rowData, fkValues); } catch (err) { console.error('Ошибка получения FK значений:', err); @@ -320,12 +316,44 @@ document.getElementById('btnInsert').addEventListener('click', async () => { } try { - await api('/api/table/insert', 'POST', { + const result = await api('/api/table/insert', 'POST', { schema: currentSchema, table: currentTable, row: rowData }); - await table.replaceData(); + + // ✅ Получаем выбранные строки + const selected = table.getSelectedRows(); + + if (selected.length > 0) { + // Вставляем НАД первой выбранной строкой + const targetRow = selected[0]; + const position = targetRow.getPosition(); + + console.log(`Вставка НАД строкой на позиции ${position}`); + + // Перезагружаем данные и перемещаемся на нужную страницу + await table.replaceData(); + + // Если нужно, можно добавить скролл к новой строке + setTimeout(() => { + const rows = table.getRows(); + if (rows[position]) { + rows[position].getElement().scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }, 100); + } else { + // Вставляем внизу (в конец таблицы) + console.log('Вставка внизу таблицы'); + await table.replaceData(); + + // Переходим на последнюю страницу + const lastPage = table.getPageMax(); + if (lastPage > 1) { + table.setPage(lastPage); + } + } + alert('✓ Строка успешно создана'); } catch (e) { console.error('Ошибка вставки:', e); @@ -333,10 +361,10 @@ document.getElementById('btnInsert').addEventListener('click', async () => { } }); -// ✅ Функция для выбора FK значений +// Функция для выбора FK значений async function promptForForeignKeys(fkFields) { - // Создаём модальное окно const modal = document.createElement('div'); + modal.className = 'fk-modal'; modal.style.cssText = ` position: fixed; top: 0; @@ -363,7 +391,6 @@ async function promptForForeignKeys(fkFields) { let html = '

Заполните обязательные поля

'; - // Загружаем доступные значения для каждого FK const fkOptions = {}; for (const fk of fkFields) { try { @@ -379,7 +406,6 @@ async function promptForForeignKeys(fkFields) { } } - // Создаём поля для каждого FK fkFields.forEach(fk => { const options = fkOptions[fk.name]; html += ` @@ -394,7 +420,6 @@ async function promptForForeignKeys(fkFields) { `; if (options.length > 0) { - // Если есть значения - показываем select html += `'; } else { - // Если нет значений - показываем input html += ` { document.getElementById('fkCancel').addEventListener('click', () => { document.body.removeChild(modal); @@ -461,41 +484,18 @@ async function promptForForeignKeys(fkFields) { }); } -// Вспомогательная функция для экранирования HTML function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } - -document.getElementById('btnUpdate').addEventListener('click', async () => { - if (!table) return; - let selected = table.getSelectedData(); - let rowToSave = selected.length === 1 ? selected[0] : lastEditedRow; - if (!rowToSave) { - alert('Кликните по строке или отредактируйте ячейку'); - return; - } - - try { - const res = await api('/api/table/update', 'POST', { - schema: currentSchema, table: currentTable, row: rowToSave - }); - lastEditedRow = null; - await table.replaceData(); - alert(`✓ Обновлено строк: ${res.updated}`); - } catch (e) { - console.error(e); - alert('Ошибка обновления: ' + e.message); - } -}); - +// ✅ УДАЛИТЬ document.getElementById('btnDelete').addEventListener('click', async () => { if (!table || !currentSchema || !currentTable) return; const selected = table.getSelectedData(); if (selected.length === 0) { - alert('Выберите строки для удаления (используйте Ctrl+Click для выбора нескольких)'); + alert('Выберите строки для удаления'); return; } @@ -519,33 +519,20 @@ document.getElementById('btnDelete').addEventListener('click', async () => { } }); -// ✅ Снятие выделения -document.getElementById('btnDeselectAll').addEventListener('click', () => { - if (table) { - table.deselectRow(); - console.log('Выделение снято'); - } -}); - - // ========== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ CSV ========== -// Функция автоопределения разделителя function detectDelimiter(text) { const firstLine = text.split('\n')[0]; const semicolonCount = (firstLine.match(/;/g) || []).length; const commaCount = (firstLine.match(/,/g) || []).length; - // Если есть точки с запятой и их больше, чем запятых, используем ; if (semicolonCount > 0 && semicolonCount >= commaCount) { return ';'; } return ','; } -// Улучшенный парсер CSV с поддержкой ; и , разделителей function parseCSV(text) { - // Автоопределение разделителя const delimiter = detectDelimiter(text); console.log('Обнаружен разделитель CSV:', delimiter); @@ -561,7 +548,7 @@ function parseCSV(text) { if (char === '"') { if (inQuotes && nextChar === '"') { currentField += '"'; - i++; // пропускаем следующую кавычку + i++; } else { inQuotes = !inQuotes; } @@ -570,7 +557,7 @@ function parseCSV(text) { currentField = ''; } else if ((char === '\n' || char === '\r') && !inQuotes) { if (char === '\r' && nextChar === '\n') { - i++; // пропускаем \n после \r + i++; } if (currentField || currentLine.length > 0) { currentLine.push(currentField.trim()); @@ -583,7 +570,6 @@ function parseCSV(text) { } } - // Добавляем последнюю строку if (currentField || currentLine.length > 0) { currentLine.push(currentField.trim()); lines.push(currentLine); @@ -611,11 +597,9 @@ document.getElementById('csvFileInput').addEventListener('change', async (e) => try { const text = await file.text(); console.log('Содержимое файла (первые 500 символов):\n', text.substring(0, 500)); - console.log('Полное содержимое файла:\n', text); const rows = parseCSV(text); console.log('Всего строк после парсинга:', rows.length); - console.log('Все распарсенные строки:', rows); if (rows.length === 0) { alert('CSV файл пуст'); @@ -633,26 +617,16 @@ document.getElementById('csvFileInput').addEventListener('change', async (e) => return; } - // Преобразуем в массив объектов const records = dataRows.map((row, idx) => { const obj = {}; headers.forEach((header, i) => { const value = row[i] || null; obj[header] = value; - console.log(` Строка ${idx + 1}, столбец "${header}": "${value}"`); }); return obj; }); console.log('Финальные записи для отправки на сервер:', JSON.stringify(records, null, 2)); - console.log('Количество записей:', records.length); - - // Отправляем на сервер - console.log('Отправка запроса на сервер:', { - schema: currentSchema, - table: currentTable, - rowCount: records.length - }); const result = await api('/api/table/import-csv', 'POST', { schema: currentSchema, @@ -670,19 +644,14 @@ document.getElementById('csvFileInput').addEventListener('change', async (e) => } await table.replaceData(); - - // Очищаем input e.target.value = ''; } catch (err) { console.error('=== КРИТИЧЕСКАЯ ОШИБКА ИМПОРТА ==='); - console.error('Тип ошибки:', err.name); - console.error('Сообщение:', err.message); - console.error('Stack:', err.stack); + console.error('Ошибка:', err); alert('Ошибка импорта: ' + err.message); } }); - // ========== ЭКСПОРТ CSV ========== document.getElementById('btnExportCSV').addEventListener('click', async () => { if (!currentSchema || !currentTable || !table) { @@ -691,7 +660,6 @@ document.getElementById('btnExportCSV').addEventListener('click', async () => { } try { - // Получаем текущие фильтры и сортировку const headerFilters = table.getHeaderFilters ? table.getHeaderFilters() : []; const filters = (headerFilters || []).map(f => ({ field: f.field, value: f.value })); const sorters = table.getSorters ? table.getSorters() : []; @@ -705,11 +673,8 @@ document.getElementById('btnExportCSV').addEventListener('click', async () => { columns: currentMeta.columns }); - // Создаем CSV текст const csv = result.csv; - - // Скачиваем файл - const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' }); // BOM для Excel + const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); diff --git a/public/index.html b/public/index.html index b22bcbd..e894865 100644 --- a/public/index.html +++ b/public/index.html @@ -52,7 +52,7 @@ } #csvFileInput { display: none; } - /* ✅ Стили для Tabulator */ + /* Стили для Tabulator */ .tabulator { border: none; background-color: white; @@ -67,7 +67,7 @@ overflow-x: auto !important; } - /* ✅ ИСПРАВЛЕНИЕ: Размер шрифта при редактировании ячеек */ + /* Размер шрифта при редактировании ячеек */ .tabulator-cell input, .tabulator-cell select, .tabulator-cell textarea { @@ -88,16 +88,16 @@ box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2) !important; } - /* ✅ Стили для выделенных строк */ + /* Стили для выделенных строк */ .tabulator-row.tabulator-selected { - background-color: #d4e9ff !important; + background-color: #e3f2fd !important; } .tabulator-row.tabulator-selected:hover { - background-color: #c0dcf5 !important; + background-color: #bbdefb !important; } - /* ✅ Стили для модальных окон FK */ + /* Стили для модальных окон FK */ .fk-modal { font-family: sans-serif; } @@ -118,19 +118,22 @@ .fk-modal button:hover { opacity: 0.9; } - - /* ✅ Подсказка для множественного выбора */ - #toolbar::after { - content: "💡 Используйте Ctrl+Click для выбора нескольких строк"; - display: inline-block; - margin-left: 20px; - font-size: 12px; - color: #666; - font-style: italic; + + /* Кнопки toolbar */ + #toolbar button { + padding: 6px 12px; + margin-right: 4px; + cursor: pointer; + border: 1px solid #ccc; + background: #fff; + border-radius: 3px; + } + + #toolbar button:hover { + background: #f0f0f0; } - -
- - - - - - - -
+
+ + + + + +
- \ No newline at end of file +