From f5ab4f8fe01351d4af69a08bfb5bac3f0799cc56 Mon Sep 17 00:00:00 2001 From: Arigii Date: Wed, 11 Jun 2025 04:19:17 +0500 Subject: [PATCH] =?UTF-8?q?=D0=B2=D1=81=D0=B5=20=D0=B3=D0=BE=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...0008_добавил_url_репозитория_у_профилей.py | 32 + API/app/domain/entities/base_profile.py | 3 +- API/app/domain/models/contests.py | 4 +- API/app/domain/models/profiles.py | 7 +- API/app/domain/models/projects.py | 2 +- API/app/infrastructure/profiles_service.py | 3 + API/app/main.py | 8 +- .../deleteContestPhoto.js | 2 +- .../downloadContestPhotoFile.js | 2 +- .../uploadContestProfilePhoto.js | 2 +- .../contest_files/downloadContestFile.js | 78 +- WEB/src/api/contests/updateContest.js | 7 - .../profile_photos/downloadPhotoFile.js | 2 +- .../profile_photos/getProfileByUserId.js | 33 + .../profile_photos/uploadProfilePhoto.js | 2 +- WEB/src/api/projects/getProjects.js | 2 +- .../project_files/downloadProjectFile.js | 71 +- WEB/src/main.js | 5 +- WEB/src/pages/AdminPage.vue | 1179 ++++++++++------- WEB/src/pages/ContestDetailPage.vue | 409 ++++-- WEB/src/pages/HomePage.vue | 481 +++++-- WEB/src/pages/ProjectDetailPage.vue | 498 +++++++ WEB/src/pages/TeamDetailPage.vue | 707 ++++++++++ WEB/src/pages/UserDetailPage.vue | 585 ++++---- WEB/src/router/index.js | 96 +- 25 files changed, 3223 insertions(+), 997 deletions(-) create mode 100644 API/app/database/migrations/versions/7c4a804d9c4b_0008_добавил_url_репозитория_у_профилей.py create mode 100644 WEB/src/api/profiles/profile_photos/getProfileByUserId.js create mode 100644 WEB/src/pages/ProjectDetailPage.vue create mode 100644 WEB/src/pages/TeamDetailPage.vue diff --git a/API/app/database/migrations/versions/7c4a804d9c4b_0008_добавил_url_репозитория_у_профилей.py b/API/app/database/migrations/versions/7c4a804d9c4b_0008_добавил_url_репозитория_у_профилей.py new file mode 100644 index 0000000..f0d071f --- /dev/null +++ b/API/app/database/migrations/versions/7c4a804d9c4b_0008_добавил_url_репозитория_у_профилей.py @@ -0,0 +1,32 @@ +"""0008_добавил_url_репозитория_у_профилей + +Revision ID: 7c4a804d9c4b +Revises: b6c6c906cd2b +Create Date: 2025-06-11 00:03:04.123410 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '7c4a804d9c4b' +down_revision: Union[str, None] = 'b6c6c906cd2b' +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('profiles', sa.Column('repository_url', sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('profiles', 'repository_url') + # ### end Alembic commands ### diff --git a/API/app/domain/entities/base_profile.py b/API/app/domain/entities/base_profile.py index 8a4953a..b5fe3c9 100644 --- a/API/app/domain/entities/base_profile.py +++ b/API/app/domain/entities/base_profile.py @@ -11,9 +11,10 @@ class BaseProfileEntity(BaseModel): birthday: datetime.date email: Optional[str] = None phone: Optional[str] = None + repository_url: Optional[str] = None role_id: int team_id: int class Config: - abstract = True + abstract = True \ No newline at end of file diff --git a/API/app/domain/models/contests.py b/API/app/domain/models/contests.py index 78375f7..27cd998 100644 --- a/API/app/domain/models/contests.py +++ b/API/app/domain/models/contests.py @@ -20,5 +20,5 @@ class Contest(AdvancedBaseModel): project = relationship('Project', back_populates='contests') status = relationship('ContestStatus', back_populates='contests') - carousel_photos = relationship('ContestCarouselPhoto', back_populates='contest') - files = relationship('ContestFile', back_populates='contest') + carousel_photos = relationship('ContestCarouselPhoto', back_populates='contest', cascade='all') + files = relationship('ContestFile', back_populates='contest', cascade='all') diff --git a/API/app/domain/models/profiles.py b/API/app/domain/models/profiles.py index 4a33b20..eedb200 100644 --- a/API/app/domain/models/profiles.py +++ b/API/app/domain/models/profiles.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, VARCHAR, Date, ForeignKey, Integer +from sqlalchemy import Column, VARCHAR, Date, ForeignKey, Integer, String from sqlalchemy.orm import relationship from app.domain.models.base import AdvancedBaseModel @@ -13,6 +13,7 @@ class Profile(AdvancedBaseModel): birthday = Column(Date, nullable=False) email = Column(VARCHAR(150)) phone = Column(VARCHAR(28)) + repository_url = Column(String, nullable=True) role_id = Column(Integer, ForeignKey('roles.id'), nullable=False) team_id = Column(Integer, ForeignKey('teams.id'), nullable=False) @@ -21,5 +22,5 @@ class Profile(AdvancedBaseModel): team = relationship('Team', back_populates='profiles') user = relationship('User', back_populates='profile', cascade='all') - profile_photos = relationship('ProfilePhoto', back_populates='profile') - projects = relationship('ProjectMember', back_populates='profile') + profile_photos = relationship('ProfilePhoto', back_populates='profile', cascade='all') + projects = relationship('ProjectMember', back_populates='profile') \ No newline at end of file diff --git a/API/app/domain/models/projects.py b/API/app/domain/models/projects.py index 96f35ff..5fee521 100644 --- a/API/app/domain/models/projects.py +++ b/API/app/domain/models/projects.py @@ -12,5 +12,5 @@ class Project(AdvancedBaseModel): repository_url = Column(String, nullable=False) contests = relationship("Contest", back_populates="project") - files = relationship("ProjectFile", back_populates="project") + files = relationship("ProjectFile", back_populates="project", cascade="all") members = relationship("ProjectMember", back_populates="project") diff --git a/API/app/infrastructure/profiles_service.py b/API/app/infrastructure/profiles_service.py index f7b7c04..0545ff3 100644 --- a/API/app/infrastructure/profiles_service.py +++ b/API/app/infrastructure/profiles_service.py @@ -83,6 +83,7 @@ class ProfilesService: profile_model.birthday = profile.birthday profile_model.email = profile.email profile_model.phone = profile.phone + profile_model.repository_url = profile.repository_url profile_model.role_id = profile.role_id profile_model.team_id = profile.team_id @@ -130,6 +131,7 @@ class ProfilesService: birthday=profile_model.birthday, email=profile_model.email, phone=profile_model.phone, + repository_url=profile_model.repository_url, role_id=profile_model.role_id, team_id=profile_model.team_id, ) @@ -143,6 +145,7 @@ class ProfilesService: birthday=profile_entity.birthday, email=profile_entity.email, phone=profile_entity.phone, + repository_url=profile_entity.repository_url, role_id=profile_entity.role_id, team_id=profile_entity.team_id, ) diff --git a/API/app/main.py b/API/app/main.py index f241ec6..042b030 100644 --- a/API/app/main.py +++ b/API/app/main.py @@ -21,7 +21,12 @@ def start_app(): api_app.add_middleware( CORSMiddleware, - allow_origins=["https://api.numerum.team", "https://numerum.team", "http://localhost:5173"], + allow_origins=[ + "https://api.numerum.team", + "https://numerum.team", + "http://localhost:5173", # Это уже есть + "http://127.0.0.1:5173" # <-- Добавьте эту строку + ], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], @@ -47,7 +52,6 @@ def start_app(): app = start_app() - @app.get("/") async def root(): return {"message": "Hello API"} diff --git a/WEB/src/api/contests/contest_carousel_photos/deleteContestPhoto.js b/WEB/src/api/contests/contest_carousel_photos/deleteContestPhoto.js index 5437d99..ad35638 100644 --- a/WEB/src/api/contests/contest_carousel_photos/deleteContestPhoto.js +++ b/WEB/src/api/contests/contest_carousel_photos/deleteContestPhoto.js @@ -7,7 +7,7 @@ const deleteContestCarouselPhoto = async (photoId) => { const token = localStorage.getItem('access_token') const response = await axios.delete( - `${CONFIG.BASE_URL}/contest_carousel_photos/${photoId}/`, // Изменено здесь + `${CONFIG.BASE_URL}/contest_carousel_photos/${photoId}/`, { withCredentials: true, headers: { diff --git a/WEB/src/api/contests/contest_carousel_photos/downloadContestPhotoFile.js b/WEB/src/api/contests/contest_carousel_photos/downloadContestPhotoFile.js index 0d3babc..ca3349f 100644 --- a/WEB/src/api/contests/contest_carousel_photos/downloadContestPhotoFile.js +++ b/WEB/src/api/contests/contest_carousel_photos/downloadContestPhotoFile.js @@ -7,7 +7,7 @@ const downloadContestCarouselPhotoFile = async (photoId) => { const token = localStorage.getItem('access_token') const response = await axios.get( - `${CONFIG.BASE_URL}/contest_carousel_photos/${photoId}/file`, + `${CONFIG.BASE_URL}/contest_carousel_photos/${photoId}/file/`, { withCredentials: true, headers: { diff --git a/WEB/src/api/contests/contest_carousel_photos/uploadContestProfilePhoto.js b/WEB/src/api/contests/contest_carousel_photos/uploadContestProfilePhoto.js index b202ae3..2b7c418 100644 --- a/WEB/src/api/contests/contest_carousel_photos/uploadContestProfilePhoto.js +++ b/WEB/src/api/contests/contest_carousel_photos/uploadContestProfilePhoto.js @@ -12,7 +12,7 @@ const uploadContestCarouselPhoto = async (contestId, file) => { formData.append('contest_id', contestId); const response = await axios.post( - `${CONFIG.BASE_URL}/contest_carousel_photos/contests/${contestId}/upload`, + `${CONFIG.BASE_URL}/contest_carousel_photos/contests/${contestId}/upload/`, formData, { withCredentials: true, diff --git a/WEB/src/api/contests/contest_files/downloadContestFile.js b/WEB/src/api/contests/contest_files/downloadContestFile.js index 16363ca..8dc7c90 100644 --- a/WEB/src/api/contests/contest_files/downloadContestFile.js +++ b/WEB/src/api/contests/contest_files/downloadContestFile.js @@ -1,38 +1,82 @@ import axios from "axios"; import CONFIG from "@/core/config.js"; -const downloadContestFile = async (fileId) => { +// Добавляем параметры suggestedFileName и suggestedFileFormat для большей надежности +const downloadContestFile = async (fileId, suggestedFileName, suggestedFileFormat) => { 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 response = await axios.get( + `${CONFIG.BASE_URL}/contest_files/${fileId}/file`, // Убедитесь, что это правильный URL для скачивания самого файла + { + responseType: 'blob', // Важно для бинарных данных + withCredentials: true, // Важно для аутентификации + } + ); + + // Получаем MIME-тип файла из заголовков ответа + const contentType = response.headers['content-type'] || 'application/octet-stream'; + // Создаем Blob с правильным MIME-типом + const blob = new Blob([response.data], { type: contentType }); + const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; + + let filename = `contest_file_${fileId}`; // Запасное имя файла + + // 1. Попытка получить имя файла из заголовка Content-Disposition const contentDisposition = response.headers['content-disposition']; - let filename = 'download'; if (contentDisposition) { - const filenameMatch = contentDisposition.match(/filename="(.+)"/); - if (filenameMatch && filenameMatch[1]) { - filename = filenameMatch[1]; + // Расширенное регулярное выражение для извлечения filename или filename* (с поддержкой UTF-8) + const filenameMatch = contentDisposition.match(/filename\*=(?:UTF-8'')?([^;]+)|filename="([^"]+)"/i); + if (filenameMatch) { + if (filenameMatch[1]) { // filename* (RFC 5987) + try { + filename = decodeURIComponent(filenameMatch[1]); + } catch (e) { + console.warn("Ошибка декодирования filename* из Content-Disposition:", e); + filename = filenameMatch[1]; // Используем как есть, если декодирование не удалось + } + } else if (filenameMatch[2]) { // Обычный filename + filename = filenameMatch[2]; + } } } + + // 2. Если имя файла всё ещё не идеально или не содержит расширения, + // используем переданные suggestedFileName и suggestedFileFormat как запасной вариант + if (!filename || filename === `contest_file_${fileId}` || !filename.includes('.')) { + let finalName = suggestedFileName || `contest_file_${fileId}`; + if (suggestedFileFormat && !finalName.toLowerCase().endsWith(`.${suggestedFileFormat.toLowerCase()}`)) { + finalName = `${finalName}.${suggestedFileFormat.toLowerCase()}`; + } + filename = finalName; + } + link.setAttribute('download', filename); document.body.appendChild(link); link.click(); link.remove(); - window.URL.revokeObjectURL(url); + window.URL.revokeObjectURL(url); // Освобождаем память Blob URL return filename; } catch (error) { - if (error.response?.status === 401) { - throw new Error("Нет доступа для скачивания файла (401)"); + // Улучшенная обработка ошибок для более информативных сообщений + if (error.response) { + if (error.response.status === 401) { + throw new Error("Недостаточно прав для скачивания файла конкурса (401). Пожалуйста, авторизуйтесь."); + } + if (error.response.status === 404) { + throw new Error(`Файл конкурса с ID ${fileId} не найден (404). Возможно, он был удалён.`); + } + // Попробуем получить сообщение об ошибке из данных ответа, если это JSON + if (error.response.data && typeof error.response.data === 'object' && error.response.data.detail) { + throw new Error(`Ошибка сервера при скачивании файла конкурса: ${error.response.data.detail}`); + } + throw new Error(`Ошибка сети или сервера при скачивании файла конкурса: Статус ${error.response.status}`); + } else if (error.request) { + throw new Error("Не удалось отправить запрос на скачивание файла конкурса. Проверьте ваше интернет-соединение."); + } else { + throw new Error(`Неизвестная ошибка при скачивании файла конкурса: ${error.message}`); } - if (error.response?.status === 404) { - throw new Error("Файл не найден (404)"); - } - throw new Error(error.message); } }; diff --git a/WEB/src/api/contests/updateContest.js b/WEB/src/api/contests/updateContest.js index 57ef747..45e67c5 100644 --- a/WEB/src/api/contests/updateContest.js +++ b/WEB/src/api/contests/updateContest.js @@ -4,12 +4,7 @@ import CONFIG from '@/core/config.js' const updateContest = async (profile) => { try { const token = localStorage.getItem('access_token') - - // Убираем id из тела запроса, он идет в URL const { id, ...profileData } = profile - - console.log('Отправляем на сервер:', profileData) - const response = await axios.put( `${CONFIG.BASE_URL}/contests/${id}/`, profileData, @@ -20,8 +15,6 @@ const updateContest = async (profile) => { } } ) - - console.log('Ответ от сервера:', response.data) return response.data } catch (error) { throw new Error(error.response?.data?.detail || error.message) diff --git a/WEB/src/api/profiles/profile_photos/downloadPhotoFile.js b/WEB/src/api/profiles/profile_photos/downloadPhotoFile.js index c06b06e..60ed6d2 100644 --- a/WEB/src/api/profiles/profile_photos/downloadPhotoFile.js +++ b/WEB/src/api/profiles/profile_photos/downloadPhotoFile.js @@ -7,7 +7,7 @@ const downloadPhotoFile = async (photoId) => { const token = localStorage.getItem('access_token') const response = await axios.get( - `${CONFIG.BASE_URL}/profile_photos/${photoId}/file`, + `${CONFIG.BASE_URL}/profile_photos/${photoId}/file/`, { withCredentials: true, headers: { diff --git a/WEB/src/api/profiles/profile_photos/getProfileByUserId.js b/WEB/src/api/profiles/profile_photos/getProfileByUserId.js new file mode 100644 index 0000000..50653e0 --- /dev/null +++ b/WEB/src/api/profiles/profile_photos/getProfileByUserId.js @@ -0,0 +1,33 @@ +import axios from 'axios'; +import CONFIG from '@/core/config.js'; + +const getProfileByUserId = async (profileId) => { + try { + const token = localStorage.getItem('access_token'); + if (!profileId) { + throw new Error('ID профиля не указан.'); + } + const response = await axios.get( + `${CONFIG.BASE_URL}/profiles/user/${profileId}/`, + { + withCredentials: true, + headers: { + Authorization: `Bearer ${token}` + } + } + ); + return response.data; // Возвращаем данные профиля + } catch (error) { + const errorMessage = error.response?.data?.detail || error.message; + console.error(`Ошибка получения профиля ${profileId}:`, errorMessage); + + if (error.response && error.response.status === 404) { + throw new Error(`Профиль с ID ${profileId} не найден.`); + } else { + throw new Error(`Не удалось загрузить профиль: ${errorMessage}`); + } + } +}; + + +export default getProfileByUserId; \ No newline at end of file diff --git a/WEB/src/api/profiles/profile_photos/uploadProfilePhoto.js b/WEB/src/api/profiles/profile_photos/uploadProfilePhoto.js index c07ec4d..b7d6738 100644 --- a/WEB/src/api/profiles/profile_photos/uploadProfilePhoto.js +++ b/WEB/src/api/profiles/profile_photos/uploadProfilePhoto.js @@ -10,7 +10,7 @@ const uploadProfilePhoto = async (profileId, file) => { formData.append('file', file) const response = await axios.post( - `${CONFIG.BASE_URL}/profile_photos/profiles/${profileId}/upload`, + `${CONFIG.BASE_URL}/profile_photos/profiles/${profileId}/upload/`, formData, { withCredentials: true, diff --git a/WEB/src/api/projects/getProjects.js b/WEB/src/api/projects/getProjects.js index bb4f226..3ee5197 100644 --- a/WEB/src/api/projects/getProjects.js +++ b/WEB/src/api/projects/getProjects.js @@ -4,7 +4,7 @@ import CONFIG from "@/core/config.js"; const fetchProjects = async () => { try { const token = localStorage.getItem("access_token"); - const response = await axios.get(`${CONFIG.BASE_URL}/projects`, { + const response = await axios.get(`${CONFIG.BASE_URL}/projects/`, { headers: { Authorization: `Bearer ${token}/`, }, diff --git a/WEB/src/api/projects/project_files/downloadProjectFile.js b/WEB/src/api/projects/project_files/downloadProjectFile.js index a988ee9..839921d 100644 --- a/WEB/src/api/projects/project_files/downloadProjectFile.js +++ b/WEB/src/api/projects/project_files/downloadProjectFile.js @@ -1,43 +1,82 @@ import axios from 'axios'; import CONFIG from '@/core/config.js'; -const downloadProjectFile = async (fileId) => { +// Добавляем параметры suggestedFileName и suggestedFileFormat +const downloadProjectFile = async (fileId, suggestedFileName, suggestedFileFormat) => { try { const response = await axios.get( - `${CONFIG.BASE_URL}/project_files/${fileId}/download/`, + `${CONFIG.BASE_URL}/project_files/${fileId}/file/`, // Убедитесь, что это правильный URL для скачивания самого файла { - responseType: 'blob', - withCredentials: true, + responseType: 'blob', // Важно для бинарных данных + withCredentials: true, // Важно для аутентификации } ); - const url = window.URL.createObjectURL(new Blob([response.data])); + // Получаем MIME-тип файла из заголовков ответа + const contentType = response.headers['content-type'] || 'application/octet-stream'; + // Создаем Blob с правильным MIME-типом + const blob = new Blob([response.data], { type: contentType }); + const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; + let filename = `project_file_${fileId}`; // Запасное имя файла + + // 1. Попытка получить имя файла из заголовка Content-Disposition 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]); + // Расширенное регулярное выражение для извлечения filename или filename* (с поддержкой UTF-8) + const filenameMatch = contentDisposition.match(/filename\*=(?:UTF-8'')?([^;]+)|filename="([^"]+)"/i); + if (filenameMatch) { + if (filenameMatch[1]) { // filename* (RFC 5987) + try { + filename = decodeURIComponent(filenameMatch[1]); + } catch (e) { + console.warn("Ошибка декодирования filename* из Content-Disposition:", e); + filename = filenameMatch[1]; // Используем как есть, если декодирование не удалось + } + } else if (filenameMatch[2]) { // Обычный filename + filename = filenameMatch[2]; + } } } + + // 2. Если имя файла всё ещё не идеально или не содержит расширения, + // используем переданные suggestedFileName и suggestedFileFormat как запасной вариант + if (!filename || filename === `project_file_${fileId}` || !filename.includes('.')) { + let finalName = suggestedFileName || `project_file_${fileId}`; + if (suggestedFileFormat && !finalName.toLowerCase().endsWith(`.${suggestedFileFormat.toLowerCase()}`)) { + finalName = `${finalName}.${suggestedFileFormat.toLowerCase()}`; + } + filename = finalName; + } + link.setAttribute('download', filename); document.body.appendChild(link); link.click(); link.remove(); - window.URL.revokeObjectURL(url); + window.URL.revokeObjectURL(url); // Освобождаем память Blob URL return filename; } catch (error) { - if (error.response?.status === 401) { - throw new Error("Недостаточно прав для скачивания файла (401)"); + // Улучшенная обработка ошибок для более информативных сообщений + if (error.response) { + if (error.response.status === 401) { + throw new Error("Недостаточно прав для скачивания файла (401). Пожалуйста, авторизуйтесь."); + } + if (error.response.status === 404) { + throw new Error(`Файл с ID ${fileId} не найден (404). Возможно, он был удалён.`); + } + // Попробуем получить сообщение об ошибке из данных ответа, если это JSON + if (error.response.data && typeof error.response.data === 'object' && error.response.data.detail) { + throw new Error(`Ошибка сервера при скачивании файла: ${error.response.data.detail}`); + } + throw new Error(`Ошибка сети или сервера при скачивании файла: Статус ${error.response.status}`); + } else if (error.request) { + throw new Error("Не удалось отправить запрос на скачивание файла. Проверьте ваше интернет-соединение."); + } else { + throw new Error(`Неизвестная ошибка при скачивании файла: ${error.message}`); } - if (error.response?.status === 404) { - throw new Error("Файл не найден (404)"); - } - throw new Error(error.message); } }; diff --git a/WEB/src/main.js b/WEB/src/main.js index 2924eda..8edc1eb 100644 --- a/WEB/src/main.js +++ b/WEB/src/main.js @@ -41,7 +41,8 @@ import { QItem, QImg, QFile, - QSelect + QSelect, + QSpinner } from 'quasar' @@ -61,7 +62,7 @@ app.use(Quasar, { QSeparator, QCardActions, QDialog, QIcon, QSpace, QAvatar, QTooltip, QBanner, QSlideTransition, QToggle, QList, QSpinnerDots, QCarouselSlide, QCarousel, - QItemSection, QItemLabel, QItem, QImg, QFile, QSelect + QItemSection, QItemLabel, QItem, QImg, QFile, QSelect, QSpinner }, directives: { Ripple diff --git a/WEB/src/pages/AdminPage.vue b/WEB/src/pages/AdminPage.vue index c24d64c..b64f879 100644 --- a/WEB/src/pages/AdminPage.vue +++ b/WEB/src/pages/AdminPage.vue @@ -11,11 +11,11 @@ animated @update:model-value="loadData" > - - - - - + + + + + @@ -168,13 +168,13 @@ - + \ No newline at end of file diff --git a/WEB/src/pages/ProjectDetailPage.vue b/WEB/src/pages/ProjectDetailPage.vue new file mode 100644 index 0000000..1a30155 --- /dev/null +++ b/WEB/src/pages/ProjectDetailPage.vue @@ -0,0 +1,498 @@ + + + + + \ No newline at end of file diff --git a/WEB/src/pages/TeamDetailPage.vue b/WEB/src/pages/TeamDetailPage.vue new file mode 100644 index 0000000..95f10ac --- /dev/null +++ b/WEB/src/pages/TeamDetailPage.vue @@ -0,0 +1,707 @@ + + + + + \ No newline at end of file diff --git a/WEB/src/pages/UserDetailPage.vue b/WEB/src/pages/UserDetailPage.vue index ade1913..b88da06 100644 --- a/WEB/src/pages/UserDetailPage.vue +++ b/WEB/src/pages/UserDetailPage.vue @@ -57,27 +57,21 @@
Основная информация
- День рождения: {{ profile.birth_date }} + День рождения: {{ formattedBirthday }}
-
- - Пол: {{ profile.gender }} -
-
- - - -
- - -
Контакты
+ -
+
- Телефон: {{ profile.phone_number }} + Телефон: {{ profile.phone }} +
+ +
+ + Репозиторий: {{ displayRepositoryUrl }}
@@ -87,20 +81,20 @@
-
+
-
Команды
+
Команда
- + - Логотип команды + Логотип команды - {{ team.name }} - {{ team.role }} + {{ profile.team.name }} + {{ profile.role_name }} @@ -111,17 +105,12 @@
-
+
Участие в проектах
- - - Логотип проекта - - {{ project.title }} {{ project.role }} @@ -136,15 +125,15 @@
-
+
-
Активность за последний год
+
Активность команды за последний год
-
+
-
+
-
@@ -203,12 +191,21 @@ \ No newline at end of file diff --git a/WEB/src/router/index.js b/WEB/src/router/index.js index 2d7e2d4..cee8368 100644 --- a/WEB/src/router/index.js +++ b/WEB/src/router/index.js @@ -1,14 +1,29 @@ -import { createRouter, createWebHistory } from 'vue-router' -import LoginPage from "../pages/LoginPage.vue" -import HomePage from "../pages/HomePage.vue" -import AdminPage from "../pages/AdminPage.vue" -import ContestDetailPage from "@/pages/ContestDetailPage.vue" +import { createRouter, createWebHistory } from 'vue-router'; +import LoginPage from "../pages/LoginPage.vue"; +import HomePage from "../pages/HomePage.vue"; +import AdminPage from "../pages/AdminPage.vue"; +import ContestDetailPage from "@/pages/ContestDetailPage.vue"; import ProfileDetailPage from "@/pages/UserDetailPage.vue"; +import TeamDetailPage from "@/pages/TeamDetailPage.vue"; +import ProjectDetailPage from "@/pages/ProjectDetailPage.vue"; // <-- Добавляем импорт ProjectDetailPage const routes = [ - { path: '/', component: HomePage }, - { path: '/login', component: LoginPage }, - { path: '/admin', component: AdminPage }, + { + path: '/', + name: 'home', // Добавим имя для главной страницы + component: HomePage + }, + { + path: '/login', + name: 'login', // Добавим имя для страницы логина + component: LoginPage + }, + { + path: '/admin', + name: 'admin', // Добавим имя для страницы админа + component: AdminPage, + meta: { requiresAuth: true, requiresAdmin: true } // Страница требует аутентификации и прав админа + }, { path: '/contests/:id', name: 'contest-detail', @@ -17,30 +32,61 @@ const routes = [ { path: '/profile/:id', name: 'profile-detail', - component: ProfileDetailPage + component: ProfileDetailPage, + }, + { + path: '/teams/:id', + name: 'team-detail', // Даем маршруту имя + component: TeamDetailPage, + + }, + { // <-- Новый маршрут для страницы проекта + path: '/projects/:id', + name: 'project-detail', // Даем маршруту имя + component: ProjectDetailPage, + } -] +]; const router = createRouter({ history: createWebHistory(), routes -}) +}); router.beforeEach((to, from, next) => { - const isAuthenticated = !!localStorage.getItem('access_token') - const userId = localStorage.getItem('user_id') + const isAuthenticated = !!localStorage.getItem('access_token'); + const userId = localStorage.getItem('user_id'); // userId из localStorage всегда будет строкой - if (to.path === '/login' && isAuthenticated) { - next('/') - } else if (to.path === '/admin') { - if (isAuthenticated && userId === '1') { - next() - } else { - next('/') - } - } else { - next() + const requiresAuth = to.matched.some(record => record.meta.requiresAuth); + const requiresAdmin = to.matched.some(record => record.meta.requiresAdmin); + + // Если пользователь уже аутентифицирован и пытается перейти на страницу логина + if (to.name === 'login' && isAuthenticated) { + // Заменяем текущую запись в истории, чтобы нельзя было вернуться назад на логин + next({ name: 'home', replace: true }); } -}) + // Если маршрут требует аутентификации + else if (requiresAuth && !isAuthenticated) { + // Перенаправляем на страницу логина. + // Передаем fullPath в query для возможного редиректа после успешного логина. + // Используем replace: true, чтобы пользователь не мог нажать "назад" и попасть на защищенную страницу. + next({ name: 'login', query: { redirectFrom: to.fullPath }, replace: true }); + } + // Если маршрут требует прав администратора + else if (requiresAdmin) { + // Проверяем, аутентифицирован ли пользователь И является ли он админом + if (isAuthenticated && userId === '1') { // Предполагаем, что admin имеет userId '1' + next(); // Разрешаем доступ + } else { + // Если не админ или не аутентифицирован, перенаправляем на главную + // и заменяем текущую запись в истории + next({ name: 'home', replace: true }); + } + } + // Для всех остальных случаев (общедоступные страницы или аутентифицированные пользователи на разрешенных страницах) + else { + next(); // Просто продолжаем навигацию + } +}); -export default router \ No newline at end of file +export default router; \ No newline at end of file