initial commit
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user