feat: библиотека документов с формой загрузки

This commit is contained in:
Zhusupova.GL 2026-09-14 05:24:30 +00:00
parent 7fa864f402
commit c1fe0745ef

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <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-fonts.css">
<link rel="stylesheet" href="design-system/kt-ai-tokens.css"> <link rel="stylesheet" href="design-system/kt-ai-tokens.css">
<link rel="stylesheet" href="design-system/kt-ai-components.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-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); } .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); } .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); } .controls { display: flex; gap: var(--kt-ai-space-3); margin-bottom: var(--kt-ai-space-6); flex-wrap: wrap; }
.search-input { flex: 1; } .search-input { flex: 1; min-width: 200px; }
.sort-select { min-width: 180px; } .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> </style>
</head> </head>
<body> <body>
<header class="kt-ai-pagehead"> <header class="kt-ai-pagehead">
<div class="kt-ai-wrap kt-ai-pagehead-in"> <div class="kt-ai-wrap kt-ai-pagehead-in">
<strong>📁 Библиотека файлов</strong> <strong>📄 Библиотека документов</strong>
</div> </div>
</header> </header>
@ -54,7 +63,7 @@
<aside class="sidebar"> <aside class="sidebar">
<div class="kt-ai-card" style="padding: var(--kt-ai-space-4)"> <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)"> <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> <button class="kt-ai-btn" data-variant="primary" data-size="sm" id="newFolderBtn">+</button>
</div> </div>
<div id="folderList"></div> <div id="folderList"></div>
@ -64,17 +73,34 @@
<main class="main-content"> <main class="main-content">
<div class="kt-ai-card" style="padding: var(--kt-ai-space-6)"> <div class="kt-ai-card" style="padding: var(--kt-ai-space-6)">
<div class="controls"> <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"> <select class="kt-ai-select sort-select" id="sortSelect">
<option value="date-desc">Сначала новые</option> <option value="date-desc">Сначала новые</option>
<option value="date-asc">Сначала старые</option> <option value="date-asc">Сначала старые</option>
<option value="name-asc">По имени (А-Я)</option> <option value="name-asc">По имени (А-Я)</option>
<option value="name-desc">По имени (Я-А)</option>
</select> </select>
<button class="kt-ai-btn" data-variant="primary" id="uploadBtn">Загрузить файл</button> <button class="kt-ai-btn" data-variant="primary" id="toggleUploadBtn">+ Документ</button>
</div> </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 id="fileList" class="file-list"></div>
</div> </div>
</main> </main>
@ -83,7 +109,7 @@
</section> </section>
<footer class="kt-ai-wrap kt-ai-pagefoot"> <footer class="kt-ai-wrap kt-ai-pagefoot">
<span>© 2026 Библиотека файлов</span> <span>© 2026 Библиотека документов</span>
</footer> </footer>
<script> <script>
@ -97,6 +123,7 @@ async function loadFolders() {
const res = await fetch('/api/folders'); const res = await fetch('/api/folders');
folders = await res.json(); folders = await res.json();
renderFolders(); renderFolders();
updateFolderSelect();
} }
async function loadFiles() { async function loadFiles() {
@ -118,6 +145,12 @@ function renderFolders() {
`).join(''); `).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() { function renderFiles() {
const container = document.getElementById('fileList'); const container = document.getElementById('fileList');
let filtered = files.filter(f => { let filtered = files.filter(f => {
@ -132,12 +165,11 @@ function renderFiles() {
if (sortMode === 'date-desc') return dateB - dateA; if (sortMode === 'date-desc') return dateB - dateA;
if (sortMode === 'date-asc') return dateA - dateB; if (sortMode === 'date-asc') return dateA - dateB;
if (sortMode === 'name-asc') return a.name.localeCompare(b.name); if (sortMode === 'name-asc') return a.name.localeCompare(b.name);
if (sortMode === 'name-desc') return b.name.localeCompare(a.name);
return 0; return 0;
}); });
if (filtered.length === 0) { if (filtered.length === 0) {
container.innerHTML = '<div class="empty-state">Нет файлов</div>'; container.innerHTML = '<div class="empty-state">Нет документов</div>';
return; return;
} }
@ -145,12 +177,16 @@ function renderFiles() {
const folder = folders.find(fl => fl.id === f.folderId); const folder = folders.find(fl => fl.id === f.folderId);
const size = formatSize(f.size); const size = formatSize(f.size);
const date = new Date(f.createdAt).toLocaleDateString('ru-RU'); const date = new Date(f.createdAt).toLocaleDateString('ru-RU');
const ext = getFileExtension(f.name);
return ` return `
<div class="file-item"> <div class="file-item">
<div style="display:flex;align-items:center;flex:1"> <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> <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>
<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 class="file-meta">${size} • ${date}${folder ? ' • ' + folder.name : ''}</div>
</div> </div>
</div> </div>
@ -160,22 +196,27 @@ function renderFiles() {
}).join(''); }).join('');
} }
function formatSize(bytes) { function getFileExtension(name) {
if (bytes < 1024) return bytes + ' Б'; const parts = (name || '').split('.');
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' КБ'; return parts.length > 1 ? parts.pop() : '';
return (bytes / 1048576).toFixed(1) + ' МБ'; }
function formatSize(kb) {
if (!kb) return '';
if (kb < 1024) return kb + ' КБ';
return (kb / 1024).toFixed(1) + ' МБ';
} }
function selectFolder(id) { function selectFolder(id) {
currentFolderId = id; currentFolderId = id;
const folder = folders.find(f => f.id === id); const folder = folders.find(f => f.id === id);
document.getElementById('currentFolderTitle').textContent = folder ? folder.name : 'Все файлы'; document.getElementById('currentFolderTitle').textContent = folder ? folder.name : 'Все документы';
renderFolders(); renderFolders();
renderFiles(); renderFiles();
} }
async function createFolder() { async function createFolder() {
const name = prompt('Название папки:'); const name = prompt('Название категории:');
if (!name || !name.trim()) return; if (!name || !name.trim()) return;
try { try {
const res = await fetch('/api/folders', { const res = await fetch('/api/folders', {
@ -186,23 +227,23 @@ async function createFolder() {
if (!res.ok) throw new Error('Ошибка: ' + res.status); if (!res.ok) throw new Error('Ошибка: ' + res.status);
await loadFolders(); await loadFolders();
} catch (err) { } catch (err) {
alert('Не удалось создать папку: ' + err.message); alert('Не удалось создать категорию: ' + err.message);
} }
} }
async function deleteFolder(id) { async function deleteFolder(id) {
if (!confirm('Удалить папку и все файлы в ней?')) return; if (!confirm('Удалить категорию и все документы в ней?')) return;
await fetch('/api/folders/' + id, { method: 'DELETE' }); await fetch('/api/folders/' + id, { method: 'DELETE' });
if (currentFolderId === id) { if (currentFolderId === id) {
currentFolderId = null; currentFolderId = null;
document.getElementById('currentFolderTitle').textContent = 'Все файлы'; document.getElementById('currentFolderTitle').textContent = 'Все документы';
} }
await loadFolders(); await loadFolders();
await loadFiles(); await loadFiles();
} }
async function deleteFile(id) { async function deleteFile(id) {
if (!confirm('Удалить файл?')) return; if (!confirm('Удалить документ?')) return;
await fetch('/api/files/' + id, { method: 'DELETE' }); await fetch('/api/files/' + id, { method: 'DELETE' });
await loadFiles(); await loadFiles();
} }
@ -219,20 +260,53 @@ document.getElementById('sortSelect').addEventListener('change', (e) => {
renderFiles(); renderFiles();
}); });
document.getElementById('uploadBtn').addEventListener('click', () => { document.getElementById('toggleUploadBtn').addEventListener('click', () => {
const folderId = currentFolderId !== null ? currentFolderId : (folders[0]?.id || null); if (folders.length === 0) {
if (!folderId && folders.length === 0) { alert('Сначала создайте категорию');
alert('Сначала создайте папку');
return; return;
} }
const name = prompt('Название файла:'); document.getElementById('uploadForm').classList.toggle('active');
if (!name) return; });
const size = parseInt(prompt('Размер (байт):', '1000')) || 0;
fetch('/api/files', { 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', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ folderId: folderId || 1, name, size }) body: JSON.stringify({ folderId, name: finalName, size })
}).then(() => loadFiles()); });
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(); loadFolders();