328 lines
13 KiB
HTML
328 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Загрузка изображений - Prompt</title>
|
|
<style>
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: #f5f5f5; padding: 20px; min-height: 100vh;
|
|
}
|
|
.container {
|
|
max-width: 800px; margin: 0 auto; background: white;
|
|
border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
}
|
|
h1 { color: #333; margin-bottom: 24px; font-size: 24px; }
|
|
.upload-area {
|
|
border: 2px dashed #ccc; border-radius: 8px; padding: 40px;
|
|
text-align: center; cursor: pointer; transition: all 0.3s ease; margin-bottom: 24px;
|
|
}
|
|
.upload-area:hover, .upload-area.dragover { border-color: #4CAF50; background: #f9fff9; }
|
|
.upload-area p { color: #666; margin-bottom: 16px; }
|
|
.upload-area input[type="file"] { display: none; }
|
|
.btn {
|
|
background: #4CAF50; color: white; border: none; padding: 12px 24px;
|
|
border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500;
|
|
transition: background 0.3s ease;
|
|
}
|
|
.btn:hover { background: #45a049; }
|
|
.btn:disabled { background: #ccc; cursor: not-allowed; }
|
|
.btn-danger { background: #f44336; }
|
|
.btn-danger:hover { background: #da190b; }
|
|
.preview {
|
|
margin-top: 20px; padding: 16px; background: #f9f9f9;
|
|
border-radius: 8px; display: none;
|
|
}
|
|
.preview.active { display: block; }
|
|
.preview img { max-width: 100%; max-height: 300px; border-radius: 8px; margin-bottom: 16px; }
|
|
.preview-info { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; }
|
|
.file-info { color: #666; font-size: 14px; }
|
|
.file-info span { margin-right: 16px; }
|
|
.progress {
|
|
width: 100%; height: 4px; background: #e0e0e0; border-radius: 2px;
|
|
margin-top: 16px; overflow: hidden; display: none;
|
|
}
|
|
.progress.active { display: block; }
|
|
.progress-bar { height: 100%; background: #4CAF50; width: 0%; transition: width 0.3s ease; }
|
|
.message { padding: 12px; border-radius: 6px; margin-top: 16px; display: none; }
|
|
.message.success { background: #e8f5e9; color: #2e7d32; display: block; }
|
|
.message.error { background: #ffebee; color: #c62828; display: block; }
|
|
.files-list { margin-top: 32px; }
|
|
.files-list h2 { color: #333; font-size: 18px; margin-bottom: 16px; }
|
|
.file-item {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 12px; background: #f9f9f9; border-radius: 8px; margin-bottom: 8px;
|
|
}
|
|
.file-item-image { width: 60px; height: 60px; object-fit: cover; border-radius: 6px; margin-right: 16px; }
|
|
.file-item-info { flex: 1; min-width: 0; }
|
|
.file-item-name { font-weight: 500; color: #333; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.file-item-meta { font-size: 12px; color: #666; margin-top: 4px; }
|
|
.file-item-actions { display: flex; gap: 8px; }
|
|
.btn-small { padding: 6px 12px; font-size: 12px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>📤 Загрузка изображений</h1>
|
|
<div class="upload-area" id="uploadArea">
|
|
<p>Перетащите файл сюда или кликните для выбора</p>
|
|
<p style="font-size: 12px; color: #999;">Поддерживаются: JPEG, PNG, GIF, WebP (макс. 10MB)</p>
|
|
<button class="btn" onclick="document.getElementById('fileInput').click()">Выбрать файл</button>
|
|
<input type="file" id="fileInput" accept="image/jpeg,image/png,image/gif,image/webp">
|
|
</div>
|
|
<div class="preview" id="preview">
|
|
<img id="previewImage" src="" alt="Preview">
|
|
<div class="preview-info">
|
|
<div class="file-info">
|
|
<span id="fileName"></span>
|
|
<span id="fileSize"></span>
|
|
</div>
|
|
<button class="btn" id="uploadBtn">Загрузить</button>
|
|
</div>
|
|
<div class="progress" id="progress">
|
|
<div class="progress-bar" id="progressBar"></div>
|
|
</div>
|
|
</div>
|
|
<div class="message" id="message"></div>
|
|
<div class="files-list" id="filesList">
|
|
<h2>📁 Мои файлы</h2>
|
|
<div id="filesContainer"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API_BASE = '/api';
|
|
let selectedFile = null;
|
|
let csrfToken = null;
|
|
let generationUuid = null;
|
|
|
|
const uploadArea = document.getElementById('uploadArea');
|
|
const fileInput = document.getElementById('fileInput');
|
|
const preview = document.getElementById('preview');
|
|
const previewImage = document.getElementById('previewImage');
|
|
const fileName = document.getElementById('fileName');
|
|
const fileSize = document.getElementById('fileSize');
|
|
const uploadBtn = document.getElementById('uploadBtn');
|
|
const progress = document.getElementById('progress');
|
|
const progressBar = document.getElementById('progressBar');
|
|
const message = document.getElementById('message');
|
|
const filesContainer = document.getElementById('filesContainer');
|
|
|
|
async function init() {
|
|
await loadCsrfToken();
|
|
await loadFiles();
|
|
}
|
|
|
|
async function loadCsrfToken() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/auth/csrf`, { credentials: 'include' });
|
|
const data = await response.json();
|
|
if (data.ok) { csrfToken = data.csrfToken; }
|
|
else { showMessage('Ошибка загрузки CSRF токена. Войдите в систему.', 'error'); }
|
|
} catch (err) {
|
|
console.error('CSRF load error:', err);
|
|
showMessage('Ошибка соединения с сервером', 'error');
|
|
}
|
|
}
|
|
|
|
async function loadFiles() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/upload/files`, { credentials: 'include' });
|
|
const data = await response.json();
|
|
if (data.ok) { renderFiles(data.data.files); }
|
|
else { console.error('Ошибка загрузки файлов:', data.error); }
|
|
} catch (err) { console.error('Files load error:', err); }
|
|
}
|
|
|
|
function renderFiles(files) {
|
|
if (!files || files.length === 0) {
|
|
filesContainer.innerHTML = '<p style="color: #999; text-align: center;">Нет загруженных файлов</p>';
|
|
return;
|
|
}
|
|
filesContainer.innerHTML = files.map(file => `
|
|
<div class="file-item">
|
|
<img class="file-item-image" src="" alt="${file.originalFilename}" data-id="${file.id}">
|
|
<div class="file-item-info">
|
|
<div class="file-item-name">${escapeHtml(file.originalFilename)}</div>
|
|
<div class="file-item-meta">${formatFileSize(file.fileSize)} • ${formatDate(file.createdAt)}</div>
|
|
</div>
|
|
<div class="file-item-actions">
|
|
<button class="btn btn-small" onclick="getFileUrl(${file.id})">Получить ссылку</button>
|
|
<button class="btn btn-small btn-danger" onclick="deleteFile(${file.id})">Удалить</button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
// Загружаем presigned URL для миниатюр
|
|
document.querySelectorAll('.file-item-image').forEach(async (img) => {
|
|
const fileId = img.dataset.id;
|
|
try {
|
|
const res = await fetch(`${API_BASE}/upload/url`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrfToken },
|
|
body: JSON.stringify({ fileId, expiresIn: 300 })
|
|
});
|
|
const data = await res.json();
|
|
if (data.ok) { img.src = data.data.url; }
|
|
} catch (e) { console.error('Thumbnail load error:', e); }
|
|
});
|
|
}
|
|
|
|
async function getFileUrl(fileId) {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/upload/url`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrfToken },
|
|
body: JSON.stringify({ fileId })
|
|
});
|
|
const data = await response.json();
|
|
if (data.ok) {
|
|
await navigator.clipboard.writeText(data.data.url);
|
|
showMessage('Ссылка скопирована в буфер обмена!', 'success');
|
|
} else { showMessage(data.message || 'Ошибка получения ссылки', 'error'); }
|
|
} catch (err) {
|
|
console.error('Get URL error:', err);
|
|
showMessage('Ошибка соединения с сервером', 'error');
|
|
}
|
|
}
|
|
|
|
async function deleteFile(fileId) {
|
|
if (!confirm('Вы уверены, что хотите удалить этот файл?')) return;
|
|
try {
|
|
const response = await fetch(`${API_BASE}/upload/file/${fileId}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include',
|
|
headers: { 'x-csrf-token': csrfToken }
|
|
});
|
|
const data = await response.json();
|
|
if (data.ok) {
|
|
showMessage('Файл удалён', 'success');
|
|
await loadFiles();
|
|
} else { showMessage(data.message || 'Ошибка удаления файла', 'error'); }
|
|
} catch (err) {
|
|
console.error('Delete error:', err);
|
|
showMessage('Ошибка соединения с сервером', 'error');
|
|
}
|
|
}
|
|
|
|
uploadArea.addEventListener('dragover', (e) => { e.preventDefault(); uploadArea.classList.add('dragover'); });
|
|
uploadArea.addEventListener('dragleave', () => { uploadArea.classList.remove('dragover'); });
|
|
uploadArea.addEventListener('drop', (e) => {
|
|
e.preventDefault(); uploadArea.classList.remove('dragover');
|
|
const files = e.dataTransfer.files;
|
|
if (files.length > 0) handleFileSelect(files[0]);
|
|
});
|
|
|
|
fileInput.addEventListener('change', (e) => {
|
|
if (e.target.files.length > 0) handleFileSelect(e.target.files[0]);
|
|
});
|
|
|
|
function handleFileSelect(file) {
|
|
const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
|
if (!validTypes.includes(file.type)) {
|
|
showMessage('Недопустимый тип файла. Разрешены: JPEG, PNG, GIF, WebP', 'error');
|
|
return;
|
|
}
|
|
if (file.size > 10 * 1024 * 1024) {
|
|
showMessage('Файл слишком большой. Максимум: 10MB', 'error');
|
|
return;
|
|
}
|
|
selectedFile = file;
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
previewImage.src = e.target.result;
|
|
fileName.textContent = file.name;
|
|
fileSize.textContent = formatFileSize(file.size);
|
|
preview.classList.add('active');
|
|
uploadBtn.disabled = false;
|
|
hideMessage();
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
|
|
uploadBtn.addEventListener('click', async () => {
|
|
if (!selectedFile || !csrfToken) return;
|
|
uploadBtn.disabled = true;
|
|
progress.classList.add('active');
|
|
progressBar.style.width = '0%';
|
|
hideMessage();
|
|
|
|
const formData = new FormData();
|
|
formData.append('file', selectedFile);
|
|
|
|
// Если есть generationUuid, передаём его
|
|
if (generationUuid) {
|
|
formData.append('generationUuid', generationUuid);
|
|
formData.append('generationStepId', '1');
|
|
}
|
|
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.upload.addEventListener('progress', (e) => {
|
|
if (e.lengthComputable) {
|
|
const percent = (e.loaded / e.total) * 100;
|
|
progressBar.style.width = percent + '%';
|
|
}
|
|
});
|
|
xhr.addEventListener('load', () => {
|
|
progress.classList.remove('active');
|
|
if (xhr.status === 201) {
|
|
const data = JSON.parse(xhr.responseText);
|
|
showMessage(`Файл "${data.data.filename}" успешно загружен!`, 'success');
|
|
preview.classList.remove('active');
|
|
fileInput.value = '';
|
|
selectedFile = null;
|
|
loadFiles();
|
|
} else {
|
|
const data = JSON.parse(xhr.responseText);
|
|
showMessage(data.message || 'Ошибка загрузки файла', 'error');
|
|
}
|
|
uploadBtn.disabled = false;
|
|
});
|
|
xhr.addEventListener('error', () => {
|
|
progress.classList.remove('active');
|
|
showMessage('Ошибка соединения с сервером', 'error');
|
|
uploadBtn.disabled = false;
|
|
});
|
|
|
|
xhr.open('POST', `${API_BASE}/upload/image`);
|
|
xhr.withCredentials = true;
|
|
xhr.setRequestHeader('x-csrf-token', csrfToken);
|
|
xhr.send(formData);
|
|
});
|
|
|
|
function formatFileSize(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
|
}
|
|
|
|
function formatDate(dateString) {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function showMessage(text, type) {
|
|
message.textContent = text;
|
|
message.className = 'message ' + type;
|
|
}
|
|
|
|
function hideMessage() {
|
|
message.className = 'message';
|
|
message.textContent = '';
|
|
}
|
|
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html>
|