diff --git a/public/app.js b/public/app.js index 86d6a0f..692f347 100644 --- a/public/app.js +++ b/public/app.js @@ -3,6 +3,7 @@ let currentTable = null; let currentMeta = null; let table = null; let enterHandler = null; +let selectedRowsData = new Map(); // Хранилище выделенных строк для всех страниц // Простая обёртка для fetch JSON async function api(url, method = 'GET', body) { @@ -17,9 +18,21 @@ async function api(url, method = 'GET', body) { return res.json(); } +// Обновление счетчика выделенных строк +function updateSelectionCounter() { + const counter = document.getElementById('selectionCounter'); + const count = selectedRowsData.size; + + if (count > 0) { + counter.textContent = `Выбрано: ${count}`; + counter.style.display = 'block'; + } else { + counter.style.display = 'none'; + } +} + // Логин document.getElementById('loginBtn').addEventListener('click', async () => { - console.log('=== LOGIN BUTTON CLICKED ==='); const user = document.getElementById('loginUser').value.trim(); const pass = document.getElementById('loginPass').value; const statusEl = document.getElementById('loginStatus'); @@ -34,9 +47,7 @@ document.getElementById('loginBtn').addEventListener('click', async () => { statusEl.style.color = ''; try { - console.log('Отправляем /api/login:', { user, pass: '***' }); const res = await api('/api/login', 'POST', { user, pass }); - console.log('Login ответ:', res); if (res.ok) { statusEl.textContent = '✓ Авторизация успешна'; @@ -55,14 +66,12 @@ document.getElementById('loginBtn').addEventListener('click', async () => { // loadTree async function loadTree() { - console.log('=== LOAD TREE ВЫЗВАН ==='); const treeEl = document.getElementById('tree'); treeEl.style.color = ''; treeEl.innerHTML = 'Загрузка...'; try { const tree = await api('/api/tree'); - console.log('Дерево получено:', tree); treeEl.innerHTML = ''; tree.forEach(schema => { @@ -94,12 +103,23 @@ async function loadTree() { let lastEditedRow = null; +// Получение уникального ключа строки на основе PK +function getRowKey(rowData) { + if (!currentMeta || !currentMeta.primaryKey || currentMeta.primaryKey.length === 0) { + // Если нет PK, используем все поля + return JSON.stringify(rowData); + } + + const pkValues = currentMeta.primaryKey.map(pk => rowData[pk]).join('|'); + return pkValues; +} + async function selectTable(schema, tableName) { currentSchema = schema; currentTable = tableName; lastEditedRow = null; - - console.log('=== SELECT TABLE ===', { schema, tableName }); + selectedRowsData.clear(); // Очищаем выделение при смене таблицы + updateSelectionCounter(); if (enterHandler) { document.removeEventListener('keydown', enterHandler); @@ -110,7 +130,7 @@ async function selectTable(schema, tableName) { `/api/table/meta?schema=${encodeURIComponent(schema)}&table=${encodeURIComponent(tableName)}` ); - // ✅ Добавляем столбец с чекбоксами в начало + // Добавляем столбец с чекбоксами в начало const columns = [ { formatter: "rowSelection", @@ -166,7 +186,7 @@ async function selectTable(schema, tableName) { dir: sorters[0].dir } : null; - const params = { + return { schema: currentSchema, table: currentTable, filters: filters, @@ -175,60 +195,60 @@ async function selectTable(schema, tableName) { page: this.getPage ? this.getPage() : 1, pageSize: this.getPageSize ? this.getPageSize() : 50 }; - - console.log('📤 Запрос к серверу:', { - page: params.page, - pageSize: params.pageSize, - filters: params.filters, - sort: params.sort - }); - - return params; }, ajaxResponse: function (url, params, response) { - console.log('📥 Ответ от сервера:', { - page: response.current_page, - last_page: response.last_page, - total: response.total, - rows: response.data ? response.data.length : 0 - }); - return { last_page: response.last_page || 1, data: response.data || [] }; }, + // Обработка выделения строк + rowSelectionChanged: function(data, rows) { + // Добавляем/удаляем строки из глобального хранилища + rows.forEach(row => { + const rowData = row.getData(); + const key = getRowKey(rowData); + + if (row.isSelected()) { + selectedRowsData.set(key, rowData); + } else { + selectedRowsData.delete(key); + } + }); + + updateSelectionCounter(); + }, + + // После загрузки данных восстанавливаем выделение + dataLoaded: function(data) { + if (selectedRowsData.size > 0) { + const rows = this.getRows(); + rows.forEach(row => { + const rowData = row.getData(); + const key = getRowKey(rowData); + + if (selectedRowsData.has(key)) { + row.select(); + } + }); + } + }, + cellEdited: function(cell) { const row = cell.getRow(); lastEditedRow = row.getData(); row.getElement().style.backgroundColor = '#fffae6'; - - console.log('Ячейка отредактирована:', cell.getField(), '→', cell.getValue()); }, - headerFilterLiveFilterDelay: 800, - - dataFiltering: function(filters) { - const activeFilters = filters.filter(f => f.value); - console.log('🔍 Активные фильтры:', activeFilters.map(f => `${f.field}=${f.value}`)); - }, - - dataFiltered: function(filters, rows) { - console.log('✅ Фильтрация применена, записей на странице:', rows.length); - }, - - pageLoaded: function(pageno) { - console.log('📄 Загружена страница:', pageno); - } + headerFilterLiveFilterDelay: 800 }); - // ✅ Сохранение по Enter + // Сохранение по Enter enterHandler = async function(e) { if (e.key === 'Enter' && lastEditedRow && currentSchema && currentTable) { e.preventDefault(); - console.log('Enter нажат, сохраняем строку:', lastEditedRow); try { const res = await api('/api/table/update', 'POST', { @@ -237,7 +257,6 @@ async function selectTable(schema, tableName) { row: lastEditedRow }); - console.log('Сохранено:', res); lastEditedRow = null; table.getRows().forEach(r => { @@ -255,7 +274,88 @@ async function selectTable(schema, tableName) { document.addEventListener('keydown', enterHandler); } -// ✅ ВСТАВИТЬ - добавляет строку НАД выбранной или внизу +// ✅ ВЫДЕЛИТЬ ВСЕ НА ТЕКУЩЕЙ СТРАНИЦЕ +document.getElementById('btnSelectPage').addEventListener('click', () => { + if (!table) return; + + const rows = table.getRows(); + rows.forEach(row => row.select()); +}); + +// ✅ ВЫДЕЛИТЬ ВСЕ (НА ВСЕХ СТРАНИЦАХ) +document.getElementById('btnSelectAll').addEventListener('click', async () => { + if (!currentSchema || !currentTable || !table) { + alert('Сначала выберите таблицу'); + return; + } + + try { + // Получаем текущие фильтры + const headerFilters = table.getHeaderFilters ? table.getHeaderFilters() : []; + const filters = (headerFilters || []).map(f => ({ field: f.field, value: f.value })); + const sorters = table.getSorters ? table.getSorters() : []; + const sort = (sorters && sorters.length > 0) ? { field: sorters[0].field, dir: sorters[0].dir } : null; + + // Получаем все данные с сервера (с учетом фильтров, но без пагинации) + const result = await api('/api/table/data', 'POST', { + schema: currentSchema, + table: currentTable, + filters: filters, + sort: sort, + columns: currentMeta.columns, + page: 1, + pageSize: 999999 // Получаем все записи + }); + + const totalRows = result.total; + + // Предупреждение для больших таблиц + if (totalRows > 1000) { + const proceed = confirm( + `Вы собираетесь выделить ${totalRows} записей.\n` + + `Это может занять некоторое время и использовать много памяти.\n\n` + + `Продолжить?` + ); + + if (!proceed) return; + } + + // Добавляем все строки в selectedRowsData + selectedRowsData.clear(); + result.data.forEach(rowData => { + const key = getRowKey(rowData); + selectedRowsData.set(key, rowData); + }); + + // Выделяем видимые строки на текущей странице + const rows = table.getRows(); + rows.forEach(row => { + const rowData = row.getData(); + const key = getRowKey(rowData); + + if (selectedRowsData.has(key)) { + row.select(); + } + }); + + updateSelectionCounter(); + alert(`Выделено записей: ${totalRows}`); + } catch (err) { + console.error('Ошибка выделения:', err); + alert('Ошибка выделения: ' + err.message); + } +}); + +// ✅ СНЯТЬ ВЫДЕЛЕНИЕ +document.getElementById('btnDeselectAll').addEventListener('click', () => { + if (!table) return; + + selectedRowsData.clear(); + table.deselectRow(); + updateSelectionCounter(); +}); + +// ✅ ВСТАВИТЬ document.getElementById('btnInsert').addEventListener('click', async () => { if (!currentSchema || !currentTable || !currentMeta) { alert('Сначала выберите таблицу'); @@ -316,38 +416,17 @@ document.getElementById('btnInsert').addEventListener('click', async () => { } try { - const result = await api('/api/table/insert', 'POST', { + await api('/api/table/insert', 'POST', { schema: currentSchema, table: currentTable, row: rowData }); - // ✅ Получаем выбранные строки 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(); - - // Переходим на последнюю страницу + await table.replaceData(); + + if (selected.length === 0) { const lastPage = table.getPageMax(); if (lastPage > 1) { table.setPage(lastPage); @@ -493,26 +572,48 @@ function escapeHtml(text) { // ✅ УДАЛИТЬ document.getElementById('btnDelete').addEventListener('click', async () => { if (!table || !currentSchema || !currentTable) return; - const selected = table.getSelectedData(); - if (selected.length === 0) { + + const count = selectedRowsData.size; + + if (count === 0) { alert('Выберите строки для удаления'); return; } - const confirmMsg = selected.length === 1 + const confirmMsg = count === 1 ? 'Удалить выбранную строку?' - : `Удалить ${selected.length} выбранных строк?`; + : `Удалить ${count} выбранных строк?`; if (!confirm(confirmMsg)) return; try { let deleted = 0; - for (const row of selected) { - await api('/api/table/delete', 'POST', { schema: currentSchema, table: currentTable, row }); - deleted++; + let errors = 0; + + // Удаляем все выделенные строки из selectedRowsData + for (const [key, rowData] of selectedRowsData) { + try { + await api('/api/table/delete', 'POST', { + schema: currentSchema, + table: currentTable, + row: rowData + }); + deleted++; + } catch (e) { + console.error(`Ошибка удаления строки ${key}:`, e); + errors++; + } } + + selectedRowsData.clear(); + updateSelectionCounter(); await table.replaceData(); - alert(`✓ Удалено строк: ${deleted}`); + + if (errors > 0) { + alert(`Удалено строк: ${deleted}\nОшибок: ${errors}`); + } else { + alert(`✓ Удалено строк: ${deleted}`); + } } catch (e) { console.error(e); alert('Ошибка удаления: ' + e.message); @@ -534,7 +635,6 @@ function detectDelimiter(text) { function parseCSV(text) { const delimiter = detectDelimiter(text); - console.log('Обнаружен разделитель CSV:', delimiter); const lines = []; let currentLine = []; @@ -608,7 +708,6 @@ document.getElementById('csvFileInput').addEventListener('change', async (e) => return; } - // Показываем прогресс для больших файлов if (dataRows.length > 100) { const proceed = confirm(`Файл содержит ${dataRows.length} строк. Импорт может занять некоторое время. Продолжить?`); if (!proceed) { @@ -617,7 +716,6 @@ document.getElementById('csvFileInput').addEventListener('change', async (e) => } } - // Преобразуем в массив объектов const records = dataRows.map(row => { const obj = {}; headers.forEach((header, i) => { @@ -626,7 +724,6 @@ document.getElementById('csvFileInput').addEventListener('change', async (e) => return obj; }); - // Отправляем на сервер const result = await api('/api/table/import-csv', 'POST', { schema: currentSchema, table: currentTable, @@ -634,7 +731,6 @@ document.getElementById('csvFileInput').addEventListener('change', async (e) => }); if (result.errorMessages && result.errorMessages.length > 0) { - // Показываем только первые 10 ошибок const errorsToShow = result.errorMessages.slice(0, 10); const moreErrors = result.errorMessages.length > 10 ? `\n... и еще ${result.errorMessages.length - 10} ошибок` : ''; alert(`Импортировано строк: ${result.inserted}\nОшибок: ${result.errors}\n\nПервые ошибки:\n${errorsToShow.join('\n')}${moreErrors}`); @@ -651,7 +747,6 @@ document.getElementById('csvFileInput').addEventListener('change', async (e) => } }); - // ========== ЭКСПОРТ CSV ========== document.getElementById('btnExportCSV').addEventListener('click', async () => { if (!currentSchema || !currentTable || !table) { @@ -684,10 +779,8 @@ document.getElementById('btnExportCSV').addEventListener('click', async () => { document.body.appendChild(link); link.click(); document.body.removeChild(link); - - console.log(`Экспортировано ${result.rowCount} строк`); } catch (err) { console.error('Ошибка экспорта:', err); alert('Ошибка экспорта: ' + err.message); } -}); +}); \ No newline at end of file diff --git a/public/index.html b/public/index.html index e894865..b158fc9 100644 --- a/public/index.html +++ b/public/index.html @@ -35,6 +35,10 @@ padding: 8px; border-bottom: 1px solid #ccc; flex-shrink: 0; + display: flex; + align-items: center; + gap: 4px; + flex-wrap: wrap; } #table { flex: 1; @@ -122,16 +126,39 @@ /* Кнопки toolbar */ #toolbar button { padding: 6px 12px; - margin-right: 4px; cursor: pointer; border: 1px solid #ccc; background: #fff; border-radius: 3px; + font-size: 13px; } #toolbar button:hover { background: #f0f0f0; } + + #toolbar button:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + /* Счетчик выделенных */ + #selectionCounter { + padding: 6px 12px; + background: #e3f2fd; + border-radius: 3px; + font-size: 13px; + color: #1976d2; + font-weight: bold; + margin-left: auto; + } + + .toolbar-divider { + width: 1px; + height: 24px; + background: #ccc; + margin: 0 4px; + } @@ -150,9 +177,15 @@