711 lines
29 KiB
Vue
711 lines
29 KiB
Vue
<template>
|
|
<DashboardLayout>
|
|
<template #header-icon>
|
|
<DashboardIcon name="document-text" size="5" class="text-primary" />
|
|
</template>
|
|
<template #header-title>Новости</template>
|
|
<template #header-subtitle>Управление публикациями</template>
|
|
<template #header-actions>
|
|
<a
|
|
:href="route('dashboard.posts.create')"
|
|
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 duration-200 shadow-sm hover:shadow-md"
|
|
>
|
|
<DashboardIcon name="plus" size="4" />
|
|
Создать новость
|
|
</a>
|
|
</template>
|
|
|
|
<template #breadcrumbs>
|
|
<Breadcrumbs :crumbs="[{ label: 'Новости', href: route('dashboard.posts.index') }]" />
|
|
</template>
|
|
|
|
<!-- Flash Messages -->
|
|
<FlashMessages />
|
|
|
|
<!-- Created Post Notification -->
|
|
<transition
|
|
enter-active-class="transition duration-300"
|
|
enter-from-class="opacity-0 scale-95"
|
|
enter-to-class="opacity-100 scale-100"
|
|
leave-active-class="transition duration-200"
|
|
leave-from-class="opacity-100 scale-100"
|
|
leave-to-class="opacity-0 scale-95"
|
|
>
|
|
<div v-if="$page.props.flash?.created_post" class="mb-6 bg-layer border border-layer-line rounded-lg shadow-xs overflow-hidden">
|
|
<div class="bg-primary-50 px-4 py-3 border-b border-primary-100">
|
|
<div class="flex items-center gap-2">
|
|
<DashboardIcon name="document-text" size="5" class="text-primary" />
|
|
<h3 class="text-sm font-medium text-primary-foreground">Новость создана</h3>
|
|
</div>
|
|
</div>
|
|
<div class="p-4">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="space-y-2">
|
|
<div>
|
|
<p class="text-xs text-muted-foreground-1 mb-1">Заголовок</p>
|
|
<p class="text-sm font-medium text-foreground">{{ $page.props.flash.created_post.title }}</p>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<div>
|
|
<p class="text-xs text-muted-foreground-1 mb-1">Статус</p>
|
|
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-amber-50 text-amber-700 border border-amber-200">
|
|
{{ $page.props.flash.created_post.status }}
|
|
</span>
|
|
</div>
|
|
<div v-if="$page.props.flash.created_post.category">
|
|
<p class="text-xs text-muted-foreground-1 mb-1">Категория</p>
|
|
<p class="text-sm font-medium text-foreground">{{ $page.props.flash.created_post.category.title }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="$page.props.flash.created_post.preview" class="flex justify-center md:justify-end">
|
|
<img
|
|
:src="`/storage/${$page.props.flash.created_post.preview}`"
|
|
alt="Preview"
|
|
class="h-28 w-auto rounded-lg border border-layer-line object-cover"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 flex flex-wrap gap-2">
|
|
<a
|
|
:href="`/admin/posts/${$page.props.flash.created_post.id}/edit`"
|
|
target="_blank" rel="external"
|
|
class="inline-flex items-center px-3 py-2 bg-primary text-white text-sm font-medium rounded-lg hover:bg-primary-hover transition-all"
|
|
>
|
|
<DashboardIcon name="pencil-square" size="4" class="mr-1.5" />
|
|
Редактировать
|
|
</a>
|
|
<button
|
|
@click.prevent="openPostModal($page.props.flash.created_post)"
|
|
class="inline-flex items-center px-3 py-2 border border-layer-line text-sm font-medium rounded-lg text-layer-foreground hover:bg-primary-50 transition-all"
|
|
>
|
|
<DashboardIcon name="eye" size="4" class="mr-1.5" />
|
|
Просмотр
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</transition>
|
|
|
|
<!-- Filters Card -->
|
|
<DataFilters title="Фильтры" @reset="resetFilters">
|
|
<SearchInput
|
|
v-model="searchQuery"
|
|
label="Поиск по заголовку"
|
|
placeholder="Введите название..."
|
|
@search="search"
|
|
/>
|
|
|
|
<SelectFilter
|
|
v-model="statusQuery"
|
|
label="Статус"
|
|
placeholder="Все статусы"
|
|
@change="filterByStatus"
|
|
>
|
|
<option v-for="status in [PostStatus.PUBLISHED, PostStatus.VERIFICATION, PostStatus.REJECTED]"
|
|
:key="status.value"
|
|
:value="status.value">
|
|
{{ status.label }}
|
|
</option>
|
|
</SelectFilter>
|
|
</DataFilters>
|
|
|
|
<!-- Posts Table -->
|
|
<div class="bg-layer border border-layer-line rounded-lg shadow-xs overflow-hidden">
|
|
<!-- Bulk Actions Bar -->
|
|
<transition
|
|
enter-active-class="transition duration-200"
|
|
enter-from-class="opacity-0 -translate-y-2"
|
|
enter-to-class="opacity-100 translate-y-0"
|
|
leave-active-class="transition duration-150"
|
|
leave-from-class="opacity-100 translate-y-0"
|
|
leave-to-class="opacity-0 -translate-y-2"
|
|
>
|
|
<div v-if="selectedPosts.length > 0" class="px-6 py-3 bg-primary/5 border-b border-primary/20 flex items-center justify-between">
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-sm font-medium text-primary">{{ selectedPosts.length }} выбрано</span>
|
|
<button
|
|
@click="selectAllOnPage"
|
|
class="text-xs text-muted-foreground-1 hover:text-primary transition-colors"
|
|
>
|
|
Выбрать все на странице
|
|
</button>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<button
|
|
@click="bulkPublish"
|
|
:disabled="bulkProcessing"
|
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-emerald-600 text-white text-xs font-medium rounded-lg hover:bg-emerald-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<DashboardIcon name="check-circle" size="4" />
|
|
Опубликовать
|
|
</button>
|
|
<button
|
|
@click="bulkSetVerification"
|
|
:disabled="bulkProcessing"
|
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-amber-600 text-white text-xs font-medium rounded-lg hover:bg-amber-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<DashboardIcon name="clock" size="4" />
|
|
На модерацию
|
|
</button>
|
|
<button
|
|
@click="bulkDelete"
|
|
:disabled="bulkProcessing"
|
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-rose-600 text-white text-xs font-medium rounded-lg hover:bg-rose-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<DashboardIcon name="trash" size="4" />
|
|
Удалить
|
|
</button>
|
|
<button
|
|
@click="clearSelection"
|
|
class="p-1.5 text-muted-foreground-1 hover:text-foreground hover:bg-muted-hover rounded-lg transition-all"
|
|
title="Снять выделение"
|
|
>
|
|
<DashboardIcon name="x-mark" size="4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</transition>
|
|
|
|
<!-- Table Header Stats -->
|
|
<div class="px-6 py-4 border-b border-line-2 bg-surface/50">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-4">
|
|
<span class="text-sm text-foreground">
|
|
Всего: <span class="font-medium">{{ posts.total }}</span>
|
|
</span>
|
|
<span class="text-xs text-muted-foreground-1 px-2 py-0.5 bg-primary/10 text-primary rounded-full">
|
|
{{ posts.data.length }} на странице
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Table -->
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-line-2">
|
|
<thead class="bg-surface/50">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground-1 uppercase tracking-wider w-10">
|
|
<input
|
|
type="checkbox"
|
|
:checked="isAllSelected"
|
|
:indeterminate.prop="isIndeterminate"
|
|
@change="toggleSelectAll"
|
|
class="w-4 h-4 text-primary border-layer-line rounded focus:ring-primary/20 cursor-pointer"
|
|
/>
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground-1 uppercase tracking-wider">
|
|
Материал
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground-1 uppercase tracking-wider">
|
|
Категория
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground-1 uppercase tracking-wider">
|
|
Статус
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground-1 uppercase tracking-wider">
|
|
Дата публикации
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground-1 uppercase tracking-wider">
|
|
Автор
|
|
</th>
|
|
<th class="px-6 py-3 text-right text-xs font-medium text-muted-foreground-1 uppercase tracking-wider">
|
|
Действия
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-line-2">
|
|
<tr
|
|
v-for="post in posts.data"
|
|
:key="post.id"
|
|
class="group hover:bg-muted-hover/50 transition-all duration-200"
|
|
:class="{ 'bg-primary/5': selectedPosts.includes(post.id) }"
|
|
>
|
|
<td class="px-6 py-4">
|
|
<input
|
|
type="checkbox"
|
|
:value="post.id"
|
|
v-model="selectedPosts"
|
|
class="w-4 h-4 text-primary border-layer-line rounded focus:ring-primary/20 cursor-pointer"
|
|
/>
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<div class="flex items-center gap-3">
|
|
<div v-if="post.preview" class="w-14 h-14 flex-shrink-0 overflow-hidden rounded-lg border border-layer-line">
|
|
<img
|
|
:src="`/storage/${post.preview}`"
|
|
:alt="post.title"
|
|
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-200"
|
|
/>
|
|
</div>
|
|
<div v-else class="w-14 h-14 flex-shrink-0 rounded-lg border border-layer-line bg-surface flex items-center justify-center">
|
|
<DashboardIcon name="photo" size="6" class="text-muted-foreground-2" />
|
|
</div>
|
|
<div class="min-w-0 flex-1">
|
|
<div class="text-sm font-medium text-foreground truncate group-hover:text-primary transition-colors" :title="post.title">
|
|
{{ TEXT_LIMIT(post.title, 60) }}
|
|
</div>
|
|
<div class="text-xs text-muted-foreground-2 font-mono mt-0.5">
|
|
{{ post.slug }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<span
|
|
v-if="post.category"
|
|
class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-primary/10 text-primary border border-primary/20"
|
|
>
|
|
{{ post.category.title }}
|
|
</span>
|
|
<span v-else class="text-xs text-muted-foreground-2">—</span>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<span :class="getStatusBadgeClass(post.status)">
|
|
<span class="w-1.5 h-1.5 rounded-full mr-1.5" :class="getStatusDotClass(post.status)"></span>
|
|
{{ getStatusLabel(post.status) }}
|
|
</span>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm text-foreground">{{ formatDate(post.publish_at) }}</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center">
|
|
<span class="text-xs font-medium text-primary">
|
|
{{ getAuthorInitials(post.author?.name) }}
|
|
</span>
|
|
</div>
|
|
<span class="text-sm text-foreground">
|
|
{{ post.author?.name || '—' }}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-right">
|
|
<div class="flex items-center justify-end gap-1.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<a
|
|
:href="route('dashboard.posts.edit', post.id)"
|
|
class="p-2 text-muted-foreground-1 hover:text-primary hover:bg-primary/10 rounded-lg transition-all"
|
|
title="Редактировать"
|
|
>
|
|
<DashboardIcon name="pencil-square" size="4" />
|
|
</a>
|
|
<button
|
|
@click.prevent="confirmDelete(post)"
|
|
class="p-2 text-muted-foreground-1 hover:text-rose-600 hover:bg-rose-500/10 rounded-lg transition-all"
|
|
title="Удалить"
|
|
>
|
|
<DashboardIcon name="trash" size="4" />
|
|
</button>
|
|
<button
|
|
@click.prevent="openPostModal(post)"
|
|
class="p-2 text-muted-foreground-1 hover:text-primary hover:bg-primary/10 rounded-lg transition-all"
|
|
title="Просмотр"
|
|
>
|
|
<DashboardIcon name="eye" size="4" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
|
|
<!-- Empty State -->
|
|
<EmptyState
|
|
v-if="posts.data.length === 0"
|
|
:columns="6"
|
|
title="Новости не найдены"
|
|
description="Создайте первую новость или измените параметры поиска"
|
|
:action-url="route('dashboard.posts.create')"
|
|
action-text="Создать новость"
|
|
/>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<Pagination :data="posts" />
|
|
</div>
|
|
|
|
<!-- Post Modal -->
|
|
<transition
|
|
enter-active-class="transition duration-200"
|
|
enter-from-class="opacity-0"
|
|
enter-to-class="opacity-100"
|
|
leave-active-class="transition duration-150"
|
|
leave-from-class="opacity-100"
|
|
leave-to-class="opacity-0"
|
|
>
|
|
<div
|
|
v-if="selectedPost"
|
|
class="fixed inset-0 bg-slate-900/70 backdrop-blur-md z-50 flex items-center justify-center p-4"
|
|
@click.self="closePostModal"
|
|
>
|
|
<div class="bg-white border border-layer-line rounded-xl shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
|
<!-- Header -->
|
|
<div class="px-4 py-3 border-b border-card-line flex items-center justify-between">
|
|
<div class="flex items-center gap-3 flex-1 min-w-0">
|
|
<div class="w-9 h-9 bg-primary/10 rounded-lg flex items-center justify-center flex-shrink-0">
|
|
<DashboardIcon name="document-text" size="5" class="text-primary" />
|
|
</div>
|
|
<h3 class="text-sm font-medium text-foreground truncate">{{ selectedPost.title }}</h3>
|
|
</div>
|
|
<button
|
|
@click="closePostModal"
|
|
class="ml-3 text-muted-foreground-2 hover:text-foreground hover:bg-layer rounded-lg p-1.5 transition-all"
|
|
>
|
|
<DashboardIcon name="x-mark" size="5" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="p-4 overflow-y-auto flex-1">
|
|
<!-- Meta -->
|
|
<div class="flex flex-wrap gap-2 mb-4">
|
|
<span class="inline-flex items-center px-2.5 py-1.5 bg-layer text-muted-foreground-1 rounded-md text-xs font-medium border border-layer-line">
|
|
ID: {{ selectedPost.id }}
|
|
</span>
|
|
<span v-if="selectedPost.category" class="inline-flex items-center px-2.5 py-1.5 bg-primary-500/10 text-primary-700 rounded-md text-xs font-medium border border-primary-200">
|
|
<DashboardIcon name="tag" size="4" class="mr-1.5" />
|
|
{{ selectedPost.category.title }}
|
|
</span>
|
|
<span
|
|
v-if="selectedPost.status"
|
|
class="inline-flex items-center px-2.5 py-1.5 rounded-md text-xs font-medium uppercase border"
|
|
:class="{
|
|
'bg-amber-50 text-amber-700 border-amber-200': selectedPost.status === 'verification',
|
|
'bg-rose-50 text-rose-700 border-rose-200': selectedPost.status === 'rejected',
|
|
'bg-emerald-50 text-emerald-700 border-emerald-200': selectedPost.status === 'published'
|
|
}"
|
|
>
|
|
<span class="w-1.5 h-1.5 rounded-full mr-1.5" :class="{
|
|
'bg-amber-500': selectedPost.status === 'verification',
|
|
'bg-rose-500': selectedPost.status === 'rejected',
|
|
'bg-emerald-500': selectedPost.status === 'published'
|
|
}"></span>
|
|
{{ selectedPost.status === 'verification' ? 'На рассмотрении' : selectedPost.status === 'rejected' ? 'Отклонено' : selectedPost.status }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Preview -->
|
|
<div v-if="selectedPost.preview" class="mb-4">
|
|
<p class="text-xs font-medium text-muted-foreground-1 mb-2">Главное изображение</p>
|
|
<div class="rounded-lg overflow-hidden border border-layer-line">
|
|
<img
|
|
:src="`/storage/${selectedPost.preview}`"
|
|
alt="Preview"
|
|
class="w-full h-auto max-h-80 object-cover"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Gallery -->
|
|
<div v-if="selectedPost.images && selectedPost.images.length > 0" class="mb-4">
|
|
<p class="text-xs font-medium text-muted-foreground-1 mb-2">Галерея ({{ selectedPost.images.length }})</p>
|
|
<div class="grid grid-cols-3 sm:grid-cols-4 gap-2">
|
|
<div
|
|
v-for="(image, index) in selectedPost.images"
|
|
:key="index"
|
|
@click="viewImage(image)"
|
|
class="group relative rounded-lg overflow-hidden border border-layer-line aspect-square cursor-pointer hover:border-primary transition-all"
|
|
>
|
|
<img
|
|
:src="`/storage/${image}`"
|
|
alt="Gallery"
|
|
class="w-full h-full object-cover group-hover:scale-105 transition-transform"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Authors -->
|
|
<div v-if="selectedPost.authors" class="mb-4 p-3 bg-surface rounded-lg border border-layer-line">
|
|
<p class="text-xs font-medium text-muted-foreground-1 mb-1">Автор(ы)</p>
|
|
<p class="text-sm text-foreground">
|
|
{{ Array.isArray(selectedPost.authors) ? selectedPost.authors.join(', ') : selectedPost.authors }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Preview Text -->
|
|
<div v-if="selectedPost.preview_text" class="mb-4">
|
|
<p class="text-xs font-medium text-muted-foreground-1 mb-2">Превью</p>
|
|
<p class="text-sm text-foreground leading-relaxed">{{ selectedPost.preview_text }}</p>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="mb-4">
|
|
<p class="text-xs font-medium text-muted-foreground-1 mb-2">Полный текст</p>
|
|
<div class="prose prose-sm max-w-none bg-surface p-4 rounded-lg border border-layer-line">
|
|
<div v-html="renderContent(selectedPost.content)" class="text-foreground"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="border-t border-line-2 pt-3">
|
|
<div class="flex flex-wrap justify-between gap-3 text-xs text-muted-foreground-2">
|
|
<div class="flex flex-wrap gap-3">
|
|
<span class="flex items-center gap-1">
|
|
<DashboardIcon name="clock" size="4" />
|
|
{{ new Date(selectedPost.created_at).toLocaleString('ru-RU') }}
|
|
</span>
|
|
<span class="flex items-center gap-1">
|
|
<DashboardIcon name="arrow-path" size="4" />
|
|
{{ new Date(selectedPost.updated_at).toLocaleString('ru-RU') }}
|
|
</span>
|
|
</div>
|
|
<span v-if="selectedPost.reading_time" class="flex items-center gap-1">
|
|
<DashboardIcon name="book-open" size="4" />
|
|
{{ selectedPost.reading_time }} мин
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="px-4 py-3 border-t border-line-2 bg-surface flex flex-col sm:flex-row justify-between items-stretch sm:items-center gap-2">
|
|
<div class="flex items-center gap-3 flex-wrap">
|
|
<!-- Edit -->
|
|
<a
|
|
:href="`/admin/posts/${selectedPost.id}/edit`"
|
|
class="inline-flex items-center px-3 py-2 bg-primary text-white text-sm font-medium rounded-lg hover:bg-primary-hover transition-all"
|
|
>
|
|
<DashboardIcon name="pencil-square" size="4" class="mr-1.5" />
|
|
Редактировать
|
|
</a>
|
|
</div>
|
|
<button
|
|
@click="closePostModal"
|
|
class="w-full sm:w-auto px-3 py-2 border border-layer-line text-sm font-medium rounded-lg text-layer-foreground hover:bg-layer transition-all"
|
|
>
|
|
Закрыть
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</transition>
|
|
</DashboardLayout>
|
|
</template>
|
|
|
|
<script>
|
|
import PostStatus from '@/Enum/PostStatus.js';
|
|
import DashboardLayout from '../Components/DashboardLayout.vue';
|
|
import DashboardIcon from '../Components/DashboardIcon.vue';
|
|
import Breadcrumbs from '../Components/shared/Breadcrumbs.vue';
|
|
import FlashMessages from '../Components/shared/FlashMessages.vue';
|
|
import DataFilters from '../Components/shared/DataFilters.vue';
|
|
import SearchInput from '../Components/shared/SearchInput.vue';
|
|
import SelectFilter from '../Components/shared/SelectFilter.vue';
|
|
import EmptyState from '../Components/shared/EmptyState.vue';
|
|
import Pagination from '../Components/shared/Pagination.vue';
|
|
|
|
export default {
|
|
name: 'PostIndex',
|
|
components: {
|
|
DashboardLayout,
|
|
DashboardIcon,
|
|
Breadcrumbs,
|
|
FlashMessages,
|
|
DataFilters,
|
|
SearchInput,
|
|
SelectFilter,
|
|
EmptyState,
|
|
Pagination
|
|
},
|
|
|
|
props: {
|
|
posts: {
|
|
type: Object,
|
|
required: true
|
|
},
|
|
filters: {
|
|
type: Object,
|
|
default: () => ({
|
|
status: '',
|
|
search: ''
|
|
})
|
|
}
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
searchQuery: this.filters?.search || '',
|
|
statusQuery: this.filters?.status || '',
|
|
deletingPost: null,
|
|
selectedPost: null,
|
|
selectedPosts: [],
|
|
bulkProcessing: false,
|
|
PostStatus,
|
|
}
|
|
},
|
|
|
|
mounted() {
|
|
this.SET_DOCUMENT_TITLE('Все новости');
|
|
},
|
|
|
|
computed: {
|
|
isAllSelected() {
|
|
return this.posts.data.length > 0 && this.selectedPosts.length === this.posts.data.length;
|
|
},
|
|
isIndeterminate() {
|
|
return this.selectedPosts.length > 0 && this.selectedPosts.length < this.posts.data.length;
|
|
},
|
|
},
|
|
|
|
methods: {
|
|
getStatusBadgeClass(status) {
|
|
const statusValue = status?.value ?? status;
|
|
const statusObj = PostStatus.fromValue(statusValue);
|
|
const colorClasses = {
|
|
gray: 'bg-stone-500/10 text-stone-700 border-stone-500/20',
|
|
success: 'bg-emerald-500/10 text-emerald-700 border-emerald-500/20',
|
|
warning: 'bg-amber-500/10 text-amber-700 border-amber-500/20',
|
|
danger: 'bg-rose-500/10 text-rose-700 border-rose-500/20'
|
|
};
|
|
const color = statusObj ? statusObj.color : 'gray';
|
|
return `inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium border ${colorClasses[color]}`;
|
|
},
|
|
|
|
getStatusDotClass(status) {
|
|
const statusValue = status?.value ?? status;
|
|
const statusObj = PostStatus.fromValue(statusValue);
|
|
const colorClasses = {
|
|
gray: 'bg-stone-500',
|
|
success: 'bg-emerald-500',
|
|
warning: 'bg-amber-500',
|
|
danger: 'bg-rose-500'
|
|
};
|
|
const color = statusObj ? statusObj.color : 'gray';
|
|
return colorClasses[color];
|
|
},
|
|
|
|
getStatusLabel(status) {
|
|
const statusValue = status?.value ?? status;
|
|
return PostStatus.getLabel(statusValue) || status;
|
|
},
|
|
|
|
formatDate(date) {
|
|
return this.FORMAT_DATE(date, 'full');
|
|
},
|
|
|
|
getAuthorInitials(name) {
|
|
return this.GET_INITIALS(name) || '?';
|
|
},
|
|
|
|
search() {
|
|
this.INERTIA_FILTER('dashboard.posts.index', {
|
|
search: this.searchQuery,
|
|
status: this.statusQuery
|
|
});
|
|
},
|
|
|
|
filterByStatus() {
|
|
this.search();
|
|
},
|
|
|
|
resetFilters() {
|
|
this.RESET_FILTERS(
|
|
['searchQuery', 'statusQuery'],
|
|
'dashboard.posts.index'
|
|
);
|
|
},
|
|
|
|
confirmDelete(post) {
|
|
this.CONFIRM_AND_DELETE(post, 'dashboard.posts.destroy', {
|
|
message: `Вы уверены, что хотите удалить новость "${post.title}"?`
|
|
});
|
|
},
|
|
|
|
openPostModal(post) {
|
|
this.selectedPost = post;
|
|
},
|
|
|
|
closePostModal() {
|
|
this.selectedPost = null;
|
|
},
|
|
|
|
renderContent(content) {
|
|
if (!content) return '';
|
|
if (Array.isArray(content)) {
|
|
return content.map(block => {
|
|
if (typeof block === 'string') return block;
|
|
if (block.data?.content) return block.data.content;
|
|
return '';
|
|
}).join('');
|
|
}
|
|
return content;
|
|
},
|
|
|
|
viewImage(imagePath) {
|
|
window.open(this.GET_BASE_STORAGE_URL() + imagePath);
|
|
},
|
|
|
|
toggleSelectAll(event) {
|
|
if (event.target.checked) {
|
|
this.selectedPosts = [...new Set([...this.selectedPosts, ...this.posts.data.map(p => p.id)])];
|
|
} else {
|
|
const pageIds = new Set(this.posts.data.map(p => p.id));
|
|
this.selectedPosts = this.selectedPosts.filter(id => !pageIds.has(id));
|
|
}
|
|
},
|
|
|
|
selectAllOnPage() {
|
|
this.selectedPosts = [...new Set([...this.selectedPosts, ...this.posts.data.map(p => p.id)])];
|
|
},
|
|
|
|
clearSelection() {
|
|
this.selectedPosts = [];
|
|
},
|
|
|
|
bulkDelete() {
|
|
if (!confirm(`Удалить ${this.selectedPosts.length} новостей?`)) return;
|
|
this.bulkProcessing = true;
|
|
this.$inertia.delete(route('dashboard.posts.bulk-destroy'), {
|
|
data: { ids: this.selectedPosts },
|
|
preserveScroll: true,
|
|
onSuccess: () => {
|
|
this.selectedPosts = [];
|
|
this.bulkProcessing = false;
|
|
},
|
|
onError: () => {
|
|
this.bulkProcessing = false;
|
|
},
|
|
});
|
|
},
|
|
|
|
bulkPublish() {
|
|
if (!confirm(`Опубликовать ${this.selectedPosts.length} новостей?`)) return;
|
|
this.bulkProcessing = true;
|
|
this.$inertia.post(route('dashboard.posts.bulk-publish'), {
|
|
ids: this.selectedPosts,
|
|
}, {
|
|
preserveScroll: true,
|
|
onSuccess: () => {
|
|
this.selectedPosts = [];
|
|
this.bulkProcessing = false;
|
|
},
|
|
onError: () => {
|
|
this.bulkProcessing = false;
|
|
},
|
|
});
|
|
},
|
|
|
|
bulkSetVerification() {
|
|
if (!confirm(`Перевести ${this.selectedPosts.length} новостей на модерацию?`)) return;
|
|
this.bulkProcessing = true;
|
|
this.$inertia.post(route('dashboard.posts.bulk-verification'), {
|
|
ids: this.selectedPosts,
|
|
}, {
|
|
preserveScroll: true,
|
|
onSuccess: () => {
|
|
this.selectedPosts = [];
|
|
this.bulkProcessing = false;
|
|
},
|
|
onError: () => {
|
|
this.bulkProcessing = false;
|
|
},
|
|
});
|
|
},
|
|
}
|
|
}
|
|
</script>
|