refactor: переход на чекбоксы для выбора строк и упрощение интерфейса
- Добавлены чекбоксы для множественного выбора строк - Убрана кнопка "Сохранить строку" (сохранение по Enter) - Изменена логика кнопки "Вставить": добавляет строку над выбранной или внизу - Убрана подсказка про Ctrl+Click - Упрощен код выбора строк (rowClick теперь не нужен) - Очищено визуальное оформление
This commit is contained in:
163
public/app.js
163
public/app.js
@@ -110,12 +110,25 @@ async function selectTable(schema, tableName) {
|
||||
`/api/table/meta?schema=${encodeURIComponent(schema)}&table=${encodeURIComponent(tableName)}`
|
||||
);
|
||||
|
||||
const columns = currentMeta.columns.map(col => ({
|
||||
title: col.COLUMN_NAME,
|
||||
field: col.COLUMN_NAME,
|
||||
editor: "input",
|
||||
headerFilter: "input"
|
||||
}));
|
||||
// ✅ Добавляем столбец с чекбоксами в начало
|
||||
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();
|
||||
@@ -123,7 +136,7 @@ async function selectTable(schema, tableName) {
|
||||
}
|
||||
|
||||
table = new Tabulator("#table", {
|
||||
selectableRows: true, // ✅ Множественное выделение (вместо selectableRows: 1)
|
||||
selectableRows: true,
|
||||
columns: columns,
|
||||
layout: "fitColumns",
|
||||
resizableColumnFit: true,
|
||||
@@ -187,17 +200,6 @@ async function selectTable(schema, tableName) {
|
||||
};
|
||||
},
|
||||
|
||||
rowClick: function(e, row) {
|
||||
// ✅ Ctrl/Cmd + Click для множественного выбора
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
row.toggleSelect();
|
||||
} else {
|
||||
// Обычный клик - снимаем выделение с других и выделяем текущую
|
||||
table.deselectRow();
|
||||
row.toggleSelect();
|
||||
}
|
||||
},
|
||||
|
||||
cellEdited: function(cell) {
|
||||
const row = cell.getRow();
|
||||
lastEditedRow = row.getData();
|
||||
@@ -222,6 +224,7 @@ async function selectTable(schema, tableName) {
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ Сохранение по Enter
|
||||
enterHandler = async function(e) {
|
||||
if (e.key === 'Enter' && lastEditedRow && currentSchema && currentTable) {
|
||||
e.preventDefault();
|
||||
@@ -252,10 +255,7 @@ async function selectTable(schema, tableName) {
|
||||
document.addEventListener('keydown', enterHandler);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// CRUD кнопки
|
||||
// ✅ ВСТАВИТЬ - добавляет строку НАД выбранной или внизу
|
||||
document.getElementById('btnInsert').addEventListener('click', async () => {
|
||||
if (!currentSchema || !currentTable || !currentMeta) {
|
||||
alert('Сначала выберите таблицу');
|
||||
@@ -300,17 +300,13 @@ document.getElementById('btnInsert').addEventListener('click', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ Если есть обязательные FK - показываем модальное окно для выбора
|
||||
const requiredFKs = fkFields.filter(f => f.required);
|
||||
if (requiredFKs.length > 0) {
|
||||
try {
|
||||
const fkValues = await promptForForeignKeys(requiredFKs);
|
||||
if (!fkValues) {
|
||||
// Пользователь отменил
|
||||
return;
|
||||
}
|
||||
|
||||
// Добавляем выбранные FK значения
|
||||
Object.assign(rowData, fkValues);
|
||||
} catch (err) {
|
||||
console.error('Ошибка получения FK значений:', err);
|
||||
@@ -320,12 +316,44 @@ document.getElementById('btnInsert').addEventListener('click', async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
await api('/api/table/insert', 'POST', {
|
||||
const result = await api('/api/table/insert', 'POST', {
|
||||
schema: currentSchema,
|
||||
table: currentTable,
|
||||
row: rowData
|
||||
});
|
||||
await table.replaceData();
|
||||
|
||||
// ✅ Получаем выбранные строки
|
||||
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();
|
||||
|
||||
// Переходим на последнюю страницу
|
||||
const lastPage = table.getPageMax();
|
||||
if (lastPage > 1) {
|
||||
table.setPage(lastPage);
|
||||
}
|
||||
}
|
||||
|
||||
alert('✓ Строка успешно создана');
|
||||
} catch (e) {
|
||||
console.error('Ошибка вставки:', e);
|
||||
@@ -333,10 +361,10 @@ document.getElementById('btnInsert').addEventListener('click', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ Функция для выбора FK значений
|
||||
// Функция для выбора FK значений
|
||||
async function promptForForeignKeys(fkFields) {
|
||||
// Создаём модальное окно
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'fk-modal';
|
||||
modal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -363,7 +391,6 @@ async function promptForForeignKeys(fkFields) {
|
||||
|
||||
let html = '<h3>Заполните обязательные поля</h3>';
|
||||
|
||||
// Загружаем доступные значения для каждого FK
|
||||
const fkOptions = {};
|
||||
for (const fk of fkFields) {
|
||||
try {
|
||||
@@ -379,7 +406,6 @@ async function promptForForeignKeys(fkFields) {
|
||||
}
|
||||
}
|
||||
|
||||
// Создаём поля для каждого FK
|
||||
fkFields.forEach(fk => {
|
||||
const options = fkOptions[fk.name];
|
||||
html += `
|
||||
@@ -394,7 +420,6 @@ async function promptForForeignKeys(fkFields) {
|
||||
`;
|
||||
|
||||
if (options.length > 0) {
|
||||
// Если есть значения - показываем select
|
||||
html += `<select id="fk_${fk.name}" style="width: 100%; padding: 8px; font-size: 14px;">`;
|
||||
html += '<option value="">-- Выберите значение --</option>';
|
||||
options.forEach(val => {
|
||||
@@ -402,7 +427,6 @@ async function promptForForeignKeys(fkFields) {
|
||||
});
|
||||
html += '</select>';
|
||||
} else {
|
||||
// Если нет значений - показываем input
|
||||
html += `
|
||||
<input type="text" id="fk_${fk.name}"
|
||||
style="width: 100%; padding: 8px; font-size: 14px; box-sizing: border-box;"
|
||||
@@ -429,7 +453,6 @@ async function promptForForeignKeys(fkFields) {
|
||||
modal.appendChild(dialog);
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Обработка кнопок
|
||||
return new Promise((resolve) => {
|
||||
document.getElementById('fkCancel').addEventListener('click', () => {
|
||||
document.body.removeChild(modal);
|
||||
@@ -461,41 +484,18 @@ async function promptForForeignKeys(fkFields) {
|
||||
});
|
||||
}
|
||||
|
||||
// Вспомогательная функция для экранирования HTML
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
|
||||
document.getElementById('btnUpdate').addEventListener('click', async () => {
|
||||
if (!table) return;
|
||||
let selected = table.getSelectedData();
|
||||
let rowToSave = selected.length === 1 ? selected[0] : lastEditedRow;
|
||||
if (!rowToSave) {
|
||||
alert('Кликните по строке или отредактируйте ячейку');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await api('/api/table/update', 'POST', {
|
||||
schema: currentSchema, table: currentTable, row: rowToSave
|
||||
});
|
||||
lastEditedRow = null;
|
||||
await table.replaceData();
|
||||
alert(`✓ Обновлено строк: ${res.updated}`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Ошибка обновления: ' + e.message);
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ УДАЛИТЬ
|
||||
document.getElementById('btnDelete').addEventListener('click', async () => {
|
||||
if (!table || !currentSchema || !currentTable) return;
|
||||
const selected = table.getSelectedData();
|
||||
if (selected.length === 0) {
|
||||
alert('Выберите строки для удаления (используйте Ctrl+Click для выбора нескольких)');
|
||||
alert('Выберите строки для удаления');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -519,33 +519,20 @@ document.getElementById('btnDelete').addEventListener('click', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ Снятие выделения
|
||||
document.getElementById('btnDeselectAll').addEventListener('click', () => {
|
||||
if (table) {
|
||||
table.deselectRow();
|
||||
console.log('Выделение снято');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// ========== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ CSV ==========
|
||||
|
||||
// Функция автоопределения разделителя
|
||||
function detectDelimiter(text) {
|
||||
const firstLine = text.split('\n')[0];
|
||||
const semicolonCount = (firstLine.match(/;/g) || []).length;
|
||||
const commaCount = (firstLine.match(/,/g) || []).length;
|
||||
|
||||
// Если есть точки с запятой и их больше, чем запятых, используем ;
|
||||
if (semicolonCount > 0 && semicolonCount >= commaCount) {
|
||||
return ';';
|
||||
}
|
||||
return ',';
|
||||
}
|
||||
|
||||
// Улучшенный парсер CSV с поддержкой ; и , разделителей
|
||||
function parseCSV(text) {
|
||||
// Автоопределение разделителя
|
||||
const delimiter = detectDelimiter(text);
|
||||
console.log('Обнаружен разделитель CSV:', delimiter);
|
||||
|
||||
@@ -561,7 +548,7 @@ function parseCSV(text) {
|
||||
if (char === '"') {
|
||||
if (inQuotes && nextChar === '"') {
|
||||
currentField += '"';
|
||||
i++; // пропускаем следующую кавычку
|
||||
i++;
|
||||
} else {
|
||||
inQuotes = !inQuotes;
|
||||
}
|
||||
@@ -570,7 +557,7 @@ function parseCSV(text) {
|
||||
currentField = '';
|
||||
} else if ((char === '\n' || char === '\r') && !inQuotes) {
|
||||
if (char === '\r' && nextChar === '\n') {
|
||||
i++; // пропускаем \n после \r
|
||||
i++;
|
||||
}
|
||||
if (currentField || currentLine.length > 0) {
|
||||
currentLine.push(currentField.trim());
|
||||
@@ -583,7 +570,6 @@ function parseCSV(text) {
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем последнюю строку
|
||||
if (currentField || currentLine.length > 0) {
|
||||
currentLine.push(currentField.trim());
|
||||
lines.push(currentLine);
|
||||
@@ -611,11 +597,9 @@ document.getElementById('csvFileInput').addEventListener('change', async (e) =>
|
||||
try {
|
||||
const text = await file.text();
|
||||
console.log('Содержимое файла (первые 500 символов):\n', text.substring(0, 500));
|
||||
console.log('Полное содержимое файла:\n', text);
|
||||
|
||||
const rows = parseCSV(text);
|
||||
console.log('Всего строк после парсинга:', rows.length);
|
||||
console.log('Все распарсенные строки:', rows);
|
||||
|
||||
if (rows.length === 0) {
|
||||
alert('CSV файл пуст');
|
||||
@@ -633,26 +617,16 @@ document.getElementById('csvFileInput').addEventListener('change', async (e) =>
|
||||
return;
|
||||
}
|
||||
|
||||
// Преобразуем в массив объектов
|
||||
const records = dataRows.map((row, idx) => {
|
||||
const obj = {};
|
||||
headers.forEach((header, i) => {
|
||||
const value = row[i] || null;
|
||||
obj[header] = value;
|
||||
console.log(` Строка ${idx + 1}, столбец "${header}": "${value}"`);
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
|
||||
console.log('Финальные записи для отправки на сервер:', JSON.stringify(records, null, 2));
|
||||
console.log('Количество записей:', records.length);
|
||||
|
||||
// Отправляем на сервер
|
||||
console.log('Отправка запроса на сервер:', {
|
||||
schema: currentSchema,
|
||||
table: currentTable,
|
||||
rowCount: records.length
|
||||
});
|
||||
|
||||
const result = await api('/api/table/import-csv', 'POST', {
|
||||
schema: currentSchema,
|
||||
@@ -670,19 +644,14 @@ document.getElementById('csvFileInput').addEventListener('change', async (e) =>
|
||||
}
|
||||
|
||||
await table.replaceData();
|
||||
|
||||
// Очищаем input
|
||||
e.target.value = '';
|
||||
} catch (err) {
|
||||
console.error('=== КРИТИЧЕСКАЯ ОШИБКА ИМПОРТА ===');
|
||||
console.error('Тип ошибки:', err.name);
|
||||
console.error('Сообщение:', err.message);
|
||||
console.error('Stack:', err.stack);
|
||||
console.error('Ошибка:', err);
|
||||
alert('Ошибка импорта: ' + err.message);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// ========== ЭКСПОРТ CSV ==========
|
||||
document.getElementById('btnExportCSV').addEventListener('click', async () => {
|
||||
if (!currentSchema || !currentTable || !table) {
|
||||
@@ -691,7 +660,6 @@ document.getElementById('btnExportCSV').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() : [];
|
||||
@@ -705,11 +673,8 @@ document.getElementById('btnExportCSV').addEventListener('click', async () => {
|
||||
columns: currentMeta.columns
|
||||
});
|
||||
|
||||
// Создаем CSV текст
|
||||
const csv = result.csv;
|
||||
|
||||
// Скачиваем файл
|
||||
const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' }); // BOM для Excel
|
||||
const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
}
|
||||
#csvFileInput { display: none; }
|
||||
|
||||
/* ✅ Стили для Tabulator */
|
||||
/* Стили для Tabulator */
|
||||
.tabulator {
|
||||
border: none;
|
||||
background-color: white;
|
||||
@@ -67,7 +67,7 @@
|
||||
overflow-x: auto !important;
|
||||
}
|
||||
|
||||
/* ✅ ИСПРАВЛЕНИЕ: Размер шрифта при редактировании ячеек */
|
||||
/* Размер шрифта при редактировании ячеек */
|
||||
.tabulator-cell input,
|
||||
.tabulator-cell select,
|
||||
.tabulator-cell textarea {
|
||||
@@ -88,16 +88,16 @@
|
||||
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2) !important;
|
||||
}
|
||||
|
||||
/* ✅ Стили для выделенных строк */
|
||||
/* Стили для выделенных строк */
|
||||
.tabulator-row.tabulator-selected {
|
||||
background-color: #d4e9ff !important;
|
||||
background-color: #e3f2fd !important;
|
||||
}
|
||||
|
||||
.tabulator-row.tabulator-selected:hover {
|
||||
background-color: #c0dcf5 !important;
|
||||
background-color: #bbdefb !important;
|
||||
}
|
||||
|
||||
/* ✅ Стили для модальных окон FK */
|
||||
/* Стили для модальных окон FK */
|
||||
.fk-modal {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
@@ -118,19 +118,22 @@
|
||||
.fk-modal button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* ✅ Подсказка для множественного выбора */
|
||||
#toolbar::after {
|
||||
content: "💡 Используйте Ctrl+Click для выбора нескольких строк";
|
||||
display: inline-block;
|
||||
margin-left: 20px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
|
||||
/* Кнопки toolbar */
|
||||
#toolbar button {
|
||||
padding: 6px 12px;
|
||||
margin-right: 4px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #ccc;
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#toolbar button:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="sidebar">
|
||||
@@ -144,18 +147,16 @@
|
||||
<button id="loginBtn">Войти</button>
|
||||
<span id="loginStatus"></span>
|
||||
</div>
|
||||
<div id="toolbar">
|
||||
<button id="btnInsert">Вставить</button>
|
||||
<button id="btnUpdate">Сохранить строку</button>
|
||||
<button id="btnDelete">Удалить</button>
|
||||
<button id="btnDeselectAll">Снять выделение</button> <!-- ✅ Новая кнопка -->
|
||||
<button id="btnImportCSV">Импорт CSV</button>
|
||||
<input type="file" id="csvFileInput" accept=".csv">
|
||||
<button id="btnExportCSV">Экспорт CSV</button>
|
||||
</div>
|
||||
<div id="toolbar">
|
||||
<button id="btnInsert">➕ Вставить</button>
|
||||
<button id="btnDelete">🗑️ Удалить</button>
|
||||
<button id="btnImportCSV">📥 Импорт CSV</button>
|
||||
<input type="file" id="csvFileInput" accept=".csv">
|
||||
<button id="btnExportCSV">📤 Экспорт CSV</button>
|
||||
</div>
|
||||
<div id="table"></div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user