email and fix

This commit is contained in:
2026-01-26 17:44:28 +05:00
parent 4985b4727b
commit 77122aa9ee
16 changed files with 3531 additions and 2330 deletions

451
public/files.js Normal file
View File

@@ -0,0 +1,451 @@
// files.js - Работа с файлами
let currentTaskFiles = [];
let currentEditTaskFiles = [];
function initializeFileUploads() {
// Создание задачи
document.getElementById('files').addEventListener('change', function(e) {
currentTaskFiles = Array.from(e.target.files);
updateFileList();
});
// Редактирование задачи
document.getElementById('edit-files').addEventListener('change', function(e) {
const newFiles = Array.from(e.target.files);
currentEditTaskFiles.push(...newFiles);
updateEditFileList();
});
}
function updateFileList() {
const fileInput = document.getElementById('files');
const fileList = document.getElementById('file-list');
updateFileListForInput(fileInput, fileList);
}
function updateEditFileList() {
const fileInput = document.getElementById('edit-files');
const fileList = document.getElementById('edit-file-list');
// Используем улучшенный рендеринг файлов
const files = fileInput.files;
const existingFiles = currentEditTaskFiles.filter(file => !(file instanceof File));
if (files.length === 0 && existingFiles.length === 0) {
fileList.innerHTML = '';
return;
}
let html = '<ul>';
let totalSize = 0;
// Существующие файлы
existingFiles.forEach(file => {
totalSize += file.file_size;
html += `<li>${file.original_name} (${(file.file_size / 1024 / 1024).toFixed(2)} MB) - <em>уже загружен</em></li>`;
});
// Новые файлы
for (let i = 0; i < files.length; i++) {
const file = files[i];
totalSize += file.size;
html += `<li>${file.name} (${(file.size / 1024 / 1024).toFixed(2)} MB) - <em>новый</em></li>`;
}
html += '</ul>';
html += `<p><strong>Общий размер: ${(totalSize / 1024 / 1024).toFixed(2)} MB / 300 MB</strong></p>`;
fileList.innerHTML = html;
}
function updateFileListForInput(fileInput, fileList) {
const files = fileInput.files;
if (files.length === 0) {
fileList.innerHTML = '';
return;
}
let html = '<ul>';
let totalSize = 0;
for (let i = 0; i < files.length; i++) {
const file = files[i];
totalSize += file.size;
html += `<li>${file.name} (${(file.size / 1024 / 1024).toFixed(2)} MB)</li>`;
}
html += '</ul>';
html += `<p><strong>Общий размер: ${(totalSize / 1024 / 1024).toFixed(2)} MB / 300 MB</strong></p>`;
fileList.innerHTML = html;
}
// Удаление файлов из списка
function removeFile(index) {
currentTaskFiles.splice(index, 1);
updateFileList();
// Обновляем input files
const dataTransfer = new DataTransfer();
currentTaskFiles.forEach(file => dataTransfer.items.add(file));
document.getElementById('files').files = dataTransfer.files;
}
function removeEditFile(index) {
currentEditTaskFiles.splice(index, 1);
updateEditFileList();
// Обновляем input files
const dataTransfer = new DataTransfer();
const newFiles = currentEditTaskFiles.filter(file => file instanceof File);
newFiles.forEach(file => dataTransfer.items.add(file));
document.getElementById('edit-files').files = dataTransfer.files;
}
async function loadTaskFiles(taskId) {
try {
const response = await fetch(`/api/tasks/${taskId}/files`);
const files = await response.json();
const container = document.getElementById(`files-${taskId}`);
if (container) {
if (files.length === 0) {
container.innerHTML = '<strong>Файлы:</strong> <span class="files-placeholder">скрыто</span>';
} else {
container.innerHTML = `
<strong>Файлы:</strong>
<div class="file-icons-container">
${files.map(file => renderFileIcon(file)).join('')}
</div>
`;
}
}
} catch (error) {
console.error('Ошибка загрузки файлов:', error);
}
}
function renderFileIcon(file) {
// Исправляем кодировку имени файла
const fixEncoding = (str) => {
if (!str) return '';
try {
// Пробуем разные способы декодирования
if (str.includes('Ð') || str.includes('Ñ')) {
// UTF-8 неправильно декодированный как Latin-1
return decodeURIComponent(escape(str));
}
return str;
} catch (e) {
return str;
}
};
const fileName = fixEncoding(file.original_name);
const fileSize = (file.file_size / 1024 / 1024).toFixed(2);
const uploadedBy = file.user_name;
let iconColor = '';
let iconText = '';
let textClass = '';
// Определяем расширение файла
const extension = fileName.includes('.') ?
fileName.split('.').pop().toLowerCase() :
'';
// Определяем тип файла на основе расширения
if (extension) {
switch (extension) {
case 'pdf':
iconColor = '#e74c3c';
iconText = 'PDF';
textClass = 'short';
break;
case 'doc':
iconColor = '#3498db';
iconText = 'DOC';
textClass = 'short';
break;
case 'docx':
iconColor = '#3498db';
iconText = 'DOCX';
textClass = 'medium';
break;
case 'xls':
iconColor = '#2ecc71';
iconText = 'XLS';
textClass = 'short';
break;
case 'xlsx':
iconColor = '#2ecc71';
iconText = 'XLSX';
textClass = 'medium';
break;
case 'csv':
iconColor = '#2ecc71';
iconText = 'CSV';
textClass = 'short';
break;
case 'ppt':
iconColor = '#e67e22';
iconText = 'PPT';
textClass = 'short';
break;
case 'pptx':
iconColor = '#e67e22';
iconText = 'PPTX';
textClass = 'medium';
break;
case 'zip':
iconColor = '#f39c12';
iconText = 'ZIP';
textClass = 'short';
break;
case 'rar':
iconColor = '#f39c12';
iconText = 'RAR';
textClass = 'short';
break;
case '7z':
iconColor = '#f39c12';
iconText = '7Z';
textClass = 'short';
break;
case 'tar':
iconColor = '#f39c12';
iconText = 'TAR';
textClass = 'short';
break;
case 'gz':
iconColor = '#f39c12';
iconText = 'GZ';
textClass = 'short';
break;
case 'txt':
iconColor = '#95a5a6';
iconText = 'TXT';
textClass = 'short';
break;
case 'log':
iconColor = '#95a5a6';
iconText = 'LOG';
textClass = 'short';
break;
case 'md':
iconColor = '#95a5a6';
iconText = 'MD';
textClass = 'short';
break;
case 'jpg':
iconColor = '#9b59b6';
iconText = 'JPG';
textClass = 'short';
break;
case 'jpeg':
iconColor = '#9b59b6';
iconText = 'JPEG';
textClass = 'medium';
break;
case 'png':
iconColor = '#9b59b6';
iconText = 'PNG';
textClass = 'short';
break;
case 'gif':
iconColor = '#9b59b6';
iconText = 'GIF';
textClass = 'short';
break;
case 'bmp':
iconColor = '#9b59b6';
iconText = 'BMP';
textClass = 'short';
break;
case 'svg':
iconColor = '#9b59b6';
iconText = 'SVG';
textClass = 'short';
break;
case 'webp':
iconColor = '#9b59b6';
iconText = 'WEBP';
textClass = 'medium';
break;
case 'mp3':
iconColor = '#1abc9c';
iconText = 'MP3';
textClass = 'short';
break;
case 'wav':
iconColor = '#1abc9c';
iconText = 'WAV';
textClass = 'short';
break;
case 'ogg':
iconColor = '#1abc9c';
iconText = 'OGG';
textClass = 'short';
break;
case 'flac':
iconColor = '#1abc9c';
iconText = 'FLAC';
textClass = 'medium';
break;
case 'mp4':
iconColor = '#d35400';
iconText = 'MP4';
textClass = 'short';
break;
case 'avi':
iconColor = '#d35400';
iconText = 'AVI';
textClass = 'short';
break;
case 'mkv':
iconColor = '#d35400';
iconText = 'MKV';
textClass = 'short';
break;
case 'mov':
iconColor = '#d35400';
iconText = 'MOV';
textClass = 'short';
break;
case 'wmv':
iconColor = '#d35400';
iconText = 'WMV';
textClass = 'short';
break;
case 'exe':
iconColor = '#c0392b';
iconText = 'EXE';
textClass = 'short';
break;
case 'msi':
iconColor = '#c0392b';
iconText = 'MSI';
textClass = 'short';
break;
case 'js':
iconColor = '#2980b9';
iconText = 'JS';
textClass = 'short';
break;
case 'html':
iconColor = '#2980b9';
iconText = 'HTML';
textClass = 'medium';
break;
case 'css':
iconColor = '#2980b9';
iconText = 'CSS';
textClass = 'short';
break;
case 'php':
iconColor = '#2980b9';
iconText = 'PHP';
textClass = 'short';
break;
case 'py':
iconColor = '#2980b9';
iconText = 'PY';
textClass = 'short';
break;
case 'java':
iconColor = '#2980b9';
iconText = 'JAVA';
textClass = 'medium';
break;
case 'json':
iconColor = '#8e44ad';
iconText = 'JSON';
textClass = 'medium';
break;
case 'xml':
iconColor = '#8e44ad';
iconText = 'XML';
textClass = 'short';
break;
case 'yml':
iconColor = '#8e44ad';
iconText = 'YML';
textClass = 'short';
break;
case 'yaml':
iconColor = '#8e44ad';
iconText = 'YAML';
textClass = 'medium';
break;
case 'sql':
iconColor = '#27ae60';
iconText = 'SQL';
textClass = 'short';
break;
case 'db':
iconColor = '#27ae60';
iconText = 'DB';
textClass = 'short';
break;
case 'sqlite':
iconColor = '#27ae60';
iconText = 'SQLITE';
textClass = 'long';
break;
default:
// Для других расширений используем расширение или первые 4 символа
iconColor = '#7f8c8d';
iconText = extension.length > 4 ?
extension.substring(0, 4).toUpperCase() :
extension.toUpperCase();
// Определяем класс по длине текста
if (iconText.length <= 2) {
textClass = 'short';
} else if (iconText.length <= 4) {
textClass = 'medium';
} else {
textClass = 'long';
}
}
} else {
// Если нет расширения
iconColor = '#7f8c8d';
iconText = 'ФАЙЛ';
textClass = 'short';
}
// Исправляем кодировку для отображения
const safeFileName = fileName;
const displayFileName = truncateFileName(safeFileName);
return `
<a href="/api/files/${file.id}/download"
download="${encodeURIComponent(safeFileName)}"
class="file-icon-container"
title="${safeFileName} (${fileSize} MB) - Загрузил: ${uploadedBy}">
<div class="file-icon" style="background: ${iconColor}">
<span class="file-extension ${textClass}">${iconText}</span>
</div>
<div class="file-name">${displayFileName}</div>
</a>
`;
}
function truncateFileName(fileName, maxLength = 20) {
if (fileName.length <= maxLength) return fileName;
const extension = fileName.split('.').pop();
const name = fileName.substring(0, fileName.lastIndexOf('.'));
const truncatedName = name.substring(0, maxLength - extension.length - 3) + '...';
return truncatedName + '.' + extension;
}
// Вспомогательная функция для форматирования размера файла
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}