сделал редактирование фотографий у пользователей, сделал методы для карусели фотографий конкурсов

This commit is contained in:
Мельников Данил 2025-06-03 07:58:26 +05:00
parent c7533fe87c
commit a884065680
11 changed files with 616 additions and 61 deletions

View 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 ContestCarouselPhoto
class ContestCarouselPhotosRepository:
def __init__(self, db: AsyncSession):
self.db = db
async def get_by_id(self, photo_id: int) -> Optional[ContestCarouselPhoto]:
stmt = select(ContestCarouselPhoto).filter_by(id=photo_id)
result = await self.db.execute(stmt)
return result.scalars().first()
async def get_by_contest_id(self, contest_id: int) -> Sequence[ContestCarouselPhoto]:
stmt = select(ContestCarouselPhoto).filter_by(contest_id=contest_id)
result = await self.db.execute(stmt)
return result.scalars().all()
async def create(self, photo: ContestCarouselPhoto) -> ContestCarouselPhoto:
self.db.add(photo)
await self.db.commit()
await self.db.refresh(photo)
return photo
async def delete(self, photo: ContestCarouselPhoto) -> ContestCarouselPhoto:
await self.db.delete(photo)
await self.db.commit()
return photo

View File

@ -0,0 +1,67 @@
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_carousel_photo import ContestCarouselPhotoEntity
from app.infrastructure.contest_carousel_photos_service import ContestCarouselPhotosService
router = APIRouter()
@router.get(
"/contests/{contest_id}/",
response_model=list[ContestCarouselPhotoEntity],
summary="Get all carousel photos metadata for a contest",
description="Returns metadata of all carousel photos for the given contest_id",
)
async def get_photos_by_contest_id(
contest_id: int,
db: AsyncSession = Depends(get_db),
):
service = ContestCarouselPhotosService(db)
return await service.get_photos_by_contest_id(contest_id)
@router.get(
"/{photo_id}/file",
response_class=FileResponse,
summary="Download carousel photo file by photo ID",
description="Returns the image file for the given carousel photo ID",
)
async def download_photo_file(
photo_id: int,
db: AsyncSession = Depends(get_db),
):
service = ContestCarouselPhotosService(db)
return await service.get_photo_file_by_id(photo_id)
@router.post(
"/contests/{contest_id}/upload",
response_model=ContestCarouselPhotoEntity,
summary="Upload a new carousel photo for a contest",
description="Uploads a new photo file and associates it with the given contest ID",
)
async def upload_photo(
contest_id: int,
file: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
):
service = ContestCarouselPhotosService(db)
return await service.upload_photo(contest_id, file)
@router.delete(
"/{photo_id}/",
response_model=ContestCarouselPhotoEntity,
summary="Delete a carousel photo by ID",
description="Deletes a carousel photo and its file from storage",
)
async def delete_photo(
photo_id: int,
db: AsyncSession = Depends(get_db),
):
service = ContestCarouselPhotosService(db)
return await service.delete_photo(photo_id)

View File

@ -0,0 +1,9 @@
from pydantic import BaseModel
from typing import Optional
class ContestCarouselPhotoEntity(BaseModel):
id: Optional[int] = None
file_path: str
number: int
contest_id: int

View File

@ -0,0 +1,112 @@
import os
import uuid
import aiofiles
import magic
from fastapi import HTTPException, UploadFile, status
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.responses import FileResponse
from werkzeug.utils import secure_filename
from app.application.contest_carousel_photos_repository import ContestCarouselPhotosRepository
from app.domain.entities.contest_carousel_photo import ContestCarouselPhotoEntity
from app.domain.models import ContestCarouselPhoto
class ContestCarouselPhotosService:
def __init__(self, db: AsyncSession):
self.contest_carousel_photos_repository = ContestCarouselPhotosRepository(db)
async def get_photo_file_by_id(self, photo_id: int) -> FileResponse:
photo = await self.contest_carousel_photos_repository.get_by_id(photo_id)
if not photo:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Photo not found")
if not os.path.exists(photo.file_path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File not found on disk")
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
)
async def get_photos_by_contest_id(self, contest_id: int) -> list[ContestCarouselPhotoEntity]:
photos = await self.contest_carousel_photos_repository.get_by_contest_id(contest_id)
return [
self.model_to_entity(photo)
for photo in photos
]
async def upload_photo(self, contest_id: int, file: UploadFile): # Removed 'user: User' for simplicity, add if needed
self.validate_file_type(file)
filename = self.generate_filename(file)
file_path = await self.save_file(file, filename=filename)
photo = ContestCarouselPhoto(
file_path=file_path,
number=0,
contest_id=contest_id
)
return self.model_to_entity(
await self.contest_carousel_photos_repository.create(photo)
)
async def delete_photo(self, photo_id: int):
photo = await self.contest_carousel_photos_repository.get_by_id(photo_id)
if not photo:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Photo not found")
if os.path.exists(photo.file_path):
os.remove(photo.file_path)
return self.model_to_entity(
await self.contest_carousel_photos_repository.delete(photo)
)
async def save_file(self, file: UploadFile, filename: str, upload_dir: str = "uploads/contest_carousel_photos") -> str:
os.makedirs(upload_dir, exist_ok=True)
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):
contents = file.file.read(1024)
file.file.seek(0)
mime = magic.Magic(mime=True)
file_type = mime.from_buffer(contents)
if file_type not in ["image/jpeg", "image/png"]:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid file type. Only JPEG and PNG images are allowed.")
@staticmethod
def generate_filename(file: UploadFile):
original_filename = secure_filename(file.filename)
return f"{uuid.uuid4()}_{original_filename}"
@staticmethod
def model_to_entity(photo_model: ContestCarouselPhoto) -> ContestCarouselPhotoEntity:
return ContestCarouselPhotoEntity(
id=photo_model.id,
file_path=photo_model.file_path,
number=photo_model.number,
contest_id=photo_model.contest_id,
)
@staticmethod
def get_media_type(file_path: str) -> str:
extension = file_path.split('.')[-1].lower()
if extension in ['jpeg', 'jpg']:
return "image/jpeg"
elif extension == 'png':
return "image/png"
else:
return "application/octet-stream"

View File

@ -12,6 +12,7 @@ from app.contollers.rss_router import router as rss_router
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.contests_router import router as contest_carousel_photos_router
from app.settings import settings
@ -30,13 +31,16 @@ def start_app():
api_app.include_router(profile_photos_router, prefix=f'{settings.PREFIX}/profile_photos', tags=['profile_photos'])
api_app.include_router(profiles_router, prefix=f'{settings.PREFIX}/profiles', tags=['profiles'])
api_app.include_router(project_files_router, prefix=f'{settings.PREFIX}/project_files', tags=['project_files'])
api_app.include_router(project_members_router, prefix=f'{settings.PREFIX}/project_members', tags=['project_members'])
api_app.include_router(project_members_router, prefix=f'{settings.PREFIX}/project_members',
tags=['project_members'])
api_app.include_router(projects_router, prefix=f'{settings.PREFIX}/projects', tags=['projects'])
api_app.include_router(register_router, prefix=f'{settings.PREFIX}/register', tags=['register'])
api_app.include_router(rss_router, prefix=f'{settings.PREFIX}/rss', tags=['rss_router'])
api_app.include_router(team_router, prefix=f'{settings.PREFIX}/teams', tags=['teams'])
api_app.include_router(users_router, prefix=f'{settings.PREFIX}/users', tags=['users'])
api_app.include_router(contest_router,prefix=f'{settings.PREFIX}/contests', tags=['contests'])
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'])
return api_app

View File

@ -0,0 +1,27 @@
import axios from 'axios'
import CONFIG from '@/core/config.js'
const deletePhoto = async (photoId) => {
try {
const token = localStorage.getItem('access_token')
const response = await axios.delete(
`${CONFIG.BASE_URL}/profile_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 deletePhoto

View File

@ -0,0 +1,28 @@
import axios from 'axios'
import CONFIG from '@/core/config.js'
const downloadPhotoFile = async (photoId) => {
try {
const token = localStorage.getItem('access_token')
const response = await axios.get(
`${CONFIG.BASE_URL}/profile_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 downloadPhotoFile

View File

@ -0,0 +1,28 @@
import axios from 'axios'
import CONFIG from '@/core/config.js'
const getPhotosByProfileId = async (profileId) => {
try {
const token = localStorage.getItem('access_token')
const response = await axios.get(
`${CONFIG.BASE_URL}/profile_photos/profiles/${profileId}/`,
{
withCredentials: true,
headers: {
Authorization: `Bearer ${token}`
}
}
)
return response.data
} catch (error) {
// Улучшенная обработка ошибок для ясности
const errorMessage = error.response?.data?.detail || error.message
console.error(`Ошибка получения фотографий для профиля ${profileId}:`, errorMessage)
throw new Error(`Не удалось загрузить фотографии: ${errorMessage}`)
}
}
export default getPhotosByProfileId

View File

@ -0,0 +1,31 @@
import axios from 'axios'
import CONFIG from '@/core/config.js'
const uploadProfilePhoto = async (profileId, file) => {
try {
const token = localStorage.getItem('access_token')
const formData = new FormData()
formData.append('file', file)
const response = await axios.post(
`${CONFIG.BASE_URL}/profile_photos/profiles/${profileId}/upload`,
formData,
{
withCredentials: true,
headers: {
Authorization: `Bearer ${token}`,
}
}
)
return response.data
} catch (error) {
const errorMessage = error.response?.data?.detail || error.message
console.error(`Ошибка загрузки фотографии для профиля ${profileId}:`, errorMessage)
throw new Error(`Не удалось загрузить фотографию: ${errorMessage}`)
}
}
export default uploadProfilePhoto

View File

@ -39,6 +39,8 @@ import {
QItemSection,
QItemLabel,
QItem,
QImg,
QFile
} from 'quasar'
@ -58,7 +60,7 @@ app.use(Quasar, {
QSeparator, QCardActions, QDialog, QIcon, QSpace,
QAvatar, QTooltip, QBanner, QSlideTransition, QToggle,
QList, QSpinnerDots, QCarouselSlide, QCarousel,
QItemSection, QItemLabel, QItem
QItemSection, QItemLabel, QItem, QImg, QFile
},
directives: {
Ripple

View File

@ -21,7 +21,6 @@
<q-page-container class="q-pa-md">
<q-tab-panels v-model="tab" animated transition-prev="slide-right" transition-next="slide-left">
<!-- Пользователи -->
<q-tab-panel name="profiles">
<div class="violet-card q-pa-md">
<div class="q-gutter-sm q-mb-sm row items-center justify-between">
@ -44,7 +43,6 @@
</div>
</q-tab-panel>
<!-- Команды -->
<q-tab-panel name="teams">
<div class="violet-card q-pa-md">
<div class="q-gutter-sm q-mb-sm row items-center justify-between">
@ -68,7 +66,6 @@
</q-tab-panel>
<!-- Проекты -->
<q-tab-panel name="projects">
<div class="violet-card q-pa-md">
<div class="q-gutter-sm q-mb-sm row items-center justify-between">
@ -91,7 +88,6 @@
</div>
</q-tab-panel>
<!-- Конкурсы -->
<q-tab-panel name="contests">
<div class="violet-card q-pa-md">
<div class="q-gutter-sm q-mb-sm row items-center justify-between">
@ -117,7 +113,6 @@
</q-tab-panels>
</q-page-container>
<!-- Модальное окно редактирования -->
<q-dialog v-model="dialogVisible" persistent>
<q-card style="min-width: 350px; max-width: 700px;">
<q-card-section>
@ -143,7 +138,6 @@
<q-separator />
<q-card-section>
<!-- Teams -->
<template v-if="dialogType === 'teams'">
<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" />
@ -151,7 +145,6 @@
<q-input v-model="dialogData.git_url" label="Git URL" dense clearable class="q-mt-sm" />
</template>
<!-- Profiles -->
<template v-else-if="dialogType === 'profiles'">
<q-input v-model="dialogData.first_name" label="Имя" dense autofocus clearable />
<q-input v-model="dialogData.last_name" label="Фамилия" dense clearable class="q-mt-sm" />
@ -161,21 +154,73 @@
<q-input v-model="dialogData.phone" label="Телефон" dense clearable class="q-mt-sm" />
<q-input v-model="dialogData.role_id" label="Роль" dense clearable class="q-mt-sm" />
<q-input v-model="dialogData.team_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="loadingProfilePhotos" class="text-center q-py-md">
<q-spinner-dots color="primary" size="2em" />
<div>Загрузка фотографий...</div>
</div>
<div v-else-if="profilePhotos.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 profilePhotos" :key="photo.id" class="col-auto" style="width: 120px; height: 120px; position: relative;">
<q-img
:src="getPhotoUrl(photo.id)"
alt="Profile 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)"
/>
</div>
</q-img>
</q-card>
</div>
<q-file
v-model="newProfilePhotoFile"
label="Выберите фото для загрузки"
outlined
dense
clearable
accept="image/*"
@update:model-value="handleNewPhotoSelected"
class="q-mt-sm"
>
<template v-slot:append>
<q-icon v-if="newProfilePhotoFile" name="check" color="positive" />
<q-icon name="photo" />
</template>
</q-file>
<q-btn
v-if="newProfilePhotoFile"
label="Загрузить фото"
color="primary"
class="q-mt-sm full-width"
@click="uploadNewPhoto"
:loading="uploadingPhoto"
/>
</template>
<!-- Projects -->
<template v-else-if="dialogType === 'projects'">
<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.repository_url" label="URL репозитория" dense clearable class="q-mt-sm" />
<!-- Другие поля проекта -->
</template>
<!-- Contests -->
<template v-else-if="dialogType === 'contests'">
<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.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" />
@ -230,7 +275,6 @@
</q-card>
</q-dialog>
<!-- Кнопка выхода / авторизации -->
<q-btn
:icon="'logout'"
class="fixed-bottom-right q-ma-md"
@ -245,7 +289,9 @@
<script setup>
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
import { Notify, useQuasar } from 'quasar' // Импортируем useQuasar для QDialog
// Импорты API для управления данными
import fetchTeams from '@/api/teams/getTeams.js'
import updateTeam from '@/api/teams/updateTeam.js'
import deleteTeamById from '@/api/teams/deleteTeam.js'
@ -265,10 +311,18 @@ import fetchContests from '@/api/contests/getContests.js'
import updateContest from '@/api/contests/updateContest.js'
import deleteContestById from '@/api/contests/deleteContest.js'
import createContest from '@/api/contests/createContest.js'
import {Notify} from "quasar";
import router from "@/router/index.js";
// Текущая вкладка 'teams' или 'projects'
import router from "@/router/index.js"
import CONFIG from '@/core/config.js' // Убедитесь, что у вас есть этот файл конфигурации
// --- Импорты для работы с фотографиями профиля ---
import getPhotosByProfileId 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'
const $q = useQuasar()
// Текущая вкладка
const tab = ref('profiles')
// --- Profiles ---
@ -321,7 +375,112 @@ const contestColumns = [
// Общие состояния для диалогов
const dialogVisible = ref(false)
const dialogData = ref({})
const dialogType = ref('') // 'teams' или 'projects'
const dialogType = ref('')
// --- Состояния для фотографий профиля ---
const profilePhotos = ref([])
const loadingProfilePhotos = ref(false)
const newProfilePhotoFile = ref(null)
const uploadingPhoto = ref(false)
// Функция для получения URL фото
const getPhotoUrl = (photoId) => {
return `${CONFIG.BASE_URL}/profile_photos/${photoId}/file`;
}
// Загрузка фотографий профиля при открытии диалога для профиля
async function loadProfilePhotos(profileId) {
loadingProfilePhotos.value = true;
try {
profilePhotos.value = await getPhotosByProfileId(profileId);
} catch (error) {
Notify.create({
type: 'negative',
message: `Ошибка загрузки фотографий профиля: ${error.message}`,
icon: 'error',
});
profilePhotos.value = [];
} finally {
loadingProfilePhotos.value = false;
}
}
// Обработчик выбора нового файла
function handleNewPhotoSelected(file) {
newProfilePhotoFile.value = file;
}
// Загрузка новой фотографии
async function uploadNewPhoto() {
if (!newProfilePhotoFile.value || !dialogData.value.id) {
Notify.create({
type: 'warning',
message: 'Выберите файл и убедитесь, что профиль выбран.',
icon: 'warning',
});
return;
}
uploadingPhoto.value = true;
try {
const uploadedPhoto = await uploadProfilePhoto(dialogData.value.id, newProfilePhotoFile.value);
// Добавляем новую фото в список
profilePhotos.value.push(uploadedPhoto);
newProfilePhotoFile.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;
}
}
// Подтверждение и удаление фотографии
function confirmDeletePhoto(photoId) {
$q.dialog({
title: 'Подтверждение удаления',
message: 'Вы уверены, что хотите удалить эту фотографию?',
cancel: true,
persistent: true,
ok: {
label: 'Удалить',
color: 'negative'
},
cancel: {
label: 'Отмена',
color: 'primary'
}
}).onOk(async () => {
await deleteExistingPhoto(photoId);
});
}
async function deleteExistingPhoto(photoId) {
try {
await deletePhoto(photoId);
profilePhotos.value = profilePhotos.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 openEdit(type, row) {
dialogType.value = type
@ -332,9 +491,20 @@ function openEdit(type, row) {
dialogData.value = { title: '', description: '', logo: '', git_url: '' }
} else if (type === 'projects') {
dialogData.value = { name: '', summary: '', deadline: '' }
} 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 }
}
}
dialogVisible.value = true
if (type === 'profiles' && dialogData.value.id) {
loadProfilePhotos(dialogData.value.id);
} else {
profilePhotos.value = [];
}
}
function onRowClick(event, row) {
@ -343,6 +513,9 @@ function onRowClick(event, row) {
function closeDialog() {
dialogVisible.value = false
profilePhotos.value = [];
newProfilePhotoFile.value = null;
uploadingPhoto.value = false;
}
async function saveChanges() {
@ -381,12 +554,22 @@ async function saveChanges() {
if (idx !== -1) contests.value[idx] = JSON.parse(JSON.stringify(dialogData.value))
} else {
const newContest = await createContest(dialogData.value)
profiles.value.push(newContest)
contests.value.push(newContest)
}
}
closeDialog()
Notify.create({
type: 'positive',
message: 'Изменения успешно сохранены!',
icon: 'check_circle',
});
} catch (error) {
console.error('Ошибка при сохранении:', error.message)
Notify.create({
type: 'negative',
message: `Ошибка при сохранении: ${error.message}`,
icon: 'error',
});
}
}
@ -397,7 +580,11 @@ async function loadData(name) {
teams.value = await fetchTeams() || []
} catch (error) {
teams.value = []
console.error(error.message)
Notify.create({
type: 'negative',
message: `Ошибка загрузки команд: ${error.message}`,
icon: 'error',
});
} finally {
loadingTeams.value = false
}
@ -407,7 +594,11 @@ async function loadData(name) {
projects.value = await fetchProjects() || []
} catch (error) {
projects.value = []
console.error(error.message)
Notify.create({
type: 'negative',
message: `Ошибка загрузки проектов: ${error.message}`,
icon: 'error',
});
} finally {
loadingProjects.value = false
}
@ -416,8 +607,12 @@ async function loadData(name) {
try {
profiles.value = await fetchProfiles() || []
} catch (error) {
projects.value = []
console.error(error.message)
profiles.value = []
Notify.create({
type: 'negative',
message: `Ошибка загрузки профилей: ${error.message}`,
icon: 'error',
});
} finally {
loadingProfiles.value = false
}
@ -427,7 +622,11 @@ async function loadData(name) {
contests.value = await fetchContests() || []
} catch (error) {
contests.value = []
console.error(error.message)
Notify.create({
type: 'negative',
message: `Ошибка загрузки конкурсов: ${error.message}`,
icon: 'error',
});
} finally {
loadingContests.value = false
}
@ -436,6 +635,20 @@ async function loadData(name) {
async function deleteItem() {
if (!dialogData.value.id) return
$q.dialog({
title: 'Подтверждение удаления',
message: `Вы уверены, что хотите удалить ${dialogType.value === 'profiles' ? 'пользователя' : dialogType.value}?`,
cancel: true,
persistent: true,
ok: {
label: 'Удалить',
color: 'negative'
},
cancel: {
label: 'Отмена',
color: 'primary'
}
}).onOk(async () => {
try {
if (dialogType.value === 'teams') {
await deleteTeamById(dialogData.value.id)
@ -451,9 +664,20 @@ async function deleteItem() {
contests.value = contests.value.filter(c => c.id !== dialogData.value.id)
}
closeDialog()
Notify.create({
type: 'positive',
message: 'Элемент успешно удален!',
icon: 'check_circle',
});
} catch (error) {
console.error('Ошибка при удалении:', error.message)
Notify.create({
type: 'negative',
message: `Ошибка при удалении: ${error.message}`,
icon: 'error',
});
}
});
}
function createHandler() {
@ -476,30 +700,11 @@ onMounted(() => {
watch(tab, (newTab) => {
loadData(newTab)
})
const handleAuthAction = () => {
const isAuthenticated = ref(!!localStorage.getItem('access_token'))
if (isAuthenticated.value) {
localStorage.removeItem('access_token')
localStorage.removeItem('user_id')
isAuthenticated.value = false
Notify.create({
type: 'positive',
message: 'Выход успешно осуществлен',
icon: 'check_circle',
})
router.push('/')
}
}
</script>
<style scoped>
.bg-violet-strong {
background: linear-gradient(135deg, #4f046f 0%, #7c3aed 100%);
min-height: 100vh;
}
.violet-card {
@ -507,4 +712,14 @@ const handleAuthAction = () => {
background: #ede9fe;
box-shadow: 0 6px 32px rgba(124, 58, 237, 0.13), 0 1.5px 6px rgba(124, 58, 237, 0.10);
}
.q-table {
background: #ede9fe;
}
.q-table th {
background-color: #7c3aed;
color: white;
font-weight: bold;
}
</style>