feat: добавлен функционал выделения всех записей на всех страницах

- Добавлена кнопка "Выделить все на странице" - выделяет записи текущей страницы
- Добавлена кнопка "Выделить все" - выделяет все записи с учетом фильтров (все страницы)
- Добавлена кнопка "Снять выделение" - убирает выделение со всех строк
- Добавлен счетчик выделенных строк в toolbar
- Добавлено предупреждение при выделении большого количества записей
- Оптимизировано удаление выделенных строк (batch delete)
This commit is contained in:
2026-01-21 04:13:11 +03:00
parent de5266f98f
commit 0fc427f11d
2 changed files with 214 additions and 88 deletions

View File

@@ -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);
}
});
});

View File

@@ -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;
}
</style>
</head>
@@ -150,9 +177,15 @@
<div id="toolbar">
<button id="btnInsert"> Вставить</button>
<button id="btnDelete">🗑️ Удалить</button>
<div class="toolbar-divider"></div>
<button id="btnSelectPage">☑️ Выделить страницу</button>
<button id="btnSelectAll">☑️ Выделить все</button>
<button id="btnDeselectAll">⬜ Снять выделение</button>
<div class="toolbar-divider"></div>
<button id="btnImportCSV">📥 Импорт CSV</button>
<input type="file" id="csvFileInput" accept=".csv">
<button id="btnExportCSV">📤 Экспорт CSV</button>
<span id="selectionCounter" style="display: none;">Выбрано: 0</span>
</div>
<div id="table"></div>
</div>