Compare commits

...

3 Commits

Author SHA1 Message Date
58da850137 Merge remote-tracking branch 'gitea/main' 2025-05-31 12:24:40 +05:00
5b1e74876d сделал проекты 2025-05-31 12:23:44 +05:00
ac42a8da7f сделал вкладку команд 2025-05-31 11:41:28 +05:00
14 changed files with 534 additions and 118 deletions

View File

@ -0,0 +1,23 @@
import axios from 'axios'
import CONFIG from '@/core/config.js'
const createProject = async (project) => {
try {
const token = localStorage.getItem('access_token') // или другой способ получения токена
const response = await axios.post(
`${CONFIG.BASE_URL}/projects`,
project,
{
withCredentials: true,
headers: {
Authorization: `Bearer ${token}`
}
}
)
return response.data
} catch (error) {
throw new Error(error.response?.data?.detail || error.message)
}
}
export default createProject

View File

@ -0,0 +1,22 @@
import axios from 'axios'
import CONFIG from '@/core/config.js'
const deleteProject = async (projectId) => {
try {
const token = localStorage.getItem('access_token') // получение токена
const response = await axios.delete(
`${CONFIG.BASE_URL}/projects/${projectId}`,
{
withCredentials: true,
headers: {
Authorization: `Bearer ${token}`
}
}
)
return response.data
} catch (error) {
throw new Error(error.response?.data?.detail || error.message)
}
}
export default deleteProject

View File

@ -3,13 +3,19 @@ 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`, {
withCredentials: true,
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);
}

View File

@ -0,0 +1,31 @@
import axios from 'axios'
import CONFIG from '@/core/config.js'
const updateProject = async (project) => {
try {
const token = localStorage.getItem('access_token')
// Убираем id из тела запроса, он идет в URL
const { id, ...projectData } = project
console.log('Отправляем на сервер:', projectData)
const response = await axios.put(
`${CONFIG.BASE_URL}/projects/${id}`,
projectData,
{
withCredentials: true,
headers: {
Authorization: `Bearer ${token}`
}
}
)
console.log('Ответ от сервера:', response.data)
return response.data
} catch (error) {
throw new Error(error.response?.data?.detail || error.message)
}
}
export default updateProject

View File

@ -0,0 +1,23 @@
import axios from 'axios'
import CONFIG from '@/core/config.js'
const createTeam = async (team) => {
try {
const token = localStorage.getItem('access_token') // или другой способ
const response = await axios.post(
`${CONFIG.BASE_URL}/teams`,
team,
{
withCredentials: true,
headers: {
Authorization: `Bearer ${token}`
}
}
)
return response.data
} catch (error) {
throw new Error(error.response?.data?.detail || error.message)
}
}
export default createTeam

View File

@ -0,0 +1,22 @@
import axios from 'axios'
import CONFIG from '@/core/config.js'
const deleteTeam = async (teamId) => {
try {
const token = localStorage.getItem('access_token') // получение токена
const response = await axios.delete(
`${CONFIG.BASE_URL}/teams/${teamId}`,
{
withCredentials: true,
headers: {
Authorization: `Bearer ${token}`
}
}
)
return response.data
} catch (error) {
throw new Error(error.response?.data?.detail || error.message)
}
}
export default deleteTeam

View File

@ -3,11 +3,25 @@ import CONFIG from '@/core/config.js'
const updateTeam = async (team) => {
try {
const token = localStorage.getItem('access_token')
// Убираем id из тела запроса, он идет в URL
const { id, ...teamData } = team
console.log('Отправляем на сервер:', teamData)
const response = await axios.put(
`${CONFIG.BASE_URL}/teams/${team.id}`,
team,
{ withCredentials: true }
`${CONFIG.BASE_URL}/teams/${id}`,
teamData,
{
withCredentials: true,
headers: {
Authorization: `Bearer ${token}`
}
}
)
console.log('Ответ от сервера:', response.data)
return response.data
} catch (error) {
throw new Error(error.response?.data?.detail || error.message)

View File

@ -0,0 +1,26 @@
<template>
<q-dialog v-model="visible" persistent>
<q-card style="min-width: 350px; max-width: 700px;">
<q-card-section>
<div class="text-h6">{{ title }}</div>
</q-card-section>
<q-separator />
<q-card-section>
<slot />
</q-card-section>
<q-card-actions align="right">
<slot name="actions" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup>
defineProps({
visible: Boolean,
title: String
})
</script>

View File

@ -0,0 +1,37 @@
<template>
<SharedDialogWrapper :visible="visible" title="Редактирование конкурса">
<q-input v-model="data.title" label="Название конкурса" dense autofocus clearable />
<q-input v-model="data.description" label="Описание" dense clearable type="textarea" class="q-mt-sm" />
<q-input v-model="data.start_date" label="Дата начала" type="date" dense clearable class="q-mt-sm" />
<q-input v-model="data.end_date" label="Дата окончания" type="date" dense clearable class="q-mt-sm" />
<template #actions>
<q-btn flat label="Удалить" color="negative" @click="onDelete" v-if="showDelete" />
<q-space />
<q-btn flat label="Закрыть" color="primary" @click="onClose" />
<q-btn flat label="Сохранить" color="primary" @click="onSave" />
</template>
</SharedDialogWrapper>
</template>
<script setup>
import SharedDialogWrapper from './SharedDialogWrapper.vue'
defineProps({
visible: Boolean,
data: Object,
showDelete: Boolean
})
const emit = defineEmits(['save', 'close', 'delete'])
function onSave() {
emit('save')
}
function onClose() {
emit('close')
}
function onDelete() {
emit('delete')
}
</script>

View File

@ -0,0 +1,36 @@
<template>
<SharedDialogWrapper :visible="visible" title="Редактирование проекта">
<q-input v-model="data.title" label="Название проекта" dense autofocus clearable />
<q-input v-model="data.description" label="Описание" dense clearable type="textarea" class="q-mt-sm" />
<q-input v-model="data.repo_url" label="URL репозитория" dense clearable class="q-mt-sm" />
<template #actions>
<q-btn flat label="Удалить" color="negative" @click="onDelete" v-if="showDelete" />
<q-space />
<q-btn flat label="Закрыть" color="primary" @click="onClose" />
<q-btn flat label="Сохранить" color="primary" @click="onSave" />
</template>
</SharedDialogWrapper>
</template>
<script setup>
import SharedDialogWrapper from './SharedDialogWrapper.vue'
defineProps({
visible: Boolean,
data: Object,
showDelete: Boolean
})
const emit = defineEmits(['save', 'close', 'delete'])
function onSave() {
emit('save')
}
function onClose() {
emit('close')
}
function onDelete() {
emit('delete')
}
</script>

View File

@ -0,0 +1,37 @@
<template>
<SharedDialogWrapper :visible="visible" title="Редактирование команды">
<q-input v-model="data.title" label="Название команды" dense autofocus clearable />
<q-input v-model="data.description" label="Описание" dense clearable type="textarea" class="q-mt-sm" />
<q-input v-model="data.logo" label="Логотип" dense clearable class="q-mt-sm" />
<q-input v-model="data.git_url" label="Git URL" dense clearable class="q-mt-sm" />
<template #actions>
<q-btn flat label="Удалить" color="negative" @click="onDelete" v-if="showDelete" />
<q-space />
<q-btn flat label="Закрыть" color="primary" @click="onClose" />
<q-btn flat label="Сохранить" color="primary" @click="onSave" />
</template>
</SharedDialogWrapper>
</template>
<script setup>
import SharedDialogWrapper from './SharedDialogWrapper.vue'
defineProps({
visible: Boolean,
data: Object,
showDelete: Boolean
})
const emit = defineEmits(['save', 'close', 'delete'])
function onSave() {
emit('save')
}
function onClose() {
emit('close')
}
function onDelete() {
emit('delete')
}
</script>

View File

@ -0,0 +1,36 @@
<template>
<SharedDialogWrapper :visible="visible" title="Редактирование пользователя">
<q-input v-model="data.login" label="Логин" dense autofocus clearable />
<q-input v-model="data.name" label="Имя" dense clearable class="q-mt-sm" />
<q-input v-model="data.email" label="Email" dense clearable class="q-mt-sm" />
<template #actions>
<q-btn flat label="Удалить" color="negative" @click="onDelete" v-if="showDelete" />
<q-space />
<q-btn flat label="Закрыть" color="primary" @click="onClose" />
<q-btn flat label="Сохранить" color="primary" @click="onSave" />
</template>
</SharedDialogWrapper>
</template>
<script setup>
import SharedDialogWrapper from '@/components/dialogs/SharedDialogWrapper.vue'
defineProps({
visible: Boolean,
data: Object,
showDelete: Boolean
})
const emit = defineEmits(['save', 'close', 'delete'])
function onSave() {
emit('save')
}
function onClose() {
emit('close')
}
function onDelete() {
emit('delete')
}
</script>

View File

@ -24,7 +24,8 @@ import {
QSeparator,
QCardActions,
QDialog,
QIcon
QIcon,
QSpace
} from 'quasar'
@ -41,7 +42,7 @@ app.use(Quasar, {
QInput, QBtn, QForm, QCard, QCardSection,
QLayout, QPageContainer, QPage,
QTabs, QTab, QTabPanels, QTabPanel, QHeader,QTable,
QSeparator, QCardActions, QDialog, QIcon
QSeparator, QCardActions, QDialog, QIcon, QSpace
}
})

View File

@ -29,7 +29,7 @@
:rows="users"
:columns="userColumns"
row-key="id"
@row-click="openEdit('users', $event)"
@row-click="onRowClick"
:loading="loadingUsers"
dense
flat
@ -40,12 +40,22 @@
<!-- Команды -->
<q-tab-panel name="teams">
<div class="violet-card q-pa-md">
<div class="q-gutter-sm q-mb-sm row items-center justify-between">
<q-btn
label="Создание команды"
color="primary"
@click="createHandler"
/>
</div>
<q-table
title="Команды"
:rows="teams"
:columns="teamColumns"
row-key="id"
@row-click="openEdit('teams', $event)"
@row-click="onRowClick"
:loading="loadingTeams"
dense
flat
@ -53,6 +63,7 @@
</div>
</q-tab-panel>
<!-- Проекты -->
<q-tab-panel name="projects">
<div class="violet-card q-pa-md">
@ -61,7 +72,7 @@
:rows="projects"
:columns="projectColumns"
row-key="id"
@row-click="openEdit('projects', $event)"
@row-click="onRowClick"
:loading="loadingProjects"
dense
flat
@ -77,7 +88,7 @@
:rows="contests"
:columns="contestColumns"
row-key="id"
@row-click="openEdit('contests', $event)"
@row-click="onRowClick"
:loading="loadingContests"
dense
flat
@ -92,55 +103,130 @@
<q-dialog v-model="dialogVisible" persistent>
<q-card style="min-width: 350px; max-width: 700px;">
<q-card-section>
<div class="text-h6">Редактирование {{ dialogType }}</div>
<div class="text-h6">
<template v-if="dialogType === 'teams'">
Редактирование команды {{ dialogData.title || '' }}
</template>
<template v-else-if="dialogType === 'users'">
Редактирование пользователя {{ dialogData.name || dialogData.login || '' }}
</template>
<template v-else-if="dialogType === 'projects'">
Редактирование проекта {{ dialogData.title || '' }}
</template>
<template v-else-if="dialogType === 'contests'">
Редактирование конкурса {{ dialogData.title || '' }}
</template>
<template v-else>
Редактирование {{ dialogType }}
</template>
</div>
</q-card-section>
<q-separator />
<q-card-section>
<pre>{{ dialogData }}</pre>
<!-- Teams -->
<template v-if="dialogType === 'teams'">
<q-input v-model="dialogData.title" label="Название команды" dense autofocus clearable />
<q-input v-model="dialogData.description" label="Описание" dense clearable type="textarea" class="q-mt-sm" />
<q-input v-model="dialogData.logo" label="Логотип" dense clearable class="q-mt-sm" />
<q-input v-model="dialogData.git_url" label="Git URL" dense clearable class="q-mt-sm" />
</template>
<!-- Users -->
<template v-else-if="dialogType === 'users'">
<q-input v-model="dialogData.login" label="Логин" dense autofocus clearable />
<q-input v-model="dialogData.name" label="Имя" dense clearable class="q-mt-sm" />
<q-input v-model="dialogData.email" label="Email" dense clearable class="q-mt-sm" />
<!-- Добавь другие поля, которые нужны для пользователя -->
</template>
<!-- Projects -->
<template v-else-if="dialogType === 'projects'">
<q-input v-model="dialogData.title" label="Название проекта" dense autofocus clearable />
<q-input v-model="dialogData.description" label="Описание" dense clearable type="textarea" class="q-mt-sm" />
<q-input v-model="dialogData.repo_url" label="URL репозитория" dense clearable class="q-mt-sm" />
<!-- Другие поля проекта -->
</template>
<!-- Contests -->
<template v-else-if="dialogType === 'contests'">
<q-input v-model="dialogData.title" label="Название конкурса" dense autofocus clearable />
<q-input v-model="dialogData.description" label="Описание" dense clearable type="textarea" class="q-mt-sm" />
<q-input v-model="dialogData.start_date" label="Дата начала" dense clearable class="q-mt-sm" type="date" />
<q-input v-model="dialogData.end_date" label="Дата окончания" dense clearable class="q-mt-sm" type="date" />
<!-- Другие поля конкурса -->
</template>
<template v-else>
<pre>{{ dialogData }}</pre>
</template>
</q-card-section>
<q-card-actions align="right">
<q-btn
v-if="dialogType === 'teams'"
flat
label="Удалить"
color="negative"
@click="deleteItem"
/>
<q-btn
v-if="dialogType === 'users'"
flat
label="Удалить"
color="negative"
@click="deleteUser"
/>
<q-btn
v-if="dialogType === 'projects'"
flat
label="Удалить"
color="negative"
@click="deleteProject"
/>
<q-btn
v-if="dialogType === 'contests'"
flat
label="Удалить"
color="negative"
@click="deleteContest"
/>
<q-space />
<q-btn flat label="Закрыть" color="primary" @click="closeDialog" />
<q-btn
v-if="['teams', 'users', 'projects', 'contests'].includes(dialogType)"
flat
label="Сохранить"
color="primary"
@click="saveChanges"
/>
</q-card-actions>
</q-card>
</q-dialog>
</q-layout>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue'
import { ref, watch, onMounted, onBeforeUnmount } 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'
import deleteTeamById from '@/api/teams/deleteTeam.js'
import createTeam from '@/api/teams/createTeam.js'
// Активная вкладка
const tab = ref('users')
import fetchProjects from '@/api/projects/getProjects.js'
import updateProject from '@/api/projects/updateProject.js'
import deleteProjectById from '@/api/projects/deleteProject.js'
import createProject from '@/api/projects/createProject.js'
// Данные
const users = ref([])
// Текущая вкладка 'teams' или 'projects'
const tab = ref('teams')
// --- Teams ---
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 },
@ -148,110 +234,126 @@ const teamColumns = [
{ name: 'git_url', label: 'Git URL', field: 'git_url', sortable: true },
]
// --- Projects ---
const projects = ref([])
const loadingProjects = ref(false)
const projectColumns = [
{ name: 'id', label: 'ID', field: 'id', sortable: true },
{ name: 'projectName', label: 'Проект', field: 'projectName', sortable: true },
{ name: 'name', label: 'Название проекта', field: 'name', sortable: true },
{ name: 'summary', label: 'Описание', field: 'summary', sortable: true },
{ name: 'deadline', label: 'Дедлайн', field: 'deadline', 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('')
const dialogType = ref('') // 'teams' или 'projects'
// Функция открытия редактирования
function openEdit(type, row) {
dialogType.value = type
// Клонируем объект, чтобы не менять данные в таблице до сохранения
dialogData.value = JSON.parse(JSON.stringify(row))
if (row) {
dialogData.value = JSON.parse(JSON.stringify(row))
} else {
if (type === 'teams') {
dialogData.value = { title: '', description: '', logo: '', git_url: '' }
} else if (type === 'projects') {
dialogData.value = { name: '', summary: '', deadline: '' }
}
}
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 onRowClick(event, row) {
openEdit(tab.value, row)
}
// Закрыть модалку
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
async function saveChanges() {
try {
if (dialogType.value === 'teams') {
if (dialogData.value.id) {
await updateTeam(dialogData.value)
const idx = teams.value.findIndex(t => t.id === dialogData.value.id)
if (idx !== -1) teams.value[idx] = JSON.parse(JSON.stringify(dialogData.value))
} else {
const newTeam = await createTeam(dialogData.value)
teams.value.push(newTeam)
}
break
case 'teams':
loadingTeams.value = true
try {
teams.value = await fetchTeams() || []
} catch (error) {
teams.value = []
console.error(error.message)
} finally {
loadingTeams.value = false
} else if (dialogType.value === 'projects') {
if (dialogData.value.id) {
await updateProject(dialogData.value)
const idx = projects.value.findIndex(p => p.id === dialogData.value.id)
if (idx !== -1) projects.value[idx] = JSON.parse(JSON.stringify(dialogData.value))
} else {
const newProject = await createProject(dialogData.value)
projects.value.push(newProject)
}
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
}
closeDialog()
} catch (error) {
console.error('Ошибка при сохранении:', error.message)
}
}
// Начальная загрузка
onMounted(() => loadData(tab.value))
async function loadData(name) {
if (name === 'teams') {
loadingTeams.value = true
try {
teams.value = await fetchTeams() || []
} catch (error) {
teams.value = []
console.error(error.message)
} finally {
loadingTeams.value = false
}
} else if (name === 'projects') {
loadingProjects.value = true
try {
projects.value = await fetchProjects() || []
} catch (error) {
projects.value = []
console.error(error.message)
} finally {
loadingProjects.value = false
}
}
}
async function deleteItem() {
if (!dialogData.value.id) return
try {
if (dialogType.value === 'teams') {
await deleteTeamById(dialogData.value.id)
teams.value = teams.value.filter(t => t.id !== dialogData.value.id)
} else if (dialogType.value === 'projects') {
await deleteProjectById(dialogData.value.id)
projects.value = projects.value.filter(p => p.id !== dialogData.value.id)
}
closeDialog()
} catch (error) {
console.error('Ошибка при удалении:', error.message)
}
}
function createHandler() {
openEdit(tab.value, null)
}
onMounted(() => {
loadData(tab.value)
const interval = setInterval(() => {
loadData(tab.value)
}, 5000)
onBeforeUnmount(() => {
clearInterval(interval)
})
})
// Загрузка при смене вкладки
watch(tab, (newTab) => {
loadData(newTab)
})
@ -268,4 +370,4 @@ watch(tab, (newTab) => {
background: #ede9fe;
box-shadow: 0 6px 32px rgba(124, 58, 237, 0.13), 0 1.5px 6px rgba(124, 58, 237, 0.10);
}
</style>
</style>