fix: удалено дублирование функции selectTable

- Функция selectTable была определена дважды, вызывая SyntaxError
- Удалено первое неполное определение функции
- Оставлено только корректное определение с исправленным rowSelectionChanged
- Исправлена структура файла app.js
This commit is contained in:
2026-01-21 04:44:12 +03:00
parent 25182132e7
commit fefc2b711a

View File

@@ -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('Сначала выберите таблицу');