сделал шаблон для участников
This commit is contained in:
parent
21bc00ee33
commit
7e6320aa9e
27
WEB/src/api/users/changeUserPassword.js
Normal file
27
WEB/src/api/users/changeUserPassword.js
Normal file
@ -0,0 +1,27 @@
|
||||
import axios from "axios";
|
||||
import CONFIG from "@/core/config.js";
|
||||
|
||||
const changeUserPassword = async (userId, newPasswordData) => {
|
||||
try {
|
||||
const token = localStorage.getItem("access_token");
|
||||
const response = await axios.patch(
|
||||
`${CONFIG.BASE_URL}/users/${userId}/password`,
|
||||
newPasswordData,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (error.response?.status === 400) {
|
||||
throw new Error(error.response.data.detail);
|
||||
} else if (error.response?.status === 403) {
|
||||
throw new Error("Доступ запрещён (403)");
|
||||
}
|
||||
throw new Error(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
export default changeUserPassword;
|
||||
@ -1,18 +0,0 @@
|
||||
import axios from "axios";
|
||||
import CONFIG from "@/core/config.js";
|
||||
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${CONFIG.BASE_URL}/users`, {
|
||||
withCredentials: true,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (error.response?.status === 401) {
|
||||
throw new Error("Нет доступа к пользователям (401)");
|
||||
}
|
||||
throw new Error(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
export default fetchUsers;
|
||||
19
WEB/src/api/users/registerUser.js
Normal file
19
WEB/src/api/users/registerUser.js
Normal file
@ -0,0 +1,19 @@
|
||||
import axios from "axios";
|
||||
import CONFIG from "@/core/config.js";
|
||||
|
||||
const registerUser = async (userData) => {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${CONFIG.BASE_URL}/users/register`,
|
||||
userData
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (error.response?.status === 400) {
|
||||
throw new Error(error.response.data.detail);
|
||||
}
|
||||
throw new Error(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
export default registerUser;
|
||||
@ -143,6 +143,7 @@
|
||||
<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" />
|
||||
<q-toggle v-model="dialogData.is_active" label="Активна" dense class="q-mt-sm" />
|
||||
</template>
|
||||
|
||||
<template v-else-if="dialogType === 'profiles'">
|
||||
@ -469,6 +470,9 @@
|
||||
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { Notify, useQuasar } from 'quasar'
|
||||
|
||||
import registerUser from '@/api/users/registerUser.js'
|
||||
import changePassword from '@/api/users/changeUserPassword.js'
|
||||
|
||||
import fetchTeams from '@/api/teams/getTeams.js'
|
||||
import updateTeam from '@/api/teams/updateTeam.js'
|
||||
import deleteTeamById from '@/api/teams/deleteTeam.js'
|
||||
@ -489,8 +493,6 @@ 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'
|
||||
@ -510,8 +512,9 @@ 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";
|
||||
|
||||
import router from "@/router/index.js";
|
||||
import CONFIG from '@/core/config.js'
|
||||
|
||||
const $q = useQuasar()
|
||||
|
||||
@ -537,6 +540,7 @@ const teamColumns = [
|
||||
{ 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 },
|
||||
{ name: 'is_active', label: 'Активна', field: 'is_active', sortable: true },
|
||||
]
|
||||
|
||||
const projects = ref([])
|
||||
@ -960,7 +964,7 @@ function openEdit(type, row) {
|
||||
dialogData.value = JSON.parse(JSON.stringify(row))
|
||||
} else {
|
||||
if (type === 'teams') {
|
||||
dialogData.value = { title: '', description: '', logo: '', git_url: '' }
|
||||
dialogData.value = { title: '', description: '', logo: '', git_url: '', is_active: '' }
|
||||
} else if (type === 'projects') {
|
||||
dialogData.value = { title: '', description: '', repository_url: '' }
|
||||
projectFiles.value = [];
|
||||
|
||||
@ -80,7 +80,7 @@
|
||||
<div v-if="contest.files && contest.files.length > 0" class="flex justify-center q-mb-xl">
|
||||
<q-card class="violet-card" style="max-width: 940px; width: 100%;">
|
||||
<q-card-section>
|
||||
<div class="text-h6 text-indigo-10 q-mb-md">Файлы</div>
|
||||
<div class="text-h6 text-indigo-10 q-mb-md">Файлы конкурса</div>
|
||||
<q-list separator bordered class="rounded-borders">
|
||||
<q-item v-for="file in contest.files" :key="file.id" clickable v-ripple :href="file.url" target="_blank">
|
||||
<q-item-section avatar>
|
||||
@ -100,45 +100,48 @@
|
||||
</div>
|
||||
|
||||
<div v-if="contestParticipants.length > 0" class="flex justify-center flex-wrap q-gutter-md q-mb-xl">
|
||||
<div class="flex justify-center q-mb-md" style="width: 100%;">
|
||||
<q-card class="contest-name-card"> <q-card-section class="text-h6 text-center text-indigo-10 q-pa-md">
|
||||
Участники конкурса
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<q-card
|
||||
v-for="member in contestParticipants"
|
||||
:key="member.id"
|
||||
class="member-card violet-card"
|
||||
bordered
|
||||
style="width: 180px;"
|
||||
v-ripple
|
||||
>
|
||||
<q-card-section class="q-pa-md flex flex-center">
|
||||
<q-avatar size="64px" class="shadow-6">
|
||||
<img :src="member.avatar" :alt="member.name"/>
|
||||
</q-avatar>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pt-none">
|
||||
<div class="text-subtitle1 text-center text-indigo-11">{{ member.name }}</div>
|
||||
<div class="text-caption text-center text-indigo-9">{{ member.role }}</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<div class="flex justify-center q-mb-md" style="width: 100%;"></div>
|
||||
</div>
|
||||
|
||||
<q-separator class="q-my-lg" color="indigo-4" style="width: 80%; margin: 0 auto;"/>
|
||||
<div class="q-mt-md"></div>
|
||||
|
||||
<div v-if="contest.repository_url" class="flex justify-center q-mb-xl">
|
||||
<div v-if="contest.repository_url || contest.project_description || (contest.project_files && contest.project_files.length > 0)" class="flex justify-center q-mb-xl">
|
||||
<q-card class="violet-card" style="max-width: 940px; width: 100%;">
|
||||
<q-card-section class="q-pa-md">
|
||||
<div class="text-h6 text-indigo-10 q-mb-md">Репозиторий решения</div>
|
||||
<div class="text-body1 text-indigo-9">
|
||||
<div class="text-h6 text-indigo-10 q-mb-md">Информация о проекте</div>
|
||||
|
||||
<div v-if="contest.project_description" class="q-mb-md text-indigo-9">
|
||||
<div class="text-subtitle1 text-indigo-10 q-mb-sm">Описание проекта</div>
|
||||
<div class="text-body1">{{ contest.project_description }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="contest.repository_url" class="q-mb-md text-indigo-9">
|
||||
<div class="text-subtitle1 text-indigo-10 q-mb-sm">Репозиторий проекта</div>
|
||||
<q-icon name="code" size="sm" class="q-mr-xs" />
|
||||
<a :href="contest.repository_url" target="_blank" class="text-indigo-9" style="text-decoration: none; word-break: break-all;">
|
||||
{{ contest.repository_url }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div v-if="contest.project_files && contest.project_files.length > 0" class="q-mt-md">
|
||||
<div class="text-subtitle1 text-indigo-10 q-mb-sm">Файлы проекта</div>
|
||||
<q-list separator bordered class="rounded-borders">
|
||||
<q-item v-for="file in contest.project_files" :key="file.id" clickable v-ripple :href="file.url" target="_blank">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="folder_open" color="indigo-8" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ file.name }}</q-item-label>
|
||||
<q-item-label caption>{{ file.description }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-icon name="download" color="indigo-6" />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
@ -197,6 +200,11 @@ async function fetchContestDetails(id) {
|
||||
description: 'Ежегодный хакатон для стартапов, где команды соревнуются в создании инновационных решений за короткий период времени. Фокус на Web3 и AI технологиях.',
|
||||
web_url: 'https://example.com/hackathon2024',
|
||||
repository_url: 'https://github.com/my-team/hackathon2024-solution',
|
||||
project_description: 'Проект представляет собой децентрализованное приложение для управления задачами, использующее блокчейн для обеспечения прозрачности и искусственный интеллект для автоматического распределения задач между участниками команды.',
|
||||
project_files: [
|
||||
{ id: 101, name: 'Техническое задание.pdf', description: 'Полное описание требований', url: 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf' },
|
||||
{ id: 102, name: 'Архитектура системы.pptx', description: 'Схема взаимодействия модулей', url: 'https://file-examples.com/wp-content/uploads/2017/02/file-example-PPT_10MB.ppt' },
|
||||
],
|
||||
photo: 'https://cdn.quasar.dev/img/parallax2.jpg',
|
||||
results: '1 место в категории "Лучшее AI-решение"',
|
||||
is_win: true,
|
||||
@ -208,7 +216,7 @@ async function fetchContestDetails(id) {
|
||||
{ id: 5, url: 'https://cdn.quasar.dev/img/parallax2.jpg' },
|
||||
],
|
||||
files: [
|
||||
{ id: 1, name: 'Презентация проекта.pdf', description: 'Финальная презентация', url: 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf' },
|
||||
{ id: 1, name: 'Презентация конкурса.pdf', description: 'Финальная презентация', url: 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf' },
|
||||
{ id: 2, name: 'Код проекта.zip', description: 'Исходный код', url: 'https://www.learningcontainer.com/wp-content/uploads/2020/07/Example-Zip-File.zip' },
|
||||
],
|
||||
project_id: 1,
|
||||
@ -220,13 +228,15 @@ async function fetchContestDetails(id) {
|
||||
description: 'Масштабное соревнование по спортивному программированию, где участники решают алгоритмические задачи. Отличная возможность проверить свои навыки.',
|
||||
web_url: 'https://codefest.org',
|
||||
repository_url: 'https://gitlab.com/awesome-devs/codefest-challenge',
|
||||
project_description: null, // No project description for this one
|
||||
project_files: [], // No project files for this one
|
||||
photo: 'https://cdn.quasar.dev/img/material.png',
|
||||
results: null,
|
||||
is_win: false,
|
||||
carousel_photos: [
|
||||
{ id: 10, url: 'https://images.unsplash.com/photo-1584291378203-674a462de8bc?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D' },
|
||||
{ id: 11, url: 'https://cdn.quasar.dev/img/chicken-salad.jpg' },
|
||||
], // Добавлены фото для второго конкурса
|
||||
],
|
||||
files: [],
|
||||
project_id: 2,
|
||||
status_id: 2,
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
<template>
|
||||
<q-page class="home-page bg-violet-strong q-pa-md">
|
||||
|
||||
<!-- Логотип по центру -->
|
||||
<div class="flex justify-center q-mb-md">
|
||||
<q-avatar size="140px" class="team-logo shadow-12">
|
||||
<img :src="teamLogo" alt="Логотип команды"/>
|
||||
</q-avatar>
|
||||
</div>
|
||||
|
||||
<!-- Название команды -->
|
||||
<div class="flex justify-center q-mb-xl">
|
||||
<q-card class="team-name-card">
|
||||
<q-card-section class="text-h4 text-center text-indigo-10 q-pa-md">
|
||||
@ -17,15 +15,15 @@
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<!-- Участники (по центру, без прокрутки) -->
|
||||
<div class="flex justify-center flex-wrap q-gutter-md q-mb-xl">
|
||||
<q-card
|
||||
v-for="member in members"
|
||||
:key="member.id"
|
||||
class="member-card violet-card"
|
||||
bordered
|
||||
style="width: 180px;"
|
||||
style="width: 180px; cursor: pointer;"
|
||||
v-ripple
|
||||
@click="router.push({ name: 'profile-detail', params: { id: member.id } })"
|
||||
>
|
||||
<q-card-section class="q-pa-md flex flex-center">
|
||||
<q-avatar size="64px" class="shadow-6">
|
||||
@ -39,7 +37,6 @@
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<!-- Конкурсы (по центру, без прокрутки) -->
|
||||
<div class="flex justify-center flex-wrap q-gutter-md q-mb-xl">
|
||||
<q-card
|
||||
v-for="contest in contests"
|
||||
@ -55,17 +52,14 @@
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<!-- Разделитель -->
|
||||
<q-separator class="q-my-lg" color="indigo-4" style="width: 80%; margin: 0 auto;"/>
|
||||
<div class="q-mt-md"></div>
|
||||
|
||||
<!-- Активность команды в стиле Gitea -->
|
||||
<div class="flex justify-center">
|
||||
<q-card class="activity-card violet-card" style="max-width: 940px; width: 100%;">
|
||||
<q-card-section class="q-pa-md">
|
||||
<div class="text-h6 text-indigo-10 q-mb-md">Активность команды за последний год</div>
|
||||
|
||||
<!-- Подписи месяцев -->
|
||||
<div class="months-row flex" style="margin-left: 40px; margin-bottom: 4px; user-select: none;">
|
||||
<div
|
||||
v-for="(monthLabel, idx) in monthLabels"
|
||||
@ -78,7 +72,6 @@
|
||||
</div>
|
||||
|
||||
<div class="activity-grid-row row no-wrap">
|
||||
<!-- Дни недели слева -->
|
||||
<div class="weekdays-column column q-pr-sm" style="width: 40px; user-select: none; justify-content: space-around;">
|
||||
<div
|
||||
v-for="(day, idx) in weekDays"
|
||||
@ -90,7 +83,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Сетка дней -->
|
||||
<div class="activity-grid" :style="{ height: (dayHeight * 7) + 'px' }">
|
||||
<div
|
||||
v-for="week in activityGrid"
|
||||
@ -109,7 +101,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Масштаб активности -->
|
||||
<div class="scale-labels row justify-end q-mt-sm text-caption text-indigo-9" style="user-select: none;">
|
||||
<span class="q-mr-md" style="cursor: pointer;" @click="decreaseScale">Меньше</span>
|
||||
<span style="cursor: pointer;" @click="increaseScale">Больше</span>
|
||||
@ -118,7 +109,6 @@
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<!-- Кнопка выхода / авторизации -->
|
||||
<q-btn
|
||||
:icon="isAuthenticated ? 'logout' : 'login'"
|
||||
class="fixed-bottom-right q-ma-md"
|
||||
@ -136,7 +126,6 @@ import {ref, computed, onMounted} from 'vue'
|
||||
import {useRouter} from 'vue-router'
|
||||
import {Ripple, Notify} from 'quasar'
|
||||
import axios from "axios";
|
||||
import {parseStringPromise} from 'xml2js'
|
||||
import CONFIG from "@/core/config.js";
|
||||
|
||||
defineExpose({directives: {ripple: Ripple}})
|
||||
@ -167,7 +156,7 @@ const teamName = ref('Digital Dream Team')
|
||||
|
||||
// --- Участники ---
|
||||
const members = ref([
|
||||
{id: 1, name: 'Иван Иванов', role: 'Team Lead', avatar: 'https://randomuser.me/api/portraits/men/32.jpg'},
|
||||
{id: 1, name: 'Иван Иванов', role: 'Team Lead', avatar: 'https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D'},
|
||||
{id: 2, name: 'Мария Петрова', role: 'Frontend', avatar: 'https://randomuser.me/api/portraits/women/44.jpg'},
|
||||
{id: 3, name: 'Алексей Смирнов', role: 'Backend', avatar: 'https://randomuser.me/api/portraits/men/65.jpg'},
|
||||
{id: 4, name: 'Анна Кузнецова', role: 'Designer', avatar: 'https://randomuser.me/api/portraits/women/56.jpg'},
|
||||
|
||||
574
WEB/src/pages/UserDetailPage.vue
Normal file
574
WEB/src/pages/UserDetailPage.vue
Normal file
@ -0,0 +1,574 @@
|
||||
<template>
|
||||
<q-page class="profile-detail-page bg-violet-strong q-pa-md">
|
||||
|
||||
<q-btn
|
||||
icon="arrow_back"
|
||||
label="Обратно на страницу"
|
||||
flat
|
||||
color="white"
|
||||
@click="router.back()"
|
||||
class="q-mb-lg"
|
||||
/>
|
||||
|
||||
<div v-if="loading" class="flex flex-center q-pt-xl" style="min-height: 50vh;">
|
||||
<q-spinner-dots color="white" size="5em" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="profile" class="q-gutter-y-xl">
|
||||
|
||||
<div class="flex justify-center q-mb-xl">
|
||||
<q-card class="profile-name-card">
|
||||
<q-card-section class="text-h4 text-center text-indigo-10 q-pa-md">
|
||||
{{ profile.first_name }} {{ profile.last_name }} {{ profile.patronymic }}
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-xs-12 col-md-6" v-if="profile.carousel_photos && profile.carousel_photos.length > 0">
|
||||
<q-card class="violet-card carousel-card" style="width: 100%;">
|
||||
<q-card-section>
|
||||
<div class="text-h6 text-indigo-10 q-mb-md">Фотографии профиля</div>
|
||||
<q-carousel
|
||||
v-model="slide"
|
||||
transition-prev="slide-right"
|
||||
transition-next="slide-left"
|
||||
swipeable
|
||||
animated
|
||||
control-color="indigo-10"
|
||||
navigation
|
||||
arrows
|
||||
autoplay
|
||||
infinite
|
||||
class="rounded-borders"
|
||||
height="300px"
|
||||
>
|
||||
<q-carousel-slide v-for="(photo, index) in profile.carousel_photos" :key="index" :name="index + 1" :img-src="photo.url" />
|
||||
</q-carousel>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-md-6">
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12">
|
||||
<q-card class="info-card violet-card">
|
||||
<q-card-section>
|
||||
<div class="text-h6 text-indigo-10 q-mb-md">Основная информация</div>
|
||||
<div class="text-body1 text-indigo-9 q-mb-sm">
|
||||
<q-icon name="cake" size="xs" class="q-mr-xs" />
|
||||
День рождения: <span class="text-weight-bold">{{ profile.birth_date }}</span>
|
||||
</div>
|
||||
<div class="text-body1 text-indigo-9">
|
||||
<q-icon name="person" size="xs" class="q-mr-xs" />
|
||||
Пол: <span class="text-weight-bold">{{ profile.gender }}</span>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<q-card class="info-card violet-card">
|
||||
<q-card-section>
|
||||
<div class="text-h6 text-indigo-10 q-mb-md">Контакты</div>
|
||||
<div class="text-body1 text-indigo-9 q-mb-sm">
|
||||
<q-icon name="email" size="xs" class="q-mr-xs" />
|
||||
Email: <a :href="'mailto:' + profile.email" class="text-indigo-9" style="text-decoration: none;">{{ profile.email }}</a>
|
||||
</div>
|
||||
<div v-if="profile.phone_number" class="text-body1 text-indigo-9">
|
||||
<q-icon name="phone" size="xs" class="q-mr-xs" />
|
||||
Телефон: <a :href="'tel:' + profile.phone_number" class="text-indigo-9" style="text-decoration: none;">{{ profile.phone_number }}</a>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-col-gutter-md justify-center q-mt-xl">
|
||||
<div class="col-xs-12 col-md-6" v-if="profile.teams && profile.teams.length > 0">
|
||||
<q-card class="violet-card">
|
||||
<q-card-section>
|
||||
<div class="text-h6 text-indigo-10 q-mb-md">Команды</div>
|
||||
<q-list separator bordered class="rounded-borders">
|
||||
<q-item v-for="team in profile.teams" :key="team.id" clickable v-ripple @click="goToTeamDetail(team.id)">
|
||||
<q-item-section avatar>
|
||||
<q-avatar size="md">
|
||||
<img :src="team.logo || 'https://cdn.quasar.dev/logo-v2/svg/logo.svg'" alt="Логотип команды"/>
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ team.name }}</q-item-label>
|
||||
<q-item-label caption>{{ team.role }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-icon name="chevron_right" color="indigo-6" />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-md-6" v-if="profile.projects && profile.projects.length > 0">
|
||||
<q-card class="violet-card">
|
||||
<q-card-section>
|
||||
<div class="text-h6 text-indigo-10 q-mb-md">Участие в проектах</div>
|
||||
<q-list separator bordered class="rounded-borders">
|
||||
<q-item v-for="project in profile.projects" :key="project.id" clickable v-ripple @click="goToProjectDetail(project.id)">
|
||||
<q-item-section avatar>
|
||||
<q-avatar size="md">
|
||||
<img :src="project.photo || 'https://cdn.quasar.dev/logo-v2/svg/logo.svg'" alt="Логотип проекта"/>
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ project.title }}</q-item-label>
|
||||
<q-item-label caption>{{ project.role }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-icon name="chevron_right" color="indigo-6" />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-separator class="q-my-lg" color="indigo-4" style="width: 80%; margin: 0 auto;"/>
|
||||
<div class="q-mt-md"></div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<q-card class="activity-card violet-card" style="max-width: 940px; width: 100%;">
|
||||
<q-card-section class="q-pa-md">
|
||||
<div class="text-h6 text-indigo-10 q-mb-md">Активность за последний год</div>
|
||||
|
||||
<div class="months-row flex" style="margin-left: 40px; margin-bottom: 4px; user-select: none;">
|
||||
<div
|
||||
v-for="(monthLabel, idx) in monthLabels"
|
||||
:key="monthLabel"
|
||||
:style="{ marginLeft: getMonthMargin(idx) + 'px' }"
|
||||
class="month-label text-caption text-indigo-9"
|
||||
>
|
||||
{{ monthLabel }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="activity-grid-row row no-wrap">
|
||||
<div class="weekdays-column column q-pr-sm" style="width: 40px; user-select: none; justify-content: space-around;">
|
||||
<div
|
||||
v-for="(day, idx) in weekDays"
|
||||
:key="day"
|
||||
class="weekday-label text-caption text-indigo-9"
|
||||
:style="{ height: dayHeight + 'px', lineHeight: dayHeight + 'px' }"
|
||||
>
|
||||
{{ day }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="activity-grid" :style="{ height: (dayHeight * 7) + 'px' }">
|
||||
<div
|
||||
v-for="week in activityGrid"
|
||||
:key="week[0]?.date"
|
||||
class="activity-week"
|
||||
:style="{ width: (squareSize + 4) + 'px' }"
|
||||
>
|
||||
<div
|
||||
v-for="day in week"
|
||||
:key="day.date"
|
||||
class="activity-square"
|
||||
:title="`Дата: ${day.date}, активность: ${day.count}`"
|
||||
:style="{ backgroundColor: getActivityColor(day.count), width: squareSize + 'px', height: squareSize + 'px', margin: '2px 0' }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scale-labels row justify-end q-mt-sm text-caption text-indigo-9" style="user-select: none;">
|
||||
<span class="q-mr-md" style="cursor: pointer;" @click="decreaseScale">Меньше</span>
|
||||
<span style="cursor: pointer;" @click="increaseScale">Больше</span>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-center q-pt-xl text-white text-h5" style="min-height: 50vh;">
|
||||
Профиль не найден :(
|
||||
</div>
|
||||
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { Ripple, Notify } from 'quasar';
|
||||
import axios from 'axios';
|
||||
import CONFIG from "@/core/config.js";
|
||||
|
||||
defineExpose({ directives: { ripple: Ripple } });
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const profile = ref(null);
|
||||
const loading = ref(true);
|
||||
const profileId = computed(() => route.params.id);
|
||||
|
||||
const slide = ref(1);
|
||||
|
||||
// --- Активность ---
|
||||
const activityData = ref([]);
|
||||
const dayHeight = 14;
|
||||
const squareSize = ref(12);
|
||||
|
||||
// Подписи месяцев (с июня 2024 по май 2025, чтобы соответствовать текущему году)
|
||||
const monthLabels = ['июн.', 'июл.', 'авг.', 'сент.', 'окт.', 'нояб.', 'дек.', 'янв.', 'февр.', 'март', 'апр.', 'май'];
|
||||
|
||||
// Дни недели (пн, ср, пт, как в Gitea)
|
||||
const weekDays = ['пн', 'ср', 'пт'];
|
||||
|
||||
// Вычисляемая сетка активности (группировка по неделям)
|
||||
const activityGrid = computed(() => {
|
||||
const weeks = [];
|
||||
let week = [];
|
||||
const firstDay = new Date();
|
||||
firstDay.setDate(firstDay.getDate() - 364); // Год назад от текущей даты
|
||||
const dayOfWeek = firstDay.getDay();
|
||||
const offset = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // Смещение для выравнивания по понедельнику
|
||||
|
||||
// Добавляем пустые ячейки в начало
|
||||
for (let i = 0; i < offset; i++) {
|
||||
week.push({ date: '', count: 0 });
|
||||
}
|
||||
|
||||
for (let i = 0; i < 365; i++) {
|
||||
const date = new Date(firstDay);
|
||||
date.setDate(firstDay.getDate() + i);
|
||||
const dateStr = date.toISOString().slice(0, 10);
|
||||
const dayData = activityData.value.find(d => d.date === dateStr) || { date: dateStr, count: 0 };
|
||||
|
||||
week.push(dayData);
|
||||
if (week.length === 7 || i === 364) {
|
||||
weeks.push(week);
|
||||
week = [];
|
||||
}
|
||||
}
|
||||
return weeks;
|
||||
});
|
||||
|
||||
// Цвета активности (как в Gitea)
|
||||
function getActivityColor(count) {
|
||||
if (count === 0) return '#ede9fe'; // Светлый фон карточек
|
||||
if (count <= 2) return '#d8cff9'; // Светлый сиреневый
|
||||
if (count <= 4) return '#a287ff'; // Светлый фиолетовый
|
||||
if (count <= 6) return '#7c3aed'; // Яркий фиолетовый
|
||||
return '#4f046f'; // Темно-фиолетовый
|
||||
}
|
||||
|
||||
// Позиционирование подписей месяцев
|
||||
function getMonthMargin(idx) {
|
||||
const daysInMonth = [30, 31, 31, 30, 31, 30, 31, 31, 28, 31, 30, 31]; // Дни в месяцах с июня 2024
|
||||
const daysBeforeMonth = daysInMonth.slice(0, idx).reduce((sum, days) => sum + days, 0);
|
||||
const weekIndex = Math.floor(daysBeforeMonth / 7);
|
||||
return weekIndex * (squareSize.value + 4); // 4 = margin (2px + 2px)
|
||||
}
|
||||
|
||||
// Загрузка активности из API
|
||||
const usernameForActivity = 'archibald'; // Фиксированный username для активности
|
||||
|
||||
async function loadActivity() {
|
||||
try {
|
||||
const response = await axios.get(`${CONFIG.BASE_URL}/rss/${usernameForActivity}/`);
|
||||
const fetchedData = response.data.map(item => ({
|
||||
date: item.date,
|
||||
count: parseInt(item.count, 10) || 0
|
||||
}));
|
||||
const dataMap = new Map(fetchedData.map(d => [d.date, d.count]));
|
||||
const lastDate = new Date();
|
||||
const startDate = new Date(lastDate);
|
||||
startDate.setDate(lastDate.getDate() - 364);
|
||||
|
||||
activityData.value = [];
|
||||
for (let i = 0; i < 365; i++) {
|
||||
const date = new Date(startDate);
|
||||
date.setDate(startDate.getDate() + i);
|
||||
const dateStr = date.toISOString().slice(0, 10);
|
||||
const count = dataMap.get(dateStr) || 0;
|
||||
activityData.value.push({date: dateStr, count});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки активности:', error);
|
||||
Notify.create({
|
||||
type: 'negative',
|
||||
message: 'Ошибка загрузки данных активности',
|
||||
icon: 'error',
|
||||
});
|
||||
|
||||
// Заполняем пустыми данными в случае ошибки
|
||||
const lastDate = new Date();
|
||||
const startDate = new Date(lastDate);
|
||||
startDate.setDate(lastDate.getDate() - 364);
|
||||
activityData.value = [];
|
||||
for (let i = 0; i < 365; i++) {
|
||||
const date = new Date(startDate);
|
||||
date.setDate(startDate.getDate() + i);
|
||||
const dateStr = date.toISOString().slice(0, 10);
|
||||
activityData.value.push({date: dateStr, count: 0});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Масштабирование
|
||||
function increaseScale() {
|
||||
if (squareSize.value < 24) squareSize.value += 2;
|
||||
}
|
||||
|
||||
function decreaseScale() {
|
||||
if (squareSize.value > 8) squareSize.value -= 2;
|
||||
}
|
||||
|
||||
async function fetchProfileDetails(id) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const mockProfiles = [
|
||||
{
|
||||
id: 1,
|
||||
first_name: 'Иван',
|
||||
last_name: 'Иванов',
|
||||
patronymic: 'Иванович',
|
||||
birth_date: '10.01.1990',
|
||||
gender: 'Мужской',
|
||||
email: 'ivan.ivanov@example.com',
|
||||
phone_number: '+79011234567',
|
||||
main_photo: 'https://randomuser.me/api/portraits/men/32.jpg',
|
||||
carousel_photos: [
|
||||
{ id: 1, url: 'https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D' },
|
||||
{ id: 2, url: 'https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D' },
|
||||
{ id: 3, url: 'https://images.unsplash.com/photo-1522075469751-3a6694fa2a86?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D' },
|
||||
],
|
||||
teams: [
|
||||
{ id: 101, name: 'Digital Dream Team', role: 'Team Lead', logo: 'https://cdn.quasar.dev/logo-v2/svg/logo.svg' },
|
||||
{ id: 102, name: 'Growth Hackers', role: 'Mentor', logo: 'https://cdn.quasar.dev/logo-v2/svg/logo.svg' },
|
||||
],
|
||||
projects: [
|
||||
{ id: 201, title: 'CRM System Dev', role: 'Backend Lead', photo: 'https://cdn.quasar.dev/img/material.png' },
|
||||
{ id: 202, title: 'Mobile App Redesign', role: 'Consultant', photo: 'https://cdn.quasar.dev/img/donuts.png' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
first_name: 'Мария',
|
||||
last_name: 'Петрова',
|
||||
patronymic: 'Александровна',
|
||||
birth_date: '22.03.1993',
|
||||
gender: 'Женский',
|
||||
email: 'maria.petrova@example.com',
|
||||
phone_number: '+79209876543',
|
||||
main_photo: 'https://randomuser.me/api/portraits/women/44.jpg',
|
||||
carousel_photos: [
|
||||
{ id: 4, url: 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D' },
|
||||
{ id: 5, url: 'https://images.unsplash.com/photo-1580489944761-15ad79929219?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D' },
|
||||
],
|
||||
teams: [
|
||||
{ id: 101, name: 'Digital Dream Team', role: 'Frontend', logo: 'https://cdn.quasar.dev/logo-v2/svg/logo.svg' },
|
||||
],
|
||||
projects: [
|
||||
{ id: 204, title: 'Marketing Website', role: 'Lead Frontend', photo: 'https://cdn.quasar.dev/img/parallax2.jpg' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
first_name: 'Алексей',
|
||||
last_name: 'Смирнов',
|
||||
patronymic: 'Сергеевич',
|
||||
birth_date: '05.09.1988',
|
||||
gender: 'Мужской',
|
||||
email: 'alex.smirnov@example.com',
|
||||
phone_number: null,
|
||||
main_photo: 'https://randomuser.me/api/portraits/men/65.jpg',
|
||||
carousel_photos: [],
|
||||
teams: [
|
||||
{ id: 101, name: 'Digital Dream Team', role: 'Backend', logo: 'https://cdn.quasar.dev/logo-v2/svg/logo.svg' },
|
||||
],
|
||||
projects: [
|
||||
{ id: 201, title: 'CRM System Dev', role: 'Backend Dev', photo: 'https://cdn.quasar.dev/img/material.png' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
first_name: 'Анна',
|
||||
last_name: 'Кузнецова',
|
||||
patronymic: 'Викторовна',
|
||||
birth_date: '30.11.1996',
|
||||
gender: 'Женский',
|
||||
email: 'anna.kuznetsova@example.com',
|
||||
phone_number: '+79151112233',
|
||||
main_photo: 'https://randomuser.me/api/portraits/women/56.jpg',
|
||||
carousel_photos: [
|
||||
{ id: 6, url: 'https://images.unsplash.com/photo-1522075469751-3a6694fa2a86?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D' },
|
||||
],
|
||||
teams: [
|
||||
{ id: 104, name: 'Digital Dream Team', role: 'Designer', logo: 'https://cdn.quasar.dev/logo-v2/svg/logo.svg' },
|
||||
],
|
||||
projects: [
|
||||
{ id: 205, title: 'Brand Identity', role: 'Lead Designer', photo: 'https://cdn.quasar.dev/img/parallax1.jpg' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
first_name: 'Дмитрий',
|
||||
last_name: 'Орлов',
|
||||
patronymic: 'Васильевич',
|
||||
birth_date: '18.07.1991',
|
||||
gender: 'Мужской',
|
||||
email: 'dmitry.orlov@example.com',
|
||||
phone_number: '+79304445566',
|
||||
main_photo: 'https://randomuser.me/api/portraits/men/78.jpg',
|
||||
carousel_photos: [],
|
||||
teams: [
|
||||
{ id: 105, name: 'Digital Dream Team', role: 'QA Engineer', logo: 'https://cdn.quasar.dev/logo-v2/svg/logo.svg' },
|
||||
],
|
||||
projects: [
|
||||
{ id: 206, title: 'Testing Automation Framework', role: 'QA Lead', photo: 'https://cdn.quasar.dev/img/quasar.jpg' },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
profile.value = mockProfiles.find(p => p.id === parseInt(id));
|
||||
|
||||
if (!profile.value) {
|
||||
Notify.create({
|
||||
type: 'negative',
|
||||
message: 'Профиль с таким ID не найден.',
|
||||
icon: 'warning',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки деталей профиля:', error);
|
||||
Notify.create({
|
||||
type: 'negative',
|
||||
message: 'Не удалось загрузить информацию о профиле.',
|
||||
icon: 'error',
|
||||
});
|
||||
profile.value = null;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function goToTeamDetail(teamId) {
|
||||
console.log(`Переход на страницу команды: ${teamId}`);
|
||||
// router.push({ name: 'team-detail', params: { id: teamId } });
|
||||
}
|
||||
|
||||
function goToProjectDetail(projectId) {
|
||||
console.log(`Переход на страницу проекта: ${projectId}`);
|
||||
// router.push({ name: 'project-detail', params: { id: projectId } });
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchProfileDetails(profileId.value);
|
||||
await loadActivity(); // Загрузка активности при монтировании компонента
|
||||
});
|
||||
|
||||
watch(profileId, async (newId) => {
|
||||
if (newId) {
|
||||
await fetchProfileDetails(newId);
|
||||
await loadActivity(); // Обновление активности при изменении ID профиля
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.profile-detail-page {
|
||||
background: linear-gradient(135deg, #4f046f 0%, #7c3aed 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.profile-name-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);
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.carousel-card {
|
||||
/* No specific width/height here, handled by Quasar's carousel and grid system */
|
||||
}
|
||||
|
||||
/* Optional: Adjust for smaller screens if needed */
|
||||
@media (max-width: 991px) { /* Breakpoint for md in Quasar grid */
|
||||
.carousel-card {
|
||||
margin-bottom: 24px; /* Добавить отступ, когда колонки становятся в ряд */
|
||||
}
|
||||
}
|
||||
|
||||
/* Стили для активности (перенесены из HomePage) */
|
||||
.activity-grid-row {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.weekdays-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
height: calc(14px * 7 + 12px); /* dayHeight * 7 + (2px margin * 6 days) */
|
||||
}
|
||||
|
||||
.activity-grid {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 8px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #a287ff #ede9fe;
|
||||
}
|
||||
|
||||
.activity-grid::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.activity-grid::-webkit-scrollbar-track {
|
||||
background: #ede9fe;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.activity-grid::-webkit-scrollbar-thumb {
|
||||
background-color: #a287ff;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #ede9fe;
|
||||
}
|
||||
|
||||
.activity-week {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.activity-square {
|
||||
border-radius: 3px;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.months-row {
|
||||
position: relative;
|
||||
margin-bottom: 24px !important; /* Увеличил отступ, чтобы месяцы не налезали на сетку */
|
||||
}
|
||||
|
||||
.month-label {
|
||||
position: absolute;
|
||||
top: -20px; /* Поднимаем метки месяцев над сеткой */
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@ -2,16 +2,22 @@ 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 ContestDetailPage from "@/pages/ContestDetailPage.vue"
|
||||
import ProfileDetailPage from "@/pages/UserDetailPage.vue";
|
||||
|
||||
const routes = [
|
||||
{ path: '/', component: HomePage },
|
||||
{ path: '/login', component: LoginPage },
|
||||
{ path: '/admin', component: AdminPage },
|
||||
{
|
||||
path: '/contests/:id', // Динамический маршрут, :id будет ID конкурса
|
||||
name: 'contest-detail', // Имя маршрута для удобства навигации (например, router.push({ name: 'contest-detail', params: { id: 123 } }))
|
||||
component: ContestDetailPage // Компонент, который будет отображаться
|
||||
path: '/contests/:id',
|
||||
name: 'contest-detail',
|
||||
component: ContestDetailPage
|
||||
},
|
||||
{
|
||||
path: '/profile/:id',
|
||||
name: 'profile-detail',
|
||||
component: ProfileDetailPage
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user