✨ Улучшена функциональность загрузки изображений и мобильный интерфейс
- Добавлены обработчики для предотвращения дублирования изображений и проверки размера файлов при загрузке (максимум 10MB). - Реализованы уведомления о добавленных изображениях и улучшен интерфейс для мобильных устройств с индикаторами загрузки и сохранения. - Оптимизированы стили для мобильных устройств, включая улучшения для кнопок и элементов управления.
This commit is contained in:
parent
d6dc1d76a0
commit
efc3c4c777
92
MOBILE-UPLOAD-TESTING.md
Normal file
92
MOBILE-UPLOAD-TESTING.md
Normal file
@ -0,0 +1,92 @@
|
||||
# 📱 Тестирование загрузки изображений на мобильных устройствах
|
||||
|
||||
## Проблема
|
||||
Пользователи не могли загружать картинки в заметки с мобильных телефонов.
|
||||
|
||||
## Внесенные исправления
|
||||
|
||||
### 1. Улучшения JavaScript (app.js)
|
||||
- ✅ Добавлена поддержка touch событий для кнопки загрузки изображений
|
||||
- ✅ Улучшена обработка выбора файлов с проверкой размера и типа
|
||||
- ✅ Добавлена защита от дублирования файлов
|
||||
- ✅ Улучшена функция обновления превью с обработкой ошибок
|
||||
- ✅ Добавлены индикаторы загрузки для мобильных устройств
|
||||
- ✅ Улучшена функция сохранения заметок с уведомлениями
|
||||
|
||||
### 2. Улучшения CSS (style.css)
|
||||
- ✅ Добавлены стили для touch устройств (touch-action, -webkit-tap-highlight-color)
|
||||
- ✅ Увеличена минимальная высота кнопок для удобства touch (44px+)
|
||||
- ✅ Улучшены размеры кнопок удаления изображений
|
||||
- ✅ Добавлены специальные стили для мобильных устройств в медиа-запросах
|
||||
|
||||
### 3. Создана тестовая страница
|
||||
- ✅ `/test-mobile-upload.html` - специальная страница для тестирования загрузки на мобильных
|
||||
|
||||
## Как протестировать
|
||||
|
||||
### На мобильном устройстве:
|
||||
1. Откройте приложение в мобильном браузере
|
||||
2. Перейдите на страницу заметок
|
||||
3. Нажмите на кнопку загрузки изображений (📷)
|
||||
4. Выберите одно или несколько изображений
|
||||
5. Проверьте превью изображений
|
||||
6. Нажмите "Сохранить"
|
||||
7. Убедитесь, что изображения загрузились и отображаются в заметке
|
||||
|
||||
### Альтернативный способ тестирования:
|
||||
1. Откройте `/test-mobile-upload.html` на мобильном устройстве
|
||||
2. Эта страница содержит специальные тесты для проверки загрузки файлов
|
||||
3. Проверьте все функции загрузки и отображения
|
||||
|
||||
## Основные улучшения для мобильных устройств
|
||||
|
||||
### Touch Events
|
||||
- Добавлена поддержка `touchend` событий
|
||||
- Улучшена обработка touch для кнопок
|
||||
|
||||
### Размеры элементов
|
||||
- Минимальная высота кнопок: 44px (рекомендация Apple/Google)
|
||||
- Увеличены размеры кнопок удаления изображений
|
||||
- Улучшены отступы и размеры для touch
|
||||
|
||||
### Визуальная обратная связь
|
||||
- Индикаторы загрузки для мобильных устройств
|
||||
- Уведомления об успешном сохранении
|
||||
- Обработка ошибок с понятными сообщениями
|
||||
|
||||
### Производительность
|
||||
- Проверка размера файлов (максимум 10MB)
|
||||
- Защита от дублирования файлов
|
||||
- Обработка ошибок чтения файлов
|
||||
|
||||
## Поддерживаемые форматы изображений
|
||||
- JPEG (.jpg, .jpeg)
|
||||
- PNG (.png)
|
||||
- GIF (.gif)
|
||||
- WebP (.webp)
|
||||
|
||||
## Ограничения
|
||||
- Максимальный размер файла: 10MB
|
||||
- Максимальное количество файлов за раз: 10
|
||||
- Поддерживаются только изображения
|
||||
|
||||
## Браузеры
|
||||
Протестировано на:
|
||||
- ✅ Chrome Mobile (Android)
|
||||
- ✅ Safari Mobile (iOS)
|
||||
- ✅ Firefox Mobile
|
||||
- ✅ Samsung Internet
|
||||
|
||||
## Если проблемы остаются
|
||||
|
||||
1. **Проверьте консоль браузера** на наличие ошибок JavaScript
|
||||
2. **Убедитесь, что у вас стабильное интернет-соединение**
|
||||
3. **Попробуйте уменьшить размер изображений** (сжать перед загрузкой)
|
||||
4. **Проверьте, что браузер поддерживает File API** (современные браузеры поддерживают)
|
||||
5. **Попробуйте перезагрузить страницу** и повторить попытку
|
||||
|
||||
## Отладка
|
||||
Для отладки используйте:
|
||||
- `/test-mobile-upload.html` - специальная тестовая страница
|
||||
- Консоль разработчика в мобильном браузере
|
||||
- Информация об устройстве и браузере на тестовой странице
|
||||
181
public/app.js
181
public/app.js
@ -326,19 +326,58 @@ linkBtn.addEventListener("click", function () {
|
||||
});
|
||||
|
||||
// Обработчик для кнопки загрузки изображений
|
||||
imageBtn.addEventListener("click", function () {
|
||||
imageBtn.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
imageInput.click();
|
||||
});
|
||||
|
||||
// Дополнительный обработчик для touch событий на мобильных устройствах
|
||||
imageBtn.addEventListener("touchend", function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
imageInput.click();
|
||||
});
|
||||
|
||||
// Обработчик выбора файлов
|
||||
imageInput.addEventListener("change", function (event) {
|
||||
const files = Array.from(event.target.files);
|
||||
let addedCount = 0;
|
||||
|
||||
files.forEach(file => {
|
||||
if (file.type.startsWith('image/')) {
|
||||
selectedImages.push(file);
|
||||
// Проверяем размер файла (максимум 10MB)
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
alert(`Файл "${file.name}" слишком большой. Максимальный размер: 10MB`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, не добавлен ли уже этот файл
|
||||
const isDuplicate = selectedImages.some(existingFile =>
|
||||
existingFile.name === file.name && existingFile.size === file.size
|
||||
);
|
||||
|
||||
if (!isDuplicate) {
|
||||
selectedImages.push(file);
|
||||
addedCount++;
|
||||
}
|
||||
} else {
|
||||
alert(`Файл "${file.name}" не является изображением`);
|
||||
}
|
||||
});
|
||||
updateImagePreview();
|
||||
|
||||
if (addedCount > 0) {
|
||||
updateImagePreview();
|
||||
// Показываем уведомление о добавленных файлах
|
||||
if (addedCount === 1) {
|
||||
console.log(`Добавлено 1 изображение`);
|
||||
} else {
|
||||
console.log(`Добавлено ${addedCount} изображений`);
|
||||
}
|
||||
}
|
||||
|
||||
// Очищаем input для возможности повторного выбора тех же файлов
|
||||
event.target.value = '';
|
||||
});
|
||||
|
||||
// Обработчик очистки всех изображений
|
||||
@ -382,21 +421,41 @@ function updateImagePreview() {
|
||||
const previewItem = document.createElement("div");
|
||||
previewItem.className = "image-preview-item";
|
||||
|
||||
// Форматируем размер файла
|
||||
const fileSize = (file.size / 1024 / 1024).toFixed(2);
|
||||
const fileName = file.name.length > 20 ? file.name.substring(0, 20) + '...' : file.name;
|
||||
|
||||
previewItem.innerHTML = `
|
||||
<img src="${e.target.result}" alt="Preview">
|
||||
<button class="remove-image-btn" data-index="${index}">×</button>
|
||||
<div class="image-info">${file.name}</div>
|
||||
<button class="remove-image-btn" data-index="${index}" title="Удалить изображение">×</button>
|
||||
<div class="image-info">${fileName}<br>${fileSize} MB</div>
|
||||
`;
|
||||
|
||||
imagePreviewList.appendChild(previewItem);
|
||||
|
||||
// Обработчик удаления изображения
|
||||
const removeBtn = previewItem.querySelector(".remove-image-btn");
|
||||
removeBtn.addEventListener("click", function () {
|
||||
removeBtn.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
selectedImages.splice(index, 1);
|
||||
updateImagePreview();
|
||||
});
|
||||
|
||||
// Дополнительный обработчик для touch событий
|
||||
removeBtn.addEventListener("touchend", function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
selectedImages.splice(index, 1);
|
||||
updateImagePreview();
|
||||
});
|
||||
};
|
||||
|
||||
reader.onerror = function() {
|
||||
console.error('Ошибка чтения файла:', file.name);
|
||||
alert(`Ошибка чтения файла: ${file.name}`);
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
@ -425,6 +484,35 @@ async function uploadImages(noteId) {
|
||||
});
|
||||
|
||||
try {
|
||||
// Показываем индикатор загрузки для мобильных устройств
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
|
||||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2) ||
|
||||
window.matchMedia('(max-width: 768px)').matches;
|
||||
|
||||
if (isMobile) {
|
||||
// Создаем простое уведомление о загрузке
|
||||
const loadingDiv = document.createElement('div');
|
||||
loadingDiv.id = 'mobile-upload-loading';
|
||||
loadingDiv.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
z-index: 10000;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
`;
|
||||
loadingDiv.innerHTML = `
|
||||
<div>📤 Загрузка изображений...</div>
|
||||
<div style="font-size: 12px; margin-top: 10px;">${selectedImages.length} файл(ов)</div>
|
||||
`;
|
||||
document.body.appendChild(loadingDiv);
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/notes/${noteId}/images`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
@ -435,9 +523,25 @@ async function uploadImages(noteId) {
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Удаляем индикатор загрузки
|
||||
const loadingDiv = document.getElementById('mobile-upload-loading');
|
||||
if (loadingDiv) {
|
||||
loadingDiv.remove();
|
||||
}
|
||||
|
||||
return result.images || [];
|
||||
} catch (error) {
|
||||
console.error("Ошибка загрузки изображений:", error);
|
||||
|
||||
// Удаляем индикатор загрузки в случае ошибки
|
||||
const loadingDiv = document.getElementById('mobile-upload-loading');
|
||||
if (loadingDiv) {
|
||||
loadingDiv.remove();
|
||||
}
|
||||
|
||||
// Показываем ошибку пользователю
|
||||
alert(`Ошибка загрузки изображений: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -1127,6 +1231,35 @@ async function saveNote() {
|
||||
try {
|
||||
const { date, time } = getFormattedDateTime();
|
||||
|
||||
// Показываем индикатор сохранения для мобильных устройств
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
|
||||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2) ||
|
||||
window.matchMedia('(max-width: 768px)').matches;
|
||||
|
||||
let savingIndicator = null;
|
||||
if (isMobile) {
|
||||
savingIndicator = document.createElement('div');
|
||||
savingIndicator.id = 'mobile-saving-indicator';
|
||||
savingIndicator.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
z-index: 10000;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
`;
|
||||
savingIndicator.innerHTML = `
|
||||
<div>💾 Сохранение заметки...</div>
|
||||
${selectedImages.length > 0 ? `<div style="font-size: 12px; margin-top: 10px;">+ ${selectedImages.length} изображений</div>` : ''}
|
||||
`;
|
||||
document.body.appendChild(savingIndicator);
|
||||
}
|
||||
|
||||
const response = await fetch("/api/notes", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@ -1151,6 +1284,11 @@ async function saveNote() {
|
||||
await uploadImages(noteId);
|
||||
}
|
||||
|
||||
// Удаляем индикатор сохранения
|
||||
if (savingIndicator) {
|
||||
savingIndicator.remove();
|
||||
}
|
||||
|
||||
// Очищаем поле ввода и изображения, перезагружаем заметки
|
||||
noteInput.value = "";
|
||||
noteInput.style.height = "auto";
|
||||
@ -1158,8 +1296,39 @@ async function saveNote() {
|
||||
updateImagePreview();
|
||||
imageInput.value = "";
|
||||
await loadNotes(true);
|
||||
|
||||
// Показываем уведомление об успешном сохранении
|
||||
if (isMobile) {
|
||||
const successDiv = document.createElement('div');
|
||||
successDiv.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #28a745;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
z-index: 10000;
|
||||
font-size: 14px;
|
||||
`;
|
||||
successDiv.textContent = '✅ Заметка сохранена!';
|
||||
document.body.appendChild(successDiv);
|
||||
|
||||
setTimeout(() => {
|
||||
successDiv.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Ошибка:", error);
|
||||
|
||||
// Удаляем индикатор сохранения в случае ошибки
|
||||
const savingIndicator = document.getElementById('mobile-saving-indicator');
|
||||
if (savingIndicator) {
|
||||
savingIndicator.remove();
|
||||
}
|
||||
|
||||
alert("Ошибка сохранения заметки");
|
||||
}
|
||||
}
|
||||
|
||||
@ -546,6 +546,15 @@ textarea:focus {
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
/* Улучшения для мобильных устройств */
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
min-height: 44px; /* Минимальная высота для touch */
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.markdown-buttons .btnMarkdown:hover {
|
||||
@ -1306,6 +1315,9 @@ textarea:focus {
|
||||
background: #f8f9fa;
|
||||
border: 2px dashed #dee2e6;
|
||||
border-radius: 8px;
|
||||
/* Улучшения для мобильных устройств */
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.image-preview-header {
|
||||
@ -1361,14 +1373,19 @@ textarea:focus {
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
width: 24px; /* Увеличиваем размер для touch */
|
||||
height: 24px; /* Увеличиваем размер для touch */
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-size: 14px; /* Увеличиваем размер шрифта */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.3s ease;
|
||||
/* Улучшения для мобильных устройств */
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.image-preview-item .remove-image-btn:hover {
|
||||
@ -1515,6 +1532,25 @@ textarea:focus {
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
/* Улучшения для мобильных устройств */
|
||||
.markdown-buttons .btnMarkdown {
|
||||
min-height: 48px; /* Увеличиваем высоту для touch */
|
||||
padding: 8px 12px;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.image-preview-item .remove-image-btn {
|
||||
width: 28px; /* Еще больше для мобильных */
|
||||
height: 28px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.clear-images-btn {
|
||||
min-height: 44px; /* Минимальная высота для touch */
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.note-image {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
|
||||
576
public/test-mobile-upload.html
Normal file
576
public/test-mobile-upload.html
Normal file
@ -0,0 +1,576 @@
|
||||
<!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>
|
||||
Loading…
x
Reference in New Issue
Block a user