- Добавлены обработчики для предотвращения дублирования изображений и проверки размера файлов при загрузке (максимум 10MB). - Реализованы уведомления о добавленных изображениях и улучшен интерфейс для мобильных устройств с индикаторами загрузки и сохранения. - Оптимизированы стили для мобильных устройств, включая улучшения для кнопок и элементов управления.
577 lines
21 KiB
HTML
577 lines
21 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Тест загрузки изображений на мобильных - NoteJS</title>
|
||
|
||
<!-- PWA Meta Tags -->
|
||
<meta name="description" content="Тестирование загрузки изображений на мобильных устройствах" />
|
||
<meta name="theme-color" content="#007bff" />
|
||
<meta name="mobile-web-app-capable" content="yes" />
|
||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||
<meta name="apple-mobile-web-app-title" content="NoteJS" />
|
||
<meta name="apple-touch-fullscreen" content="yes" />
|
||
|
||
<!-- Icons -->
|
||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||
<link rel="icon" type="image/png" sizes="32x32" href="/icons/icon-32x32.png" />
|
||
<link rel="icon" type="image/png" sizes="16x16" href="/icons/icon-16x16.png" />
|
||
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-192x192.png" />
|
||
|
||
<!-- Styles -->
|
||
<link rel="stylesheet" href="/style.css" />
|
||
|
||
<style>
|
||
.test-container {
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
font-family: Arial, sans-serif;
|
||
}
|
||
|
||
.test-section {
|
||
background: #f8f9fa;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.upload-area {
|
||
border: 2px dashed #007bff;
|
||
border-radius: 8px;
|
||
padding: 40px 20px;
|
||
text-align: center;
|
||
background: #f8f9fa;
|
||
margin: 20px 0;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.upload-area:hover {
|
||
background: #e7f3ff;
|
||
border-color: #0056b3;
|
||
}
|
||
|
||
.upload-area.dragover {
|
||
background: #d4edda;
|
||
border-color: #28a745;
|
||
}
|
||
|
||
.upload-icon {
|
||
font-size: 48px;
|
||
color: #007bff;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.upload-text {
|
||
font-size: 16px;
|
||
color: #333;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.upload-hint {
|
||
font-size: 14px;
|
||
color: #6c757d;
|
||
}
|
||
|
||
.file-input {
|
||
display: none;
|
||
}
|
||
|
||
.preview-container {
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.preview-item {
|
||
position: relative;
|
||
display: inline-block;
|
||
margin: 10px;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
background: white;
|
||
}
|
||
|
||
.preview-item img {
|
||
width: 150px;
|
||
height: 150px;
|
||
object-fit: cover;
|
||
display: block;
|
||
}
|
||
|
||
.preview-item .remove-btn {
|
||
position: absolute;
|
||
top: 5px;
|
||
right: 5px;
|
||
background: rgba(220, 53, 69, 0.8);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 50%;
|
||
width: 24px;
|
||
height: 24px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.preview-item .file-info {
|
||
padding: 5px;
|
||
font-size: 12px;
|
||
color: #6c757d;
|
||
background: #f8f9fa;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.test-button {
|
||
background: #007bff;
|
||
color: white;
|
||
border: none;
|
||
padding: 12px 24px;
|
||
border-radius: 6px;
|
||
font-size: 16px;
|
||
cursor: pointer;
|
||
width: 100%;
|
||
margin: 10px 0;
|
||
}
|
||
|
||
.test-button:hover {
|
||
background: #0056b3;
|
||
}
|
||
|
||
.test-button:disabled {
|
||
background: #6c757d;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.status {
|
||
padding: 10px;
|
||
border-radius: 4px;
|
||
margin: 10px 0;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.status.success {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
border: 1px solid #c3e6cb;
|
||
}
|
||
|
||
.status.error {
|
||
background: #f8d7da;
|
||
color: #721c24;
|
||
border: 1px solid #f5c6cb;
|
||
}
|
||
|
||
.status.info {
|
||
background: #d1ecf1;
|
||
color: #0c5460;
|
||
border: 1px solid #bee5eb;
|
||
}
|
||
|
||
.debug-info {
|
||
background: #f8f9fa;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 4px;
|
||
padding: 10px;
|
||
font-family: monospace;
|
||
font-size: 12px;
|
||
white-space: pre-wrap;
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
margin: 10px 0;
|
||
}
|
||
|
||
.device-info {
|
||
background: #e7f3ff;
|
||
border: 1px solid #b3d9ff;
|
||
border-radius: 6px;
|
||
padding: 15px;
|
||
margin: 15px 0;
|
||
font-size: 14px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="test-container">
|
||
<h1>📱 Тест загрузки изображений на мобильных</h1>
|
||
|
||
<div class="device-info" id="device-info">
|
||
Определение устройства...
|
||
</div>
|
||
|
||
<div class="test-section">
|
||
<h3>Тест загрузки файлов</h3>
|
||
|
||
<div class="upload-area" id="upload-area">
|
||
<div class="upload-icon">📷</div>
|
||
<div class="upload-text">Нажмите для выбора изображений</div>
|
||
<div class="upload-hint">или перетащите файлы сюда</div>
|
||
<input type="file" id="file-input" class="file-input" accept="image/*" multiple>
|
||
</div>
|
||
|
||
<div class="preview-container" id="preview-container"></div>
|
||
|
||
<button class="test-button" id="upload-btn" disabled>Загрузить на сервер</button>
|
||
<button class="test-button" id="clear-btn">Очистить все</button>
|
||
</div>
|
||
|
||
<div class="test-section">
|
||
<h3>Результаты тестов</h3>
|
||
<div id="test-results"></div>
|
||
</div>
|
||
|
||
<div class="test-section">
|
||
<h3>Отладочная информация</h3>
|
||
<button class="test-button" onclick="showDebugInfo()">Показать отладочную информацию</button>
|
||
<div id="debug-info" class="debug-info" style="display: none;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let selectedFiles = [];
|
||
|
||
// Определение типа устройства
|
||
function detectDevice() {
|
||
const userAgent = navigator.userAgent;
|
||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) ||
|
||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2) ||
|
||
window.matchMedia('(max-width: 768px)').matches;
|
||
|
||
const isIOS = /iPad|iPhone|iPod/.test(userAgent);
|
||
const isAndroid = /Android/.test(userAgent);
|
||
|
||
const deviceInfo = document.getElementById('device-info');
|
||
let deviceText = '';
|
||
|
||
if (isMobile) {
|
||
deviceText = '📱 <strong>Мобильное устройство</strong><br>';
|
||
if (isIOS) {
|
||
deviceText += '🍎 iOS устройство<br>';
|
||
} else if (isAndroid) {
|
||
deviceText += '🤖 Android устройство<br>';
|
||
} else {
|
||
deviceText += '📱 Другое мобильное устройство<br>';
|
||
}
|
||
} else {
|
||
deviceText = '💻 <strong>ПК/Десктоп</strong><br>';
|
||
}
|
||
|
||
deviceText += `User Agent: ${userAgent}<br>`;
|
||
deviceText += `Touch Points: ${navigator.maxTouchPoints || 0}<br>`;
|
||
deviceText += `Screen: ${screen.width}x${screen.height}<br>`;
|
||
deviceText += `Viewport: ${window.innerWidth}x${window.innerHeight}`;
|
||
|
||
deviceInfo.innerHTML = deviceText;
|
||
|
||
return { isMobile, isIOS, isAndroid };
|
||
}
|
||
|
||
// Инициализация
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const device = detectDevice();
|
||
setupFileUpload();
|
||
runTests(device);
|
||
});
|
||
|
||
// Настройка загрузки файлов
|
||
function setupFileUpload() {
|
||
const uploadArea = document.getElementById('upload-area');
|
||
const fileInput = document.getElementById('file-input');
|
||
const uploadBtn = document.getElementById('upload-btn');
|
||
const clearBtn = document.getElementById('clear-btn');
|
||
|
||
// Клик по области загрузки
|
||
uploadArea.addEventListener('click', function() {
|
||
fileInput.click();
|
||
});
|
||
|
||
// Выбор файлов
|
||
fileInput.addEventListener('change', function(e) {
|
||
handleFiles(e.target.files);
|
||
});
|
||
|
||
// Drag and drop
|
||
uploadArea.addEventListener('dragover', function(e) {
|
||
e.preventDefault();
|
||
uploadArea.classList.add('dragover');
|
||
});
|
||
|
||
uploadArea.addEventListener('dragleave', function(e) {
|
||
e.preventDefault();
|
||
uploadArea.classList.remove('dragover');
|
||
});
|
||
|
||
uploadArea.addEventListener('drop', function(e) {
|
||
e.preventDefault();
|
||
uploadArea.classList.remove('dragover');
|
||
handleFiles(e.dataTransfer.files);
|
||
});
|
||
|
||
// Кнопка загрузки
|
||
uploadBtn.addEventListener('click', function() {
|
||
uploadFiles();
|
||
});
|
||
|
||
// Кнопка очистки
|
||
clearBtn.addEventListener('click', function() {
|
||
clearAll();
|
||
});
|
||
}
|
||
|
||
// Обработка выбранных файлов
|
||
function handleFiles(files) {
|
||
const fileArray = Array.from(files);
|
||
const imageFiles = fileArray.filter(file => file.type.startsWith('image/'));
|
||
|
||
if (imageFiles.length === 0) {
|
||
showStatus('error', 'Пожалуйста, выберите только изображения');
|
||
return;
|
||
}
|
||
|
||
selectedFiles = [...selectedFiles, ...imageFiles];
|
||
updatePreview();
|
||
updateUploadButton();
|
||
|
||
showStatus('success', `Добавлено ${imageFiles.length} изображений. Всего: ${selectedFiles.length}`);
|
||
}
|
||
|
||
// Обновление превью
|
||
function updatePreview() {
|
||
const container = document.getElementById('preview-container');
|
||
container.innerHTML = '';
|
||
|
||
selectedFiles.forEach((file, index) => {
|
||
const reader = new FileReader();
|
||
reader.onload = function(e) {
|
||
const previewItem = document.createElement('div');
|
||
previewItem.className = 'preview-item';
|
||
previewItem.innerHTML = `
|
||
<img src="${e.target.result}" alt="Preview">
|
||
<button class="remove-btn" onclick="removeFile(${index})">×</button>
|
||
<div class="file-info">
|
||
${file.name}<br>
|
||
${(file.size / 1024 / 1024).toFixed(2)} MB<br>
|
||
${file.type}
|
||
</div>
|
||
`;
|
||
container.appendChild(previewItem);
|
||
};
|
||
reader.readAsDataURL(file);
|
||
});
|
||
}
|
||
|
||
// Удаление файла
|
||
function removeFile(index) {
|
||
selectedFiles.splice(index, 1);
|
||
updatePreview();
|
||
updateUploadButton();
|
||
}
|
||
|
||
// Обновление кнопки загрузки
|
||
function updateUploadButton() {
|
||
const uploadBtn = document.getElementById('upload-btn');
|
||
uploadBtn.disabled = selectedFiles.length === 0;
|
||
}
|
||
|
||
// Очистка всех файлов
|
||
function clearAll() {
|
||
selectedFiles = [];
|
||
updatePreview();
|
||
updateUploadButton();
|
||
document.getElementById('file-input').value = '';
|
||
showStatus('info', 'Все файлы очищены');
|
||
}
|
||
|
||
// Загрузка файлов на сервер
|
||
async function uploadFiles() {
|
||
if (selectedFiles.length === 0) {
|
||
showStatus('error', 'Нет файлов для загрузки');
|
||
return;
|
||
}
|
||
|
||
const uploadBtn = document.getElementById('upload-btn');
|
||
uploadBtn.disabled = true;
|
||
uploadBtn.textContent = 'Загрузка...';
|
||
|
||
try {
|
||
const formData = new FormData();
|
||
selectedFiles.forEach(file => {
|
||
formData.append('images', file);
|
||
});
|
||
|
||
// Создаем тестовую заметку
|
||
const noteResponse = await fetch('/api/notes', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
content: 'Тестовая заметка для проверки загрузки изображений',
|
||
date: new Date().toLocaleDateString('ru-RU'),
|
||
time: new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })
|
||
}),
|
||
});
|
||
|
||
if (!noteResponse.ok) {
|
||
throw new Error('Ошибка создания заметки');
|
||
}
|
||
|
||
const noteData = await noteResponse.json();
|
||
const noteId = noteData.id;
|
||
|
||
// Загружаем изображения
|
||
const uploadResponse = await fetch(`/api/notes/${noteId}/images`, {
|
||
method: 'POST',
|
||
body: formData,
|
||
});
|
||
|
||
if (!uploadResponse.ok) {
|
||
throw new Error('Ошибка загрузки изображений');
|
||
}
|
||
|
||
const uploadData = await uploadResponse.json();
|
||
showStatus('success', `Успешно загружено ${uploadData.images.length} изображений!`);
|
||
|
||
// Очищаем после успешной загрузки
|
||
clearAll();
|
||
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки:', error);
|
||
showStatus('error', `Ошибка загрузки: ${error.message}`);
|
||
} finally {
|
||
uploadBtn.disabled = false;
|
||
uploadBtn.textContent = 'Загрузить на сервер';
|
||
}
|
||
}
|
||
|
||
// Показ статуса
|
||
function showStatus(type, message) {
|
||
const results = document.getElementById('test-results');
|
||
const statusDiv = document.createElement('div');
|
||
statusDiv.className = `status ${type}`;
|
||
statusDiv.textContent = `${new Date().toLocaleTimeString()}: ${message}`;
|
||
results.appendChild(statusDiv);
|
||
results.scrollTop = results.scrollHeight;
|
||
}
|
||
|
||
// Запуск тестов
|
||
function runTests(device) {
|
||
const tests = [
|
||
{
|
||
name: 'Поддержка File API',
|
||
test: () => typeof File !== 'undefined' && typeof FileReader !== 'undefined'
|
||
},
|
||
{
|
||
name: 'Поддержка FormData',
|
||
test: () => typeof FormData !== 'undefined'
|
||
},
|
||
{
|
||
name: 'Поддержка fetch API',
|
||
test: () => typeof fetch !== 'undefined'
|
||
},
|
||
{
|
||
name: 'Поддержка input[type="file"]',
|
||
test: () => {
|
||
const input = document.createElement('input');
|
||
input.type = 'file';
|
||
return input.type === 'file';
|
||
}
|
||
},
|
||
{
|
||
name: 'Поддержка multiple атрибута',
|
||
test: () => {
|
||
const input = document.createElement('input');
|
||
input.type = 'file';
|
||
input.multiple = true;
|
||
return input.multiple === true;
|
||
}
|
||
},
|
||
{
|
||
name: 'Поддержка accept атрибута',
|
||
test: () => {
|
||
const input = document.createElement('input');
|
||
input.type = 'file';
|
||
input.accept = 'image/*';
|
||
return input.accept === 'image/*';
|
||
}
|
||
},
|
||
{
|
||
name: 'Поддержка Drag and Drop',
|
||
test: () => 'draggable' in document.createElement('div')
|
||
},
|
||
{
|
||
name: 'Поддержка Touch Events',
|
||
test: () => 'ontouchstart' in window
|
||
}
|
||
];
|
||
|
||
tests.forEach(test => {
|
||
try {
|
||
const result = test.test();
|
||
showStatus(result ? 'success' : 'error', `${test.name}: ${result ? '✅ Поддерживается' : '❌ Не поддерживается'}`);
|
||
} catch (error) {
|
||
showStatus('error', `${test.name}: ❌ Ошибка - ${error.message}`);
|
||
}
|
||
});
|
||
|
||
// Дополнительные тесты для мобильных устройств
|
||
if (device.isMobile) {
|
||
showStatus('info', '📱 Мобильные тесты:');
|
||
|
||
// Тест размера экрана
|
||
const screenSize = screen.width * screen.height;
|
||
showStatus('info', `Размер экрана: ${screen.width}x${screen.height} (${screenSize} пикселей)`);
|
||
|
||
// Тест viewport
|
||
const viewportSize = window.innerWidth * window.innerHeight;
|
||
showStatus('info', `Размер viewport: ${window.innerWidth}x${window.innerHeight} (${viewportSize} пикселей)`);
|
||
|
||
// Тест ориентации
|
||
const orientation = screen.orientation ? screen.orientation.type : 'unknown';
|
||
showStatus('info', `Ориентация: ${orientation}`);
|
||
}
|
||
}
|
||
|
||
// Показать отладочную информацию
|
||
function showDebugInfo() {
|
||
const debugInfo = document.getElementById('debug-info');
|
||
const info = {
|
||
userAgent: navigator.userAgent,
|
||
platform: navigator.platform,
|
||
language: navigator.language,
|
||
onLine: navigator.onLine,
|
||
cookieEnabled: navigator.cookieEnabled,
|
||
maxTouchPoints: navigator.maxTouchPoints,
|
||
screen: {
|
||
width: screen.width,
|
||
height: screen.height,
|
||
availWidth: screen.availWidth,
|
||
availHeight: screen.availHeight
|
||
},
|
||
window: {
|
||
innerWidth: window.innerWidth,
|
||
innerHeight: window.innerHeight,
|
||
outerWidth: window.outerWidth,
|
||
outerHeight: window.outerHeight
|
||
},
|
||
devicePixelRatio: window.devicePixelRatio,
|
||
selectedFiles: selectedFiles.map(f => ({
|
||
name: f.name,
|
||
size: f.size,
|
||
type: f.type,
|
||
lastModified: f.lastModified
|
||
}))
|
||
};
|
||
|
||
debugInfo.textContent = JSON.stringify(info, null, 2);
|
||
debugInfo.style.display = debugInfo.style.display === 'none' ? 'block' : 'none';
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|