feat(chat): реализован рендеринг изображений и видео в чате
Проблема: - Изображения и видео отображались только как ссылки с иконками - Нет preview изображений в composer - Вся инфраструктура существовала (DB поля, CSS, WebSocket), но не была подключена Реализация: messageRendererV2.js (_renderAttachment): - Добавлена проверка file_type === 'image': рендеринг <img class="chat-media chat-media--image"> - Добавлена проверка file_type === 'video': рендеринг <video class="chat-media chat-media--video"> с controls - Используется thumbnail URL для изображений (если доступен), иначе file_url - Атрибуты: loading="lazy", playsinline, preload="metadata" для оптимизации - Файлы (PDF, Word, Audio) рендерятся как ссылки с иконками chatComposer.js (renderPreview): - Для изображений: показываем thumbnail через URL.createObjectURL - Для других файлов: иконка (как раньше) - Добавлен URL.revokeObjectURL при удалении для предотвращения утечек памяти - Preview размеры: 60x60px с border-radius и object-fit: cover CSS (_chat-detail.scss): - Все стили .chat-media уже были готовы: * max-width: min(640px, 85vw) * border-radius: 12px * object-fit: cover (изображения) / contain (видео) * Responsive sizing для мобильных - Стили применяются автоматически при рендеринге Технические детали: - WebSocket уже передает: file_type, file_url, thumbnail, mime_type - Database модель MessageAttachment поддерживает thumbnail поле - CSS классы .chat-media--image и .chat-media--video определены - Lazy loading для изображений (атрибут loading="lazy") - Video с preload="metadata" для быстрой загрузки первого кадра Тестирование: Требуется загрузка реальных изображений/видео для проверки
This commit is contained in:
parent
f00f7c0be5
commit
9d5f70f16f
@ -629,22 +629,46 @@ class ChatComposer {
|
||||
this.preview.innerHTML = '';
|
||||
|
||||
this.selectedFiles.forEach((entry) => {
|
||||
const isImage = entry.file.type.startsWith('image/');
|
||||
const iconClass = pickIcon(entry.file.type);
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'attachment-item d-flex align-items-center gap-2 p-2 rounded mb-2';
|
||||
wrapper.innerHTML = `
|
||||
<i class="${iconClass} fs-4"></i>
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-semibold text-truncate">${entry.file.name}</div>
|
||||
<div class="small text-secondary">${formatFileSize(entry.file.size)}</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-ghost" aria-label="Удалить файл" data-file-id="${entry.id}">
|
||||
<i class="bi-x-lg"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Для изображений показываем превью
|
||||
if (isImage) {
|
||||
const imgUrl = URL.createObjectURL(entry.file);
|
||||
wrapper.innerHTML = `
|
||||
<img src="${imgUrl}" alt="${entry.file.name}"
|
||||
style="max-width: 60px; max-height: 60px; border-radius: 6px; object-fit: cover;" />
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-semibold text-truncate">${entry.file.name}</div>
|
||||
<div class="small text-secondary">${formatFileSize(entry.file.size)}</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-ghost" aria-label="Удалить файл" data-file-id="${entry.id}">
|
||||
<i class="bi-x-lg"></i>
|
||||
</button>
|
||||
`;
|
||||
} else {
|
||||
// Для других файлов - иконка
|
||||
wrapper.innerHTML = `
|
||||
<i class="${iconClass} fs-4"></i>
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-semibold text-truncate">${entry.file.name}</div>
|
||||
<div class="small text-secondary">${formatFileSize(entry.file.size)}</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-ghost" aria-label="Удалить файл" data-file-id="${entry.id}">
|
||||
<i class="bi-x-lg"></i>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
wrapper.querySelector('button')?.addEventListener('click', () => {
|
||||
this.removeFile(entry.id);
|
||||
// Очищаем URL.createObjectURL для изображений
|
||||
if (isImage) {
|
||||
const img = wrapper.querySelector('img');
|
||||
if (img) URL.revokeObjectURL(img.src);
|
||||
}
|
||||
});
|
||||
|
||||
this.preview.appendChild(wrapper);
|
||||
|
||||
@ -659,24 +659,54 @@ export class MessageRendererV2 {
|
||||
const fileUrl = att.file_url || att.url || '#';
|
||||
const fileSize = att.file_size || att.size || 0;
|
||||
const fileType = att.file_type || att.type || '';
|
||||
const thumbnailUrl = att.thumbnail || null;
|
||||
|
||||
// Форматируем размер
|
||||
// Рендеринг изображений
|
||||
if (fileType === 'image' || fileType.startsWith('image/')) {
|
||||
const imgSrc = thumbnailUrl || fileUrl;
|
||||
return `
|
||||
<a href="${fileUrl}" target="_blank" class="attachment-item d-block">
|
||||
<img src="${imgSrc}"
|
||||
alt="${fileName}"
|
||||
class="chat-media chat-media--image"
|
||||
loading="lazy"
|
||||
style="max-width: 100%; height: auto; display: block;" />
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
// Рендеринг видео
|
||||
if (fileType === 'video' || fileType.startsWith('video/')) {
|
||||
return `
|
||||
<div class="attachment-item">
|
||||
<video src="${fileUrl}"
|
||||
class="chat-media chat-media--video"
|
||||
controls
|
||||
playsinline
|
||||
preload="metadata"
|
||||
style="max-width: 100%; height: auto; display: block;">
|
||||
Ваш браузер не поддерживает воспроизведение видео.
|
||||
</video>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Форматируем размер для файлов
|
||||
const sizeStr = fileSize > 0
|
||||
? `${(fileSize / 1024).toFixed(1)} KB`
|
||||
: '';
|
||||
|
||||
// Определяем иконку по типу
|
||||
// Определяем иконку для файлов
|
||||
let icon = 'bi-file-earmark';
|
||||
if (fileType.startsWith('image/')) {
|
||||
icon = 'bi-file-image';
|
||||
} else if (fileType.startsWith('video/')) {
|
||||
icon = 'bi-file-play';
|
||||
} else if (fileType.includes('pdf')) {
|
||||
if (fileType.includes('pdf')) {
|
||||
icon = 'bi-file-pdf';
|
||||
} else if (fileType.includes('word') || fileType.includes('document')) {
|
||||
icon = 'bi-file-word';
|
||||
} else if (fileType.startsWith('audio/')) {
|
||||
icon = 'bi-file-music';
|
||||
}
|
||||
|
||||
// Рендеринг обычных файлов (документы, аудио и т.д.)
|
||||
return `
|
||||
<a href="${fileUrl}"
|
||||
target="_blank"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user