feat: библиотека документов с формой загрузки
This commit is contained in:
parent
7fa864f402
commit
c1fe0745ef
144
index.html
144
index.html
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Библиотека файлов</title>
|
||||
<title>Библиотека документов</title>
|
||||
<link rel="stylesheet" href="design-system/kt-ai-fonts.css">
|
||||
<link rel="stylesheet" href="design-system/kt-ai-tokens.css">
|
||||
<link rel="stylesheet" href="design-system/kt-ai-components.css">
|
||||
@ -35,16 +35,25 @@
|
||||
.file-list { display: flex; flex-direction: column; gap: var(--kt-ai-space-2); }
|
||||
.file-meta { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); }
|
||||
.folder-item.active .file-meta { color: rgba(255,255,255,0.7); }
|
||||
.controls { display: flex; gap: var(--kt-ai-space-3); margin-bottom: var(--kt-ai-space-6); }
|
||||
.search-input { flex: 1; }
|
||||
.controls { display: flex; gap: var(--kt-ai-space-3); margin-bottom: var(--kt-ai-space-6); flex-wrap: wrap; }
|
||||
.search-input { flex: 1; min-width: 200px; }
|
||||
.sort-select { min-width: 180px; }
|
||||
.upload-form { display: none; padding: var(--kt-ai-space-4); background: var(--kt-ai-bg-soft); border-radius: var(--kt-ai-radius-lg); margin-bottom: var(--kt-ai-space-4); }
|
||||
.upload-form.active { display: block; }
|
||||
.form-row { display: flex; gap: var(--kt-ai-space-3); margin-bottom: var(--kt-ai-space-3); flex-wrap: wrap; }
|
||||
.form-row input, .form-row select { flex: 1; min-width: 150px; }
|
||||
.doc-badge {
|
||||
display: inline-block; padding: 2px 8px; border-radius: var(--kt-ai-radius-md);
|
||||
font-size: var(--kt-ai-text-xs); font-weight: var(--kt-ai-weight-medium);
|
||||
background: var(--kt-ai-bg-soft); color: var(--kt-ai-fg-muted);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="kt-ai-pagehead">
|
||||
<div class="kt-ai-wrap kt-ai-pagehead-in">
|
||||
<strong>📁 Библиотека файлов</strong>
|
||||
<strong>📄 Библиотека документов</strong>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@ -54,7 +63,7 @@
|
||||
<aside class="sidebar">
|
||||
<div class="kt-ai-card" style="padding: var(--kt-ai-space-4)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--kt-ai-space-4)">
|
||||
<h3 style="margin:0">Папки</h3>
|
||||
<h3 style="margin:0">Категории</h3>
|
||||
<button class="kt-ai-btn" data-variant="primary" data-size="sm" id="newFolderBtn">+</button>
|
||||
</div>
|
||||
<div id="folderList"></div>
|
||||
@ -64,17 +73,34 @@
|
||||
<main class="main-content">
|
||||
<div class="kt-ai-card" style="padding: var(--kt-ai-space-6)">
|
||||
<div class="controls">
|
||||
<input class="kt-ai-input search-input" id="searchInput" placeholder="Поиск по названию...">
|
||||
<input class="kt-ai-input search-input" id="searchInput" placeholder="Поиск документа...">
|
||||
<select class="kt-ai-select sort-select" id="sortSelect">
|
||||
<option value="date-desc">Сначала новые</option>
|
||||
<option value="date-asc">Сначала старые</option>
|
||||
<option value="name-asc">По имени (А-Я)</option>
|
||||
<option value="name-desc">По имени (Я-А)</option>
|
||||
</select>
|
||||
<button class="kt-ai-btn" data-variant="primary" id="uploadBtn">Загрузить файл</button>
|
||||
<button class="kt-ai-btn" data-variant="primary" id="toggleUploadBtn">+ Документ</button>
|
||||
</div>
|
||||
|
||||
<h2 id="currentFolderTitle" style="margin-bottom: var(--kt-ai-space-4)">Все файлы</h2>
|
||||
<div class="upload-form" id="uploadForm">
|
||||
<h4 style="margin:0 0 var(--kt-ai-space-3) 0">Загрузка документа</h4>
|
||||
<div class="form-row">
|
||||
<input class="kt-ai-input" id="fileName" placeholder="Название документа*" required>
|
||||
<select class="kt-ai-select" id="fileFolder">
|
||||
<option value="">Выберите категорию</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<input class="kt-ai-input" id="fileType" placeholder="Тип (pdf, docx, xlsx...)" style="max-width:150px">
|
||||
<input class="kt-ai-input" id="fileSize" placeholder="Размер (КБ)" type="number" style="max-width:120px">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<button class="kt-ai-btn" data-variant="primary" id="submitUploadBtn">Загрузить</button>
|
||||
<button class="kt-ai-btn" data-variant="ghost" id="cancelUploadBtn">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 id="currentFolderTitle" style="margin-bottom: var(--kt-ai-space-4)">Все документы</h2>
|
||||
<div id="fileList" class="file-list"></div>
|
||||
</div>
|
||||
</main>
|
||||
@ -83,7 +109,7 @@
|
||||
</section>
|
||||
|
||||
<footer class="kt-ai-wrap kt-ai-pagefoot">
|
||||
<span>© 2026 Библиотека файлов</span>
|
||||
<span>© 2026 Библиотека документов</span>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
@ -97,6 +123,7 @@ async function loadFolders() {
|
||||
const res = await fetch('/api/folders');
|
||||
folders = await res.json();
|
||||
renderFolders();
|
||||
updateFolderSelect();
|
||||
}
|
||||
|
||||
async function loadFiles() {
|
||||
@ -118,6 +145,12 @@ function renderFolders() {
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function updateFolderSelect() {
|
||||
const select = document.getElementById('fileFolder');
|
||||
select.innerHTML = '<option value="">Выберите категорию</option>' +
|
||||
folders.map(f => `<option value="${f.id}">${f.name}</option>`).join('');
|
||||
}
|
||||
|
||||
function renderFiles() {
|
||||
const container = document.getElementById('fileList');
|
||||
let filtered = files.filter(f => {
|
||||
@ -132,12 +165,11 @@ function renderFiles() {
|
||||
if (sortMode === 'date-desc') return dateB - dateA;
|
||||
if (sortMode === 'date-asc') return dateA - dateB;
|
||||
if (sortMode === 'name-asc') return a.name.localeCompare(b.name);
|
||||
if (sortMode === 'name-desc') return b.name.localeCompare(a.name);
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (filtered.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state">Нет файлов</div>';
|
||||
container.innerHTML = '<div class="empty-state">Нет документов</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
@ -145,12 +177,16 @@ function renderFiles() {
|
||||
const folder = folders.find(fl => fl.id === f.folderId);
|
||||
const size = formatSize(f.size);
|
||||
const date = new Date(f.createdAt).toLocaleDateString('ru-RU');
|
||||
const ext = getFileExtension(f.name);
|
||||
return `
|
||||
<div class="file-item">
|
||||
<div style="display:flex;align-items:center;flex:1">
|
||||
<svg class="file-icon" viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>
|
||||
<div>
|
||||
<div>${f.name}</div>
|
||||
<div style="display:flex;align-items:center;gap:var(--kt-ai-space-2)">
|
||||
<span>${f.name}</span>
|
||||
${ext ? '<span class="doc-badge">' + ext.toUpperCase() + '</span>' : ''}
|
||||
</div>
|
||||
<div class="file-meta">${size} • ${date}${folder ? ' • ' + folder.name : ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -160,22 +196,27 @@ function renderFiles() {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' Б';
|
||||
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' КБ';
|
||||
return (bytes / 1048576).toFixed(1) + ' МБ';
|
||||
function getFileExtension(name) {
|
||||
const parts = (name || '').split('.');
|
||||
return parts.length > 1 ? parts.pop() : '';
|
||||
}
|
||||
|
||||
function formatSize(kb) {
|
||||
if (!kb) return '';
|
||||
if (kb < 1024) return kb + ' КБ';
|
||||
return (kb / 1024).toFixed(1) + ' МБ';
|
||||
}
|
||||
|
||||
function selectFolder(id) {
|
||||
currentFolderId = id;
|
||||
const folder = folders.find(f => f.id === id);
|
||||
document.getElementById('currentFolderTitle').textContent = folder ? folder.name : 'Все файлы';
|
||||
document.getElementById('currentFolderTitle').textContent = folder ? folder.name : 'Все документы';
|
||||
renderFolders();
|
||||
renderFiles();
|
||||
}
|
||||
|
||||
async function createFolder() {
|
||||
const name = prompt('Название папки:');
|
||||
const name = prompt('Название категории:');
|
||||
if (!name || !name.trim()) return;
|
||||
try {
|
||||
const res = await fetch('/api/folders', {
|
||||
@ -186,23 +227,23 @@ async function createFolder() {
|
||||
if (!res.ok) throw new Error('Ошибка: ' + res.status);
|
||||
await loadFolders();
|
||||
} catch (err) {
|
||||
alert('Не удалось создать папку: ' + err.message);
|
||||
alert('Не удалось создать категорию: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteFolder(id) {
|
||||
if (!confirm('Удалить папку и все файлы в ней?')) return;
|
||||
if (!confirm('Удалить категорию и все документы в ней?')) return;
|
||||
await fetch('/api/folders/' + id, { method: 'DELETE' });
|
||||
if (currentFolderId === id) {
|
||||
currentFolderId = null;
|
||||
document.getElementById('currentFolderTitle').textContent = 'Все файлы';
|
||||
document.getElementById('currentFolderTitle').textContent = 'Все документы';
|
||||
}
|
||||
await loadFolders();
|
||||
await loadFiles();
|
||||
}
|
||||
|
||||
async function deleteFile(id) {
|
||||
if (!confirm('Удалить файл?')) return;
|
||||
if (!confirm('Удалить документ?')) return;
|
||||
await fetch('/api/files/' + id, { method: 'DELETE' });
|
||||
await loadFiles();
|
||||
}
|
||||
@ -219,20 +260,53 @@ document.getElementById('sortSelect').addEventListener('change', (e) => {
|
||||
renderFiles();
|
||||
});
|
||||
|
||||
document.getElementById('uploadBtn').addEventListener('click', () => {
|
||||
const folderId = currentFolderId !== null ? currentFolderId : (folders[0]?.id || null);
|
||||
if (!folderId && folders.length === 0) {
|
||||
alert('Сначала создайте папку');
|
||||
document.getElementById('toggleUploadBtn').addEventListener('click', () => {
|
||||
if (folders.length === 0) {
|
||||
alert('Сначала создайте категорию');
|
||||
return;
|
||||
}
|
||||
const name = prompt('Название файла:');
|
||||
if (!name) return;
|
||||
const size = parseInt(prompt('Размер (байт):', '1000')) || 0;
|
||||
fetch('/api/files', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ folderId: folderId || 1, name, size })
|
||||
}).then(() => loadFiles());
|
||||
document.getElementById('uploadForm').classList.toggle('active');
|
||||
});
|
||||
|
||||
document.getElementById('cancelUploadBtn').addEventListener('click', () => {
|
||||
document.getElementById('uploadForm').classList.remove('active');
|
||||
document.getElementById('fileName').value = '';
|
||||
document.getElementById('fileSize').value = '';
|
||||
document.getElementById('fileType').value = '';
|
||||
});
|
||||
|
||||
document.getElementById('submitUploadBtn').addEventListener('click', async () => {
|
||||
const name = document.getElementById('fileName').value.trim();
|
||||
const folderId = parseInt(document.getElementById('fileFolder').value);
|
||||
const size = parseFloat(document.getElementById('fileSize').value) || 0;
|
||||
const type = document.getElementById('fileType').value.trim();
|
||||
|
||||
if (!name) {
|
||||
alert('Введите название документа');
|
||||
return;
|
||||
}
|
||||
if (!folderId) {
|
||||
alert('Выберите категорию');
|
||||
return;
|
||||
}
|
||||
|
||||
const finalName = type && !name.includes('.') ? name + '.' + type : name;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/files', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ folderId, name: finalName, size })
|
||||
});
|
||||
if (!res.ok) throw new Error('Ошибка загрузки');
|
||||
document.getElementById('uploadForm').classList.remove('active');
|
||||
document.getElementById('fileName').value = '';
|
||||
document.getElementById('fileSize').value = '';
|
||||
document.getElementById('fileType').value = '';
|
||||
await loadFiles();
|
||||
} catch (err) {
|
||||
alert('Ошибка: ' + err.message);
|
||||
}
|
||||
});
|
||||
|
||||
loadFolders();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user