Files
uno-click/bff/public/prompt.html
T
2026-05-13 14:20:41 +00:00

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>