сделал редактирование файлов у конкурсов и проектов
This commit is contained in:
parent
03338413c1
commit
56725b681b
32
API/app/application/contest_files_repository.py
Normal file
32
API/app/application/contest_files_repository.py
Normal file
@ -0,0 +1,32 @@
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.domain.models import ContestFile
|
||||
|
||||
|
||||
class ContestFilesRepository:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def get_by_id(self, file_id: int) -> Optional[ContestFile]:
|
||||
stmt = select(ContestFile).filter_by(id=file_id)
|
||||
result = await self.db.execute(stmt)
|
||||
return result.scalars().first()
|
||||
|
||||
async def get_by_contest_id(self, contest_id: int) -> Sequence[ContestFile]:
|
||||
stmt = select(ContestFile).filter_by(contest_id=contest_id)
|
||||
result = await self.db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
async def create(self, contest_file: ContestFile) -> ContestFile:
|
||||
self.db.add(contest_file)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(contest_file)
|
||||
return contest_file
|
||||
|
||||
async def delete(self, contest_file: ContestFile) -> ContestFile:
|
||||
await self.db.delete(contest_file)
|
||||
await self.db.commit()
|
||||
return contest_file
|
||||
69
API/app/contollers/contest_files_router.py
Normal file
69
API/app/contollers/contest_files_router.py
Normal file
@ -0,0 +1,69 @@
|
||||
from fastapi import Depends, File, UploadFile, APIRouter
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from starlette.responses import FileResponse
|
||||
|
||||
from app.database.session import get_db
|
||||
from app.domain.entities.contest_file import ContestFileEntity
|
||||
from app.infrastructure.dependencies import require_admin
|
||||
from app.infrastructure.contest_files_service import ContestFilesService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/contests/{contest_id}/",
|
||||
response_model=list[ContestFileEntity],
|
||||
summary="Get all contest files",
|
||||
description="Returns metadata of all files uploaded for the specified contest."
|
||||
)
|
||||
async def get_files_by_contest_id(
|
||||
contest_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = ContestFilesService(db)
|
||||
return await service.get_files_by_contest_id(contest_id)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{file_id}/file",
|
||||
response_class=FileResponse,
|
||||
summary="Download contest file by ID",
|
||||
description="Returns the file for the specified file ID."
|
||||
)
|
||||
async def download_contest_file(
|
||||
file_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
service = ContestFilesService(db)
|
||||
return await service.get_file_by_id(file_id)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/contests/{contest_id}/upload",
|
||||
response_model=ContestFileEntity,
|
||||
summary="Upload a new file for the contest",
|
||||
description="Uploads a new file and associates it with the specified contest."
|
||||
)
|
||||
async def upload_contest_file(
|
||||
contest_id: int,
|
||||
file: UploadFile = File(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(require_admin),
|
||||
):
|
||||
service = ContestFilesService(db)
|
||||
return await service.upload_file(contest_id, file, user)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{file_id}/",
|
||||
response_model=ContestFileEntity,
|
||||
summary="Delete a contest file by ID",
|
||||
description="Deletes the file and its database entry."
|
||||
)
|
||||
async def delete_contest_file(
|
||||
file_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(require_admin),
|
||||
):
|
||||
service = ContestFilesService(db)
|
||||
return await service.delete_file(file_id, user)
|
||||
@ -4,7 +4,7 @@ from starlette.responses import FileResponse
|
||||
|
||||
from app.database.session import get_db
|
||||
from app.domain.entities.project_file import ProjectFileEntity
|
||||
from app.infrastructure.dependencies import get_current_user, require_admin
|
||||
from app.infrastructure.dependencies import require_admin
|
||||
from app.infrastructure.project_files_service import ProjectFilesService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
"""0005_добавил_поле_filename_у_файлов_конкурсов
|
||||
|
||||
Revision ID: de2777da99c9
|
||||
Revises: e53896c51cf8
|
||||
Create Date: 2025-06-03 10:23:08.013605
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'de2777da99c9'
|
||||
down_revision: Union[str, None] = 'e53896c51cf8'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('contest_files', sa.Column('filename', sa.String(), nullable=False))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('contest_files', 'filename')
|
||||
# ### end Alembic commands ###
|
||||
10
API/app/domain/entities/contest_file.py
Normal file
10
API/app/domain/entities/contest_file.py
Normal file
@ -0,0 +1,10 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ContestFileEntity(BaseModel):
|
||||
id: Optional[int] = None
|
||||
filename: str
|
||||
file_path: str
|
||||
contest_id: int
|
||||
@ -1,8 +1,10 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ProfilePhotoEntity(BaseModel):
|
||||
id: int
|
||||
id: Optional[int] = None
|
||||
filename: str
|
||||
file_path: str
|
||||
profile_id: int
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ProjectFileEntity(BaseModel):
|
||||
id: int
|
||||
id: Optional[int] = None
|
||||
filename: str
|
||||
file_path: str
|
||||
project_id: int
|
||||
|
||||
@ -7,6 +7,7 @@ from app.domain.models.base import AdvancedBaseModel
|
||||
class ContestFile(AdvancedBaseModel):
|
||||
__tablename__ = 'contest_files'
|
||||
|
||||
filename = Column(String, nullable=False)
|
||||
file_path = Column(String, nullable=False)
|
||||
|
||||
contest_id = Column(Integer, ForeignKey('contests.id'), nullable=False)
|
||||
|
||||
@ -29,8 +29,8 @@ class ContestCarouselPhotosService:
|
||||
|
||||
return FileResponse(
|
||||
photo.file_path,
|
||||
media_type=self.get_media_type(photo.file_path), # Use file_path to infer type
|
||||
filename=os.path.basename(photo.file_path), # Extract filename from path
|
||||
media_type=self.get_media_type(photo.file_path),
|
||||
filename=os.path.basename(photo.file_path),
|
||||
)
|
||||
|
||||
async def get_photos_by_contest_id(self, contest_id: int) -> list[ContestCarouselPhotoEntity]:
|
||||
@ -40,7 +40,7 @@ class ContestCarouselPhotosService:
|
||||
for photo in photos
|
||||
]
|
||||
|
||||
async def upload_photo(self, contest_id: int, file: UploadFile): # Removed 'user: User' for simplicity, add if needed
|
||||
async def upload_photo(self, contest_id: int, file: UploadFile):
|
||||
self.validate_file_type(file)
|
||||
|
||||
filename = self.generate_filename(file)
|
||||
@ -89,8 +89,7 @@ class ContestCarouselPhotosService:
|
||||
|
||||
@staticmethod
|
||||
def generate_filename(file: UploadFile):
|
||||
original_filename = secure_filename(file.filename)
|
||||
return f"{uuid.uuid4()}_{original_filename}"
|
||||
return secure_filename(f"{uuid.uuid4()}_{file.filename}")
|
||||
|
||||
@staticmethod
|
||||
def model_to_entity(photo_model: ContestCarouselPhoto) -> ContestCarouselPhotoEntity:
|
||||
|
||||
134
API/app/infrastructure/contest_files_service.py
Normal file
134
API/app/infrastructure/contest_files_service.py
Normal file
@ -0,0 +1,134 @@
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import aiofiles
|
||||
import magic
|
||||
from fastapi import HTTPException, UploadFile
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from starlette.responses import FileResponse
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from app.application.contest_files_repository import ContestFilesRepository
|
||||
from app.application.contests_repository import ContestsRepository
|
||||
from app.domain.entities.contest_file import ContestFileEntity
|
||||
from app.domain.models import ContestFile, User
|
||||
|
||||
|
||||
class ContestFilesService:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.contest_files_repository = ContestFilesRepository(db)
|
||||
self.contests_repository = ContestsRepository(db)
|
||||
|
||||
async def get_file_by_id(self, file_id: int) -> FileResponse:
|
||||
contest_file = await self.contest_files_repository.get_by_id(file_id)
|
||||
|
||||
if not contest_file:
|
||||
raise HTTPException(404, "Файл не найден")
|
||||
|
||||
if not os.path.exists(contest_file.file_path):
|
||||
raise HTTPException(404, "Файл не найден на диске")
|
||||
|
||||
return FileResponse(
|
||||
contest_file.file_path,
|
||||
media_type=self.get_media_type(contest_file.file_path),
|
||||
filename=os.path.basename(contest_file.file_path),
|
||||
)
|
||||
|
||||
async def get_files_by_contest_id(self, contest_id: int) -> list[ContestFileEntity]:
|
||||
files = await self.contest_files_repository.get_by_contest_id(contest_id)
|
||||
return [self.model_to_entity(file) for file in files]
|
||||
|
||||
async def upload_file(self, contest_id: int, file: UploadFile, user: User) -> ContestFileEntity:
|
||||
contest = await self.contests_repository.get_by_id(contest_id)
|
||||
if not contest:
|
||||
raise HTTPException(404, "Конкурс не найден")
|
||||
|
||||
self.validate_file_type(file)
|
||||
file_path = await self.save_file(file)
|
||||
|
||||
contest_file = ContestFile(
|
||||
filename=file.filename,
|
||||
file_path=file_path,
|
||||
contest_id=contest_id,
|
||||
)
|
||||
|
||||
return self.model_to_entity(
|
||||
await self.contest_files_repository.create(contest_file)
|
||||
)
|
||||
|
||||
async def delete_file(self, file_id: int, user: User) -> ContestFileEntity:
|
||||
contest_file = await self.contest_files_repository.get_by_id(file_id)
|
||||
if not contest_file:
|
||||
raise HTTPException(404, "Файл не найден")
|
||||
|
||||
if os.path.exists(contest_file.file_path):
|
||||
os.remove(contest_file.file_path)
|
||||
|
||||
return self.model_to_entity(
|
||||
await self.contest_files_repository.delete(contest_file)
|
||||
)
|
||||
|
||||
async def save_file(self, file: UploadFile, upload_dir: str = "uploads/contest_files") -> str:
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
filename = self.generate_filename(file)
|
||||
file_path = os.path.join(upload_dir, filename)
|
||||
|
||||
async with aiofiles.open(file_path, 'wb') as out_file:
|
||||
content = await file.read()
|
||||
await out_file.write(content)
|
||||
return file_path
|
||||
|
||||
@staticmethod
|
||||
def validate_file_type(file: UploadFile):
|
||||
mime = magic.Magic(mime=True)
|
||||
file_type = mime.from_buffer(file.file.read(1024))
|
||||
file.file.seek(0)
|
||||
|
||||
allowed_types = [
|
||||
"application/pdf",
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"application/zip",
|
||||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/vnd.ms-excel",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/vnd.ms-powerpoint",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"text/plain",
|
||||
]
|
||||
|
||||
if file_type not in allowed_types:
|
||||
raise HTTPException(400, f"Недопустимый тип файла: {file_type}")
|
||||
|
||||
@staticmethod
|
||||
def generate_filename(file: UploadFile) -> str:
|
||||
return secure_filename(f"{uuid.uuid4()}_{file.filename}")
|
||||
|
||||
@staticmethod
|
||||
def model_to_entity(contest_file_model: ContestFile) -> ContestFileEntity:
|
||||
return ContestFileEntity(
|
||||
id=contest_file_model.id,
|
||||
filename=contest_file_model.filename,
|
||||
file_path=contest_file_model.file_path,
|
||||
contest_id=contest_file_model.contest_id,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_media_type(filename: str) -> str:
|
||||
extension = filename.split('.')[-1].lower()
|
||||
if extension in ['jpeg', 'jpg', 'png']:
|
||||
return f"image/{extension}"
|
||||
if extension == 'pdf':
|
||||
return "application/pdf"
|
||||
if extension in ['zip']:
|
||||
return "application/zip"
|
||||
if extension in ['doc', 'docx']:
|
||||
return "application/msword"
|
||||
if extension in ['xls', 'xlsx']:
|
||||
return "application/vnd.ms-excel"
|
||||
if extension in ['ppt', 'pptx']:
|
||||
return "application/vnd.ms-powerpoint"
|
||||
if extension in ['txt']:
|
||||
return "text/plain"
|
||||
return "application/octet-stream"
|
||||
@ -13,6 +13,7 @@ from app.contollers.teams_router import router as team_router
|
||||
from app.contollers.users_router import router as users_router
|
||||
from app.contollers.contests_router import router as contest_router
|
||||
from app.contollers.contest_carousel_photos_router import router as contest_carousel_photos_router
|
||||
from app.contollers.contest_files_router import router as contest_files_router
|
||||
from app.settings import settings
|
||||
|
||||
|
||||
@ -41,6 +42,7 @@ def start_app():
|
||||
api_app.include_router(contest_router, prefix=f'{settings.PREFIX}/contests', tags=['contests'])
|
||||
api_app.include_router(contest_carousel_photos_router, prefix=f'{settings.PREFIX}/contest_carousel_photos',
|
||||
tags=['contest_carousel_photos'])
|
||||
api_app.include_router(contest_files_router, prefix=f'{settings.PREFIX}/contest_files', tags=['contest_files'])
|
||||
|
||||
return api_app
|
||||
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
import axios from 'axios'
|
||||
import CONFIG from '@/core/config.js'
|
||||
|
||||
|
||||
const deleteContestCarouselPhoto = async (photoId) => {
|
||||
try {
|
||||
const token = localStorage.getItem('access_token')
|
||||
|
||||
const response = await axios.delete(
|
||||
`${CONFIG.BASE_URL}/contest_carousel_photos/${photoId}/`, // Изменено здесь
|
||||
{
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.detail || error.message
|
||||
console.error(`Ошибка удаления фотографии карусели конкурса с ID ${photoId}:`, errorMessage)
|
||||
throw new Error(`Не удалось удалить фотографию карусели конкурса: ${errorMessage}`)
|
||||
}
|
||||
}
|
||||
|
||||
export default deleteContestCarouselPhoto
|
||||
@ -0,0 +1,28 @@
|
||||
import axios from 'axios'
|
||||
import CONFIG from '@/core/config.js'
|
||||
|
||||
|
||||
const downloadContestCarouselPhotoFile = async (photoId) => {
|
||||
try {
|
||||
const token = localStorage.getItem('access_token')
|
||||
|
||||
const response = await axios.get(
|
||||
`${CONFIG.BASE_URL}/contest_carousel_photos/${photoId}/file`,
|
||||
{
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
responseType: 'blob' // Важно для загрузки файлов
|
||||
}
|
||||
)
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.detail || error.message
|
||||
console.error(`Ошибка загрузки файла фотографии карусели конкурса с ID ${photoId}:`, errorMessage)
|
||||
throw new Error(`Не удалось загрузить файл фотографии карусели конкурса: ${errorMessage}`)
|
||||
}
|
||||
}
|
||||
|
||||
export default downloadContestCarouselPhotoFile
|
||||
@ -0,0 +1,27 @@
|
||||
import axios from 'axios'
|
||||
import CONFIG from '@/core/config.js'
|
||||
|
||||
|
||||
const getContestCarouselPhotosByContestId = async (contestId) => {
|
||||
try {
|
||||
const token = localStorage.getItem('access_token')
|
||||
|
||||
const response = await axios.get(
|
||||
`${CONFIG.BASE_URL}/contest_carousel_photos/contests/${contestId}/`,
|
||||
{
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.detail || error.message
|
||||
console.error(`Ошибка получения фотографий карусели конкурса для конкурса ${contestId}:`, errorMessage)
|
||||
throw new Error(`Не удалось загрузить фотографии карусели конкурса: ${errorMessage}`)
|
||||
}
|
||||
}
|
||||
|
||||
export default getContestCarouselPhotosByContestId
|
||||
@ -0,0 +1,33 @@
|
||||
import axios from 'axios'
|
||||
import CONFIG from '@/core/config.js'
|
||||
|
||||
|
||||
const uploadContestCarouselPhoto = async (contestId, file) => {
|
||||
try {
|
||||
const token = localStorage.getItem('access_token')
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
formData.append('contest_id', contestId);
|
||||
|
||||
const response = await axios.post(
|
||||
`${CONFIG.BASE_URL}/contest_carousel_photos/contests/${contestId}/upload`,
|
||||
formData,
|
||||
{
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.detail || error.message
|
||||
console.error(`Ошибка загрузки фотографии в карусель конкурса с ID ${contestId}:`, errorMessage)
|
||||
throw new Error(`Не удалось загрузить фотографию в карусель конкурса: ${errorMessage}`)
|
||||
}
|
||||
}
|
||||
|
||||
export default uploadContestCarouselPhoto
|
||||
31
WEB/src/api/contests/contest_files/deleteContestFile.js
Normal file
31
WEB/src/api/contests/contest_files/deleteContestFile.js
Normal file
@ -0,0 +1,31 @@
|
||||
import axios from "axios";
|
||||
import CONFIG from "@/core/config.js";
|
||||
|
||||
const deleteContestFile = async (fileId) => {
|
||||
try {
|
||||
const token = localStorage.getItem('access_token'); // Получаем токен из localStorage
|
||||
|
||||
const response = await axios.delete(
|
||||
`${CONFIG.BASE_URL}/contest_files/${fileId}/`,
|
||||
{
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.detail || error.message;
|
||||
console.error(`Ошибка удаления файла конкурса с ID ${fileId}:`, errorMessage);
|
||||
if (error.response?.status === 401) {
|
||||
throw new Error("Недостаточно прав для удаления файла (401)");
|
||||
}
|
||||
if (error.response?.status === 404) {
|
||||
throw new Error("Файл не найден (404)");
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
export default deleteContestFile;
|
||||
39
WEB/src/api/contests/contest_files/downloadContestFile.js
Normal file
39
WEB/src/api/contests/contest_files/downloadContestFile.js
Normal file
@ -0,0 +1,39 @@
|
||||
import axios from "axios";
|
||||
import CONFIG from "@/core/config.js";
|
||||
|
||||
const downloadContestFile = async (fileId) => {
|
||||
try {
|
||||
const response = await axios.get(`${CONFIG.BASE_URL}/contest_files/${fileId}/file`, {
|
||||
responseType: 'blob',
|
||||
withCredentials: true,
|
||||
});
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
const contentDisposition = response.headers['content-disposition'];
|
||||
let filename = 'download';
|
||||
if (contentDisposition) {
|
||||
const filenameMatch = contentDisposition.match(/filename="(.+)"/);
|
||||
if (filenameMatch && filenameMatch[1]) {
|
||||
filename = filenameMatch[1];
|
||||
}
|
||||
}
|
||||
link.setAttribute('download', filename);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
return filename;
|
||||
} catch (error) {
|
||||
if (error.response?.status === 401) {
|
||||
throw new Error("Нет доступа для скачивания файла (401)");
|
||||
}
|
||||
if (error.response?.status === 404) {
|
||||
throw new Error("Файл не найден (404)");
|
||||
}
|
||||
throw new Error(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
export default downloadContestFile;
|
||||
21
WEB/src/api/contests/contest_files/getContestFiles.js
Normal file
21
WEB/src/api/contests/contest_files/getContestFiles.js
Normal file
@ -0,0 +1,21 @@
|
||||
import axios from "axios";
|
||||
import CONFIG from "@/core/config.js";
|
||||
|
||||
const getContestFiles = async (contestId) => {
|
||||
try {
|
||||
const response = await axios.get(`${CONFIG.BASE_URL}/contest_files/contests/${contestId}/`, {
|
||||
withCredentials: true,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (error.response?.status === 401) {
|
||||
throw new Error("Нет доступа к файлам конкурса (401)");
|
||||
}
|
||||
if (error.response?.status === 404) {
|
||||
throw new Error("Конкурс не найден (404)");
|
||||
}
|
||||
throw new Error(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
export default getContestFiles;
|
||||
38
WEB/src/api/contests/contest_files/uploadContestFile.js
Normal file
38
WEB/src/api/contests/contest_files/uploadContestFile.js
Normal file
@ -0,0 +1,38 @@
|
||||
import axios from "axios";
|
||||
import CONFIG from "@/core/config.js";
|
||||
|
||||
const uploadContestFile = async (contestId, file) => {
|
||||
try {
|
||||
const token = localStorage.getItem('access_token');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
const response = await axios.post(
|
||||
`${CONFIG.BASE_URL}/contest_files/contests/${contestId}/upload`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
withCredentials: true,
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.detail || error.message;
|
||||
if (error.response?.status === 401) {
|
||||
throw new Error("Недостаточно прав для загрузки файла (401)");
|
||||
}
|
||||
if (error.response?.status === 400) {
|
||||
throw new Error(`Ошибка загрузки: ${errorMessage}`);
|
||||
}
|
||||
if (error.response?.status === 404) {
|
||||
throw new Error("Конкурс не найден (404)");
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
export default uploadContestFile;
|
||||
30
WEB/src/api/projects/project_files/deleteProjectFile.js
Normal file
30
WEB/src/api/projects/project_files/deleteProjectFile.js
Normal file
@ -0,0 +1,30 @@
|
||||
import axios from 'axios';
|
||||
import CONFIG from '@/core/config.js';
|
||||
|
||||
const deleteProjectFile = async (fileId) => {
|
||||
try {
|
||||
const token = localStorage.getItem('access_token');
|
||||
const response = await axios.delete(
|
||||
`${CONFIG.BASE_URL}/project_files/${fileId}/`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
withCredentials: true,
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.detail || error.message;
|
||||
console.error(`Ошибка удаления файла проекта с ID ${fileId}:`, errorMessage);
|
||||
if (error.response?.status === 401) {
|
||||
throw new Error("Недостаточно прав для удаления файла (401)");
|
||||
}
|
||||
if (error.response?.status === 404) {
|
||||
throw new Error("Файл не найден (404)");
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
export default deleteProjectFile;
|
||||
51
WEB/src/api/projects/project_files/downloadProjectFile.js
Normal file
51
WEB/src/api/projects/project_files/downloadProjectFile.js
Normal file
@ -0,0 +1,51 @@
|
||||
import axios from 'axios';
|
||||
import CONFIG from '@/core/config.js';
|
||||
|
||||
const downloadProjectFile = async (fileId) => {
|
||||
try {
|
||||
const token = localStorage.getItem('access_token');
|
||||
const response = await axios.get(
|
||||
`${CONFIG.BASE_URL}/project_files/${fileId}/download`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
responseType: 'blob',
|
||||
withCredentials: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Создаем ссылку для скачивания
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
|
||||
const contentDisposition = response.headers['content-disposition'];
|
||||
let filename = `project_file_${fileId}`;
|
||||
if (contentDisposition) {
|
||||
const filenameMatch = contentDisposition.match(/filename="([^"]+)"/);
|
||||
if (filenameMatch && filenameMatch[1]) {
|
||||
filename = decodeURIComponent(filenameMatch[1]);
|
||||
}
|
||||
}
|
||||
link.setAttribute('download', filename);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
return { success: true, filename: filename };
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.detail || error.message;
|
||||
console.error(`Ошибка скачивания файла проекта с ID ${fileId}:`, errorMessage);
|
||||
if (error.response?.status === 401) {
|
||||
throw new Error("Недостаточно прав для скачивания файла (401)");
|
||||
}
|
||||
if (error.response?.status === 404) {
|
||||
throw new Error("Файл не найден (404)");
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
export default downloadProjectFile;
|
||||
24
WEB/src/api/projects/project_files/getProjectFiles.js
Normal file
24
WEB/src/api/projects/project_files/getProjectFiles.js
Normal file
@ -0,0 +1,24 @@
|
||||
import axios from 'axios';
|
||||
import CONFIG from '@/core/config.js';
|
||||
|
||||
const getProjectFiles = async (projectId) => {
|
||||
try {
|
||||
const token = localStorage.getItem('access_token');
|
||||
const response = await axios.get(
|
||||
`${CONFIG.BASE_URL}/project_files/projects/${projectId}/`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
withCredentials: true,
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.detail || error.message;
|
||||
console.error(`Ошибка загрузки файлов проекта с ID ${projectId}:`, errorMessage);
|
||||
throw new Error(`Не удалось загрузить файлы проекта: ${errorMessage}`);
|
||||
}
|
||||
};
|
||||
|
||||
export default getProjectFiles;
|
||||
39
WEB/src/api/projects/project_files/uploadProjectFile.js
Normal file
39
WEB/src/api/projects/project_files/uploadProjectFile.js
Normal file
@ -0,0 +1,39 @@
|
||||
import axios from 'axios';
|
||||
import CONFIG from '@/core/config.js';
|
||||
|
||||
const uploadProjectFile = async (projectId, file) => {
|
||||
try {
|
||||
const token = localStorage.getItem('access_token');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
const response = await axios.post(
|
||||
`${CONFIG.BASE_URL}/project_files/projects/${projectId}/upload`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
withCredentials: true,
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.detail || error.message;
|
||||
console.error(`Ошибка загрузки файла проекта с ID ${projectId}:`, errorMessage);
|
||||
if (error.response?.status === 401) {
|
||||
throw new Error("Недостаточно прав для загрузки файла (401)");
|
||||
}
|
||||
if (error.response?.status === 400) {
|
||||
throw new Error(`Ошибка загрузки: ${errorMessage}`);
|
||||
}
|
||||
if (error.response?.status === 404) {
|
||||
throw new Error("Проект не найден (404)");
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
export default uploadProjectFile;
|
||||
@ -168,7 +168,7 @@
|
||||
<div v-else class="q-gutter-md q-mb-md row wrap justify-center">
|
||||
<q-card v-for="photo in profilePhotos" :key="photo.id" class="col-auto" style="width: 120px; height: 120px; position: relative;">
|
||||
<q-img
|
||||
:src="getPhotoUrl(photo.id)"
|
||||
:src="getPhotoUrl(photo.id, 'profile')"
|
||||
alt="Profile Photo"
|
||||
style="width: 100%; height: 100%; object-fit: cover;"
|
||||
>
|
||||
@ -179,7 +179,7 @@
|
||||
round
|
||||
dense
|
||||
size="sm"
|
||||
@click="confirmDeletePhoto(photo.id)"
|
||||
@click="confirmDeletePhoto(photo.id, 'profile')"
|
||||
/>
|
||||
</div>
|
||||
</q-img>
|
||||
@ -206,7 +206,7 @@
|
||||
label="Загрузить фото"
|
||||
color="primary"
|
||||
class="q-mt-sm full-width"
|
||||
@click="uploadNewPhoto"
|
||||
@click="uploadNewProfilePhoto"
|
||||
:loading="uploadingPhoto"
|
||||
/>
|
||||
</template>
|
||||
@ -221,11 +221,127 @@
|
||||
<q-input v-model="dialogData.title" label="Название конкурса" dense autofocus clearable />
|
||||
<q-input v-model="dialogData.description" label="Описание" dense clearable type="textarea" class="q-mt-sm" />
|
||||
<q-input v-model="dialogData.web_url" label="URL сайта" dense clearable class="q-mt-sm" />
|
||||
<q-input v-model="dialogData.photo" label="Фото" dense clearable class="q-mt-sm" />
|
||||
<q-input v-model="dialogData.results" label="Результаты" dense clearable class="q-mt-sm" />
|
||||
<q-toggle v-model="dialogData.is_win" label="Победа (Да/Нет)" dense class="q-mt-sm" />
|
||||
<q-input v-model="dialogData.project_id" label="Проект" dense clearable class="q-mt-sm" />
|
||||
<q-input v-model="dialogData.status_id" label="Статус" dense clearable class="q-mt-sm" />
|
||||
|
||||
<q-separator class="q-my-md" />
|
||||
<div class="text-h6 q-mb-sm">Фотографии карусели конкурса</div>
|
||||
|
||||
<div v-if="loadingContestPhotos" class="text-center q-py-md">
|
||||
<q-spinner-dots color="primary" size="2em" />
|
||||
<div>Загрузка фотографий...</div>
|
||||
</div>
|
||||
<div v-else-if="contestPhotos.length === 0" class="text-center q-py-md text-grey-7">
|
||||
Пока нет фотографий.
|
||||
</div>
|
||||
<div v-else class="q-gutter-md q-mb-md row wrap justify-center">
|
||||
<q-card v-for="photo in contestPhotos" :key="photo.id" class="col-auto" style="width: 120px; height: 120px; position: relative;">
|
||||
<q-img
|
||||
:src="getPhotoUrl(photo.id, 'contest')"
|
||||
alt="Contest Photo"
|
||||
style="width: 100%; height: 100%; object-fit: cover;"
|
||||
>
|
||||
<div class="absolute-bottom text-right q-pa-xs">
|
||||
<q-btn
|
||||
icon="delete"
|
||||
color="negative"
|
||||
round
|
||||
dense
|
||||
size="sm"
|
||||
@click="confirmDeletePhoto(photo.id, 'contest')"
|
||||
/>
|
||||
</div>
|
||||
</q-img>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-file
|
||||
v-model="newContestPhotoFile"
|
||||
label="Выберите фото для загрузки"
|
||||
outlined
|
||||
dense
|
||||
clearable
|
||||
accept="image/*"
|
||||
@update:model-value="handleNewContestPhotoSelected"
|
||||
class="q-mt-sm"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-icon v-if="newContestPhotoFile" name="check" color="positive" />
|
||||
<q-icon name="photo" />
|
||||
</template>
|
||||
</q-file>
|
||||
<q-btn
|
||||
v-if="newContestPhotoFile"
|
||||
label="Загрузить фото"
|
||||
color="primary"
|
||||
class="q-mt-sm full-width"
|
||||
@click="uploadNewContestPhoto"
|
||||
:loading="uploadingPhoto"
|
||||
/>
|
||||
|
||||
<q-separator class="q-my-md" />
|
||||
<div class="text-h6 q-mb-sm">Файлы конкурса</div>
|
||||
|
||||
<div v-if="loadingContestFiles" class="text-center q-py-md">
|
||||
<q-spinner-dots color="primary" size="2em" />
|
||||
<div>Загрузка файлов...</div>
|
||||
</div>
|
||||
<div v-else-if="contestFiles.length === 0" class="text-center q-py-md text-grey-7">
|
||||
Пока нет файлов.
|
||||
</div>
|
||||
<q-list v-else bordered separator class="rounded-borders">
|
||||
<q-item v-for="fileItem in contestFiles" :key="fileItem.id" clickable v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ fileItem.filename }}</q-item-label>
|
||||
<q-item-label caption>{{ fileItem.file_path }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<div class="q-gutter-xs">
|
||||
<q-btn
|
||||
icon="download"
|
||||
color="primary"
|
||||
round
|
||||
dense
|
||||
size="sm"
|
||||
@click.stop="downloadExistingContestFile(fileItem.id)"
|
||||
/>
|
||||
<q-btn
|
||||
icon="delete"
|
||||
color="negative"
|
||||
round
|
||||
dense
|
||||
size="sm"
|
||||
@click.stop="confirmDeleteContestFile(fileItem.id)"
|
||||
/>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
|
||||
<q-file
|
||||
v-model="newContestFile"
|
||||
label="Выберите файл для загрузки"
|
||||
outlined
|
||||
dense
|
||||
clearable
|
||||
@update:model-value="handleNewContestFileSelected"
|
||||
class="q-mt-sm"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-icon v-if="newContestFile" name="check" color="positive" />
|
||||
<q-icon name="attach_file" />
|
||||
</template>
|
||||
</q-file>
|
||||
<q-btn
|
||||
v-if="newContestFile"
|
||||
label="Загрузить файл"
|
||||
color="primary"
|
||||
class="q-mt-sm full-width"
|
||||
@click="uploadNewContestFile"
|
||||
:loading="uploadingFile"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
@ -289,9 +405,8 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { Notify, useQuasar } from 'quasar' // Импортируем useQuasar для QDialog
|
||||
import { Notify, useQuasar } from 'quasar'
|
||||
|
||||
// Импорты API для управления данными
|
||||
import fetchTeams from '@/api/teams/getTeams.js'
|
||||
import updateTeam from '@/api/teams/updateTeam.js'
|
||||
import deleteTeamById from '@/api/teams/deleteTeam.js'
|
||||
@ -312,17 +427,33 @@ import updateContest from '@/api/contests/updateContest.js'
|
||||
import deleteContestById from '@/api/contests/deleteContest.js'
|
||||
import createContest from '@/api/contests/createContest.js'
|
||||
|
||||
import router from "@/router/index.js"
|
||||
import CONFIG from '@/core/config.js' // Убедитесь, что у вас есть этот файл конфигурации
|
||||
import CONFIG from '@/core/config.js'
|
||||
|
||||
// --- Импорты для работы с фотографиями профиля ---
|
||||
import getPhotosByProfileId from '@/api/profiles/profile_photos/getPhotoFileById.js'
|
||||
import getProfilePhotosByProfileId from '@/api/profiles/profile_photos/getPhotoFileById.js'
|
||||
import uploadProfilePhoto from '@/api/profiles/profile_photos/uploadProfilePhoto.js'
|
||||
import deletePhoto from '@/api/profiles/profile_photos/deletePhoto.js'
|
||||
import deleteProfilePhoto from '@/api/profiles/profile_photos/deletePhoto.js'
|
||||
import downloadProfilePhotoFile from '@/api/profiles/profile_photos/downloadPhotoFile.js'
|
||||
|
||||
import getContestCarouselPhotosByContestId from '@/api/contests/contest_carousel_photos/getContestPhotoFileById.js'
|
||||
import uploadContestCarouselPhoto from '@/api/contests/contest_carousel_photos/uploadContestProfilePhoto.js'
|
||||
import deleteContestCarouselPhoto from '@/api/contests/contest_carousel_photos/deleteContestPhoto.js'
|
||||
import downloadContestCarouselPhotoFile from '@/api/contests/contest_carousel_photos/downloadContestPhotoFile.js'
|
||||
|
||||
// --- Imports for Contest Files ---
|
||||
import getContestFiles from '@/api/contests/contest_files/getContestFiles.js'
|
||||
import uploadContestFile from '@/api/contests/contest_files/uploadContestFile.js'
|
||||
import deleteContestFile from '@/api/contests/contest_files/deleteContestFile.js'
|
||||
import downloadContestFile from '@/api/contests/contest_files/downloadContestFile.js'
|
||||
|
||||
// --- Imports for Project Files ---
|
||||
import getProjectFiles from '@/api/projects/project_files/getProjectFiles.js'
|
||||
import uploadProjectFile from '@/api/projects/project_files/uploadProjectFile.js'
|
||||
import deleteProjectFile from '@/api/projects/project_files/deleteProjectFile.js'
|
||||
import downloadProjectFile from '@/api/projects/project_files/downloadProjectFile.js'
|
||||
|
||||
|
||||
const $q = useQuasar()
|
||||
|
||||
// Текущая вкладка
|
||||
const tab = ref('profiles')
|
||||
|
||||
// --- Profiles ---
|
||||
@ -362,9 +493,9 @@ const projectColumns = [
|
||||
const contests = ref([])
|
||||
const loadingContests = ref(false)
|
||||
const contestColumns = [
|
||||
{ name: 'title', label: 'Название проекта', field: 'title', sortable: true },
|
||||
{ name: 'title', label: 'Название конкурса', field: 'title', sortable: true },
|
||||
{ name: 'description', label: 'Описание', field: 'description', sortable: true },
|
||||
{ name: 'web_url', label: 'Репозиторий', field: 'repository_url', sortable: true },
|
||||
{ name: 'web_url', label: 'URL сайта', field: 'web_url', sortable: true },
|
||||
{ name: 'photo', label: 'Фото', field: 'photo', sortable: true },
|
||||
{ name: 'results', label: 'Результаты', field: 'results', sortable: true },
|
||||
{ name: 'is_win', label: 'Победа', field: 'is_win', sortable: true },
|
||||
@ -381,18 +512,37 @@ const dialogType = ref('')
|
||||
const profilePhotos = ref([])
|
||||
const loadingProfilePhotos = ref(false)
|
||||
const newProfilePhotoFile = ref(null)
|
||||
const uploadingPhoto = ref(false)
|
||||
const uploadingPhoto = ref(false) // Общее состояние для загрузки фото
|
||||
|
||||
// --- Состояния для фотографий карусели конкурса ---
|
||||
const contestPhotos = ref([])
|
||||
const loadingContestPhotos = ref(false)
|
||||
const newContestPhotoFile = ref(null)
|
||||
|
||||
// --- Состояния для файлов конкурса ---
|
||||
const contestFiles = ref([])
|
||||
const loadingContestFiles = ref(false)
|
||||
const newContestFile = ref(null)
|
||||
const uploadingFile = ref(false) // Отдельное состояние для загрузки файлов
|
||||
|
||||
|
||||
// Функция для получения URL фото
|
||||
const getPhotoUrl = (photoId) => {
|
||||
const getPhotoUrl = (photoId, type) => {
|
||||
if (type === 'profile') {
|
||||
// Используем шаблонные строки JavaScript для корректного формирования URL
|
||||
return `${CONFIG.BASE_URL}/profile_photos/${photoId}/file`;
|
||||
} else if (type === 'contest') {
|
||||
// Используем шаблонные строки JavaScript для корректного формирования URL
|
||||
return `${CONFIG.BASE_URL}/contest_carousel_photos/${photoId}/file`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// Загрузка фотографий профиля при открытии диалога для профиля
|
||||
async function loadProfilePhotos(profileId) {
|
||||
loadingProfilePhotos.value = true;
|
||||
try {
|
||||
profilePhotos.value = await getPhotosByProfileId(profileId);
|
||||
profilePhotos.value = await getProfilePhotosByProfileId(profileId);
|
||||
} catch (error) {
|
||||
Notify.create({
|
||||
type: 'negative',
|
||||
@ -405,13 +555,58 @@ async function loadProfilePhotos(profileId) {
|
||||
}
|
||||
}
|
||||
|
||||
// Обработчик выбора нового файла
|
||||
// Загрузка фотографий карусели конкурса
|
||||
async function loadContestPhotos(contestId) {
|
||||
loadingContestPhotos.value = true;
|
||||
try {
|
||||
contestPhotos.value = await getContestCarouselPhotosByContestId(contestId);
|
||||
} catch (error) {
|
||||
Notify.create({
|
||||
type: 'negative',
|
||||
message: `Ошибка загрузки фотографий карусели конкурса: ${error.message}`,
|
||||
icon: 'error',
|
||||
});
|
||||
contestPhotos.value = [];
|
||||
} finally {
|
||||
loadingContestPhotos.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка файлов конкурса
|
||||
async function loadContestFiles(contestId) {
|
||||
loadingContestFiles.value = true;
|
||||
try {
|
||||
contestFiles.value = await getContestFiles(contestId);
|
||||
} catch (error) {
|
||||
Notify.create({
|
||||
type: 'negative',
|
||||
message: `Ошибка загрузки файлов конкурса: ${error.message}`,
|
||||
icon: 'error',
|
||||
});
|
||||
contestFiles.value = [];
|
||||
} finally {
|
||||
loadingContestFiles.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Обработчик выбора нового файла для профиля
|
||||
function handleNewPhotoSelected(file) {
|
||||
newProfilePhotoFile.value = file;
|
||||
}
|
||||
|
||||
// Загрузка новой фотографии
|
||||
async function uploadNewPhoto() {
|
||||
// Обработчик выбора нового файла для конкурса (фото)
|
||||
function handleNewContestPhotoSelected(file) {
|
||||
newContestPhotoFile.value = file;
|
||||
}
|
||||
|
||||
// Обработчик выбора нового файла для конкурса (обычный файл)
|
||||
function handleNewContestFileSelected(file) {
|
||||
newContestFile.value = file;
|
||||
}
|
||||
|
||||
|
||||
// Загрузка новой фотографии профиля
|
||||
async function uploadNewProfilePhoto() {
|
||||
if (!newProfilePhotoFile.value || !dialogData.value.id) {
|
||||
Notify.create({
|
||||
type: 'warning',
|
||||
@ -424,18 +619,17 @@ async function uploadNewPhoto() {
|
||||
uploadingPhoto.value = true;
|
||||
try {
|
||||
const uploadedPhoto = await uploadProfilePhoto(dialogData.value.id, newProfilePhotoFile.value);
|
||||
// Добавляем новую фото в список
|
||||
profilePhotos.value.push(uploadedPhoto);
|
||||
newProfilePhotoFile.value = null; // Сбрасываем выбранный файл
|
||||
newProfilePhotoFile.value = null;
|
||||
Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Фотография успешно загружена!',
|
||||
message: 'Фотография профиля успешно загружена!',
|
||||
icon: 'check_circle',
|
||||
});
|
||||
} catch (error) {
|
||||
Notify.create({
|
||||
type: 'negative',
|
||||
message: `Ошибка загрузки фотографии: ${error.message}`,
|
||||
message: `Ошибка загрузки фотографии профиля: ${error.message}`,
|
||||
icon: 'error',
|
||||
});
|
||||
} finally {
|
||||
@ -443,8 +637,73 @@ async function uploadNewPhoto() {
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка новой фотографии карусели конкурса
|
||||
async function uploadNewContestPhoto() {
|
||||
if (!newContestPhotoFile.value || !dialogData.value.id) {
|
||||
Notify.create({
|
||||
type: 'warning',
|
||||
message: 'Выберите файл и убедитесь, что конкурс выбран.',
|
||||
icon: 'warning',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
uploadingPhoto.value = true;
|
||||
try {
|
||||
const uploadedPhoto = await uploadContestCarouselPhoto(dialogData.value.id, newContestPhotoFile.value);
|
||||
contestPhotos.value.push(uploadedPhoto);
|
||||
newContestPhotoFile.value = null;
|
||||
Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Фотография карусели конкурса успешно загружена!',
|
||||
icon: 'check_circle',
|
||||
});
|
||||
} catch (error) {
|
||||
Notify.create({
|
||||
type: 'negative',
|
||||
message: `Ошибка загрузки фотографии карусели конкурса: ${error.message}`,
|
||||
icon: 'error',
|
||||
});
|
||||
} finally {
|
||||
uploadingPhoto.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка нового файла конкурса
|
||||
async function uploadNewContestFile() {
|
||||
if (!newContestFile.value || !dialogData.value.id) {
|
||||
Notify.create({
|
||||
type: 'warning',
|
||||
message: 'Выберите файл и убедитесь, что конкурс выбран.',
|
||||
icon: 'warning',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
uploadingFile.value = true; // Use separate loading state
|
||||
try {
|
||||
const uploadedFile = await uploadContestFile(dialogData.value.id, newContestFile.value);
|
||||
contestFiles.value.push(uploadedFile);
|
||||
newContestFile.value = null;
|
||||
Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Файл конкурса успешно загружен!',
|
||||
icon: 'check_circle',
|
||||
});
|
||||
} catch (error) {
|
||||
Notify.create({
|
||||
type: 'negative',
|
||||
message: `Ошибка загрузки файла конкурса: ${error.message}`,
|
||||
icon: 'error',
|
||||
});
|
||||
} finally {
|
||||
uploadingFile.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Подтверждение и удаление фотографии
|
||||
function confirmDeletePhoto(photoId) {
|
||||
function confirmDeletePhoto(photoId, type) {
|
||||
$q.dialog({
|
||||
title: 'Подтверждение удаления',
|
||||
message: 'Вы уверены, что хотите удалить эту фотографию?',
|
||||
@ -459,23 +718,89 @@ function confirmDeletePhoto(photoId) {
|
||||
color: 'primary'
|
||||
}
|
||||
}).onOk(async () => {
|
||||
await deleteExistingPhoto(photoId);
|
||||
await deleteExistingPhoto(photoId, type);
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteExistingPhoto(photoId) {
|
||||
async function deleteExistingPhoto(photoId, type) {
|
||||
try {
|
||||
await deletePhoto(photoId);
|
||||
if (type === 'profile') {
|
||||
await deleteProfilePhoto(photoId);
|
||||
profilePhotos.value = profilePhotos.value.filter(p => p.id !== photoId);
|
||||
Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Фотография успешно удалена!',
|
||||
message: 'Фотография профиля успешно удалена!',
|
||||
icon: 'check_circle',
|
||||
});
|
||||
} else if (type === 'contest') {
|
||||
await deleteContestCarouselPhoto(photoId);
|
||||
contestPhotos.value = contestPhotos.value.filter(p => p.id !== photoId);
|
||||
Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Фотография карусели конкурса успешно удалена!',
|
||||
icon: 'check_circle',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
Notify.create({
|
||||
type: 'negative',
|
||||
message: `Ошибка удаления фотографии: ${error.message}`,
|
||||
icon: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Подтверждение и удаление файла конкурса
|
||||
function confirmDeleteContestFile(fileId) {
|
||||
$q.dialog({
|
||||
title: 'Подтверждение удаления',
|
||||
message: 'Вы уверены, что хотите удалить этот файл?',
|
||||
cancel: true,
|
||||
persistent: true,
|
||||
ok: {
|
||||
label: 'Удалить',
|
||||
color: 'negative'
|
||||
},
|
||||
cancel: {
|
||||
label: 'Отмена',
|
||||
color: 'primary'
|
||||
}
|
||||
}).onOk(async () => {
|
||||
await deleteExistingContestFile(fileId);
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteExistingContestFile(fileId) {
|
||||
try {
|
||||
await deleteContestFile(fileId);
|
||||
contestFiles.value = contestFiles.value.filter(f => f.id !== fileId);
|
||||
Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Файл конкурса успешно удален!',
|
||||
icon: 'check_circle',
|
||||
});
|
||||
} catch (error) {
|
||||
Notify.create({
|
||||
type: 'negative',
|
||||
message: `Ошибка удаления фотографии: ${error.message}`,
|
||||
message: `Ошибка удаления файла конкурса: ${error.message}`,
|
||||
icon: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Скачивание файла конкурса
|
||||
async function downloadExistingContestFile(fileId) {
|
||||
try {
|
||||
await downloadContestFile(fileId);
|
||||
Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Файл конкурса успешно скачан!',
|
||||
icon: 'check_circle',
|
||||
});
|
||||
} catch (error) {
|
||||
Notify.create({
|
||||
type: 'negative',
|
||||
message: `Ошибка скачивания файла конкурса: ${error.message}`,
|
||||
icon: 'error',
|
||||
});
|
||||
}
|
||||
@ -490,20 +815,27 @@ function openEdit(type, row) {
|
||||
if (type === 'teams') {
|
||||
dialogData.value = { title: '', description: '', logo: '', git_url: '' }
|
||||
} else if (type === 'projects') {
|
||||
dialogData.value = { name: '', summary: '', deadline: '' }
|
||||
dialogData.value = { title: '', description: '', repository_url: '' }
|
||||
} else if (type === 'profiles') {
|
||||
dialogData.value = { first_name: '', last_name: '', patronymic: '', birthday: '', email: '', phone: '', role_id: null, team_id: null }
|
||||
profilePhotos.value = [];
|
||||
} else if (type === 'contests') {
|
||||
dialogData.value = { title: '', description: '', web_url: '', photo: '', results: '', is_win: false, project_id: null, status_id: null }
|
||||
contestPhotos.value = [];
|
||||
contestFiles.value = []; // Clear contest files when opening dialog
|
||||
}
|
||||
}
|
||||
dialogVisible.value = true
|
||||
|
||||
if (type === 'profiles' && dialogData.value.id) {
|
||||
loadProfilePhotos(dialogData.value.id);
|
||||
} else if (type === 'contests' && dialogData.value.id) {
|
||||
loadContestPhotos(dialogData.value.id);
|
||||
loadContestFiles(dialogData.value.id); // Load contest files
|
||||
} else {
|
||||
profilePhotos.value = [];
|
||||
contestPhotos.value = [];
|
||||
contestFiles.value = []; // Clear contest files
|
||||
}
|
||||
}
|
||||
|
||||
@ -515,7 +847,12 @@ function closeDialog() {
|
||||
dialogVisible.value = false
|
||||
profilePhotos.value = [];
|
||||
newProfilePhotoFile.value = null;
|
||||
contestPhotos.value = [];
|
||||
newContestPhotoFile.value = null;
|
||||
contestFiles.value = []; // Clear contest files
|
||||
newContestFile.value = null; // Clear new contest file input
|
||||
uploadingPhoto.value = false;
|
||||
uploadingFile.value = false; // Reset file uploading state
|
||||
}
|
||||
|
||||
async function saveChanges() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user