changes
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user