feat: Добавил управление ролями пользователей в админке
Frontend CI / build-and-check (push) Failing after 5m12s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 14s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 16s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 22s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 11s
Frontend CI / build-and-check (push) Failing after 5m12s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 14s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 16s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 22s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 11s
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { usersApi } from '@/api'
|
import { usersApi } from '@/api'
|
||||||
import type { UserDto } from '@/api/types'
|
import type { ApiUserRole, UserDto } from '@/api/types'
|
||||||
import GlassCard from '@/components/ui/GlassCard.vue'
|
import GlassCard from '@/components/ui/GlassCard.vue'
|
||||||
import DataTable from '@/components/ui/DataTable.vue'
|
import DataTable from '@/components/ui/DataTable.vue'
|
||||||
import EmptyState from '@/components/ui/EmptyState.vue'
|
import EmptyState from '@/components/ui/EmptyState.vue'
|
||||||
@@ -23,14 +23,14 @@ const columns = [
|
|||||||
{ key: 'actions', label: 'Действия', align: 'right' },
|
{ key: 'actions', label: 'Действия', align: 'right' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const allRoles: ApiUserRole[] = ['Student', 'Teacher', 'Admin']
|
||||||
const roleLabels = { Student: 'Студент', Teacher: 'Преподаватель', Admin: 'Администратор' } as const
|
const roleLabels = { Student: 'Студент', Teacher: 'Преподаватель', Admin: 'Администратор' } as const
|
||||||
const roleApi = { Студент: 'Student', Преподаватель: 'Teacher', Администратор: 'Admin' } as const
|
const roleApi = { Студент: 'Student', Преподаватель: 'Teacher', Администратор: 'Admin' } as const
|
||||||
type ApiUserRole = 'Student' | 'Teacher' | 'Admin'
|
const roleBadgeClasses: Record<ApiUserRole, string> = {
|
||||||
const roleSetSequence: ApiUserRole[][] = [
|
Student: 'badge-green',
|
||||||
['Student'],
|
Teacher: 'badge-blue',
|
||||||
['Student', 'Teacher'],
|
Admin: 'badge-purple',
|
||||||
['Student', 'Teacher', 'Admin'],
|
}
|
||||||
]
|
|
||||||
|
|
||||||
const rows = computed(() =>
|
const rows = computed(() =>
|
||||||
users.value.map(user => ({
|
users.value.map(user => ({
|
||||||
@@ -69,17 +69,31 @@ async function toggleActive(row: Record<string, unknown>) {
|
|||||||
await fetchUsers()
|
await fetchUsers()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function promoteRole(row: Record<string, unknown>) {
|
function getRowRoles(row: Record<string, unknown>) {
|
||||||
|
const apiRoles = Array.isArray(row.apiRoles) ? row.apiRoles : []
|
||||||
|
return Array.from(
|
||||||
|
new Set(apiRoles.filter((role): role is ApiUserRole => allRoles.includes(role as ApiUserRole))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasRole(row: Record<string, unknown>, role: ApiUserRole) {
|
||||||
|
return getRowRoles(row).includes(role)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRoleChipClass(row: Record<string, unknown>, role: ApiUserRole) {
|
||||||
|
return hasRole(row, role) ? ['badge', roleBadgeClasses[role]] : ['btn-ghost', 'role-chip-inactive']
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleRole(row: Record<string, unknown>, role: ApiUserRole) {
|
||||||
const id = Number(row.id)
|
const id = Number(row.id)
|
||||||
const apiRoles = (Array.isArray(row.apiRoles) ? row.apiRoles : []) as ApiUserRole[]
|
const currentRoles = getRowRoles(row)
|
||||||
const currentKey = [...new Set(apiRoles)].sort().join(',')
|
const nextRoles = currentRoles.includes(role)
|
||||||
const currentIndex = roleSetSequence.findIndex(set => set.slice().sort().join(',') === currentKey)
|
? currentRoles.filter(currentRole => currentRole !== role)
|
||||||
const next: ApiUserRole[] = (
|
: [...currentRoles, role]
|
||||||
currentIndex >= 0
|
|
||||||
? roleSetSequence[(currentIndex + 1) % roleSetSequence.length]
|
if (!nextRoles.length) return
|
||||||
: roleSetSequence[0]
|
|
||||||
) ?? ['Student']
|
await usersApi.setRole(id, nextRoles)
|
||||||
await usersApi.setRole(id, next)
|
|
||||||
await fetchUsers()
|
await fetchUsers()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,15 +127,25 @@ onMounted(fetchUsers)
|
|||||||
<EmptyState v-if="error" title="Не удалось загрузить пользователей" :subtitle="error" />
|
<EmptyState v-if="error" title="Не удалось загрузить пользователей" :subtitle="error" />
|
||||||
<EmptyState v-else-if="!rows.length && !loading" title="Пользователей не найдено" subtitle="Попробуйте изменить фильтры." />
|
<EmptyState v-else-if="!rows.length && !loading" title="Пользователей не найдено" subtitle="Попробуйте изменить фильтры." />
|
||||||
<DataTable :columns="columns" :rows="rows">
|
<DataTable :columns="columns" :rows="rows">
|
||||||
<template #role="{ value }">
|
<template #role="{ row }">
|
||||||
<span :class="value === 'Студент' ? 'badge badge-green' : value === 'Преподаватель' ? 'badge badge-blue' : 'badge badge-purple'">{{ value }}</span>
|
<div class="role-chips">
|
||||||
|
<button
|
||||||
|
v-for="role in allRoles"
|
||||||
|
:key="role"
|
||||||
|
class="role-chip"
|
||||||
|
:class="getRoleChipClass(row, role)"
|
||||||
|
type="button"
|
||||||
|
@click="toggleRole(row, role)"
|
||||||
|
>
|
||||||
|
{{ roleLabels[role] }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #activity="{ value }">
|
<template #activity="{ value }">
|
||||||
<span class="badge" :class="value === 'Активен' ? 'badge-green' : 'badge-orange'">{{ value }}</span>
|
<span class="badge" :class="value === 'Активен' ? 'badge-green' : 'badge-orange'">{{ value }}</span>
|
||||||
</template>
|
</template>
|
||||||
<template #actions="{ row }">
|
<template #actions="{ row }">
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn-ghost" @click="promoteRole(row)">Назначить роль</button>
|
|
||||||
<button class="btn-ghost" @click="toggleActive(row)">{{ row.isActive ? 'Заблокировать' : 'Активировать' }}</button>
|
<button class="btn-ghost" @click="toggleActive(row)">{{ row.isActive ? 'Заблокировать' : 'Активировать' }}</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -135,4 +159,9 @@ onMounted(fetchUsers)
|
|||||||
.header { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
|
.header { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
|
||||||
.filters { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 12px; }
|
.filters { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 12px; }
|
||||||
.actions { display: flex; gap: 6px; justify-content: flex-end; flex-wrap: wrap; }
|
.actions { display: flex; gap: 6px; justify-content: flex-end; flex-wrap: wrap; }
|
||||||
|
.role-chips { display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; }
|
||||||
|
.role-chip { cursor: pointer; border: 0; transition: transform 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease; }
|
||||||
|
.role-chip:hover { transform: translateY(-1px); box-shadow: 0 4px 12px var(--color-primary-a15); }
|
||||||
|
.role-chip:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
|
||||||
|
.role-chip-inactive { opacity: 0.75; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user