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