This commit is contained in:
2026-03-18 17:19:02 +07:00
parent 9fa65ba4af
commit d1ba0eb58b
3 changed files with 96 additions and 148 deletions

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PostgreSQL Admin Panel</title>
<link rel="icon" type="image/svg+xml" href="favicon.svg">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
@@ -162,7 +163,7 @@
</div>
<div class="flex items-center gap-3" id="recordControls">
<input id="recordSearch" type="text" placeholder="Поиск по записям..." class="w-56 border border-slate-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
<button onclick="app.showFiltersModal()" class="flex items-center gap-2 px-4 py-2 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200 transition-colors text-sm">
<button onclick="app.toggleFilters()" class="flex items-center gap-2 px-4 py-2 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200 transition-colors text-sm">
<i data-lucide="filter" class="w-4 h-4"></i>
Фильтры
</button>
@@ -186,6 +187,9 @@
<thead class="bg-slate-50 text-slate-600 font-medium border-b border-slate-200">
<tr id="tableHeaders"></tr>
</thead>
<tbody id="filterRow" class="hidden bg-blue-50 border-b border-slate-200">
<tr id="filterInputs"></tr>
</tbody>
<tbody id="tableBody" class="divide-y divide-slate-200"></tbody>
</table>
</div>
@@ -393,36 +397,6 @@ SELECT * FROM users LIMIT 10;"></textarea>
</div>
</div>
<!-- Filters Modal -->
<div id="filtersModal" class="hidden fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col fade-in">
<div class="p-6 border-b border-slate-200 flex items-center justify-between">
<h3 class="text-xl font-bold text-slate-800">Фильтры</h3>
<button onclick="app.closeModal('filtersModal')" class="p-2 hover:bg-slate-100 rounded-lg transition-colors">
<i data-lucide="x" class="w-5 h-5 text-slate-500"></i>
</button>
</div>
<div class="p-6 overflow-y-auto flex-1">
<div id="filtersContainer" class="space-y-4">
<!-- Filters will be added here -->
</div>
<div class="mt-4 flex justify-end">
<button onclick="app.addFilter()" class="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm">
<i data-lucide="plus" class="w-4 h-4"></i>
Добавить фильтр
</button>
</div>
</div>
<div class="p-6 border-t border-slate-200 bg-slate-50 flex justify-between">
<button onclick="app.clearFilters()" class="px-4 py-2 text-slate-600 hover:bg-slate-200 rounded-lg">Очистить</button>
<div class="flex gap-3">
<button onclick="app.closeModal('filtersModal')" class="px-4 py-2 text-slate-600 hover:bg-slate-200 rounded-lg">Отмена</button>
<button onclick="app.applyFilters()" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg">Применить</button>
</div>
</div>
</div>
</div>
<!-- Toast Notifications -->
<div id="toastContainer" class="fixed bottom-6 right-6 z-50 flex flex-col gap-2"></div>
@@ -439,7 +413,7 @@ SELECT * FROM users LIMIT 10;"></textarea>
this.editingColumn = null;
this.primaryKey = null;
this.tableStructure = [];
this.filters = [];
this.filters = {};
this.sortColumn = '';
this.sortDirection = 'ASC';
this.init();
@@ -606,10 +580,12 @@ SELECT * FROM users LIMIT 10;"></textarea>
renderTableData(records) {
const headers = document.getElementById('tableHeaders');
const filterInputs = document.getElementById('filterInputs');
const body = document.getElementById('tableBody');
if (records.length === 0) {
headers.innerHTML = '';
filterInputs.innerHTML = '';
body.innerHTML = '<tr><td colspan="100%" class="text-center py-8 text-slate-500">Нет данных</td></tr>';
return;
}
@@ -622,6 +598,11 @@ SELECT * FROM users LIMIT 10;"></textarea>
return `<th class="text-left p-3 cursor-pointer hover:bg-slate-100 select-none" onclick="app.sortBy('${col}')">${col} ${arrow}</th>`;
}).join('') + '<th class="text-left p-3">Действия</th>';
// Generate filter inputs
filterInputs.innerHTML = columns.map(col => {
return `<td class="p-2"><input type="text" placeholder="Фильтр ${col}" class="w-full px-2 py-1 text-xs border border-slate-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500" oninput="app.updateFilter('${col}', this.value)" value="${this.filters[col] || ''}"></td>`;
}).join('') + '<td class="p-2"><button onclick="app.clearFilters()" class="text-xs text-slate-500 hover:text-slate-700">Очистить</button></td>';
// Generate rows
body.innerHTML = records.map(record => {
const pkValue = this.primaryKey ? record[this.primaryKey] : null;
@@ -1190,45 +1171,6 @@ SELECT * FROM users LIMIT 10;"></textarea>
}
}
showFiltersModal() {
this.renderFilters();
document.getElementById('filtersModal').classList.remove('hidden');
}
renderFilters() {
const container = document.getElementById('filtersContainer');
container.innerHTML = this.filters.map((filter, index) => `
<div class="flex items-center gap-2 p-3 bg-slate-50 rounded-lg">
<select class="px-3 py-2 border border-slate-300 rounded focus:ring-2 focus:ring-blue-500 outline-none" onchange="app.updateFilterColumn(${index}, this.value)">
<option value="">Выберите колонку</option>
${this.tableStructure.map(col => `<option value="${col.name}" ${filter.column === col.name ? 'selected' : ''}>${col.name}</option>`).join('')}
</select>
<select class="px-3 py-2 border border-slate-300 rounded focus:ring-2 focus:ring-blue-500 outline-none" onchange="app.updateFilterOperator(${index}, this.value)">
<option value="=" ${filter.operator === '=' ? 'selected' : ''}>=</option>
<option value="!=" ${filter.operator === '!=' ? 'selected' : ''}>!=</option>
<option value=">" ${filter.operator === '>' ? 'selected' : ''}>></option>
<option value="<" ${filter.operator === '<' ? 'selected' : ''}><</option>
<option value=">=" ${filter.operator === '>=' ? 'selected' : ''}>>=</option>
<option value="<=" ${filter.operator === '<=' ? 'selected' : ''}><=</option>
<option value="LIKE" ${filter.operator === 'LIKE' ? 'selected' : ''}>Содержит</option>
<option value="ILIKE" ${filter.operator === 'ILIKE' ? 'selected' : ''}>Содержит (регистронезависимо)</option>
<option value="IS NULL" ${filter.operator === 'IS NULL' ? 'selected' : ''}>Пустое</option>
<option value="IS NOT NULL" ${filter.operator === 'IS NOT NULL' ? 'selected' : ''}>Не пустое</option>
</select>
<input type="text" value="${filter.value || ''}" class="flex-1 px-3 py-2 border border-slate-300 rounded focus:ring-2 focus:ring-blue-500 outline-none" placeholder="Значение" oninput="app.updateFilterValue(${index}, this.value)" ${filter.operator === 'IS NULL' || filter.operator === 'IS NOT NULL' ? 'disabled' : ''}>
<button onclick="app.removeFilter(${index})" class="p-2 text-red-600 hover:bg-red-50 rounded">
<i data-lucide="trash-2" class="w-4 h-4"></i>
</button>
</div>
`).join('');
lucide.createIcons();
}
addFilter() {
this.filters.push({ column: '', operator: '=', value: '' });
this.renderFilters();
}
updateFilterColumn(index, column) {
this.filters[index].column = column;
this.renderFilters(); // Re-render to update disabled state
@@ -1239,23 +1181,28 @@ SELECT * FROM users LIMIT 10;"></textarea>
this.renderFilters(); // Re-render to update disabled state
}
updateFilterValue(index, value) {
this.filters[index].value = value;
toggleFilters() {
const filterRow = document.getElementById('filterRow');
filterRow.classList.toggle('hidden');
}
removeFilter(index) {
this.filters.splice(index, 1);
this.renderFilters();
updateFilter(column, value) {
if (value.trim()) {
this.filters[column] = value.trim();
} else {
delete this.filters[column];
}
this.currentPage = 1;
this.loadTableData();
}
clearFilters() {
this.filters = [];
this.renderFilters();
}
applyFilters() {
this.filters = {};
// Update all input values
document.querySelectorAll('#filterInputs input').forEach(input => {
input.value = '';
});
this.currentPage = 1;
this.closeModal('filtersModal');
this.loadTableData();
}
@@ -1266,7 +1213,7 @@ SELECT * FROM users LIMIT 10;"></textarea>
// Find primary key
const pkColumn = this.tableStructure.find(col => col.is_primary);
this.primaryKey = pkColumn ? pkColumn.name : null;
this.primaryKey = pkColumn ? pkColumn.name : (this.tableStructure.find(col => col.name === 'id') ? 'id' : null);
} catch (err) {
this.showToast('Ошибка загрузки структуры таблицы', 'error');
}