1232 lines
42 KiB
Vue
1232 lines
42 KiB
Vue
<template>
|
|
<q-layout view="lHh Lpr lFf" class="bg-violet-strong text-dark">
|
|
<q-header elevated class="bg-transparent">
|
|
<q-tabs
|
|
v-model="tab"
|
|
class="text-white"
|
|
active-color="white"
|
|
indicator-color="white"
|
|
align="justify"
|
|
dense
|
|
animated
|
|
@update:model-value="loadData"
|
|
>
|
|
<q-tab name="profiles" label="Пользователи" />
|
|
<q-tab name="teams" label="Команды" />
|
|
<q-tab name="projects" label="Проекты" />
|
|
<q-tab name="contests" label="Конкурсы" />
|
|
</q-tabs>
|
|
</q-header>
|
|
|
|
<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">
|
|
<q-btn
|
|
label="Создание пользователя"
|
|
color="primary"
|
|
@click="createHandler"
|
|
/>
|
|
</div>
|
|
<q-table
|
|
title="Пользователи"
|
|
:rows="profiles"
|
|
:columns="profileColumns"
|
|
row-key="id"
|
|
@row-click="onRowClick"
|
|
:loading="loadingProfiles"
|
|
dense
|
|
flat
|
|
/>
|
|
</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">
|
|
<q-btn
|
|
label="Создание команды"
|
|
color="primary"
|
|
@click="createHandler"
|
|
/>
|
|
</div>
|
|
<q-table
|
|
title="Команды"
|
|
:rows="teams"
|
|
:columns="teamColumns"
|
|
row-key="id"
|
|
@row-click="onRowClick"
|
|
:loading="loadingTeams"
|
|
dense
|
|
flat
|
|
/>
|
|
</div>
|
|
</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">
|
|
<q-btn
|
|
label="Создание проекта"
|
|
color="primary"
|
|
@click="createHandler"
|
|
/>
|
|
</div>
|
|
<q-table
|
|
title="Проекты"
|
|
:rows="projects"
|
|
:columns="projectColumns"
|
|
row-key="id"
|
|
@row-click="onRowClick"
|
|
:loading="loadingProjects"
|
|
dense
|
|
flat
|
|
/>
|
|
</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">
|
|
<q-btn
|
|
label="Создание конкурса"
|
|
color="primary"
|
|
@click="createHandler"
|
|
/>
|
|
</div>
|
|
<q-table
|
|
title="Конкурсы"
|
|
:rows="contests"
|
|
:columns="contestColumns"
|
|
row-key="id"
|
|
@row-click="onRowClick"
|
|
:loading="loadingContests"
|
|
dense
|
|
flat
|
|
/>
|
|
</div>
|
|
</q-tab-panel>
|
|
|
|
</q-tab-panels>
|
|
</q-page-container>
|
|
|
|
<q-dialog v-model="dialogVisible" persistent>
|
|
<q-card style="min-width: 350px; max-width: 700px;">
|
|
<q-card-section>
|
|
<div class="text-h6">
|
|
<template v-if="dialogType === 'teams'">
|
|
Редактирование команды {{ dialogData.title || '' }}
|
|
</template>
|
|
<template v-else-if="dialogType === 'profiles'">
|
|
Редактирование пользователя {{ dialogData.name || dialogData.login || '' }}
|
|
</template>
|
|
<template v-else-if="dialogType === 'projects'">
|
|
Редактирование проекта {{ dialogData.title || '' }}
|
|
</template>
|
|
<template v-else-if="dialogType === 'contests'">
|
|
Редактирование конкурса {{ dialogData.title || '' }}
|
|
</template>
|
|
<template v-else>
|
|
Редактирование {{ dialogType }}
|
|
</template>
|
|
</div>
|
|
</q-card-section>
|
|
|
|
<q-separator />
|
|
|
|
<q-card-section>
|
|
<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" />
|
|
<q-input v-model="dialogData.logo" label="Логотип" dense clearable class="q-mt-sm" />
|
|
<q-input v-model="dialogData.git_url" label="Git URL" dense clearable class="q-mt-sm" />
|
|
</template>
|
|
|
|
<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" />
|
|
<q-input v-model="dialogData.patronymic" label="Отчество" dense clearable class="q-mt-sm" />
|
|
<q-input v-model="dialogData.birthday" label="День рождения" dense clearable class="q-mt-sm" type="date" />
|
|
<q-input v-model="dialogData.email" label="Почта" dense clearable class="q-mt-sm" />
|
|
<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, 'profile')"
|
|
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, 'profile')"
|
|
/>
|
|
</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="uploadNewProfilePhoto"
|
|
:loading="uploadingPhoto"
|
|
/>
|
|
</template>
|
|
|
|
<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" />
|
|
|
|
<q-separator class="q-my-md" />
|
|
<div class="text-h6 q-mb-sm">Файлы проекта</div>
|
|
|
|
<div v-if="loadingProjectFiles" class="text-center q-py-md">
|
|
<q-spinner-dots color="primary" size="2em" />
|
|
<div>Загрузка файлов...</div>
|
|
</div>
|
|
<div v-else-if="projectFiles.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 projectFiles" :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="downloadExistingProjectFile(fileItem.id)"
|
|
/>
|
|
<q-btn
|
|
icon="delete"
|
|
color="negative"
|
|
round
|
|
dense
|
|
size="sm"
|
|
@click.stop="confirmDeleteProjectFile(fileItem.id)"
|
|
/>
|
|
</div>
|
|
</q-item-section>
|
|
</q-item>
|
|
</q-list>
|
|
|
|
<q-file
|
|
v-model="newProjectFile"
|
|
label="Выберите файл для загрузки"
|
|
outlined
|
|
dense
|
|
clearable
|
|
@update:model-value="handleNewProjectFileSelected"
|
|
class="q-mt-sm"
|
|
>
|
|
<template v-slot:append>
|
|
<q-icon v-if="newProjectFile" name="check" color="positive" />
|
|
<q-icon name="attach_file" />
|
|
</template>
|
|
</q-file>
|
|
<q-btn
|
|
v-if="newProjectFile"
|
|
label="Загрузить файл"
|
|
color="primary"
|
|
class="q-mt-sm full-width"
|
|
@click="uploadNewProjectFile"
|
|
:loading="uploadingFile"
|
|
/>
|
|
</template>
|
|
|
|
<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.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>
|
|
<pre>{{ dialogData }}</pre>
|
|
</template>
|
|
</q-card-section>
|
|
|
|
<q-card-actions align="right">
|
|
<q-btn
|
|
v-if="dialogType === 'teams' && dialogData.id"
|
|
flat
|
|
label="Удалить"
|
|
color="negative"
|
|
@click="deleteItem"
|
|
/>
|
|
<q-btn
|
|
v-if="dialogType === 'profiles' && dialogData.id"
|
|
flat
|
|
label="Удалить"
|
|
color="negative"
|
|
@click="deleteItem"
|
|
/>
|
|
<q-btn
|
|
v-if="dialogType === 'projects' && dialogData.id"
|
|
flat
|
|
label="Удалить"
|
|
color="negative"
|
|
@click="deleteItem"
|
|
/>
|
|
<q-btn
|
|
v-if="dialogType === 'contests' && dialogData.id"
|
|
flat
|
|
label="Удалить"
|
|
color="negative"
|
|
@click="deleteItem"
|
|
/>
|
|
<q-space />
|
|
<q-btn flat label="Закрыть" color="primary" @click="closeDialog" />
|
|
<q-btn
|
|
v-if="['teams', 'profiles', 'projects', 'contests'].includes(dialogType)"
|
|
flat
|
|
label="Сохранить"
|
|
color="primary"
|
|
@click="saveChanges"
|
|
/>
|
|
</q-card-actions>
|
|
</q-card>
|
|
</q-dialog>
|
|
|
|
<q-btn
|
|
:icon="'logout'"
|
|
class="fixed-bottom-right q-ma-md"
|
|
size="20px"
|
|
color="indigo-10"
|
|
round
|
|
@click="handleAuthAction"
|
|
/>
|
|
</q-layout>
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
|
import { Notify, useQuasar } from 'quasar'
|
|
|
|
import fetchTeams from '@/api/teams/getTeams.js'
|
|
import updateTeam from '@/api/teams/updateTeam.js'
|
|
import deleteTeamById from '@/api/teams/deleteTeam.js'
|
|
import createTeam from '@/api/teams/createTeam.js'
|
|
|
|
import fetchProjects from '@/api/projects/getProjects.js'
|
|
import updateProject from '@/api/projects/updateProject.js'
|
|
import deleteProjectById from '@/api/projects/deleteProject.js'
|
|
import createProject from '@/api/projects/createProject.js'
|
|
|
|
import fetchProfiles from '@/api/profiles/getProfiles.js'
|
|
import updateProfile from '@/api/profiles/updateProfile.js'
|
|
import deleteProfileById from '@/api/profiles/deleteProfile.js'
|
|
import createProfile from '@/api/profiles/createProfile.js'
|
|
|
|
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 CONFIG from '@/core/config.js'
|
|
|
|
import getProfilePhotosByProfileId from '@/api/profiles/profile_photos/getPhotoFileById.js'
|
|
import uploadProfilePhoto from '@/api/profiles/profile_photos/uploadProfilePhoto.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'
|
|
|
|
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'
|
|
|
|
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'
|
|
import router from "@/router/index.js";
|
|
|
|
|
|
const $q = useQuasar()
|
|
|
|
const tab = ref('profiles')
|
|
|
|
const profiles = ref([])
|
|
const loadingProfiles = ref(false)
|
|
const profileColumns = [
|
|
{ name: 'first_name', label: 'Имя', field: 'first_name', sortable: true },
|
|
{ name: 'last_name', label: 'Фамилия', field: 'last_name', sortable: true },
|
|
{ name: 'patronymic', label: 'Отчество', field: 'patronymic', sortable: true },
|
|
{ name: 'birthday', label: 'День рождения', field: 'birthday', sortable: true },
|
|
{ name: 'email', label: 'Почта', field: 'email', sortable: true },
|
|
{ name: 'phone', label: 'Телефон', field: 'phone', sortable: true },
|
|
{ name: 'role_id', label: 'Роль', field: 'role_id', sortable: true },
|
|
{ name: 'team_id', label: 'Команда', field: 'team_id', sortable: true },
|
|
]
|
|
|
|
const teams = ref([])
|
|
const loadingTeams = ref(false)
|
|
const teamColumns = [
|
|
{ name: 'title', label: 'Название команды', field: 'title', sortable: true },
|
|
{ name: 'description', label: 'Описание', field: 'description', sortable: true },
|
|
{ name: 'logo', label: 'Логотип', field: 'logo', sortable: true },
|
|
{ name: 'git_url', label: 'Git URL', field: 'git_url', sortable: true },
|
|
]
|
|
|
|
const projects = ref([])
|
|
const loadingProjects = ref(false)
|
|
const projectColumns = [
|
|
{ name: 'title', label: 'Название проекта', field: 'title', sortable: true },
|
|
{ name: 'description', label: 'Описание', field: 'description', sortable: true },
|
|
{ name: 'repository_url', label: 'Репозиторий', field: 'repository_url', sortable: true },
|
|
]
|
|
|
|
const contests = ref([])
|
|
const loadingContests = ref(false)
|
|
const contestColumns = [
|
|
{ name: 'title', label: 'Название конкурса', field: 'title', sortable: true },
|
|
{ name: 'description', label: 'Описание', field: 'description', 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 },
|
|
{ name: 'project_id', label: 'Проект', field: 'project_id', sortable: true },
|
|
{ name: 'status_id', label: 'Статус', field: 'status_id', sortable: true },
|
|
]
|
|
|
|
const dialogVisible = ref(false)
|
|
const dialogData = ref({})
|
|
const dialogType = ref('')
|
|
|
|
const profilePhotos = ref([])
|
|
const loadingProfilePhotos = ref(false)
|
|
const newProfilePhotoFile = ref(null)
|
|
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)
|
|
|
|
const projectFiles = ref([])
|
|
const loadingProjectFiles = ref(false)
|
|
const newProjectFile = ref(null)
|
|
|
|
|
|
const getPhotoUrl = (photoId, type) => {
|
|
if (type === 'profile') {
|
|
return `${CONFIG.BASE_URL}/profile_photos/${photoId}/file`;
|
|
} else if (type === 'contest') {
|
|
return `${CONFIG.BASE_URL}/contest_carousel_photos/${photoId}/file`;
|
|
}
|
|
return '';
|
|
}
|
|
|
|
async function loadProfilePhotos(profileId) {
|
|
loadingProfilePhotos.value = true;
|
|
try {
|
|
profilePhotos.value = await getProfilePhotosByProfileId(profileId);
|
|
} catch (error) {
|
|
Notify.create({
|
|
type: 'negative',
|
|
message: `Ошибка загрузки фотографий профиля: ${error.message}`,
|
|
icon: 'error',
|
|
});
|
|
profilePhotos.value = [];
|
|
} finally {
|
|
loadingProfilePhotos.value = false;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
async function loadProjectFiles(projectId) {
|
|
loadingProjectFiles.value = true;
|
|
try {
|
|
projectFiles.value = await getProjectFiles(projectId);
|
|
} catch (error) {
|
|
Notify.create({
|
|
type: 'negative',
|
|
message: `Ошибка загрузки файлов проекта: ${error.message}`,
|
|
icon: 'error',
|
|
});
|
|
projectFiles.value = [];
|
|
} finally {
|
|
loadingProjectFiles.value = false;
|
|
}
|
|
}
|
|
|
|
function handleNewPhotoSelected(file) {
|
|
newProfilePhotoFile.value = file;
|
|
}
|
|
|
|
function handleNewContestPhotoSelected(file) {
|
|
newContestPhotoFile.value = file;
|
|
}
|
|
|
|
function handleNewContestFileSelected(file) {
|
|
newContestFile.value = file;
|
|
}
|
|
|
|
function handleNewProjectFileSelected(file) {
|
|
newProjectFile.value = file;
|
|
}
|
|
|
|
|
|
async function uploadNewProfilePhoto() {
|
|
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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
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;
|
|
}
|
|
}
|
|
|
|
async function uploadNewProjectFile() {
|
|
if (!newProjectFile.value || !dialogData.value.id) {
|
|
Notify.create({
|
|
type: 'warning',
|
|
message: 'Выберите файл и убедитесь, что проект выбран.',
|
|
icon: 'warning',
|
|
});
|
|
return;
|
|
}
|
|
|
|
uploadingFile.value = true;
|
|
try {
|
|
const uploadedFile = await uploadProjectFile(dialogData.value.id, newProjectFile.value);
|
|
projectFiles.value.push(uploadedFile);
|
|
newProjectFile.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, type) {
|
|
$q.dialog({
|
|
title: 'Подтверждение удаления',
|
|
message: 'Вы уверены, что хотите удалить эту фотографию?',
|
|
cancel: true,
|
|
persistent: true,
|
|
ok: {
|
|
label: 'Удалить',
|
|
color: 'negative'
|
|
},
|
|
cancel: {
|
|
label: 'Отмена',
|
|
color: 'primary'
|
|
}
|
|
}).onOk(async () => {
|
|
await deleteExistingPhoto(photoId, type);
|
|
});
|
|
}
|
|
|
|
async function deleteExistingPhoto(photoId, type) {
|
|
try {
|
|
if (type === 'profile') {
|
|
await deleteProfilePhoto(photoId);
|
|
profilePhotos.value = profilePhotos.value.filter(p => p.id !== photoId);
|
|
Notify.create({
|
|
type: 'positive',
|
|
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}`,
|
|
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',
|
|
});
|
|
}
|
|
}
|
|
|
|
function confirmDeleteProjectFile(fileId) {
|
|
$q.dialog({
|
|
title: 'Подтверждение удаления',
|
|
message: 'Вы уверены, что хотите удалить этот файл?',
|
|
cancel: true,
|
|
persistent: true,
|
|
ok: {
|
|
label: 'Удалить',
|
|
color: 'negative'
|
|
},
|
|
cancel: {
|
|
label: 'Отмена',
|
|
color: 'primary'
|
|
}
|
|
}).onOk(async () => {
|
|
await deleteExistingProjectFile(fileId);
|
|
});
|
|
}
|
|
|
|
async function deleteExistingProjectFile(fileId) {
|
|
try {
|
|
await deleteProjectFile(fileId);
|
|
projectFiles.value = projectFiles.value.filter(f => f.id !== fileId);
|
|
Notify.create({
|
|
type: 'positive',
|
|
message: 'Файл проекта успешно удален!',
|
|
icon: 'check_circle',
|
|
});
|
|
} catch (error) {
|
|
Notify.create({
|
|
type: 'negative',
|
|
message: `Ошибка удаления файла проекта: ${error.message}`,
|
|
icon: 'error',
|
|
});
|
|
}
|
|
}
|
|
|
|
async function downloadExistingProjectFile(fileId) {
|
|
try {
|
|
await downloadProjectFile(fileId);
|
|
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
|
|
if (row) {
|
|
dialogData.value = JSON.parse(JSON.stringify(row))
|
|
} else {
|
|
if (type === 'teams') {
|
|
dialogData.value = { title: '', description: '', logo: '', git_url: '' }
|
|
} else if (type === 'projects') {
|
|
dialogData.value = { title: '', description: '', repository_url: '' }
|
|
projectFiles.value = [];
|
|
} 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 = [];
|
|
}
|
|
}
|
|
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);
|
|
} else if (type === 'projects' && dialogData.value.id) {
|
|
loadProjectFiles(dialogData.value.id);
|
|
} else {
|
|
profilePhotos.value = [];
|
|
contestPhotos.value = [];
|
|
contestFiles.value = [];
|
|
projectFiles.value = [];
|
|
}
|
|
}
|
|
|
|
function onRowClick(event, row) {
|
|
openEdit(tab.value, row)
|
|
}
|
|
|
|
function closeDialog() {
|
|
dialogVisible.value = false
|
|
profilePhotos.value = [];
|
|
newProfilePhotoFile.value = null;
|
|
contestPhotos.value = [];
|
|
newContestPhotoFile.value = null;
|
|
contestFiles.value = [];
|
|
newContestFile.value = null;
|
|
projectFiles.value = [];
|
|
newProjectFile.value = null;
|
|
uploadingPhoto.value = false;
|
|
uploadingFile.value = false;
|
|
}
|
|
|
|
async function saveChanges() {
|
|
try {
|
|
if (dialogType.value === 'teams') {
|
|
if (dialogData.value.id) {
|
|
await updateTeam(dialogData.value)
|
|
const idx = teams.value.findIndex(t => t.id === dialogData.value.id)
|
|
if (idx !== -1) teams.value[idx] = JSON.parse(JSON.stringify(dialogData.value))
|
|
} else {
|
|
const newTeam = await createTeam(dialogData.value)
|
|
teams.value.push(newTeam)
|
|
}
|
|
} else if (dialogType.value === 'projects') {
|
|
if (dialogData.value.id) {
|
|
await updateProject(dialogData.value)
|
|
const idx = projects.value.findIndex(p => p.id === dialogData.value.id)
|
|
if (idx !== -1) projects.value[idx] = JSON.parse(JSON.stringify(dialogData.value))
|
|
} else {
|
|
const newProject = await createProject(dialogData.value)
|
|
projects.value.push(newProject)
|
|
}
|
|
} else if (dialogType.value === 'profiles') {
|
|
if (dialogData.value.id) {
|
|
await updateProfile(dialogData.value)
|
|
const idx = profiles.value.findIndex(p => p.id === dialogData.value.id)
|
|
if (idx !== -1) profiles.value[idx] = JSON.parse(JSON.stringify(dialogData.value))
|
|
} else {
|
|
const newProfile = await createProfile(dialogData.value)
|
|
profiles.value.push(newProfile)
|
|
}
|
|
} else if (dialogType.value === 'contests') {
|
|
if (dialogData.value.id) {
|
|
await updateContest(dialogData.value)
|
|
const idx = contests.value.findIndex(c => c.id === dialogData.value.id)
|
|
if (idx !== -1) contests.value[idx] = JSON.parse(JSON.stringify(dialogData.value))
|
|
} else {
|
|
const newContest = await createContest(dialogData.value)
|
|
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',
|
|
});
|
|
}
|
|
}
|
|
|
|
async function loadData(name) {
|
|
if (name === 'teams') {
|
|
loadingTeams.value = true
|
|
try {
|
|
teams.value = await fetchTeams() || []
|
|
} catch (error) {
|
|
teams.value = []
|
|
Notify.create({
|
|
type: 'negative',
|
|
message: `Ошибка загрузки команд: ${error.message}`,
|
|
icon: 'error',
|
|
});
|
|
} finally {
|
|
loadingTeams.value = false
|
|
}
|
|
} else if (name === 'projects') {
|
|
loadingProjects.value = true
|
|
try {
|
|
projects.value = await fetchProjects() || []
|
|
} catch (error) {
|
|
projects.value = []
|
|
Notify.create({
|
|
type: 'negative',
|
|
message: `Ошибка загрузки проектов: ${error.message}`,
|
|
icon: 'error',
|
|
});
|
|
} finally {
|
|
loadingProjects.value = false
|
|
}
|
|
} else if (name === 'profiles') {
|
|
loadingProfiles.value = true
|
|
try {
|
|
profiles.value = await fetchProfiles() || []
|
|
} catch (error) {
|
|
profiles.value = []
|
|
Notify.create({
|
|
type: 'negative',
|
|
message: `Ошибка загрузки профилей: ${error.message}`,
|
|
icon: 'error',
|
|
});
|
|
} finally {
|
|
loadingProfiles.value = false
|
|
}
|
|
} else if (name === 'contests') {
|
|
loadingContests.value = true
|
|
try {
|
|
contests.value = await fetchContests() || []
|
|
} catch (error) {
|
|
contests.value = []
|
|
Notify.create({
|
|
type: 'negative',
|
|
message: `Ошибка загрузки конкурсов: ${error.message}`,
|
|
icon: 'error',
|
|
});
|
|
} finally {
|
|
loadingContests.value = false
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
teams.value = teams.value.filter(t => t.id !== dialogData.value.id)
|
|
} else if (dialogType.value === 'projects') {
|
|
await deleteProjectById(dialogData.value.id)
|
|
projects.value = projects.value.filter(p => p.id !== dialogData.value.id)
|
|
} else if (dialogType.value === 'profiles') {
|
|
await deleteProfileById(dialogData.value.id)
|
|
profiles.value = profiles.value.filter(p => p.id !== dialogData.value.id)
|
|
} else if (dialogType.value === 'contests') {
|
|
await deleteContestById(dialogData.value.id)
|
|
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() {
|
|
dialogData.value = {};
|
|
openEdit(tab.value, null)
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadData(tab.value)
|
|
|
|
const interval = setInterval(() => {
|
|
loadData(tab.value)
|
|
}, 5000)
|
|
|
|
onBeforeUnmount(() => {
|
|
clearInterval(interval)
|
|
})
|
|
})
|
|
|
|
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%);
|
|
}
|
|
|
|
.violet-card {
|
|
border-radius: 22px;
|
|
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> |