убрал пока ненужную модульность, изменил главную страницу, логин и админку

This commit is contained in:
Мельников Данил 2025-05-31 17:35:20 +05:00
parent 58da850137
commit c812273f1b
10 changed files with 368 additions and 241 deletions

View File

@ -1,9 +1,16 @@
<script setup>
import { ref } from 'vue'
import { QLayout, QHeader, QPageContainer} from 'quasar'
const drawer = ref(false)
</script>
<template>
<router-view />
</template>
<q-layout view="lHh Lpr lFf">
<q-header elevated>
</q-header>
<style scoped>
</style>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>

View File

@ -1,26 +0,0 @@
<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

@ -1,37 +0,0 @@
<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

@ -1,36 +0,0 @@
<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

@ -1,37 +0,0 @@
<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

@ -1,36 +0,0 @@
<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

@ -25,7 +25,12 @@ import {
QCardActions,
QDialog,
QIcon,
QSpace
QSpace,
QAvatar,
QTooltip,
QBanner,
QSlideTransition,
Ripple
} from 'quasar'
@ -42,7 +47,11 @@ app.use(Quasar, {
QInput, QBtn, QForm, QCard, QCardSection,
QLayout, QPageContainer, QPage,
QTabs, QTab, QTabPanels, QTabPanel, QHeader,QTable,
QSeparator, QCardActions, QDialog, QIcon, QSpace
QSeparator, QCardActions, QDialog, QIcon, QSpace,
QAvatar, QTooltip, QBanner, QSlideTransition
},
directives: {
Ripple
}
})

View File

@ -24,6 +24,13 @@
<!-- Пользователи -->
<q-tab-panel name="users">
<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="users"
@ -40,16 +47,13 @@
<!-- Команды -->
<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"
@ -67,6 +71,13 @@
<!-- Проекты -->
<q-tab-panel name="projects">
<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="projects"
@ -83,6 +94,13 @@
<!-- Конкурсы -->
<q-tab-panel name="contests">
<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="contests"
@ -176,21 +194,21 @@
flat
label="Удалить"
color="negative"
@click="deleteUser"
@click="deleteItem"
/>
<q-btn
v-if="dialogType === 'projects'"
flat
label="Удалить"
color="negative"
@click="deleteProject"
@click="deleteItem"
/>
<q-btn
v-if="dialogType === 'contests'"
flat
label="Удалить"
color="negative"
@click="deleteContest"
@click="deleteItem"
/>
<q-space />
<q-btn flat label="Закрыть" color="primary" @click="closeDialog" />
@ -238,10 +256,9 @@ const teamColumns = [
const projects = ref([])
const loadingProjects = ref(false)
const projectColumns = [
{ name: 'name', label: 'Название проекта', field: 'name', sortable: true },
{ name: 'summary', label: 'Описание', field: 'summary', sortable: true },
{ name: 'deadline', label: 'Дедлайн', field: 'deadline', sortable: true },
// добавьте нужные поля
{ name: 'title', label: 'Название проекта', field: 'title', sortable: true },
{ name: 'description', label: 'Описание', field: 'description', sortable: true },
{ name: 'repository_url', label: 'Репозиторий', field: 'repository_url', sortable: true },
]
// Общие состояния для диалогов

View File

@ -1,46 +1,329 @@
<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">
{{ teamName }}
</q-card-section>
</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;"
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>
<!-- Конкурсы (по центру, без прокрутки) -->
<div class="flex justify-center flex-wrap q-gutter-md q-mb-xl">
<q-card
v-for="contest in contests"
:key="contest.id"
class="contest-card violet-card"
bordered
style="width: 220px;"
v-ripple
>
<q-card-section class="q-pa-md">
<div class="text-h6">{{ contest.title }}</div>
<div class="text-subtitle2 text-indigo-8">{{ contest.description }}</div>
</q-card-section>
</q-card>
</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 q-mb-xl" style="max-width: 920px; width: 100%;">
<q-card-section class="q-pa-md">
<div class="text-h6 text-indigo-10 q-mb-md">Активность команды и история коммитов</div>
<div class="activity-container row no-wrap items-start q-mb-md">
<div class="dates-column column q-pr-md" style="min-width: 50px;">
<div
v-for="item in activityData"
:key="item.date"
class="activity-date text-caption text-indigo-9"
style="height: 16px; line-height: 16px;"
>
{{ item.date.slice(5) }}
</div>
</div>
<div class="activity-grid row wrap" style="flex: 1; gap: 4px;">
<div
v-for="item in activityData"
:key="item.date"
class="activity-square"
:title="`Дата: ${item.date}, активность: ${item.count}`"
:style="{ backgroundColor: getActivityColor(item.count) }"
></div>
</div>
</div>
<div class="commits-list q-pa-sm" style="max-height: 150px; overflow-y: auto;">
<div
v-for="commit in commits"
:key="commit.id"
class="commit-item text-caption text-indigo-9 q-mb-xs"
>
<strong>{{ commit.date }}:</strong> {{ commit.message }}
</div>
</div>
</q-card-section>
</q-card>
</div>
<!-- Кнопка выхода / авторизации -->
<q-btn
:icon="isAuthenticated ? 'logout' : 'login'"
class="fixed-bottom-right q-ma-md"
size="20px"
color="indigo-10"
round
@click="handleAuthAction"
/>
</q-page>
</template>
<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { ref, computed } from 'vue'
import { useRouter} from 'vue-router'
import { Ripple, Notify } from 'quasar'
defineExpose({ directives: { ripple: Ripple } })
const router = useRouter()
// Реактивное вычисление статуса авторизации
const isAuthenticated = computed(() => !!localStorage.getItem('access_token'))
const isAuthenticated = ref(!!localStorage.getItem('access_token'))
const goToLogin = () => router.push('/login')
const logout = () => {
localStorage.removeItem('access_token')
router.go(0) // Принудительное обновление страницы для обновления состояния
const handleAuthAction = () => {
if (isAuthenticated.value) {
localStorage.removeItem('access_token')
localStorage.removeItem('user_id')
isAuthenticated.value = false
Notify.create({
type: 'positive',
message: 'Выход успешно осуществлен',
icon: 'check_circle',
})
} else {
router.push('/login')
}
}
// --- Данные команды ---
const teamLogo = ref('https://cdn.quasar.dev/logo-v2/svg/logo.svg')
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: 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' },
{ id: 5, name: 'Дмитрий Орлов', role: 'QA', avatar: 'https://randomuser.me/api/portraits/men/78.jpg' },
])
// --- Конкурсы ---
const contests = ref([
{ id: 1, title: 'Hackathon 2024', description: 'Ежегодный хакатон для стартапов' },
{ id: 2, title: 'CodeFest', description: 'Соревнование по программированию' },
{ id: 3, title: 'UI/UX Challenge', description: 'Конкурс дизайна интерфейсов' },
{ id: 4, title: 'AI Innovation', description: 'Инициатива в области искусственного интеллекта' },
])
// --- Активность ---
const activityData = ref([])
const today = new Date()
const activityDays = 30
function pad(num) { return num < 10 ? '0' + num : num }
for (let i = activityDays - 1; i >= 0; i--) {
const d = new Date(today)
d.setDate(d.getDate() - i)
const count = Math.floor(Math.random() * 6)
activityData.value.push({ date: d.toISOString().slice(0, 10), count })
}
// --- Коммиты ---
const commits = ref([
{ id: 1, message: 'Добавлен график активности', date: '2025-05-28' },
{ id: 2, message: 'Исправлена верстка карточек', date: '2025-05-27' },
{ id: 3, message: 'Обновлены данные команды', date: '2025-05-26' },
{ id: 4, message: 'Оптимизирован компонент участников', date: '2025-05-25' },
])
function getActivityColor(count) {
if (count === 0) return '#ddd'
const opacity = 0.15 + (count / 5) * 0.6
return `rgba(124, 58, 237, ${opacity.toFixed(2)})`
}
</script>
<template>
<div class="home-page q-pa-md q-gutter-md">
<h4 class="text-center">Добро пожаловать!</h4>
<div v-if="isAuthenticated" class="q-gutter-sm">
<q-btn label="Редактировать профиль" color="secondary" />
<q-btn label="Выход" color="negative" @click="logout" />
</div>
<q-btn
v-else
label="Авторизация"
color="primary"
@click="goToLogin"
/>
</div>
</template>
<style scoped>
.home-page {
.bg-violet-strong {
background: linear-gradient(135deg, #4f046f 0%, #7c3aed 100%);
min-height: 100vh;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: #3e2465;
overflow-x: hidden;
}
.team-logo {
background: #fff;
border-radius: 50%;
width: 140px;
height: 140px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
gap: 20px;
transition: transform 0.3s ease;
}
</style>
.team-logo:hover {
transform: scale(1.1);
box-shadow: 0 0 14px #a287ffaa;
}
.team-name-card {
border-radius: 20px;
background: #ede9fe;
box-shadow:
0 8px 24px rgba(124, 58, 237, 0.18),
0 2px 8px rgba(124, 58, 237, 0.12);
max-width: 900px;
}
.violet-card {
border-radius: 22px;
background: #ede9fe;
box-shadow:
0 8px 24px rgba(124, 58, 237, 0.18),
0 2px 8px rgba(124, 58, 237, 0.12);
transition: box-shadow 0.3s ease, transform 0.3s ease;
}
.violet-card:hover {
box-shadow:
0 14px 40px rgba(124, 58, 237, 0.30),
0 6px 16px rgba(124, 58, 237, 0.20);
transform: translateY(-6px);
cursor: pointer;
}
.member-card, .contest-card {
min-height: 140px;
padding: 8px 12px;
display: flex;
flex-direction: column;
justify-content: center;
}
.horizontal-scroll {
display: flex;
overflow-x: auto;
padding-bottom: 6px;
-webkit-overflow-scrolling: touch;
}
.horizontal-scroll::-webkit-scrollbar {
height: 6px;
}
.horizontal-scroll::-webkit-scrollbar-thumb {
background: rgba(124, 58, 237, 0.4);
border-radius: 3px;
}
.activity-card {
max-width: 920px;
border-radius: 20px;
padding: 16px;
}
.activity-container {
/* align-items: center; */
}
.dates-column {
display: flex;
flex-direction: column;
justify-content: flex-start;
color: #5e35b1;
font-weight: 600;
user-select: none;
font-size: 11px;
}
.activity-date {
margin-bottom: 4px;
text-align: right;
white-space: nowrap;
}
.activity-grid {
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
user-select: none;
scrollbar-width: thin;
scrollbar-color: rgba(124, 58, 237, 0.4) transparent;
}
.activity-grid::-webkit-scrollbar {
height: 10px;
}
.activity-grid::-webkit-scrollbar-thumb {
background: rgba(124, 58, 237, 0.4);
border-radius: 5px;
}
.activity-square {
width: 16px;
height: 16px;
border-radius: 4px;
margin-right: 2px;
box-shadow: 0 0 3px rgba(124, 58, 237, 0.3);
cursor: default;
transition: background-color 0.3s ease;
}
.commits-list {
background: #f5f3ff;
border-radius: 12px;
padding: 8px 12px;
font-family: 'Courier New', Courier, monospace;
color: #4b0082;
box-shadow: inset 0 0 6px #c7b3f7;
}
.commit-item strong {
color: #7c3aed;
}
</style>

View File

@ -61,20 +61,16 @@ const authorisation = async () => {
icon: 'check_circle'
})
console.log('Role ID:', roleId)
if (roleId === 1) {
console.log('Переход на /admin')
router.push('/admin')
await router.push('/admin')
} else {
console.log('Переход на /')
router.push('/')
await router.push('/')
}
if (roleId === 1) {
router.push('/admin')
await router.push('/admin')
} else {
router.push('/')
await router.push('/')
}
} catch (error) {
Notify.create({
@ -230,13 +226,6 @@ const authorisation = async () => {
100% { transform: translateX(calc(-1 * var(--marquee-width, 370px))); }
}
.q-input,
.q-field__control,
.q-field__native,
.q-field__inner {
border-radius: 14px !important;
}
.btn-rounded {
border-radius: 16px;
}
@ -279,12 +268,6 @@ const authorisation = async () => {
background: #ede9fe !important;
}
.lavender-input .q-field__control,
.lavender-input .q-field__native,
.lavender-input .q-field__inner {
background: #f6f0ff !important;
}
.text-violet-strong {
color: #4f046f;
}