24124124
This commit is contained in:
113
index.html
113
index.html
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user