This commit is contained in:
F4ilji
2026-04-07 17:54:26 +05:00
parent 04adba6682
commit 936b1fb5b0
468 changed files with 43498 additions and 4176 deletions
@@ -0,0 +1,89 @@
<template>
<div v-if="posts.length > 0" class="bg-layer border border-layer-line rounded-lg shadow-xs">
<div class="p-5 border-b border-layer-line">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<div class="w-8 h-8 bg-purple-500/10 rounded-lg flex items-center justify-center">
<DashboardIcon name="sparkles" size="4" class="text-purple-600" />
</div>
<div>
<h3 class="text-sm font-semibold text-foreground">AI подготовленные публикации</h3>
<p class="text-xs text-muted-foreground-1">Генерированы автоматически</p>
</div>
</div>
<span class="inline-flex items-center justify-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-500/10 text-purple-700">
{{ posts.length }}
</span>
</div>
</div>
<ul class="divide-y divide-layer-line">
<li
v-for="post in posts"
:key="post.id"
class="p-4 hover:bg-muted/30 transition-colors"
>
<div class="flex items-start justify-between gap-3">
<div class="flex-1 min-w-0">
<h4 class="text-sm font-medium text-foreground truncate mb-1">
{{ post.title }}
</h4>
<p v-if="post.preview_text" class="text-xs text-muted-foreground-1 line-clamp-2">
{{ post.preview_text }}
</p>
<div class="flex items-center gap-2 mt-2">
<span
class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium"
:class="{
'bg-amber-500/10 text-amber-700': post.status === 'verification',
'bg-gray-500/10 text-gray-700': post.status === 'rejected',
}"
>
{{ STATUS_LABEL(post.status) }}
</span>
<span class="text-xs text-muted-foreground-3"></span>
<span class="text-xs text-muted-foreground-1">{{ FORMAT_DATE(post.created_at, 'short') }}</span>
</div>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<Link
:href="route('dashboard.posts.edit', post.id)"
class="inline-flex items-center px-3 py-1.5 border border-primary/20 text-xs font-medium rounded-md text-primary hover:bg-primary/5 transition-colors"
>
Редактировать
</Link>
</div>
</div>
</li>
</ul>
<div class="p-4 border-t border-layer-line">
<Link
:href="route('dashboard.posts.ai-prepared')"
class="text-sm text-primary hover:text-primary/80 transition-colors font-medium"
>
Посмотреть все
</Link>
</div>
</div>
</template>
<script>
import { Link } from '@inertiajs/vue3';
import DashboardIcon from '../DashboardIcon.vue';
export default {
name: 'AiPreparedWidget',
components: {
Link,
DashboardIcon,
},
props: {
posts: {
type: Array,
required: true,
default: () => [],
},
},
}
</script>
@@ -0,0 +1,48 @@
<template>
<nav aria-label="Навигация" class="mb-4">
<ol class="flex items-center gap-1.5 text-sm text-muted-foreground-1 flex-wrap">
<li>
<Link
:href="route('dashboard.index')"
class="inline-flex items-center gap-1 text-muted-foreground-1 hover:text-primary transition-colors"
>
<DashboardIcon name="home" size="4" />
<span>Главная</span>
</Link>
</li>
<li v-for="(crumb, index) in crumbs" :key="index" class="flex items-center gap-1.5">
<DashboardIcon name="chevron-right" size="3" class="text-muted-foreground-2" />
<template v-if="crumb.href && index < crumbs.length - 1">
<Link
:href="crumb.href"
class="text-muted-foreground-1 hover:text-primary transition-colors"
>
{{ crumb.label }}
</Link>
</template>
<template v-else>
<span class="text-foreground font-medium">{{ crumb.label }}</span>
</template>
</li>
</ol>
</nav>
</template>
<script>
import { Link } from '@inertiajs/vue3';
import DashboardIcon from '../DashboardIcon.vue';
export default {
name: 'Breadcrumbs',
components: {
Link,
DashboardIcon,
},
props: {
crumbs: {
type: Array,
default: () => [],
},
},
}
</script>
@@ -0,0 +1,48 @@
<template>
<div class="bg-layer border border-layer-line rounded-lg shadow-xs mb-6">
<div class="p-4 border-b border-line-2">
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-muted-foreground-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
</svg>
<h2 class="text-sm font-medium text-foreground">{{ title }}</h2>
</div>
</div>
<div class="p-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<slot></slot>
<!-- Reset Button -->
<div class="flex items-end">
<button
type="button"
@click="$emit('reset')"
class="w-full inline-flex items-center justify-center gap-2 px-4 py-2 bg-surface border border-layer-line text-foreground text-sm font-medium rounded-lg hover:bg-muted-hover transition-all duration-200"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
{{ resetText }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'DataFilters',
props: {
title: {
type: String,
default: 'Фильтры'
},
resetText: {
type: String,
default: 'Сбросить фильтры'
}
},
emits: ['reset']
}
</script>
@@ -0,0 +1,84 @@
<template>
<tr>
<td :colspan="columns" class="px-6 py-16 text-center">
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-surface border border-layer-line flex items-center justify-center">
<DashboardIcon
v-if="iconName"
:name="iconName"
size="8"
class="text-muted-foreground-2"
/>
<svg v-else class="w-8 h-8 text-muted-foreground-2" fill="none" stroke="currentColor" :viewBox="iconViewBox">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="iconPath" />
</svg>
</div>
<p class="text-foreground font-medium">{{ title }}</p>
<p class="text-sm text-muted-foreground-1 mt-1 mb-4">{{ description }}</p>
<a
v-if="actionUrl"
:href="actionUrl"
class="inline-flex items-center gap-2 px-4 py-2 bg-primary text-white text-sm font-medium rounded-lg hover:bg-primary-hover transition-all"
>
<DashboardIcon name="plus" size="4" color="currentColor" />
{{ actionText }}
</a>
<button
v-else-if="onAction"
@click="onAction"
class="inline-flex items-center gap-2 px-4 py-2 bg-primary text-white text-sm font-medium rounded-lg hover:bg-primary-hover transition-all"
>
<DashboardIcon name="plus" size="4" color="currentColor" />
{{ actionText }}
</button>
</td>
</tr>
</template>
<script>
import DashboardIcon from '../DashboardIcon.vue';
export default {
name: 'EmptyState',
components: {
DashboardIcon,
},
props: {
columns: {
type: Number,
default: 6
},
title: {
type: String,
default: 'Ничего не найдено'
},
description: {
type: String,
default: 'Попробуйте изменить параметры поиска или создайте новую запись'
},
actionUrl: {
type: String,
default: null
},
actionText: {
type: String,
default: 'Создать'
},
onAction: {
type: Function,
default: null
},
iconName: {
type: String,
default: null
},
iconPath: {
type: String,
default: 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10'
},
iconViewBox: {
type: String,
default: '0 0 24 24'
}
}
}
</script>
@@ -0,0 +1,95 @@
<template>
<div>
<!-- Success Message -->
<transition
enter-active-class="transition duration-300 ease-out"
enter-from-class="opacity-0 -translate-y-2"
enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition duration-200 ease-in"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 -translate-y-2"
>
<div v-if="$page.props.flash?.success" class="mb-4 p-4 bg-emerald-500/10 border border-emerald-500/20 rounded-lg">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 text-emerald-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-sm text-foreground font-medium">{{ $page.props.flash.success }}</span>
</div>
</div>
</transition>
<!-- Error Message -->
<transition
enter-active-class="transition duration-300 ease-out"
enter-from-class="opacity-0 -translate-y-2"
enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition duration-200 ease-in"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 -translate-y-2"
>
<div v-if="$page.props.flash?.error" class="mb-4 p-4 bg-rose-500/10 border border-rose-500/20 rounded-lg">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 text-rose-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-sm text-foreground font-medium">{{ $page.props.flash.error }}</span>
</div>
</div>
</transition>
<!-- Local Success Message -->
<transition
enter-active-class="transition duration-300 ease-out"
enter-from-class="opacity-0 -translate-y-2"
enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition duration-200 ease-in"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 -translate-y-2"
>
<div v-if="localSuccess" class="mb-4 p-4 bg-emerald-500/10 border border-emerald-500/20 rounded-lg">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 text-emerald-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-sm text-foreground font-medium">{{ localSuccess }}</span>
</div>
</div>
</transition>
<!-- Local Error Message -->
<transition
enter-active-class="transition duration-300 ease-out"
enter-from-class="opacity-0 -translate-y-2"
enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition duration-200 ease-in"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 -translate-y-2"
>
<div v-if="localError" class="mb-4 p-4 bg-rose-500/10 border border-rose-500/20 rounded-lg">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 text-rose-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-sm text-foreground font-medium">{{ localError }}</span>
</div>
</div>
</transition>
</div>
</template>
<script>
export default {
name: 'FlashMessages',
props: {
localSuccess: {
type: String,
default: null
},
localError: {
type: String,
default: null
}
}
}
</script>
@@ -0,0 +1,41 @@
<template>
<div v-if="data.last_page > 1" class="px-6 py-4 border-t border-line-2 bg-surface/50">
<div class="flex items-center justify-between">
<div class="text-sm text-muted-foreground-1">
Показано <span class="font-medium text-foreground">{{ data.from }}</span> <span class="font-medium text-foreground">{{ data.to }}</span> из <span class="font-medium text-foreground">{{ data.total }}</span>
</div>
<div class="flex items-center gap-2">
<template v-for="(link, index) in data.links" :key="index">
<a
v-if="link.url"
:href="link.url"
:class="[
'px-3 py-1.5 text-sm font-medium rounded-lg border transition-all duration-200',
link.active
? 'bg-primary text-white border-primary'
: 'bg-surface text-foreground border-layer-line hover:bg-muted-hover hover:border-primary/30'
]"
v-html="link.label"
></a>
<span
v-else
class="px-3 py-1.5 text-sm text-muted-foreground-2 bg-surface border border-layer-line rounded-lg"
v-html="link.label"
></span>
</template>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Pagination',
props: {
data: {
type: Object,
required: true
}
}
}
</script>
@@ -0,0 +1,83 @@
<template>
<div class="bg-layer border border-layer-line rounded-lg shadow-xs p-5">
<h3 class="text-sm font-semibold text-foreground mb-4">Быстрые действия</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<Link
v-for="action in actions"
:key="action.route"
:href="route(action.route)"
class="group flex items-center gap-3 p-3 rounded-lg border border-transparent transition-all duration-150"
:class="action.hoverClass"
>
<div class="w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0 transition-colors" :class="action.bgClass">
<DashboardIcon :name="action.icon" size="4" :class="action.iconClass" />
</div>
<div>
<p class="text-sm font-medium text-foreground transition-colors" :class="action.textClass">
{{ action.label }}
</p>
<p class="text-xs text-muted-foreground-1">{{ action.desc }}</p>
</div>
</Link>
</div>
</div>
</template>
<script>
import { Link } from '@inertiajs/vue3';
import DashboardIcon from '../DashboardIcon.vue';
export default {
name: 'QuickActions',
components: {
Link,
DashboardIcon,
},
data() {
return {
actions: [
{
route: 'dashboard.posts.create',
label: 'Создать новость',
desc: 'Новая публикация на сайт',
icon: 'plus',
bgClass: 'bg-blue-500/10 group-hover:bg-blue-500/20',
iconClass: 'text-blue-600',
textClass: 'group-hover:text-primary',
hoverClass: 'hover:border-primary/20 hover:bg-primary/5',
},
{
route: 'dashboard.schedules.upload.create',
label: 'Загрузить расписание',
desc: 'Массовая загрузка файлов',
icon: 'arrow-up-tray',
bgClass: 'bg-emerald-500/10 group-hover:bg-emerald-500/20',
iconClass: 'text-emerald-600',
textClass: 'group-hover:text-emerald-600',
hoverClass: 'hover:border-emerald-500/20 hover:bg-emerald-500/5',
},
{
route: 'dashboard.educational-groups.create',
label: 'Добавить группу',
desc: 'Учебная группа',
icon: 'user-group',
bgClass: 'bg-violet-500/10 group-hover:bg-violet-500/20',
iconClass: 'text-violet-600',
textClass: 'group-hover:text-violet-600',
hoverClass: 'hover:border-violet-500/20 hover:bg-violet-500/5',
},
{
route: 'dashboard.quick-upload.create',
label: 'Быстрая загрузка',
desc: 'Загрузка файлов',
icon: 'paper-clip',
bgClass: 'bg-amber-500/10 group-hover:bg-amber-500/20',
iconClass: 'text-amber-600',
textClass: 'group-hover:text-amber-600',
hoverClass: 'hover:border-amber-500/20 hover:bg-amber-500/5',
},
],
};
},
}
</script>
@@ -0,0 +1,136 @@
<template>
<div class="bg-layer border border-layer-line rounded-lg shadow-xs">
<div class="p-5 border-b border-layer-line">
<h3 class="text-sm font-semibold text-foreground">Последняя активность</h3>
</div>
<div class="divide-y divide-layer-line">
<!-- Recent Posts -->
<div v-if="recentActivity.recent_posts.length > 0" class="p-5">
<div class="flex items-center justify-between mb-3">
<h4 class="text-xs font-medium text-muted-foreground-1 uppercase tracking-wide">Публикации</h4>
<Link :href="route('dashboard.posts.index')" class="text-xs text-primary hover:text-primary/80 transition-colors">
Все новости
</Link>
</div>
<ul class="space-y-2">
<li v-for="post in recentActivity.recent_posts" :key="post.id" class="flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
<div
class="w-2 h-2 rounded-full"
:class="{
'bg-amber-500': post.status === 'verification',
'bg-emerald-500': post.status === 'published',
'bg-gray-400': post.status === 'rejected',
}"
></div>
</div>
<div class="flex-1 min-w-0">
<Link
:href="route('dashboard.posts.edit', post.id)"
class="text-sm font-medium text-foreground hover:text-primary transition-colors truncate block"
>
{{ post.title }}
</Link>
<div class="flex items-center gap-2 mt-1">
<span class="text-xs text-muted-foreground-1">{{ FORMAT_DATE(post.created_at, 'short') }}</span>
<span
class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium"
:class="{
'bg-amber-500/10 text-amber-700': post.status === 'verification',
'bg-emerald-500/10 text-emerald-700': post.status === 'published',
'bg-gray-500/10 text-gray-700': post.status === 'rejected',
}"
>
{{ STATUS_LABEL(post.status) }}
</span>
</div>
</div>
</li>
</ul>
</div>
<!-- Recent Schedules -->
<div v-if="recentActivity.recent_schedules.length > 0" class="p-5">
<div class="flex items-center justify-between mb-3">
<h4 class="text-xs font-medium text-muted-foreground-1 uppercase tracking-wide">Расписания</h4>
<Link :href="route('dashboard.schedules.index')" class="text-xs text-primary hover:text-primary/80 transition-colors">
Все расписания
</Link>
</div>
<ul class="space-y-2">
<li v-for="schedule in recentActivity.recent_schedules" :key="schedule.id" class="flex items-center justify-between">
<div class="flex items-center gap-3 min-w-0">
<DashboardIcon name="calendar" size="4" class="text-emerald-600 flex-shrink-0" />
<span class="text-sm text-foreground truncate">
{{ schedule.educational_group?.title || 'Без группы' }}
</span>
</div>
<span class="text-xs text-muted-foreground-1 flex-shrink-0">{{ FORMAT_DATE(schedule.created_at, 'short') }}</span>
</li>
</ul>
</div>
<!-- Recent Sliders -->
<div v-if="recentActivity.recent_sliders.length > 0" class="p-5">
<div class="flex items-center justify-between mb-3">
<h4 class="text-xs font-medium text-muted-foreground-1 uppercase tracking-wide">Слайдеры</h4>
<Link :href="route('dashboard.sliders.index')" class="text-xs text-primary hover:text-primary/80 transition-colors">
Все слайдеры
</Link>
</div>
<ul class="space-y-2">
<li v-for="slider in recentActivity.recent_sliders" :key="slider.id" class="flex items-center justify-between">
<div class="flex items-center gap-3 min-w-0">
<DashboardIcon name="photo" size="4" class="text-rose-600 flex-shrink-0" />
<div class="min-w-0">
<span class="text-sm text-foreground block truncate">{{ slider.title }}</span>
<span class="text-xs text-muted-foreground-1">{{ slider.slides_count }} слайд(ов)</span>
</div>
</div>
<span class="text-xs text-muted-foreground-1 flex-shrink-0">{{ FORMAT_DATE(slider.created_at, 'short') }}</span>
</li>
</ul>
</div>
<!-- Empty State -->
<div v-if="isEmpty" class="p-8 text-center">
<DashboardIcon name="clock" size="8" class="text-muted-foreground-3 mx-auto mb-3" />
<p class="text-sm text-muted-foreground-1">Пока нет активности</p>
</div>
</div>
</div>
</template>
<script>
import { Link } from '@inertiajs/vue3';
import DashboardIcon from '../DashboardIcon.vue';
export default {
name: 'RecentActivity',
components: {
Link,
DashboardIcon,
},
props: {
recentActivity: {
type: Object,
required: true,
default: () => ({
recent_posts: [],
recent_schedules: [],
recent_sliders: [],
}),
},
},
computed: {
isEmpty() {
return (
this.recentActivity.recent_posts.length === 0 &&
this.recentActivity.recent_schedules.length === 0 &&
this.recentActivity.recent_sliders.length === 0
);
},
},
}
</script>
@@ -0,0 +1,46 @@
<template>
<div>
<label :for="id" class="block text-xs font-medium text-muted-foreground-1 mb-1.5">
{{ label }}
</label>
<div class="relative">
<input
:id="id"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
@keyup.enter="$emit('search')"
type="text"
:placeholder="placeholder"
class="w-full pl-9 pr-4 py-2 bg-surface border border-layer-line rounded-lg text-sm text-foreground placeholder-muted-foreground-2 focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all"
/>
<svg class="absolute left-3 top-2.5 w-4 h-4 text-muted-foreground-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
</div>
</template>
<script>
export default {
name: 'SearchInput',
props: {
modelValue: {
type: String,
default: ''
},
label: {
type: String,
default: 'Поиск'
},
placeholder: {
type: String,
default: 'Введите текст...'
},
id: {
type: String,
default: 'search'
}
},
emits: ['update:modelValue', 'search']
}
</script>
@@ -0,0 +1,41 @@
<template>
<div>
<label :for="id" class="block text-xs font-medium text-muted-foreground-1 mb-1.5">
{{ label }}
</label>
<select
:id="id"
:value="modelValue"
@change="$emit('update:modelValue', $event.target.value); $emit('change')"
class="w-full px-3 py-2 bg-surface border border-layer-line rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all"
>
<option value="">{{ placeholder }}</option>
<slot></slot>
</select>
</div>
</template>
<script>
export default {
name: 'SelectFilter',
props: {
modelValue: {
type: [String, Number],
default: ''
},
label: {
type: String,
default: 'Фильтр'
},
placeholder: {
type: String,
default: 'Все'
},
id: {
type: String,
default: 'filter'
}
},
emits: ['update:modelValue', 'change']
}
</script>
@@ -0,0 +1,104 @@
<template>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div
v-for="card in statCards"
:key="card.label"
class="bg-layer border border-layer-line rounded-lg p-5 shadow-xs"
>
<div class="flex items-center justify-between mb-3">
<div class="w-10 h-10 rounded-lg flex items-center justify-center" :class="card.bgClass">
<DashboardIcon :name="card.icon" size="5" :class="card.iconClass" />
</div>
<span
v-if="card.weekCount > 0"
class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium"
:class="card.badgeClass"
>
+{{ card.weekCount }}
</span>
</div>
<div class="space-y-1">
<p class="text-2xl font-semibold text-foreground">{{ card.total }}</p>
<p class="text-xs text-muted-foreground-1">{{ card.label }}</p>
</div>
<div v-if="card.link" class="mt-3 pt-3 border-t border-layer-line">
<a
:href="route(card.link.route)"
class="text-xs font-medium transition-colors"
:class="card.link.class"
>
{{ card.link.text }}
</a>
</div>
</div>
</div>
</template>
<script>
import DashboardIcon from '../DashboardIcon.vue';
export default {
name: 'StatsOverview',
components: {
DashboardIcon,
},
props: {
stats: {
type: Object,
required: true,
},
},
computed: {
statCards() {
return [
{
icon: 'document',
bgClass: 'bg-blue-500/10',
iconClass: 'text-blue-600',
badgeClass: 'bg-blue-500/10 text-blue-700',
total: this.stats.posts.total,
weekCount: this.stats.posts.week,
label: 'Всего публикаций',
link: this.stats.posts.verification > 0
? {
route: 'dashboard.posts.index',
text: `${this.stats.posts.verification} на модерации →`,
class: 'text-amber-600 hover:text-amber-700',
}
: null,
},
{
icon: 'calendar',
bgClass: 'bg-emerald-500/10',
iconClass: 'text-emerald-600',
badgeClass: 'bg-emerald-500/10 text-emerald-700',
total: this.stats.schedules.total,
weekCount: this.stats.schedules.week,
label: 'Расписаний',
link: null,
},
{
icon: 'academic-cap',
bgClass: 'bg-violet-500/10',
iconClass: 'text-violet-600',
badgeClass: 'bg-violet-500/10 text-violet-700',
total: this.stats.educational_groups.total,
weekCount: this.stats.educational_groups.week,
label: 'Учебных групп',
link: null,
},
{
icon: 'photo',
bgClass: 'bg-rose-500/10',
iconClass: 'text-rose-600',
badgeClass: 'bg-rose-500/10 text-rose-700',
total: this.stats.sliders.total,
weekCount: this.stats.sliders.week,
label: 'Слайдеров',
link: null,
},
];
},
},
}
</script>