fix: удалено дублирование функции selectTable
- Функция selectTable была определена дважды, вызывая SyntaxError - Удалено первое неполное определение функции - Оставлено только корректное определение с исправленным rowSelectionChanged - Исправлена структура файла app.js
This commit is contained in:
267
public/app.js
267
public/app.js
@@ -3,9 +3,8 @@ let currentTable = null;
|
||||
let currentMeta = null;
|
||||
let table = null;
|
||||
let enterHandler = null;
|
||||
let selectedRowsData = new Map(); // Хранилище выделенных строк для всех страниц
|
||||
let selectedRowsData = new Map();
|
||||
|
||||
// Простая обёртка для fetch JSON
|
||||
async function api(url, method = 'GET', body) {
|
||||
const opts = { method, headers: { 'Content-Type': 'application/json' } };
|
||||
if (body) opts.body = JSON.stringify(body);
|
||||
@@ -18,7 +17,6 @@ async function api(url, method = 'GET', body) {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// Обновление счетчика выделенных строк
|
||||
function updateSelectionCounter() {
|
||||
const counter = document.getElementById('selectionCounter');
|
||||
const count = selectedRowsData.size;
|
||||
@@ -31,7 +29,6 @@ function updateSelectionCounter() {
|
||||
}
|
||||
}
|
||||
|
||||
// Функция создания модального окна с прогресс-баром
|
||||
function createProgressModal(message) {
|
||||
const modal = document.createElement('div');
|
||||
modal.style.cssText = `
|
||||
@@ -75,12 +72,11 @@ function createProgressModal(message) {
|
||||
|
||||
modal.appendChild(dialog);
|
||||
|
||||
// Анимация прогресс-бара (имитация)
|
||||
const progressBar = dialog.querySelector('.progress-bar');
|
||||
let progress = 0;
|
||||
const interval = setInterval(() => {
|
||||
progress += Math.random() * 15;
|
||||
if (progress > 90) progress = 90; // Останавливаемся на 90%
|
||||
if (progress > 90) progress = 90;
|
||||
progressBar.style.width = progress + '%';
|
||||
}, 200);
|
||||
|
||||
@@ -98,7 +94,15 @@ function escapeHtml(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Логин
|
||||
function getRowKey(rowData) {
|
||||
if (!currentMeta || !currentMeta.primaryKey || currentMeta.primaryKey.length === 0) {
|
||||
return JSON.stringify(rowData);
|
||||
}
|
||||
|
||||
const pkValues = currentMeta.primaryKey.map(pk => rowData[pk]).join('|');
|
||||
return pkValues;
|
||||
}
|
||||
|
||||
document.getElementById('loginBtn').addEventListener('click', async () => {
|
||||
const user = document.getElementById('loginUser').value.trim();
|
||||
const pass = document.getElementById('loginPass').value;
|
||||
@@ -131,7 +135,6 @@ document.getElementById('loginBtn').addEventListener('click', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// loadTree
|
||||
async function loadTree() {
|
||||
const treeEl = document.getElementById('tree');
|
||||
treeEl.style.color = '';
|
||||
@@ -170,118 +173,6 @@ async function loadTree() {
|
||||
|
||||
let lastEditedRow = null;
|
||||
|
||||
// Получение уникального ключа строки на основе PK
|
||||
function getRowKey(rowData) {
|
||||
if (!currentMeta || !currentMeta.primaryKey || currentMeta.primaryKey.length === 0) {
|
||||
// Если нет PK, используем все поля
|
||||
const key = JSON.stringify(rowData);
|
||||
console.log(' 🔑 getRowKey (no PK):', key.substring(0, 100));
|
||||
return key;
|
||||
}
|
||||
|
||||
const pkValues = currentMeta.primaryKey.map(pk => {
|
||||
const value = rowData[pk];
|
||||
if (value === undefined || value === null) {
|
||||
console.warn(` ⚠️ PK field '${pk}' is undefined/null in rowData:`, rowData);
|
||||
}
|
||||
return value;
|
||||
}).join('|');
|
||||
|
||||
console.log(' 🔑 getRowKey (PK):', pkValues, 'from fields:', currentMeta.primaryKey);
|
||||
return pkValues;
|
||||
}
|
||||
|
||||
|
||||
async function selectTable(schema, tableName) {
|
||||
currentSchema = schema;
|
||||
currentTable = tableName;
|
||||
lastEditedRow = null;
|
||||
selectedRowsData.clear(); // Очищаем выделение при смене таблицы
|
||||
updateSelectionCounter();
|
||||
|
||||
if (enterHandler) {
|
||||
document.removeEventListener('keydown', enterHandler);
|
||||
enterHandler = null;
|
||||
}
|
||||
|
||||
currentMeta = await api(
|
||||
`/api/table/meta?schema=${encodeURIComponent(schema)}&table=${encodeURIComponent(tableName)}`
|
||||
);
|
||||
|
||||
// Добавляем столбец с чекбоксами в начало
|
||||
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();
|
||||
table = null;
|
||||
}
|
||||
|
||||
table = new Tabulator("#table", {
|
||||
selectableRows: true,
|
||||
columns: columns,
|
||||
layout: "fitColumns",
|
||||
resizableColumnFit: true,
|
||||
|
||||
pagination: true,
|
||||
paginationMode: "remote",
|
||||
paginationSize: 50,
|
||||
paginationSizeSelector: [25, 50, 100, 200],
|
||||
|
||||
filterMode: "remote",
|
||||
sortMode: "remote",
|
||||
|
||||
ajaxURL: "/api/table/data",
|
||||
ajaxConfig: "POST",
|
||||
ajaxContentType: "json",
|
||||
|
||||
ajaxParams: function () {
|
||||
const headerFilters = this.getHeaderFilters ? this.getHeaderFilters() : [];
|
||||
const filters = (headerFilters || []).map(f => ({
|
||||
field: f.field,
|
||||
value: f.value
|
||||
})).filter(f => f.value !== null && f.value !== '');
|
||||
|
||||
const sorters = this.getSorters ? this.getSorters() : [];
|
||||
const sort = (sorters && sorters.length > 0) ? {
|
||||
field: sorters[0].field,
|
||||
dir: sorters[0].dir
|
||||
} : null;
|
||||
|
||||
return {
|
||||
schema: currentSchema,
|
||||
table: currentTable,
|
||||
filters: filters,
|
||||
sort: sort,
|
||||
columns: currentMeta.columns,
|
||||
page: this.getPage ? this.getPage() : 1,
|
||||
pageSize: this.getPageSize ? this.getPageSize() : 50
|
||||
};
|
||||
},
|
||||
|
||||
ajaxResponse: function (url, params, response) {
|
||||
return {
|
||||
last_page: response.last_page || 1,
|
||||
data: response.data || []
|
||||
};
|
||||
},
|
||||
|
||||
async function selectTable(schema, tableName) {
|
||||
currentSchema = schema;
|
||||
currentTable = tableName;
|
||||
@@ -371,58 +262,39 @@ async function selectTable(schema, tableName) {
|
||||
};
|
||||
},
|
||||
|
||||
// ✅ ИСПРАВЛЕНО: Используем параметр data, который содержит ВСЕ выделенные строки
|
||||
rowSelectionChanged: function(data, rows) {
|
||||
console.log('🔔 rowSelectionChanged called:', {
|
||||
console.log('🔔 rowSelectionChanged:', {
|
||||
'data (all selected)': data.length,
|
||||
'rows (changed)': rows.length,
|
||||
'selectedRowsData before': selectedRowsData.size
|
||||
'rows (changed)': rows.length
|
||||
});
|
||||
|
||||
// ВАЖНО: параметр data содержит массив данных ВСЕХ выделенных строк
|
||||
// параметр rows содержит только Row объекты измененных строк
|
||||
|
||||
// Получаем все выделенные строки напрямую из Tabulator
|
||||
const allSelectedData = data; // это уже массив данных всех выделенных строк
|
||||
|
||||
// Удаляем из selectedRowsData все строки текущей страницы
|
||||
const currentPageRows = this.getRows();
|
||||
currentPageRows.forEach(row => {
|
||||
const rowData = row.getData();
|
||||
const key = getRowKey(rowData);
|
||||
selectedRowsData.delete(key);
|
||||
console.log(` 🧹 Removed from selectedRowsData: ${key}`);
|
||||
});
|
||||
|
||||
// Добавляем все выделенные строки обратно
|
||||
allSelectedData.forEach(rowData => {
|
||||
data.forEach(rowData => {
|
||||
const key = getRowKey(rowData);
|
||||
selectedRowsData.set(key, rowData);
|
||||
console.log(` ✅ Added to selectedRowsData: ${key}`);
|
||||
});
|
||||
|
||||
console.log(` 📊 selectedRowsData after: ${selectedRowsData.size}`);
|
||||
console.log(` 📊 selectedRowsData: ${selectedRowsData.size}`);
|
||||
updateSelectionCounter();
|
||||
},
|
||||
|
||||
dataLoaded: function(data) {
|
||||
console.log('📄 dataLoaded, восстанавливаем выделение для', selectedRowsData.size, 'строк');
|
||||
|
||||
if (selectedRowsData.size > 0) {
|
||||
const rows = this.getRows();
|
||||
let restoredCount = 0;
|
||||
|
||||
rows.forEach(row => {
|
||||
const rowData = row.getData();
|
||||
const key = getRowKey(rowData);
|
||||
|
||||
if (selectedRowsData.has(key)) {
|
||||
row.select();
|
||||
restoredCount++;
|
||||
}
|
||||
});
|
||||
|
||||
console.log(' ✅ Восстановлено выделение для', restoredCount, 'строк на странице');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -463,67 +335,6 @@ async function selectTable(schema, tableName) {
|
||||
document.addEventListener('keydown', enterHandler);
|
||||
}
|
||||
|
||||
// После загрузки данных восстанавливаем выделение
|
||||
dataLoaded: function(data) {
|
||||
console.log('📄 dataLoaded, восстанавливаем выделение для', selectedRowsData.size, 'строк');
|
||||
|
||||
if (selectedRowsData.size > 0) {
|
||||
const rows = this.getRows();
|
||||
let restoredCount = 0;
|
||||
|
||||
rows.forEach(row => {
|
||||
const rowData = row.getData();
|
||||
const key = getRowKey(rowData);
|
||||
|
||||
if (selectedRowsData.has(key)) {
|
||||
row.select();
|
||||
restoredCount++;
|
||||
}
|
||||
});
|
||||
|
||||
console.log(' ✅ Восстановлено выделение для', restoredCount, 'строк на странице');
|
||||
}
|
||||
},
|
||||
|
||||
cellEdited: function(cell) {
|
||||
const row = cell.getRow();
|
||||
lastEditedRow = row.getData();
|
||||
row.getElement().style.backgroundColor = '#fffae6';
|
||||
},
|
||||
|
||||
headerFilterLiveFilterDelay: 800
|
||||
});
|
||||
|
||||
// Сохранение по Enter
|
||||
enterHandler = async function(e) {
|
||||
if (e.key === 'Enter' && lastEditedRow && currentSchema && currentTable) {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
const res = await api('/api/table/update', 'POST', {
|
||||
schema: currentSchema,
|
||||
table: currentTable,
|
||||
row: lastEditedRow
|
||||
});
|
||||
|
||||
lastEditedRow = null;
|
||||
|
||||
table.getRows().forEach(r => {
|
||||
r.getElement().style.backgroundColor = '';
|
||||
});
|
||||
|
||||
await table.replaceData();
|
||||
} catch (err) {
|
||||
console.error('Ошибка сохранения:', err);
|
||||
alert('Ошибка: ' + err.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', enterHandler);
|
||||
}
|
||||
|
||||
// ✅ ВЫДЕЛИТЬ ВСЕ (НА ВСЕХ СТРАНИЦАХ)
|
||||
document.getElementById('btnSelectAll').addEventListener('click', async () => {
|
||||
if (!currentSchema || !currentTable || !table) {
|
||||
alert('Сначала выберите таблицу');
|
||||
@@ -531,13 +342,11 @@ document.getElementById('btnSelectAll').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() : [];
|
||||
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,
|
||||
@@ -545,12 +354,11 @@ document.getElementById('btnSelectAll').addEventListener('click', async () => {
|
||||
sort: sort,
|
||||
columns: currentMeta.columns,
|
||||
page: 1,
|
||||
pageSize: 999999 // Получаем все записи
|
||||
pageSize: 999999
|
||||
});
|
||||
|
||||
const totalRows = result.total;
|
||||
|
||||
// Предупреждение для больших таблиц
|
||||
if (totalRows > 1000) {
|
||||
const proceed = confirm(
|
||||
`Вы собираетесь выделить ${totalRows} записей.\n` +
|
||||
@@ -561,14 +369,12 @@ document.getElementById('btnSelectAll').addEventListener('click', async () => {
|
||||
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();
|
||||
@@ -587,7 +393,6 @@ document.getElementById('btnSelectAll').addEventListener('click', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ СНЯТЬ ВЫДЕЛЕНИЕ
|
||||
document.getElementById('btnDeselectAll').addEventListener('click', () => {
|
||||
if (!table) return;
|
||||
|
||||
@@ -596,7 +401,6 @@ document.getElementById('btnDeselectAll').addEventListener('click', () => {
|
||||
updateSelectionCounter();
|
||||
});
|
||||
|
||||
// ✅ ВСТАВИТЬ
|
||||
document.getElementById('btnInsert').addEventListener('click', async () => {
|
||||
if (!currentSchema || !currentTable || !currentMeta) {
|
||||
alert('Сначала выберите таблицу');
|
||||
@@ -663,16 +467,12 @@ document.getElementById('btnInsert').addEventListener('click', async () => {
|
||||
row: rowData
|
||||
});
|
||||
|
||||
const selected = table.getSelectedRows();
|
||||
|
||||
await table.replaceData();
|
||||
|
||||
if (selected.length === 0) {
|
||||
const lastPage = table.getPageMax();
|
||||
if (lastPage > 1) {
|
||||
table.setPage(lastPage);
|
||||
}
|
||||
}
|
||||
|
||||
alert('✓ Строка успешно создана');
|
||||
} catch (e) {
|
||||
@@ -681,7 +481,6 @@ document.getElementById('btnInsert').addEventListener('click', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Функция для выбора FK значений
|
||||
async function promptForForeignKeys(fkFields) {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'fk-modal';
|
||||
@@ -804,12 +603,7 @@ async function promptForForeignKeys(fkFields) {
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ УДАЛИТЬ (оптимизированное с batch delete)
|
||||
document.getElementById('btnDelete').addEventListener('click', async () => {
|
||||
// ВРЕМЕННАЯ ОТЛАДКА
|
||||
console.log('Current Primary Keys:', currentMeta?.primaryKey);
|
||||
console.log('Sample row from selectedRowsData:', Array.from(selectedRowsData.values())[0]);
|
||||
|
||||
if (!table || !currentSchema || !currentTable) {
|
||||
alert('Сначала выберите таблицу');
|
||||
return;
|
||||
@@ -819,19 +613,15 @@ console.log('Sample row from selectedRowsData:', Array.from(selectedRowsData.val
|
||||
|
||||
console.log('🗑️ Удаление:', {
|
||||
selectedRowsDataSize: count,
|
||||
selectedRowsDataKeys: Array.from(selectedRowsData.keys())
|
||||
primaryKeys: currentMeta?.primaryKey
|
||||
});
|
||||
|
||||
if (count === 0) {
|
||||
// Дополнительная проверка - может быть строки выделены в Tabulator, но не в selectedRowsData?
|
||||
const tabulatorSelected = table.getSelectedData();
|
||||
console.log('Tabulator selected rows:', tabulatorSelected.length);
|
||||
|
||||
if (tabulatorSelected.length > 0) {
|
||||
console.error('❌ Несоответствие: Tabulator имеет выделенные строки, но selectedRowsData пуст!');
|
||||
console.log('Попытка синхронизации...');
|
||||
|
||||
// Синхронизируем
|
||||
selectedRowsData.clear();
|
||||
tabulatorSelected.forEach(rowData => {
|
||||
const key = getRowKey(rowData);
|
||||
@@ -856,25 +646,18 @@ console.log('Sample row from selectedRowsData:', Array.from(selectedRowsData.val
|
||||
|
||||
if (!confirm(confirmMsg)) return;
|
||||
|
||||
// Создаем модальное окно с прогресс-баром
|
||||
const modal = createProgressModal('Удаление записей...');
|
||||
document.body.appendChild(modal);
|
||||
|
||||
try {
|
||||
// Преобразуем Map в массив
|
||||
const rowsArray = Array.from(selectedRowsData.values());
|
||||
|
||||
console.log('Отправка на удаление:', rowsArray.length, 'строк');
|
||||
|
||||
// Используем batch delete для оптимизации
|
||||
const result = await api('/api/table/delete-batch', 'POST', {
|
||||
schema: currentSchema,
|
||||
table: currentTable,
|
||||
rows: rowsArray
|
||||
});
|
||||
|
||||
console.log('Результат удаления:', result);
|
||||
|
||||
document.body.removeChild(modal);
|
||||
|
||||
selectedRowsData.clear();
|
||||
@@ -897,9 +680,6 @@ console.log('Sample row from selectedRowsData:', Array.from(selectedRowsData.val
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// ========== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ CSV ==========
|
||||
|
||||
function detectDelimiter(text) {
|
||||
const firstLine = text.split('\n')[0];
|
||||
const semicolonCount = (firstLine.match(/;/g) || []).length;
|
||||
@@ -956,7 +736,6 @@ function parseCSV(text) {
|
||||
return lines;
|
||||
}
|
||||
|
||||
// ========== ИМПОРТ CSV ==========
|
||||
document.getElementById('btnImportCSV').addEventListener('click', () => {
|
||||
if (!currentSchema || !currentTable) {
|
||||
alert('Сначала выберите таблицу');
|
||||
@@ -1008,24 +787,20 @@ document.getElementById('csvFileInput').addEventListener('change', async (e) =>
|
||||
rows: records
|
||||
});
|
||||
|
||||
if (result.errorMessages && result.errorMessages.length > 0) {
|
||||
// Показываем только первые 10 ошибок
|
||||
if (result.errorMessages && result.errorMessages.length > 0) {
|
||||
const errorsToShow = result.errorMessages.slice(0, 10);
|
||||
const moreErrors = result.errorMessages.length > 10
|
||||
? `\n... и еще ${result.errorMessages.length - 10} ошибок`
|
||||
: '';
|
||||
|
||||
// Создаем детальное сообщение
|
||||
let errorDetails = `Импортировано строк: ${result.inserted}\nОшибок: ${result.errors}\n\n`;
|
||||
errorDetails += 'ПЕРВЫЕ ОШИБКИ:\n';
|
||||
errorDetails += '═══════════════════════════════════════\n';
|
||||
errorDetails += errorsToShow.join('\n\n');
|
||||
errorDetails += moreErrors;
|
||||
|
||||
// Выводим в alert (для больших сообщений можно использовать модальное окно)
|
||||
alert(errorDetails);
|
||||
|
||||
// Также выводим в консоль для детального анализа
|
||||
console.group('📋 Детали импорта CSV');
|
||||
console.log(`✅ Импортировано: ${result.inserted}`);
|
||||
console.log(`❌ Ошибок: ${result.errors}`);
|
||||
@@ -1034,10 +809,9 @@ if (result.errorMessages && result.errorMessages.length > 0) {
|
||||
console.log(`${idx + 1}. ${msg}`);
|
||||
});
|
||||
console.groupEnd();
|
||||
} else {
|
||||
} else {
|
||||
alert(`✓ Импортировано строк: ${result.inserted}\nОшибок: ${result.errors}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
await table.replaceData();
|
||||
e.target.value = '';
|
||||
@@ -1048,7 +822,6 @@ if (result.errorMessages && result.errorMessages.length > 0) {
|
||||
}
|
||||
});
|
||||
|
||||
// ========== ЭКСПОРТ CSV ==========
|
||||
document.getElementById('btnExportCSV').addEventListener('click', async () => {
|
||||
if (!currentSchema || !currentTable || !table) {
|
||||
alert('Сначала выберите таблицу');
|
||||
|
||||
Reference in New Issue
Block a user