Merge branch 'main' of https://git.numerum.team/archibald/business-card-site
This commit is contained in:
commit
673f20188e
@ -1,6 +1,6 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.database.session import get_db
|
from app.database.session import get_db
|
||||||
@ -55,3 +55,22 @@ async def delete_profile(
|
|||||||
):
|
):
|
||||||
profiles_service = ProfilesService(db)
|
profiles_service = ProfilesService(db)
|
||||||
return await profiles_service.delete(profile_id, user)
|
return await profiles_service.delete(profile_id, user)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
'/user/{user_id}/',
|
||||||
|
response_model=Optional[ProfileEntity],
|
||||||
|
summary='Get profile by user ID',
|
||||||
|
description='Retrieve profile data by user ID',
|
||||||
|
)
|
||||||
|
async def get_profile_by_user_id(
|
||||||
|
user_id: int,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
profiles_service = ProfilesService(db)
|
||||||
|
profile = await profiles_service.get_by_user_id(user_id)
|
||||||
|
|
||||||
|
return profile
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -102,6 +102,17 @@ class ProfilesService:
|
|||||||
|
|
||||||
return self.model_to_entity(result)
|
return self.model_to_entity(result)
|
||||||
|
|
||||||
|
async def get_by_user_id(self, user_id: int) -> Optional[ProfileEntity]:
|
||||||
|
user = await self.users_repository.get_by_id(user_id)
|
||||||
|
if user is None:
|
||||||
|
raise HTTPException(status_code=404, detail='User not found')
|
||||||
|
|
||||||
|
profile_model = await self.profiles_repository.get_by_id(user.profile_id)
|
||||||
|
if not profile_model:
|
||||||
|
raise HTTPException(status_code=404, detail='Profile not found')
|
||||||
|
|
||||||
|
return self.model_to_entity(profile_model)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def model_to_entity(profile_model: Profile) -> ProfileEntity:
|
def model_to_entity(profile_model: Profile) -> ProfileEntity:
|
||||||
return ProfileEntity(
|
return ProfileEntity(
|
||||||
|
|||||||
@ -33,8 +33,8 @@ class TeamsService:
|
|||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Team not found")
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Team not found")
|
||||||
|
|
||||||
team_model.title = team.title
|
team_model.title = team.title
|
||||||
team_model.description = team_model.description
|
team_model.description = team.description
|
||||||
team_model.git_url = team_model.git_url
|
team_model.git_url = team.git_url
|
||||||
|
|
||||||
await self.teams_repository.update(team_model)
|
await self.teams_repository.update(team_model)
|
||||||
|
|
||||||
|
|||||||
@ -6,10 +6,13 @@ const loginUser = async (loginData) => {
|
|||||||
const response = await axios.post(`${CONFIG.BASE_URL}/auth/`, loginData, {
|
const response = await axios.post(`${CONFIG.BASE_URL}/auth/`, loginData, {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
return response.data.access_token;
|
|
||||||
|
const { access_token, user_id } = response.data;
|
||||||
|
|
||||||
|
return { access_token, user_id };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
throw new Error("Неверное имя пользователя или пароль")
|
throw new Error("Неверное имя пользователя или пароль");
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(error.message);
|
throw new Error(error.message);
|
||||||
|
|||||||
18
WEB/src/api/contests/getContests.js
Normal file
18
WEB/src/api/contests/getContests.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import CONFIG from "@/core/config.js";
|
||||||
|
|
||||||
|
const fetchContests = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${CONFIG.BASE_URL}/contests`, {
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
throw new Error("Нет доступа к конкурсам (401)");
|
||||||
|
}
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default fetchContests;
|
||||||
17
WEB/src/api/profiles/getUserProfile.js
Normal file
17
WEB/src/api/profiles/getUserProfile.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import CONFIG from '../../core/config.js'
|
||||||
|
|
||||||
|
const getUserProfile = async (user_id, token) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${CONFIG.BASE_URL}/profiles/${user_id}/`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.response?.data?.detail || 'Ошибка получения профиля')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getUserProfile
|
||||||
18
WEB/src/api/projects/getProjects.js
Normal file
18
WEB/src/api/projects/getProjects.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import CONFIG from "@/core/config.js";
|
||||||
|
|
||||||
|
const fetchProjects = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${CONFIG.BASE_URL}/projects`, {
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
throw new Error("Нет доступа к проектам (401)");
|
||||||
|
}
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default fetchProjects;
|
||||||
24
WEB/src/api/teams/getTeams.js
Normal file
24
WEB/src/api/teams/getTeams.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import CONFIG from "@/core/config.js";
|
||||||
|
|
||||||
|
const fetchTeams = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("access_token");
|
||||||
|
const response = await axios.get(`${CONFIG.BASE_URL}/teams`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
throw new Error("Нет доступа к командам (401)");
|
||||||
|
} else if (error.response?.status === 403) {
|
||||||
|
throw new Error("Доступ запрещён (403)");
|
||||||
|
}
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default fetchTeams;
|
||||||
17
WEB/src/api/teams/updateTeam.js
Normal file
17
WEB/src/api/teams/updateTeam.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import CONFIG from '@/core/config.js'
|
||||||
|
|
||||||
|
const updateTeam = async (team) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.put(
|
||||||
|
`${CONFIG.BASE_URL}/teams/${team.id}`,
|
||||||
|
team,
|
||||||
|
{ withCredentials: true }
|
||||||
|
)
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.response?.data?.detail || error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default updateTeam
|
||||||
18
WEB/src/api/users/getUsers.js
Normal file
18
WEB/src/api/users/getUsers.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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;
|
||||||
@ -1,43 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
msg: String,
|
|
||||||
})
|
|
||||||
|
|
||||||
const count = ref(0)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<h1>{{ msg }}</h1>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<button type="button" @click="count++">count is {{ count }}</button>
|
|
||||||
<p>
|
|
||||||
Edit
|
|
||||||
<code>components/HelloWorld.vue</code> to test HMR
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Check out
|
|
||||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
|
||||||
>create-vue</a
|
|
||||||
>, the official Vue + Vite starter
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Learn more about IDE Support for Vue in the
|
|
||||||
<a
|
|
||||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
|
||||||
target="_blank"
|
|
||||||
>Vue Docs Scaling up Guide</a
|
|
||||||
>.
|
|
||||||
</p>
|
|
||||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.read-the-docs {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,8 +1,33 @@
|
|||||||
import { createApp } from 'vue'
|
import {createApp} from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import { Quasar, Notify, Dialog, Loading, QInput, QBtn, QForm, QCard, QCardSection } from 'quasar'
|
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Quasar,
|
||||||
|
Notify,
|
||||||
|
Dialog,
|
||||||
|
Loading,
|
||||||
|
QInput,
|
||||||
|
QBtn,
|
||||||
|
QForm,
|
||||||
|
QCard,
|
||||||
|
QCardSection,
|
||||||
|
QLayout,
|
||||||
|
QPageContainer,
|
||||||
|
QPage,
|
||||||
|
QTabs,
|
||||||
|
QTab,
|
||||||
|
QTabPanels,
|
||||||
|
QTabPanel,
|
||||||
|
QHeader,
|
||||||
|
QTable,
|
||||||
|
QSeparator,
|
||||||
|
QCardActions,
|
||||||
|
QDialog,
|
||||||
|
QIcon
|
||||||
|
} from 'quasar'
|
||||||
|
|
||||||
|
|
||||||
import '@quasar/extras/material-icons/material-icons.css'
|
import '@quasar/extras/material-icons/material-icons.css'
|
||||||
|
|
||||||
|
|
||||||
@ -11,8 +36,13 @@ import 'quasar/src/css/index.sass'
|
|||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
app.use(Quasar, {
|
app.use(Quasar, {
|
||||||
plugins: { Notify, Dialog, Loading }, // уведомления, диалоги, загрузка
|
plugins: {Notify, Dialog, Loading},
|
||||||
components: { QInput, QBtn, QForm, QCard, QCardSection } // обязательно указать используемые компоненты
|
components: {
|
||||||
|
QInput, QBtn, QForm, QCard, QCardSection,
|
||||||
|
QLayout, QPageContainer, QPage,
|
||||||
|
QTabs, QTab, QTabPanels, QTabPanel, QHeader,QTable,
|
||||||
|
QSeparator, QCardActions, QDialog, QIcon
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|||||||
271
WEB/src/pages/AdminPage.vue
Normal file
271
WEB/src/pages/AdminPage.vue
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
<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="users" 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="users">
|
||||||
|
<div class="violet-card q-pa-md">
|
||||||
|
<q-table
|
||||||
|
title="Пользователи"
|
||||||
|
:rows="users"
|
||||||
|
:columns="userColumns"
|
||||||
|
row-key="id"
|
||||||
|
@row-click="openEdit('users', $event)"
|
||||||
|
:loading="loadingUsers"
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<!-- Команды -->
|
||||||
|
<q-tab-panel name="teams">
|
||||||
|
<div class="violet-card q-pa-md">
|
||||||
|
<q-table
|
||||||
|
title="Команды"
|
||||||
|
:rows="teams"
|
||||||
|
:columns="teamColumns"
|
||||||
|
row-key="id"
|
||||||
|
@row-click="openEdit('teams', $event)"
|
||||||
|
:loading="loadingTeams"
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<!-- Проекты -->
|
||||||
|
<q-tab-panel name="projects">
|
||||||
|
<div class="violet-card q-pa-md">
|
||||||
|
<q-table
|
||||||
|
title="Проекты"
|
||||||
|
:rows="projects"
|
||||||
|
:columns="projectColumns"
|
||||||
|
row-key="id"
|
||||||
|
@row-click="openEdit('projects', $event)"
|
||||||
|
:loading="loadingProjects"
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<!-- Конкурсы -->
|
||||||
|
<q-tab-panel name="contests">
|
||||||
|
<div class="violet-card q-pa-md">
|
||||||
|
<q-table
|
||||||
|
title="Конкурсы"
|
||||||
|
:rows="contests"
|
||||||
|
:columns="contestColumns"
|
||||||
|
row-key="id"
|
||||||
|
@row-click="openEdit('contests', $event)"
|
||||||
|
: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">Редактирование {{ dialogType }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-separator />
|
||||||
|
|
||||||
|
<q-card-section>
|
||||||
|
<pre>{{ dialogData }}</pre>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn flat label="Закрыть" color="primary" @click="closeDialog" />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</q-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch, onMounted } from 'vue'
|
||||||
|
|
||||||
|
// Импорты API
|
||||||
|
import fetchTeams from '@/api/teams/getTeams.js'
|
||||||
|
import fetchUsers from '@/api/users/getUsers.js'
|
||||||
|
import fetchProjects from '@/api/projects/getProjects.js'
|
||||||
|
import fetchContests from '@/api/contests/getContests.js'
|
||||||
|
import updateTeam from '@/api/teams/updateTeam.js'
|
||||||
|
|
||||||
|
// Активная вкладка
|
||||||
|
const tab = ref('users')
|
||||||
|
|
||||||
|
// Данные
|
||||||
|
const users = ref([])
|
||||||
|
const teams = ref([])
|
||||||
|
const projects = ref([])
|
||||||
|
const contests = ref([])
|
||||||
|
|
||||||
|
// Загрузка
|
||||||
|
const loadingUsers = ref(false)
|
||||||
|
const loadingTeams = ref(false)
|
||||||
|
const loadingProjects = ref(false)
|
||||||
|
const loadingContests = ref(false)
|
||||||
|
|
||||||
|
// Колонки
|
||||||
|
const userColumns = [
|
||||||
|
{ name: 'id', label: 'ID', field: 'id', sortable: true },
|
||||||
|
{ name: 'name', label: 'Имя', field: 'name', sortable: true },
|
||||||
|
{ name: 'email', label: 'Email', field: 'email', sortable: true },
|
||||||
|
]
|
||||||
|
|
||||||
|
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 projectColumns = [
|
||||||
|
{ name: 'id', label: 'ID', field: 'id', sortable: true },
|
||||||
|
{ name: 'projectName', label: 'Проект', field: 'projectName', sortable: true },
|
||||||
|
]
|
||||||
|
|
||||||
|
const contestColumns = [
|
||||||
|
{ name: 'id', label: 'ID', field: 'id', sortable: true },
|
||||||
|
{ name: 'contestName', label: 'Конкурс', field: 'contestName', sortable: true },
|
||||||
|
]
|
||||||
|
|
||||||
|
// Модалка
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogData = ref({})
|
||||||
|
const dialogType = ref('')
|
||||||
|
|
||||||
|
// Функция открытия редактирования
|
||||||
|
function openEdit(type, row) {
|
||||||
|
dialogType.value = type
|
||||||
|
// Клонируем объект, чтобы не менять данные в таблице до сохранения
|
||||||
|
dialogData.value = JSON.parse(JSON.stringify(row))
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция сохранения изменений
|
||||||
|
async function saveChanges() {
|
||||||
|
if (dialogType.value === 'teams') {
|
||||||
|
try {
|
||||||
|
await updateTeam(dialogData.value)
|
||||||
|
// Обновляем локальные данные команды
|
||||||
|
const index = teams.value.findIndex(t => t.id === dialogData.value.id)
|
||||||
|
if (index !== -1) {
|
||||||
|
teams.value[index] = JSON.parse(JSON.stringify(dialogData.value))
|
||||||
|
}
|
||||||
|
closeDialog()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при сохранении:', error.message)
|
||||||
|
// Можно добавить уведомление об ошибке
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Закрыть модалку
|
||||||
|
function closeDialog() {
|
||||||
|
dialogVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загрузка данных
|
||||||
|
async function loadData(name) {
|
||||||
|
switch (name) {
|
||||||
|
case 'users':
|
||||||
|
loadingUsers.value = true
|
||||||
|
try {
|
||||||
|
users.value = await fetchUsers() || []
|
||||||
|
} catch (error) {
|
||||||
|
users.value = []
|
||||||
|
console.error(error.message)
|
||||||
|
} finally {
|
||||||
|
loadingUsers.value = false
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'teams':
|
||||||
|
loadingTeams.value = true
|
||||||
|
try {
|
||||||
|
teams.value = await fetchTeams() || []
|
||||||
|
} catch (error) {
|
||||||
|
teams.value = []
|
||||||
|
console.error(error.message)
|
||||||
|
} finally {
|
||||||
|
loadingTeams.value = false
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'projects':
|
||||||
|
loadingProjects.value = true
|
||||||
|
try {
|
||||||
|
projects.value = await fetchProjects() || []
|
||||||
|
} catch (error) {
|
||||||
|
projects.value = []
|
||||||
|
console.error(error.message)
|
||||||
|
} finally {
|
||||||
|
loadingProjects.value = false
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'contests':
|
||||||
|
loadingContests.value = true
|
||||||
|
try {
|
||||||
|
contests.value = await fetchContests() || []
|
||||||
|
} catch (error) {
|
||||||
|
contests.value = []
|
||||||
|
console.error(error.message)
|
||||||
|
} finally {
|
||||||
|
loadingContests.value = false
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Начальная загрузка
|
||||||
|
onMounted(() => loadData(tab.value))
|
||||||
|
|
||||||
|
// Загрузка при смене вкладки
|
||||||
|
watch(tab, (newTab) => {
|
||||||
|
loadData(newTab)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.bg-violet-strong {
|
||||||
|
background: linear-gradient(135deg, #4f046f 0%, #7c3aed 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -2,7 +2,11 @@
|
|||||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { Notify } from 'quasar'
|
import { Notify } from 'quasar'
|
||||||
import loginUser from "../api/auth/loginRequest.js"
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import CONFIG from '../core/config.js'
|
||||||
|
import loginUser from '../api/auth/loginRequest.js'
|
||||||
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const login = ref('')
|
const login = ref('')
|
||||||
@ -11,14 +15,12 @@ const loading = ref(false)
|
|||||||
|
|
||||||
const marqueeTrack = ref(null)
|
const marqueeTrack = ref(null)
|
||||||
const marqueeText = ref(null)
|
const marqueeText = ref(null)
|
||||||
const animationDuration = ref(7) // секунд
|
const animationDuration = ref(7)
|
||||||
|
|
||||||
// Динамически рассчитываем длительность анимации в зависимости от ширины текста
|
|
||||||
const updateAnimation = () => {
|
const updateAnimation = () => {
|
||||||
if (marqueeTrack.value && marqueeText.value) {
|
if (marqueeTrack.value && marqueeText.value) {
|
||||||
const textWidth = marqueeText.value.offsetWidth
|
const textWidth = marqueeText.value.offsetWidth
|
||||||
const containerWidth = marqueeTrack.value.offsetWidth / 2
|
const containerWidth = marqueeTrack.value.offsetWidth / 2
|
||||||
// Скорость: 100px/sec (можно подстроить)
|
|
||||||
const speed = 100
|
const speed = 100
|
||||||
animationDuration.value = (textWidth + containerWidth) / speed
|
animationDuration.value = (textWidth + containerWidth) / speed
|
||||||
marqueeTrack.value.style.setProperty('--marquee-width', `${textWidth}px`)
|
marqueeTrack.value.style.setProperty('--marquee-width', `${textWidth}px`)
|
||||||
@ -27,7 +29,7 @@ const updateAnimation = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setTimeout(updateAnimation, 100) // Дать DOM отрисоваться
|
setTimeout(updateAnimation, 100)
|
||||||
window.addEventListener('resize', updateAnimation)
|
window.addEventListener('resize', updateAnimation)
|
||||||
})
|
})
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@ -37,12 +39,21 @@ onBeforeUnmount(() => {
|
|||||||
const authorisation = async () => {
|
const authorisation = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const accessToken = await loginUser({
|
const { access_token, user_id } = await loginUser({
|
||||||
login: login.value,
|
login: login.value,
|
||||||
password: password.value
|
password: password.value
|
||||||
})
|
})
|
||||||
|
|
||||||
localStorage.setItem('access_token', accessToken)
|
localStorage.setItem('access_token', access_token)
|
||||||
|
localStorage.setItem('user_id', user_id)
|
||||||
|
|
||||||
|
const profileResponse = await axios.get(`${CONFIG.BASE_URL}/profiles/user/${user_id}/`, {
|
||||||
|
headers: { Authorization: `Bearer ${access_token}` }
|
||||||
|
})
|
||||||
|
|
||||||
|
const roleId = profileResponse.data.role_id
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Notify.create({
|
Notify.create({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
@ -50,7 +61,21 @@ const authorisation = async () => {
|
|||||||
icon: 'check_circle'
|
icon: 'check_circle'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log('Role ID:', roleId)
|
||||||
|
if (roleId === 1) {
|
||||||
|
console.log('Переход на /admin')
|
||||||
|
router.push('/admin')
|
||||||
|
} else {
|
||||||
|
console.log('Переход на /')
|
||||||
router.push('/')
|
router.push('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (roleId === 1) {
|
||||||
|
router.push('/admin')
|
||||||
|
} else {
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Notify.create({
|
Notify.create({
|
||||||
type: 'negative',
|
type: 'negative',
|
||||||
@ -63,6 +88,7 @@ const authorisation = async () => {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="fullscreen flex flex-center bg-violet-strong">
|
<div class="fullscreen flex flex-center bg-violet-strong">
|
||||||
<q-card class="q-pa-xl shadow-violet card-animate violet-card" style="width: 370px; max-width: 92vw;">
|
<q-card class="q-pa-xl shadow-violet card-animate violet-card" style="width: 370px; max-width: 92vw;">
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
import {createRouter, createWebHistory} from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import LoginPage from "../pages/LoginPage.vue"
|
import LoginPage from "../pages/LoginPage.vue"
|
||||||
import HomePage from "../pages/HomePage.vue"
|
import HomePage from "../pages/HomePage.vue"
|
||||||
|
import AdminPage from "../pages/AdminPage.vue"
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{ path: '/', component: HomePage },
|
||||||
path: '/',
|
{ path: '/login', component: LoginPage },
|
||||||
component: HomePage
|
{ path: '/admin', component: AdminPage }
|
||||||
},
|
|
||||||
{path: '/login', component: LoginPage}
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@ -17,9 +16,16 @@ const router = createRouter({
|
|||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
const isAuthenticated = !!localStorage.getItem('access_token')
|
const isAuthenticated = !!localStorage.getItem('access_token')
|
||||||
|
const userId = localStorage.getItem('user_id')
|
||||||
|
|
||||||
if (to.path === '/login' && isAuthenticated) {
|
if (to.path === '/login' && isAuthenticated) {
|
||||||
next('/') // теперь редирект на главную страницу
|
next('/')
|
||||||
|
} else if (to.path === '/admin') {
|
||||||
|
if (isAuthenticated && userId === '1') {
|
||||||
|
next()
|
||||||
|
} else {
|
||||||
|
next('/')
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user