This commit is contained in:
2025-12-14 22:53:57 +05:00
parent d25ac311f3
commit d8e8298c27
4 changed files with 805 additions and 40 deletions

View File

@@ -40,7 +40,6 @@
<nav>
<button onclick="showSection('tasks')">Задачи</button>
<button onclick="showSection('create-task')">Создать задачу</button>
<button onclick="showSection('tasks')">Мои задачи</button>
<button onclick="showTasksWithoutDate()" id="tasks-no-date-btn">Задачи без срока</button>
<button onclick="showSection('logs')">Лог активности</button>
<button onclick="window.location.href = '/admin'" style="background: linear-gradient(135deg, #e74c3c, #c0392b);">Админ-панель</button>

View File

@@ -214,11 +214,24 @@ async function loadTasks() {
const response = await fetch(url);
tasks = await response.json();
// Загружаем файлы для всех задач
await Promise.all(tasks.map(async (task) => {
try {
const filesResponse = await fetch(`/api/tasks/${task.id}/files`);
if (filesResponse.ok) {
task.files = await filesResponse.json();
} else {
task.files = [];
}
} catch (error) {
console.error(`Ошибка загрузки файлов для задачи ${task.id}:`, error);
task.files = [];
}
}));
renderTasks();
tasks.forEach(task => {
loadTaskFiles(task.id);
});
} catch (error) {
console.error('Ошибка загрузки задач:', error);
}
@@ -244,6 +257,21 @@ async function loadTasksWithoutDate() {
return hasTaskDueDate && hasAssignmentDueDates;
});
// Загружаем файлы для всех задач
await Promise.all(tasks.map(async (task) => {
try {
const filesResponse = await fetch(`/api/tasks/${task.id}/files`);
if (filesResponse.ok) {
task.files = await filesResponse.json();
} else {
task.files = [];
}
} catch (error) {
console.error(`Ошибка загрузки файлов для задачи ${task.id}:`, error);
task.files = [];
}
}));
renderTasks();
} catch (error) {
console.error('Ошибка загрузки задач без срока:', error);
@@ -389,7 +417,11 @@ function renderTasks() {
${showingTasksWithoutDate ? '<span class="no-date-badge">Без срока</span>' : ''}
</div>
<div class="file-list" id="files-${task.id}">
<strong>Файлы:</strong> <span class="files-placeholder">скрыто</span>
<strong>Файлы:</strong>
${task.files && task.files.length > 0 ?
`<div class="file-icons-container">${task.files.map(file => renderFileIcon(file)).join('')}</div>` :
'<span class="no-files">нет файлов</span>'
}
</div>
</div>
@@ -417,6 +449,7 @@ function toggleTask(taskId) {
expandedTasks.delete(taskId);
} else {
expandedTasks.add(taskId);
loadTaskFiles(taskId); // Эта строка должна быть
}
renderTasks();
}
@@ -1122,19 +1155,328 @@ async function loadTaskFiles(taskId) {
} else {
container.innerHTML = `
<strong>Файлы:</strong>
${files.map(file => `
<div class="file-item">
<a href="/api/files/${file.id}/download" download="${file.original_name}">
${file.original_name}
</a>
(${(file.file_size / 1024 / 1024).toFixed(2)} MB)
<small> - загрузил: ${file.user_name}</small>
</div>
`).join('')}
<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;
}

View File

@@ -425,6 +425,11 @@ button.edit-date-btn:hover {
color: #856404;
}
.task-dates-files .file-list strong {
display: block;
margin-bottom: 5px;
}
.task-dates-files .files-placeholder {
color: #6c757d;
font-style: italic;
@@ -1342,4 +1347,391 @@ button.reopen-btn:hover {
.task-dates-files .file-list {
min-width: 100%;
}
}
/* Стили для иконок файлов */
.file-icons-container {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-top: 10px;
}
.file-icon-container {
display: flex;
flex-direction: column;
align-items: center;
width: 80px;
cursor: pointer;
transition: transform 0.2s ease;
position: relative;
}
.file-icon-container:hover {
transform: translateY(-5px);
}
.file-icon {
width: 60px;
height: 60px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.file-icon:hover {
transform: scale(1.05);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
}
.file-icon::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 20px;
background: rgba(255, 255, 255, 0.2);
}
.file-extension {
color: white;
font-size: 0.8rem;
font-weight: bold;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
z-index: 1;
}
.file-name {
margin-top: 5px;
font-size: 0.75rem;
color: #495057;
text-align: center;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: color 0.2s ease;
}
.file-download-link {
text-decoration: none;
color: inherit;
width: 100%;
}
.file-download-link:hover .file-name {
color: #3498db;
text-decoration: underline;
}
/* Тултип при наведении */
.file-icon-container::before {
content: attr(title);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 0.8rem;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
z-index: 100;
pointer-events: none;
}
.file-icon-container:hover::before {
opacity: 1;
visibility: visible;
}
/* Адаптивность */
@media (max-width: 768px) {
.file-icons-container {
gap: 10px;
justify-content: center;
}
.file-icon-container {
width: 70px;
}
.file-icon {
width: 50px;
height: 50px;
}
.file-extension {
font-size: 0.7rem;
}
.file-name {
font-size: 0.7rem;
}
}
@media (max-width: 480px) {
.file-icons-container {
gap: 8px;
}
.file-icon-container {
width: 60px;
}
.file-icon {
width: 45px;
height: 45px;
}
.file-extension {
font-size: 0.65rem;
}
.file-name {
font-size: 0.65rem;
}
}
.no-files {
color: #6c757d;
font-style: italic;
font-size: 0.9rem;
}
/* Сделаем файлы видимыми всегда */
.task-dates-files .file-list {
flex: 1;
min-width: 250px;
font-size: 0.95rem;
color: #856404;
}
.task-dates-files .file-list strong {
display: block;
margin-bottom: 10px;
}
/* Уменьшим отступы для компактности */
.task-dates-files {
margin: 10px 0;
padding: 12px;
background: linear-gradient(135deg, #fff3cd, #ffeaa7);
border-radius: 10px;
border-left: 4px solid #f39c12;
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 15px;
}
/* Стили для иконок файлов */
.file-icons-container {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-top: 10px;
}
.file-icon-container {
display: flex;
flex-direction: column;
align-items: center;
width: 80px;
cursor: pointer;
transition: transform 0.2s ease;
position: relative;
}
.file-icon-container:hover {
transform: translateY(-5px);
}
.file-icon {
width: 60px;
height: 60px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.file-icon:hover {
transform: scale(1.05);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
}
.file-icon::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 20px;
background: rgba(255, 255, 255, 0.2);
}
.file-extension {
color: white;
font-size: 0.8rem;
font-weight: bold;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
z-index: 1;
padding: 0 5px;
text-align: center;
line-height: 1.2;
max-width: 100%;
word-break: break-all;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
/* Для коротких текстов (1-2 символа) */
.file-extension.short {
font-size: 1rem;
}
/* Для средних текстов (3-4 символа) */
.file-extension.medium {
font-size: 0.9rem;
}
/* Для длинных текстов (5+ символов) */
.file-extension.long {
font-size: 0.7rem;
}
.file-name {
margin-top: 5px;
font-size: 0.75rem;
color: #495057;
text-align: center;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: color 0.2s ease;
}
.file-download-link {
text-decoration: none;
color: inherit;
width: 100%;
}
.file-download-link:hover .file-name {
color: #3498db;
text-decoration: underline;
}
/* Тултип при наведении */
.file-icon-container::before {
content: attr(title);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 0.8rem;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
z-index: 100;
pointer-events: none;
}
.file-icon-container:hover::before {
opacity: 1;
visibility: visible;
}
/* Адаптивность */
@media (max-width: 768px) {
.file-icons-container {
gap: 10px;
justify-content: center;
}
.file-icon-container {
width: 70px;
}
.file-icon {
width: 50px;
height: 50px;
}
.file-extension {
font-size: 0.7rem;
}
.file-extension.short {
font-size: 0.9rem;
}
.file-extension.medium {
font-size: 0.8rem;
}
.file-extension.long {
font-size: 0.6rem;
}
.file-name {
font-size: 0.7rem;
}
}
@media (max-width: 480px) {
.file-icons-container {
gap: 8px;
}
.file-icon-container {
width: 60px;
}
.file-icon {
width: 45px;
height: 45px;
}
.file-extension {
font-size: 0.65rem;
}
.file-extension.short {
font-size: 0.8rem;
}
.file-extension.medium {
font-size: 0.7rem;
}
.file-extension.long {
font-size: 0.55rem;
}
.file-name {
font-size: 0.65rem;
}
}