админ страница
This commit is contained in:
parent
3fbaaf1b45
commit
75a25f9d47
@ -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>
|
||||
912
WEB/src/pages/AdminPage.vue
Normal file
912
WEB/src/pages/AdminPage.vue
Normal file
@ -0,0 +1,912 @@
|
||||
<template>
|
||||
<div class="q-pa-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="text-h4 q-mb-md">
|
||||
<q-icon name="admin_panel_settings" class="q-mr-sm" />
|
||||
Управление данными
|
||||
</div>
|
||||
|
||||
<!-- Навигационные вкладки -->
|
||||
<q-tabs
|
||||
v-model="activeTab"
|
||||
dense
|
||||
class="text-grey"
|
||||
active-color="primary"
|
||||
indicator-color="primary"
|
||||
align="justify"
|
||||
narrow-indicator
|
||||
>
|
||||
<q-tab name="team" label="Команда" />
|
||||
<q-tab name="members" label="Участники" />
|
||||
<q-tab name="contests" label="Конкурсы" />
|
||||
<q-tab name="projects" label="Проекты" />
|
||||
<q-tab name="achievements" label="Достижения" />
|
||||
</q-tabs>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section>
|
||||
<q-tab-panels v-model="activeTab">
|
||||
|
||||
<!-- Управление командой -->
|
||||
<q-tab-panel name="team">
|
||||
<div class="text-h5 q-mb-md">Информация о команде</div>
|
||||
|
||||
<q-form @submit="onSubmitTeam" class="q-gutter-md">
|
||||
<div class="row q-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
filled
|
||||
v-model="teamForm.name"
|
||||
label="Название команды *"
|
||||
lazy-rules
|
||||
:rules="[val => val && val.length > 0 || 'Поле обязательно']"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-5">
|
||||
<q-file
|
||||
filled
|
||||
bottom-slots
|
||||
v-model="teamForm.logo"
|
||||
label="Логотип команды"
|
||||
counter
|
||||
accept=".jpg, .png, .jpeg, image/*"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="cloud_upload" @click.stop.prevent />
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<q-icon name="close" @click.stop.prevent="teamForm.logo = null" class="cursor-pointer" />
|
||||
</template>
|
||||
</q-file>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
type="textarea"
|
||||
v-model="teamForm.description"
|
||||
label="Краткое описание команды *"
|
||||
lazy-rules
|
||||
:rules="[val => val && val.length > 0 || 'Поле обязательно']"
|
||||
rows="3"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
v-model="teamForm.giteaUrl"
|
||||
label="URL профиля команды на Gitea"
|
||||
hint="Например: https://gitea.example.com/team"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<q-btn label="Сохранить изменения" type="submit" color="primary"/>
|
||||
<q-btn label="Очистить" type="reset" color="primary" flat class="q-ml-sm" />
|
||||
</div>
|
||||
</q-form>
|
||||
</q-tab-panel>
|
||||
|
||||
<!-- Управление участниками -->
|
||||
<q-tab-panel name="members">
|
||||
<div class="row justify-between items-center q-mb-md">
|
||||
<div class="text-h5">Участники команды</div>
|
||||
<q-btn color="primary" label="Добавить участника" @click="showAddMemberDialog" />
|
||||
</div>
|
||||
|
||||
<!-- Список участников -->
|
||||
<q-list bordered separator>
|
||||
<q-item v-for="member in members" :key="member.id" clickable v-ripple>
|
||||
<q-item-section avatar>
|
||||
<q-avatar>
|
||||
<img :src="member.avatar || '/api/placeholder/40/40'" />
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
<q-item-label>{{ member.fullName }}</q-item-label>
|
||||
<q-item-label caption>{{ member.role || 'Участник' }}</q-item-label>
|
||||
<q-item-label caption>{{ member.email }}</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<div class="text-grey-8 q-gutter-xs">
|
||||
<q-btn class="gt-xs" size="12px" flat dense round icon="edit" @click="editMember(member)" />
|
||||
<q-btn class="gt-xs" size="12px" flat dense round icon="delete" color="negative" @click="deleteMember(member.id)" />
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-tab-panel>
|
||||
|
||||
<!-- Управление конкурсами -->
|
||||
<q-tab-panel name="contests">
|
||||
<div class="row justify-between items-center q-mb-md">
|
||||
<div class="text-h5">Конкурсы</div>
|
||||
<q-btn color="primary" label="Добавить конкурс" @click="showAddContestDialog" />
|
||||
</div>
|
||||
|
||||
<!-- Список конкурсов -->
|
||||
<div class="row q-gutter-md">
|
||||
<q-card v-for="contest in contests" :key="contest.id" class="col-12 col-md-5">
|
||||
<q-img
|
||||
:src="contest.image || '/api/placeholder/300/200'"
|
||||
style="height: 140px"
|
||||
>
|
||||
<div class="absolute-bottom">
|
||||
<div class="text-h6">{{ contest.name }}</div>
|
||||
</div>
|
||||
</q-img>
|
||||
|
||||
<q-card-section>
|
||||
<div class="text-caption text-grey">{{ contest.year }}</div>
|
||||
<div class="text-body2 q-mt-sm">{{ contest.description?.substring(0, 100) }}...</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions>
|
||||
<q-btn flat color="primary" label="Редактировать" @click="editContest(contest)" />
|
||||
<q-btn flat color="negative" label="Удалить" @click="deleteContest(contest.id)" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
|
||||
<!-- Управление проектами -->
|
||||
<q-tab-panel name="projects">
|
||||
<div class="row justify-between items-center q-mb-md">
|
||||
<div class="text-h5">Проекты</div>
|
||||
<q-btn color="primary" label="Добавить проект" @click="showAddProjectDialog" />
|
||||
</div>
|
||||
|
||||
<!-- Список проектов -->
|
||||
<q-list bordered separator>
|
||||
<q-item v-for="project in projects" :key="project.id" clickable v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ project.name }}</q-item-label>
|
||||
<q-item-label caption>{{ project.description }}</q-item-label>
|
||||
<q-item-label caption>
|
||||
<q-chip v-for="tech in project.technologies" :key="tech" size="sm" color="primary" text-color="white">
|
||||
{{ tech }}
|
||||
</q-chip>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<div class="text-grey-8 q-gutter-xs">
|
||||
<q-btn class="gt-xs" size="12px" flat dense round icon="edit" @click="editProject(project)" />
|
||||
<q-btn class="gt-xs" size="12px" flat dense round icon="delete" color="negative" @click="deleteProject(project.id)" />
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-tab-panel>
|
||||
|
||||
<!-- Управление достижениями -->
|
||||
<q-tab-panel name="achievements">
|
||||
<div class="row justify-between items-center q-mb-md">
|
||||
<div class="text-h5">Достижения и курсы</div>
|
||||
<q-btn color="primary" label="Добавить достижение" @click="showAddAchievementDialog" />
|
||||
</div>
|
||||
|
||||
<!-- Список достижений -->
|
||||
<q-list bordered separator>
|
||||
<q-item v-for="achievement in achievements" :key="achievement.id" clickable v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ achievement.title }}</q-item-label>
|
||||
<q-item-label caption>{{ achievement.description }}</q-item-label>
|
||||
<q-item-label caption>{{ achievement.date }} • {{ achievement.type }}</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<div class="text-grey-8 q-gutter-xs">
|
||||
<q-btn class="gt-xs" size="12px" flat dense round icon="edit" @click="editAchievement(achievement)" />
|
||||
<q-btn class="gt-xs" size="12px" flat dense round icon="delete" color="negative" @click="deleteAchievement(achievement.id)" />
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-tab-panel>
|
||||
|
||||
</q-tab-panels>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<!-- Dialog для добавления/редактирования участника -->
|
||||
<q-dialog v-model="memberDialog">
|
||||
<q-card style="min-width: 400px">
|
||||
<q-card-section>
|
||||
<div class="text-h6">{{ editingMember ? 'Редактировать участника' : 'Добавить участника' }}</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-none">
|
||||
<q-form @submit="onSubmitMember" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
v-model="memberForm.fullName"
|
||||
label="ФИО *"
|
||||
lazy-rules
|
||||
:rules="[val => val && val.length > 0 || 'Поле обязательно']"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
v-model="memberForm.email"
|
||||
label="Email *"
|
||||
type="email"
|
||||
lazy-rules
|
||||
:rules="[
|
||||
val => val && val.length > 0 || 'Поле обязательно',
|
||||
val => /.+@.+\..+/.test(val) || 'Введите корректный email'
|
||||
]"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
v-model="memberForm.role"
|
||||
label="Роль в команде"
|
||||
hint="Например: Frontend разработчик, Team Lead"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
type="textarea"
|
||||
v-model="memberForm.bio"
|
||||
label="Краткая информация"
|
||||
rows="3"
|
||||
/>
|
||||
|
||||
<q-file
|
||||
filled
|
||||
v-model="memberForm.avatar"
|
||||
label="Фото профиля"
|
||||
accept=".jpg, .png, .jpeg, image/*"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
v-model="memberForm.giteaUsername"
|
||||
label="Username на Gitea"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<q-btn label="Сохранить" type="submit" color="primary"/>
|
||||
<q-btn label="Отмена" v-close-popup flat class="q-ml-sm" />
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!-- Dialog для добавления/редактирования конкурса -->
|
||||
<q-dialog v-model="contestDialog">
|
||||
<q-card style="min-width: 500px">
|
||||
<q-card-section>
|
||||
<div class="text-h6">{{ editingContest ? 'Редактировать конкурс' : 'Добавить конкурс' }}</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-none">
|
||||
<q-form @submit="onSubmitContest" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
v-model="contestForm.name"
|
||||
label="Название конкурса *"
|
||||
lazy-rules
|
||||
:rules="[val => val && val.length > 0 || 'Поле обязательно']"
|
||||
/>
|
||||
|
||||
<div class="row q-gutter-md">
|
||||
<div class="col">
|
||||
<q-input
|
||||
filled
|
||||
v-model="contestForm.year"
|
||||
label="Год *"
|
||||
type="number"
|
||||
lazy-rules
|
||||
:rules="[val => val && val > 2000 || 'Введите корректный год']"
|
||||
/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-file
|
||||
filled
|
||||
v-model="contestForm.image"
|
||||
label="Главное фото"
|
||||
accept="image/*"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
type="textarea"
|
||||
v-model="contestForm.description"
|
||||
label="Описание конкурса *"
|
||||
lazy-rules
|
||||
:rules="[val => val && val.length > 0 || 'Поле обязательно']"
|
||||
rows="4"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
v-model="contestForm.websiteUrl"
|
||||
label="Ссылка на сайт конкурса"
|
||||
type="url"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
v-model="contestForm.giteaRepo"
|
||||
label="Репозиторий на Gitea"
|
||||
hint="Например: team/contest-project-2024"
|
||||
/>
|
||||
|
||||
<q-select
|
||||
filled
|
||||
v-model="contestForm.participants"
|
||||
:options="members"
|
||||
option-value="id"
|
||||
option-label="fullName"
|
||||
label="Участники от команды"
|
||||
multiple
|
||||
use-chips
|
||||
stack-label
|
||||
/>
|
||||
|
||||
<div>
|
||||
<q-btn label="Сохранить" type="submit" color="primary"/>
|
||||
<q-btn label="Отмена" v-close-popup flat class="q-ml-sm" />
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!-- Dialog для добавления/редактирования проекта -->
|
||||
<q-dialog v-model="projectDialog">
|
||||
<q-card style="min-width: 500px">
|
||||
<q-card-section>
|
||||
<div class="text-h6">{{ editingProject ? 'Редактировать проект' : 'Добавить проект' }}</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-none">
|
||||
<q-form @submit="onSubmitProject" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
v-model="projectForm.name"
|
||||
label="Название проекта *"
|
||||
lazy-rules
|
||||
:rules="[val => val && val.length > 0 || 'Поле обязательно']"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
type="textarea"
|
||||
v-model="projectForm.description"
|
||||
label="Описание проекта *"
|
||||
lazy-rules
|
||||
:rules="[val => val && val.length > 0 || 'Поле обязательно']"
|
||||
rows="3"
|
||||
/>
|
||||
|
||||
<q-select
|
||||
filled
|
||||
v-model="projectForm.technologies"
|
||||
:options="technologyOptions"
|
||||
label="Технологии"
|
||||
multiple
|
||||
use-input
|
||||
use-chips
|
||||
@new-value="createValue"
|
||||
stack-label
|
||||
/>
|
||||
|
||||
<div class="row q-gutter-md">
|
||||
<div class="col">
|
||||
<q-input
|
||||
filled
|
||||
v-model="projectForm.giteaRepo"
|
||||
label="Репозиторий на Gitea"
|
||||
/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-input
|
||||
filled
|
||||
v-model="projectForm.demoUrl"
|
||||
label="Ссылка на демо"
|
||||
type="url"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-select
|
||||
filled
|
||||
v-model="projectForm.author"
|
||||
:options="members"
|
||||
option-value="id"
|
||||
option-label="fullName"
|
||||
label="Автор проекта"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<q-btn label="Сохранить" type="submit" color="primary"/>
|
||||
<q-btn label="Отмена" v-close-popup flat class="q-ml-sm" />
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!-- Dialog для добавления/редактирования достижения -->
|
||||
<q-dialog v-model="achievementDialog">
|
||||
<q-card style="min-width: 500px">
|
||||
<q-card-section>
|
||||
<div class="text-h6">{{ editingAchievement ? 'Редактировать достижение' : 'Добавить достижение' }}</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-none">
|
||||
<q-form @submit="onSubmitAchievement" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
v-model="achievementForm.title"
|
||||
label="Название достижения/курса *"
|
||||
lazy-rules
|
||||
:rules="[val => val && val.length > 0 || 'Поле обязательно']"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
type="textarea"
|
||||
v-model="achievementForm.description"
|
||||
label="Описание *"
|
||||
lazy-rules
|
||||
:rules="[val => val && val.length > 0 || 'Поле обязательно']"
|
||||
rows="3"
|
||||
/>
|
||||
|
||||
<div class="row q-gutter-md">
|
||||
<div class="col">
|
||||
<q-select
|
||||
filled
|
||||
v-model="achievementForm.type"
|
||||
:options="['Достижение', 'Курс', 'Сертификат', 'Диплом']"
|
||||
label="Тип *"
|
||||
lazy-rules
|
||||
:rules="[val => val && val.length > 0 || 'Поле обязательно']"
|
||||
/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-input
|
||||
filled
|
||||
v-model="achievementForm.date"
|
||||
label="Дата получения"
|
||||
type="date"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-file
|
||||
filled
|
||||
v-model="achievementForm.certificate"
|
||||
label="Скан диплома/сертификата"
|
||||
accept=".pdf,.jpg,.png,.jpeg"
|
||||
/>
|
||||
|
||||
<q-select
|
||||
filled
|
||||
v-model="achievementForm.owner"
|
||||
:options="members"
|
||||
option-value="id"
|
||||
option-label="fullName"
|
||||
label="Владелец достижения *"
|
||||
lazy-rules
|
||||
:rules="[val => val !== null || 'Поле обязательно']"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<q-btn label="Сохранить" type="submit" color="primary"/>
|
||||
<q-btn label="Отмена" v-close-popup flat class="q-ml-sm" />
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'AdminDataManagement',
|
||||
setup() {
|
||||
const activeTab = ref('team')
|
||||
|
||||
// Dialogs
|
||||
const memberDialog = ref(false)
|
||||
const contestDialog = ref(false)
|
||||
const projectDialog = ref(false)
|
||||
const achievementDialog = ref(false)
|
||||
|
||||
// Editing states
|
||||
const editingMember = ref(false)
|
||||
const editingContest = ref(false)
|
||||
const editingProject = ref(false)
|
||||
const editingAchievement = ref(false)
|
||||
|
||||
// Forms
|
||||
const teamForm = reactive({
|
||||
name: 'DevTeam',
|
||||
description: 'Команда разработчиков, специализирующаяся на создании инновационных веб-решений',
|
||||
logo: null,
|
||||
giteaUrl: 'https://gitea.example.com/devteam'
|
||||
})
|
||||
|
||||
const memberForm = reactive({
|
||||
fullName: '',
|
||||
email: '',
|
||||
role: '',
|
||||
bio: '',
|
||||
avatar: null,
|
||||
giteaUsername: ''
|
||||
})
|
||||
|
||||
const contestForm = reactive({
|
||||
name: '',
|
||||
year: new Date().getFullYear(),
|
||||
description: '',
|
||||
image: null,
|
||||
websiteUrl: '',
|
||||
giteaRepo: '',
|
||||
participants: []
|
||||
})
|
||||
|
||||
const projectForm = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
technologies: [],
|
||||
giteaRepo: '',
|
||||
demoUrl: '',
|
||||
author: null
|
||||
})
|
||||
|
||||
const achievementForm = reactive({
|
||||
title: '',
|
||||
description: '',
|
||||
type: '',
|
||||
date: '',
|
||||
certificate: null,
|
||||
owner: null
|
||||
})
|
||||
|
||||
// Data
|
||||
const members = ref([
|
||||
{
|
||||
id: 1,
|
||||
fullName: 'Иван Иванов',
|
||||
email: 'ivan@example.com',
|
||||
role: 'Frontend Developer',
|
||||
avatar: '/api/placeholder/40/40'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
fullName: 'Мария Петрова',
|
||||
email: 'maria@example.com',
|
||||
role: 'Backend Developer',
|
||||
avatar: '/api/placeholder/40/40'
|
||||
}
|
||||
])
|
||||
|
||||
const contests = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: 'Хакатон IT-решений 2024',
|
||||
year: 2024,
|
||||
description: 'Региональный хакатон по разработке IT-решений для бизнеса',
|
||||
image: '/api/placeholder/300/200'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Web Development Challenge',
|
||||
year: 2023,
|
||||
description: 'Международный конкурс веб-разработки',
|
||||
image: '/api/placeholder/300/200'
|
||||
}
|
||||
])
|
||||
|
||||
const projects = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: 'Система управления проектами',
|
||||
description: 'Веб-приложение для управления проектами команды',
|
||||
technologies: ['Vue.js', 'Node.js', 'PostgreSQL']
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Мобильное приложение для заказа еды',
|
||||
description: 'React Native приложение с интеграцией платежных систем',
|
||||
technologies: ['React Native', 'Firebase', 'Stripe']
|
||||
}
|
||||
])
|
||||
|
||||
const achievements = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: 'Сертификат Vue.js Developer',
|
||||
description: 'Успешное прохождение курса по Vue.js разработке',
|
||||
type: 'Сертификат',
|
||||
date: '2024-01-15'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '1 место в хакатоне',
|
||||
description: 'Победа в региональном хакатоне IT-решений',
|
||||
type: 'Достижение',
|
||||
date: '2024-03-20'
|
||||
}
|
||||
])
|
||||
|
||||
const technologyOptions = ref([
|
||||
'Vue.js', 'React', 'Angular', 'Node.js', 'Express', 'FastAPI',
|
||||
'PostgreSQL', 'MongoDB', 'Docker', 'Kubernetes', 'AWS', 'Azure'
|
||||
])
|
||||
|
||||
// Methods
|
||||
const onSubmitTeam = () => {
|
||||
console.log('Saving team info:', teamForm)
|
||||
// Здесь будет API вызов для сохранения информации о команде
|
||||
}
|
||||
|
||||
const showAddMemberDialog = () => {
|
||||
editingMember.value = false
|
||||
resetMemberForm()
|
||||
memberDialog.value = true
|
||||
}
|
||||
|
||||
const editMember = (member) => {
|
||||
editingMember.value = true
|
||||
Object.assign(memberForm, member)
|
||||
memberDialog.value = true
|
||||
}
|
||||
|
||||
const onSubmitMember = () => {
|
||||
if (editingMember.value) {
|
||||
console.log('Updating member:', memberForm)
|
||||
// API вызов для обновления участника
|
||||
} else {
|
||||
console.log('Adding new member:', memberForm)
|
||||
// API вызов для добавления участника
|
||||
const newMember = { ...memberForm, id: Date.now() }
|
||||
members.value.push(newMember)
|
||||
}
|
||||
memberDialog.value = false
|
||||
resetMemberForm()
|
||||
}
|
||||
|
||||
const deleteMember = (id) => {
|
||||
members.value = members.value.filter(m => m.id !== id)
|
||||
console.log('Deleting member:', id)
|
||||
}
|
||||
|
||||
const resetMemberForm = () => {
|
||||
Object.assign(memberForm, {
|
||||
fullName: '',
|
||||
email: '',
|
||||
role: '',
|
||||
bio: '',
|
||||
avatar: null,
|
||||
giteaUsername: ''
|
||||
})
|
||||
}
|
||||
|
||||
const showAddContestDialog = () => {
|
||||
editingContest.value = false
|
||||
resetContestForm()
|
||||
contestDialog.value = true
|
||||
}
|
||||
|
||||
const editContest = (contest) => {
|
||||
editingContest.value = true
|
||||
Object.assign(contestForm, contest)
|
||||
contestDialog.value = true
|
||||
}
|
||||
|
||||
const onSubmitContest = () => {
|
||||
if (editingContest.value) {
|
||||
console.log('Updating contest:', contestForm)
|
||||
} else {
|
||||
console.log('Adding new contest:', contestForm)
|
||||
const newContest = { ...contestForm, id: Date.now() }
|
||||
contests.value.push(newContest)
|
||||
}
|
||||
contestDialog.value = false
|
||||
resetContestForm()
|
||||
}
|
||||
|
||||
const deleteContest = (id) => {
|
||||
contests.value = contests.value.filter(c => c.id !== id)
|
||||
console.log('Deleting contest:', id)
|
||||
}
|
||||
|
||||
const resetContestForm = () => {
|
||||
Object.assign(contestForm, {
|
||||
name: '',
|
||||
year: new Date().getFullYear(),
|
||||
description: '',
|
||||
image: null,
|
||||
websiteUrl: '',
|
||||
giteaRepo: '',
|
||||
participants: []
|
||||
})
|
||||
}
|
||||
|
||||
const showAddProjectDialog = () => {
|
||||
editingProject.value = false
|
||||
resetProjectForm()
|
||||
projectDialog.value = true
|
||||
}
|
||||
|
||||
const editProject = (project) => {
|
||||
editingProject.value = true
|
||||
Object.assign(projectForm, project)
|
||||
projectDialog.value = true
|
||||
}
|
||||
|
||||
const onSubmitProject = () => {
|
||||
if (editingProject.value) {
|
||||
console.log('Updating project:', projectForm)
|
||||
} else {
|
||||
console.log('Adding new project:', projectForm)
|
||||
const newProject = { ...projectForm, id: Date.now() }
|
||||
projects.value.push(newProject)
|
||||
}
|
||||
projectDialog.value = false
|
||||
resetProjectForm()
|
||||
}
|
||||
|
||||
const deleteProject = (id) => {
|
||||
projects.value = projects.value.filter(p => p.id !== id)
|
||||
console.log('Deleting project:', id)
|
||||
}
|
||||
|
||||
const resetProjectForm = () => {
|
||||
Object.assign(projectForm, {
|
||||
name: '',
|
||||
description: '',
|
||||
technologies: [],
|
||||
giteaRepo: '',
|
||||
demoUrl: '',
|
||||
author: null
|
||||
})
|
||||
}
|
||||
|
||||
const showAddAchievementDialog = () => {
|
||||
editingAchievement.value = false
|
||||
resetAchievementForm()
|
||||
achievementDialog.value = true
|
||||
}
|
||||
|
||||
const editAchievement = (achievement) => {
|
||||
editingAchievement.value = true
|
||||
Object.assign(achievementForm, achievement)
|
||||
achievementDialog.value = true
|
||||
}
|
||||
|
||||
const onSubmitAchievement = () => {
|
||||
if (editingAchievement.value) {
|
||||
console.log('Updating achievement:', achievementForm)
|
||||
} else {
|
||||
console.log('Adding new achievement:', achievementForm)
|
||||
const newAchievement = { ...achievementForm, id: Date.now() }
|
||||
achievements.value.push(newAchievement)
|
||||
}
|
||||
achievementDialog.value = false
|
||||
resetAchievementForm()
|
||||
}
|
||||
|
||||
const deleteAchievement = (id) => {
|
||||
achievements.value = achievements.value.filter(a => a.id !== id)
|
||||
console.log('Deleting achievement:', id)
|
||||
}
|
||||
|
||||
const resetAchievementForm = () => {
|
||||
Object.assign(achievementForm, {
|
||||
title: '',
|
||||
description: '',
|
||||
type: '',
|
||||
date: '',
|
||||
certificate: null,
|
||||
owner: null
|
||||
})
|
||||
}
|
||||
|
||||
const createValue = (val, done) => {
|
||||
if (val.length > 0) {
|
||||
if (!technologyOptions.value.includes(val)) {
|
||||
technologyOptions.value.push(val)
|
||||
}
|
||||
done(val, 'toggle')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
activeTab,
|
||||
|
||||
// Dialogs
|
||||
memberDialog,
|
||||
contestDialog,
|
||||
projectDialog,
|
||||
achievementDialog,
|
||||
|
||||
// Editing states
|
||||
editingMember,
|
||||
editingContest,
|
||||
editingProject,
|
||||
editingAchievement,
|
||||
|
||||
// Forms
|
||||
teamForm,
|
||||
memberForm,
|
||||
contestForm,
|
||||
projectForm,
|
||||
achievementForm,
|
||||
|
||||
// Data
|
||||
members,
|
||||
contests,
|
||||
projects,
|
||||
achievements,
|
||||
technologyOptions,
|
||||
|
||||
// Methods
|
||||
onSubmitTeam,
|
||||
showAddMemberDialog,
|
||||
editMember,
|
||||
onSubmitMember,
|
||||
deleteMember,
|
||||
showAddContestDialog,
|
||||
editContest,
|
||||
onSubmitContest,
|
||||
deleteContest,
|
||||
showAddProjectDialog,
|
||||
editProject,
|
||||
onSubmitProject,
|
||||
deleteProject,
|
||||
showAddAchievementDialog,
|
||||
editAchievement,
|
||||
onSubmitAchievement,
|
||||
deleteAchievement,
|
||||
createValue
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.q-tab-panel {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.q-card {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.2), 0 2px 2px rgba(0,0,0,0.14), 0 3px 1px -2px rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
.q-item-section.side {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.text-h4, .text-h5, .text-h6 {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.q-chip {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.gt-xs {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,15 +1,17 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
|
||||
import { isAuthenticated as checkAuth, isAdmin as checkAdmin } from '../utils/auth.js'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// Реактивное вычисление статуса авторизации
|
||||
const isAuthenticated = computed(() => !!localStorage.getItem('access_token'))
|
||||
const isAuthenticated = computed(() => checkAuth())
|
||||
|
||||
const isAdmin = computed(() => checkAdmin())
|
||||
|
||||
const goToLogin = () => router.push('/login')
|
||||
const goToAdmin = () => router.push('/admin')
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('access_token')
|
||||
router.go(0) // Принудительное обновление страницы для обновления состояния
|
||||
@ -22,6 +24,15 @@ const logout = () => {
|
||||
|
||||
<div v-if="isAuthenticated" class="q-gutter-sm">
|
||||
<q-btn label="Редактировать профиль" color="secondary" />
|
||||
|
||||
<q-btn
|
||||
v-if="isAdmin"
|
||||
label="Управление данными"
|
||||
color="accent"
|
||||
@click="goToAdmin"
|
||||
icon="admin_panel_settings"
|
||||
/>
|
||||
|
||||
<q-btn label="Выход" color="negative" @click="logout" />
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,13 +1,23 @@
|
||||
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 { isAuthenticated, getUserRole } from "../utils/auth.js"
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
component: HomePage
|
||||
},
|
||||
{path: '/login', component: LoginPage}
|
||||
{
|
||||
path: '/login',
|
||||
component: LoginPage
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
component: AdminPage,
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
@ -16,13 +26,30 @@ const router = createRouter({
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
const isAuthenticated = !!localStorage.getItem('access_token')
|
||||
const authenticated = isAuthenticated()
|
||||
|
||||
if (to.path === '/login' && isAuthenticated) {
|
||||
next('/') // теперь редирект на главную страницу
|
||||
} else {
|
||||
next()
|
||||
// Если переходим на страницу логина и уже авторизованы
|
||||
if (to.path === '/login' && authenticated) {
|
||||
next('/')
|
||||
return
|
||||
}
|
||||
|
||||
// Проверка доступа к админ-панели
|
||||
if (to.meta.requiresAdmin) {
|
||||
if (!authenticated) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
|
||||
const userRole = getUserRole()
|
||||
if (userRole !== 'admin') {
|
||||
// Редирект на главную, если не админ
|
||||
next('/')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
35
WEB/src/utils/auth.js
Normal file
35
WEB/src/utils/auth.js
Normal file
@ -0,0 +1,35 @@
|
||||
export const decodeToken = (token) => {
|
||||
try {
|
||||
const base64Url = token.split('.')[1]
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
|
||||
const jsonPayload = decodeURIComponent(
|
||||
atob(base64)
|
||||
.split('')
|
||||
.map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
||||
.join('')
|
||||
)
|
||||
return JSON.parse(jsonPayload)
|
||||
} catch (error) {
|
||||
console.error('Ошибка декодирования токена:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const getUserRole = () => {
|
||||
const token = localStorage.getItem('access_token')
|
||||
if (!token) return null
|
||||
|
||||
const decoded = decodeToken(token)
|
||||
return decoded?.role || null
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const isAuthenticated = () => {
|
||||
return !!localStorage.getItem('access_token')
|
||||
}
|
||||
|
||||
|
||||
export const isAdmin = () => {
|
||||
return getUserRole() === 'admin'
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user