feat: добавлен функционал выделения всех записей на всех страницах
- Добавлена кнопка "Выделить все на странице" - выделяет записи текущей страницы - Добавлена кнопка "Выделить все" - выделяет все записи с учетом фильтров (все страницы) - Добавлена кнопка "Снять выделение" - убирает выделение со всех строк - Добавлен счетчик выделенных строк в toolbar - Добавлено предупреждение при выделении большого количества записей - Оптимизировано удаление выделенных строк (batch delete)
This commit is contained in:
267
public/app.js
267
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user