Merge remote-tracking branch 'gitea/main'

This commit is contained in:
Андрей Дувакин 2025-06-03 11:42:44 +05:00
commit 562166086e
2 changed files with 206 additions and 43 deletions

View File

@ -3,19 +3,14 @@ import CONFIG from '@/core/config.js';
const downloadProjectFile = async (fileId) => { const downloadProjectFile = async (fileId) => {
try { try {
const token = localStorage.getItem('access_token');
const response = await axios.get( const response = await axios.get(
`${CONFIG.BASE_URL}/project_files/${fileId}/download`, `${CONFIG.BASE_URL}/project_files/${fileId}/download`,
{ {
headers: {
Authorization: `Bearer ${token}`,
},
responseType: 'blob', responseType: 'blob',
withCredentials: true, withCredentials: true,
} }
); );
// Создаем ссылку для скачивания
const url = window.URL.createObjectURL(new Blob([response.data])); const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a'); const link = document.createElement('a');
link.href = url; link.href = url;
@ -34,17 +29,15 @@ const downloadProjectFile = async (fileId) => {
link.remove(); link.remove();
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
return { success: true, filename: filename }; return filename;
} catch (error) { } catch (error) {
const errorMessage = error.response?.data?.detail || error.message;
console.error(`Ошибка скачивания файла проекта с ID ${fileId}:`, errorMessage);
if (error.response?.status === 401) { if (error.response?.status === 401) {
throw new Error("Недостаточно прав для скачивания файла (401)"); throw new Error("Недостаточно прав для скачивания файла (401)");
} }
if (error.response?.status === 404) { if (error.response?.status === 404) {
throw new Error("Файл не найден (404)"); throw new Error("Файл не найден (404)");
} }
throw new Error(errorMessage); throw new Error(error.message);
} }
}; };

View File

@ -215,6 +215,68 @@
<q-input v-model="dialogData.title" label="Название проекта" dense autofocus clearable /> <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.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-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>
<template v-else-if="dialogType === 'contests'"> <template v-else-if="dialogType === 'contests'">
@ -439,24 +501,22 @@ import uploadContestCarouselPhoto from '@/api/contests/contest_carousel_photos/u
import deleteContestCarouselPhoto from '@/api/contests/contest_carousel_photos/deleteContestPhoto.js' import deleteContestCarouselPhoto from '@/api/contests/contest_carousel_photos/deleteContestPhoto.js'
import downloadContestCarouselPhotoFile from '@/api/contests/contest_carousel_photos/downloadContestPhotoFile.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 getContestFiles from '@/api/contests/contest_files/getContestFiles.js'
import uploadContestFile from '@/api/contests/contest_files/uploadContestFile.js' import uploadContestFile from '@/api/contests/contest_files/uploadContestFile.js'
import deleteContestFile from '@/api/contests/contest_files/deleteContestFile.js' import deleteContestFile from '@/api/contests/contest_files/deleteContestFile.js'
import downloadContestFile from '@/api/contests/contest_files/downloadContestFile.js' import downloadContestFile from '@/api/contests/contest_files/downloadContestFile.js'
// --- Imports for Project Files ---
import getProjectFiles from '@/api/projects/project_files/getProjectFiles.js' import getProjectFiles from '@/api/projects/project_files/getProjectFiles.js'
import uploadProjectFile from '@/api/projects/project_files/uploadProjectFile.js' import uploadProjectFile from '@/api/projects/project_files/uploadProjectFile.js'
import deleteProjectFile from '@/api/projects/project_files/deleteProjectFile.js' import deleteProjectFile from '@/api/projects/project_files/deleteProjectFile.js'
import downloadProjectFile from '@/api/projects/project_files/downloadProjectFile.js' import downloadProjectFile from '@/api/projects/project_files/downloadProjectFile.js'
import router from "@/router/index.js";
const $q = useQuasar() const $q = useQuasar()
const tab = ref('profiles') const tab = ref('profiles')
// --- Profiles ---
const profiles = ref([]) const profiles = ref([])
const loadingProfiles = ref(false) const loadingProfiles = ref(false)
const profileColumns = [ const profileColumns = [
@ -470,7 +530,6 @@ const profileColumns = [
{ name: 'team_id', label: 'Команда', field: 'team_id', sortable: true }, { name: 'team_id', label: 'Команда', field: 'team_id', sortable: true },
] ]
// --- Teams ---
const teams = ref([]) const teams = ref([])
const loadingTeams = ref(false) const loadingTeams = ref(false)
const teamColumns = [ const teamColumns = [
@ -480,7 +539,6 @@ const teamColumns = [
{ name: 'git_url', label: 'Git URL', field: 'git_url', sortable: true }, { name: 'git_url', label: 'Git URL', field: 'git_url', sortable: true },
] ]
// --- Projects ---
const projects = ref([]) const projects = ref([])
const loadingProjects = ref(false) const loadingProjects = ref(false)
const projectColumns = [ const projectColumns = [
@ -489,7 +547,6 @@ const projectColumns = [
{ name: 'repository_url', label: 'Репозиторий', field: 'repository_url', sortable: true }, { name: 'repository_url', label: 'Репозиторий', field: 'repository_url', sortable: true },
] ]
// --- Contests ---
const contests = ref([]) const contests = ref([])
const loadingContests = ref(false) const loadingContests = ref(false)
const contestColumns = [ const contestColumns = [
@ -503,42 +560,38 @@ const contestColumns = [
{ name: 'status_id', label: 'Статус', field: 'status_id', sortable: true }, { name: 'status_id', label: 'Статус', field: 'status_id', sortable: true },
] ]
// Общие состояния для диалогов
const dialogVisible = ref(false) const dialogVisible = ref(false)
const dialogData = ref({}) const dialogData = ref({})
const dialogType = ref('') const dialogType = ref('')
// --- Состояния для фотографий профиля ---
const profilePhotos = ref([]) const profilePhotos = ref([])
const loadingProfilePhotos = ref(false) const loadingProfilePhotos = ref(false)
const newProfilePhotoFile = ref(null) const newProfilePhotoFile = ref(null)
const uploadingPhoto = ref(false) // Общее состояние для загрузки фото const uploadingPhoto = ref(false)
// --- Состояния для фотографий карусели конкурса ---
const contestPhotos = ref([]) const contestPhotos = ref([])
const loadingContestPhotos = ref(false) const loadingContestPhotos = ref(false)
const newContestPhotoFile = ref(null) const newContestPhotoFile = ref(null)
// --- Состояния для файлов конкурса ---
const contestFiles = ref([]) const contestFiles = ref([])
const loadingContestFiles = ref(false) const loadingContestFiles = ref(false)
const newContestFile = ref(null) const newContestFile = ref(null)
const uploadingFile = ref(false) // Отдельное состояние для загрузки файлов const uploadingFile = ref(false)
const projectFiles = ref([])
const loadingProjectFiles = ref(false)
const newProjectFile = ref(null)
// Функция для получения URL фото
const getPhotoUrl = (photoId, type) => { const getPhotoUrl = (photoId, type) => {
if (type === 'profile') { if (type === 'profile') {
// Используем шаблонные строки JavaScript для корректного формирования URL
return `${CONFIG.BASE_URL}/profile_photos/${photoId}/file`; return `${CONFIG.BASE_URL}/profile_photos/${photoId}/file`;
} else if (type === 'contest') { } else if (type === 'contest') {
// Используем шаблонные строки JavaScript для корректного формирования URL
return `${CONFIG.BASE_URL}/contest_carousel_photos/${photoId}/file`; return `${CONFIG.BASE_URL}/contest_carousel_photos/${photoId}/file`;
} }
return ''; return '';
} }
// Загрузка фотографий профиля при открытии диалога для профиля
async function loadProfilePhotos(profileId) { async function loadProfilePhotos(profileId) {
loadingProfilePhotos.value = true; loadingProfilePhotos.value = true;
try { try {
@ -555,7 +608,6 @@ async function loadProfilePhotos(profileId) {
} }
} }
// Загрузка фотографий карусели конкурса
async function loadContestPhotos(contestId) { async function loadContestPhotos(contestId) {
loadingContestPhotos.value = true; loadingContestPhotos.value = true;
try { try {
@ -572,7 +624,6 @@ async function loadContestPhotos(contestId) {
} }
} }
// Загрузка файлов конкурса
async function loadContestFiles(contestId) { async function loadContestFiles(contestId) {
loadingContestFiles.value = true; loadingContestFiles.value = true;
try { try {
@ -589,23 +640,39 @@ async function loadContestFiles(contestId) {
} }
} }
// Обработчик выбора нового файла для профиля 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) { function handleNewPhotoSelected(file) {
newProfilePhotoFile.value = file; newProfilePhotoFile.value = file;
} }
// Обработчик выбора нового файла для конкурса (фото)
function handleNewContestPhotoSelected(file) { function handleNewContestPhotoSelected(file) {
newContestPhotoFile.value = file; newContestPhotoFile.value = file;
} }
// Обработчик выбора нового файла для конкурса (обычный файл)
function handleNewContestFileSelected(file) { function handleNewContestFileSelected(file) {
newContestFile.value = file; newContestFile.value = file;
} }
function handleNewProjectFileSelected(file) {
newProjectFile.value = file;
}
// Загрузка новой фотографии профиля
async function uploadNewProfilePhoto() { async function uploadNewProfilePhoto() {
if (!newProfilePhotoFile.value || !dialogData.value.id) { if (!newProfilePhotoFile.value || !dialogData.value.id) {
Notify.create({ Notify.create({
@ -637,7 +704,6 @@ async function uploadNewProfilePhoto() {
} }
} }
// Загрузка новой фотографии карусели конкурса
async function uploadNewContestPhoto() { async function uploadNewContestPhoto() {
if (!newContestPhotoFile.value || !dialogData.value.id) { if (!newContestPhotoFile.value || !dialogData.value.id) {
Notify.create({ Notify.create({
@ -669,7 +735,6 @@ async function uploadNewContestPhoto() {
} }
} }
// Загрузка нового файла конкурса
async function uploadNewContestFile() { async function uploadNewContestFile() {
if (!newContestFile.value || !dialogData.value.id) { if (!newContestFile.value || !dialogData.value.id) {
Notify.create({ Notify.create({
@ -680,7 +745,7 @@ async function uploadNewContestFile() {
return; return;
} }
uploadingFile.value = true; // Use separate loading state uploadingFile.value = true;
try { try {
const uploadedFile = await uploadContestFile(dialogData.value.id, newContestFile.value); const uploadedFile = await uploadContestFile(dialogData.value.id, newContestFile.value);
contestFiles.value.push(uploadedFile); contestFiles.value.push(uploadedFile);
@ -701,8 +766,38 @@ async function uploadNewContestFile() {
} }
} }
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) { function confirmDeletePhoto(photoId, type) {
$q.dialog({ $q.dialog({
title: 'Подтверждение удаления', title: 'Подтверждение удаления',
@ -750,7 +845,6 @@ async function deleteExistingPhoto(photoId, type) {
} }
} }
// Подтверждение и удаление файла конкурса
function confirmDeleteContestFile(fileId) { function confirmDeleteContestFile(fileId) {
$q.dialog({ $q.dialog({
title: 'Подтверждение удаления', title: 'Подтверждение удаления',
@ -788,7 +882,6 @@ async function deleteExistingContestFile(fileId) {
} }
} }
// Скачивание файла конкурса
async function downloadExistingContestFile(fileId) { async function downloadExistingContestFile(fileId) {
try { try {
await downloadContestFile(fileId); await downloadContestFile(fileId);
@ -806,6 +899,60 @@ async function downloadExistingContestFile(fileId) {
} }
} }
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) { function openEdit(type, row) {
dialogType.value = type dialogType.value = type
@ -816,13 +963,14 @@ function openEdit(type, row) {
dialogData.value = { title: '', description: '', logo: '', git_url: '' } dialogData.value = { title: '', description: '', logo: '', git_url: '' }
} else if (type === 'projects') { } else if (type === 'projects') {
dialogData.value = { title: '', description: '', repository_url: '' } dialogData.value = { title: '', description: '', repository_url: '' }
projectFiles.value = [];
} else if (type === 'profiles') { } else if (type === 'profiles') {
dialogData.value = { first_name: '', last_name: '', patronymic: '', birthday: '', email: '', phone: '', role_id: null, team_id: null } dialogData.value = { first_name: '', last_name: '', patronymic: '', birthday: '', email: '', phone: '', role_id: null, team_id: null }
profilePhotos.value = []; profilePhotos.value = [];
} else if (type === 'contests') { } else if (type === 'contests') {
dialogData.value = { title: '', description: '', web_url: '', photo: '', results: '', is_win: false, project_id: null, status_id: null } dialogData.value = { title: '', description: '', web_url: '', photo: '', results: '', is_win: false, project_id: null, status_id: null }
contestPhotos.value = []; contestPhotos.value = [];
contestFiles.value = []; // Clear contest files when opening dialog contestFiles.value = [];
} }
} }
dialogVisible.value = true dialogVisible.value = true
@ -831,11 +979,14 @@ function openEdit(type, row) {
loadProfilePhotos(dialogData.value.id); loadProfilePhotos(dialogData.value.id);
} else if (type === 'contests' && dialogData.value.id) { } else if (type === 'contests' && dialogData.value.id) {
loadContestPhotos(dialogData.value.id); loadContestPhotos(dialogData.value.id);
loadContestFiles(dialogData.value.id); // Load contest files loadContestFiles(dialogData.value.id);
} else if (type === 'projects' && dialogData.value.id) {
loadProjectFiles(dialogData.value.id);
} else { } else {
profilePhotos.value = []; profilePhotos.value = [];
contestPhotos.value = []; contestPhotos.value = [];
contestFiles.value = []; // Clear contest files contestFiles.value = [];
projectFiles.value = [];
} }
} }
@ -849,10 +1000,12 @@ function closeDialog() {
newProfilePhotoFile.value = null; newProfilePhotoFile.value = null;
contestPhotos.value = []; contestPhotos.value = [];
newContestPhotoFile.value = null; newContestPhotoFile.value = null;
contestFiles.value = []; // Clear contest files contestFiles.value = [];
newContestFile.value = null; // Clear new contest file input newContestFile.value = null;
projectFiles.value = [];
newProjectFile.value = null;
uploadingPhoto.value = false; uploadingPhoto.value = false;
uploadingFile.value = false; // Reset file uploading state uploadingFile.value = false;
} }
async function saveChanges() { async function saveChanges() {
@ -1037,6 +1190,23 @@ onMounted(() => {
watch(tab, (newTab) => { watch(tab, (newTab) => {
loadData(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> </script>
<style scoped> <style scoped>