changes
This commit is contained in:
@@ -8,6 +8,7 @@ use Filament\Support\Contracts\HasColor;
|
||||
enum PostStatus: string implements HasLabel, HasColor
|
||||
{
|
||||
|
||||
case DRAFT = 'draft';
|
||||
case VERIFICATION = 'verification';
|
||||
case PUBLISHED = 'published';
|
||||
case REJECTED = 'rejected';
|
||||
@@ -15,6 +16,7 @@ enum PostStatus: string implements HasLabel, HasColor
|
||||
public function getLabel(): ?string
|
||||
{
|
||||
return match ($this) {
|
||||
self::DRAFT => 'Черновик',
|
||||
self::VERIFICATION => 'На рассмотрении',
|
||||
self::PUBLISHED => 'Опубликовано',
|
||||
self::REJECTED => 'Отклонено',
|
||||
@@ -24,6 +26,7 @@ enum PostStatus: string implements HasLabel, HasColor
|
||||
public function getColor(): string|array|null
|
||||
{
|
||||
return match ($this) {
|
||||
self::DRAFT => 'gray',
|
||||
self::VERIFICATION => 'warning',
|
||||
self::PUBLISHED => 'success',
|
||||
self::REJECTED => 'gray',
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace App\Containers\Dashboard\Actions\Posts;
|
||||
use App\Containers\Article\Models\Post;
|
||||
use App\Containers\Dashboard\Tasks\Posts\CreatePostTask;
|
||||
use App\Containers\Dashboard\Tasks\Posts\HandlePostSliderTask;
|
||||
use App\Containers\Dashboard\Tasks\Posts\PublishPostToVkTask;
|
||||
use App\Containers\Dashboard\Tasks\Posts\SendPostNotificationTask;
|
||||
|
||||
class CreatePostAction
|
||||
@@ -14,7 +13,6 @@ class CreatePostAction
|
||||
private readonly CreatePostTask $createPostTask,
|
||||
private readonly HandlePostSliderTask $handleSliderTask,
|
||||
private readonly SendPostNotificationTask $sendNotificationTask,
|
||||
private readonly PublishPostToVkTask $publishToVkTask,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -27,7 +25,6 @@ class CreatePostAction
|
||||
{
|
||||
// Извлекаем данные слайдера и публикации
|
||||
$slideData = $data['slide'] ?? [];
|
||||
$shouldPublishToVk = $data['publication']['vk'] ?? false;
|
||||
|
||||
// Удаляем служебные данные перед созданием
|
||||
unset($data['slide'], $data['publication']);
|
||||
@@ -41,9 +38,6 @@ class CreatePostAction
|
||||
// Отправляем уведомления (Task)
|
||||
$this->sendNotificationTask->run($post, null, true);
|
||||
|
||||
// Публикуем в VK (Task)
|
||||
$this->publishToVkTask->run($post, $shouldPublishToVk, false);
|
||||
|
||||
return $post;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace App\Containers\Dashboard\Actions\Posts;
|
||||
|
||||
use App\Containers\Article\Models\Post;
|
||||
use App\Containers\Dashboard\Tasks\Posts\HandlePostSliderTask;
|
||||
use App\Containers\Dashboard\Tasks\Posts\PublishPostToVkTask;
|
||||
use App\Containers\Dashboard\Tasks\Posts\SendPostNotificationTask;
|
||||
use App\Containers\Dashboard\Tasks\Posts\UpdatePostTask;
|
||||
|
||||
@@ -14,7 +13,6 @@ class UpdatePostAction
|
||||
private readonly UpdatePostTask $updatePostTask,
|
||||
private readonly HandlePostSliderTask $handleSliderTask,
|
||||
private readonly SendPostNotificationTask $sendNotificationTask,
|
||||
private readonly PublishPostToVkTask $publishToVkTask,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -28,8 +26,6 @@ class UpdatePostAction
|
||||
{
|
||||
// Извлекаем данные слайдера и публикации
|
||||
$slideData = $data['slide'] ?? [];
|
||||
$shouldPublishToVk = $data['publication']['vk'] ?? false;
|
||||
$newStatus = $data['status'] ?? null;
|
||||
|
||||
// Удаляем служебные данные перед обновлением
|
||||
unset($data['slide'], $data['publication']);
|
||||
@@ -41,10 +37,7 @@ class UpdatePostAction
|
||||
$this->handleSliderTask->run($post, $slideData, false);
|
||||
|
||||
// Отправляем уведомления (Task)
|
||||
$this->sendNotificationTask->run($post, $newStatus, false);
|
||||
|
||||
// Публикуем в VK (Task)
|
||||
$this->publishToVkTask->run($post, $shouldPublishToVk, true);
|
||||
$this->sendNotificationTask->run($post, $data['status'] ?? null, false);
|
||||
|
||||
return $post;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Dashboard\Actions\Posts;
|
||||
|
||||
use App\Containers\Dashboard\Tasks\Files\UploadFileTask;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class UploadPostImagesAction
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UploadFileTask $uploadFileTask,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Загружает массив изображений и возвращает пути
|
||||
*
|
||||
* @param UploadedFile[] $files Массив загруженных файлов
|
||||
* @return string[] Массив путей к файлам
|
||||
*/
|
||||
public function run(array $files): array
|
||||
{
|
||||
$paths = [];
|
||||
|
||||
foreach ($files as $file) {
|
||||
if (!$file instanceof UploadedFile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Проверяем что это изображение
|
||||
if (!str_starts_with($file->getMimeType(), 'image/')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = $this->uploadFileTask->run($file);
|
||||
$paths[] = $result['path'];
|
||||
}
|
||||
|
||||
return $paths;
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,6 @@ namespace App\Containers\Dashboard\Tasks\Posts;
|
||||
|
||||
use App\Containers\Article\Enums\PostStatus;
|
||||
use App\Containers\Article\Models\Category;
|
||||
use App\Containers\Widget\Models\Slider;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class GetPostFormDataTask
|
||||
{
|
||||
@@ -13,7 +11,6 @@ class GetPostFormDataTask
|
||||
{
|
||||
return [
|
||||
'categories' => Category::all(['id', 'title']),
|
||||
'sliders' => Slider::where('is_active', true)->get(['id', 'title']),
|
||||
'statuses' => PostStatus::cases(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Containers\Dashboard\Actions\Posts\GetPostFormDataAction;
|
||||
use App\Containers\Dashboard\Actions\Posts\ListAiPreparedPostsAction;
|
||||
use App\Containers\Dashboard\Actions\Posts\ListPostsAction;
|
||||
use App\Containers\Dashboard\Actions\Posts\UpdatePostAction;
|
||||
use App\Containers\Dashboard\Actions\Posts\UploadPostImagesAction;
|
||||
use App\Containers\Dashboard\UI\WEB\Requests\StorePostRequest;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -31,6 +32,7 @@ class PostController extends Controller
|
||||
private readonly BulkDeletePostsAction $bulkDeletePostsAction,
|
||||
private readonly BulkPublishPostsAction $bulkPublishPostsAction,
|
||||
private readonly BulkVerificationPostsAction $bulkVerificationPostsAction,
|
||||
private readonly UploadPostImagesAction $uploadPostImagesAction,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -49,7 +51,7 @@ class PostController extends Controller
|
||||
public function store(StorePostRequest $request): RedirectResponse
|
||||
{
|
||||
try {
|
||||
$this->createPostAction->run($request->validated());
|
||||
$post = $this->createPostAction->run($request->validated());
|
||||
|
||||
return redirect()->route('dashboard.posts.index')
|
||||
->with('success', 'Новость успешно создана!');
|
||||
@@ -71,6 +73,7 @@ class PostController extends Controller
|
||||
return Inertia::render('Dashboard/Posts/Edit', [
|
||||
'post' => [
|
||||
...$post->toArray(),
|
||||
'status' => $post->status?->value ?? $post->status,
|
||||
'publish_setting' => [
|
||||
'publish_after' => $post->publish_at !== null,
|
||||
'publish_at' => $post->publish_at,
|
||||
@@ -215,4 +218,29 @@ class PostController extends Controller
|
||||
->with('error', 'Ошибка: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает изображения для поста и возвращает пути
|
||||
*/
|
||||
public function uploadImages(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'images' => 'required|array',
|
||||
'images.*' => 'required|image|mimes:jpeg,png,jpg,webp|max:20480',
|
||||
]);
|
||||
|
||||
try {
|
||||
$paths = $this->uploadPostImagesAction->run($request->file('images'));
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'paths' => $paths,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'Ошибка при загрузке изображений: ' . $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,67 @@ class StorePostRequest extends FormRequest
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
*
|
||||
* Decodes JSON-encoded fields from FormData back to arrays.
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$fieldsToDecode = [
|
||||
'content',
|
||||
'tags',
|
||||
'authors',
|
||||
'images',
|
||||
'publish_setting',
|
||||
'publication',
|
||||
'slide',
|
||||
];
|
||||
|
||||
foreach ($fieldsToDecode as $field) {
|
||||
$value = $this->input($field);
|
||||
if (is_string($value)) {
|
||||
$decoded = json_decode($value, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$this->merge([$field => $decoded]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle nested slide properties
|
||||
if (is_array($this->input('slide'))) {
|
||||
$slideData = $this->input('slide');
|
||||
|
||||
if (isset($slideData['image']) && is_string($slideData['image'])) {
|
||||
$decoded = json_decode($slideData['image'], true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$slideData['image'] = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($slideData['settings']) && is_string($slideData['settings'])) {
|
||||
$decoded = json_decode($slideData['settings'], true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$slideData['settings'] = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
$this->merge(['slide' => $slideData]);
|
||||
}
|
||||
|
||||
// Удаляем preview если это пустой объект
|
||||
$preview = $this->input('preview');
|
||||
if ($preview === '{}' || $preview === 'null' || $preview === null || $preview === '') {
|
||||
$this->merge(['preview' => null]);
|
||||
}
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['required', 'string', 'max:255'],
|
||||
'slug' => ['required', 'string', 'max:255', Rule::unique('posts', 'slug')->ignore($this->route('post'))],
|
||||
'status' => ['required', 'integer', Rule::in([0, 1, 2, 3])], // PostStatus enum values
|
||||
'status' => ['required', 'string', Rule::in(['draft', 'published', 'verification', 'rejected'])],
|
||||
'category_id' => ['nullable', 'integer', 'exists:categories,id'],
|
||||
'tags' => ['nullable', 'array'],
|
||||
'authors' => ['nullable', 'array'],
|
||||
@@ -26,7 +81,7 @@ class StorePostRequest extends FormRequest
|
||||
'images' => ['nullable', 'array'],
|
||||
'publish_setting' => ['nullable', 'array'],
|
||||
'publish_setting.publish_after' => ['nullable', 'boolean'],
|
||||
'publish_setting.publish_at' => ['nullable', 'date', 'after:now'],
|
||||
'publish_setting.publish_at' => ['nullable', 'date'],
|
||||
'publication' => ['nullable', 'array'],
|
||||
'publication.vk' => ['nullable', 'boolean'],
|
||||
'publication.telegram' => ['nullable', 'boolean'],
|
||||
@@ -54,7 +109,7 @@ class StorePostRequest extends FormRequest
|
||||
'slug.required' => 'URL-адрес обязателен',
|
||||
'slug.unique' => 'Такой URL-адрес уже используется',
|
||||
'status.required' => 'Статус публикации обязателен',
|
||||
'status.in' => 'Некорректный статус публикации',
|
||||
'status.in' => 'Некорректный статус публикации. Допустимые значения: draft, published, verification, rejected',
|
||||
'category_id.exists' => 'Выбранная категория не существует',
|
||||
'content.required' => 'Содержание новости обязательно',
|
||||
'publish_setting.publish_at.after' => 'Дата публикации должна быть в будущем',
|
||||
|
||||
@@ -68,6 +68,7 @@ Route::middleware(['access-check', 'dashboard.auth'])->group(function () {
|
||||
Route::post('/ai-prepared/parse-email', [ParseEmailNewsController::class, '__invoke'])->name('ai-prepared.parse-email');
|
||||
Route::get('/create', [PostController::class, 'create'])->name('create');
|
||||
Route::post('/', [PostController::class, 'store'])->name('store');
|
||||
Route::post('/upload-images', [PostController::class, 'uploadImages'])->name('upload-images');
|
||||
Route::delete('/bulk-destroy', [PostController::class, 'bulkDestroy'])->name('bulk-destroy');
|
||||
Route::post('/bulk-publish', [PostController::class, 'bulkPublish'])->name('bulk-publish');
|
||||
Route::post('/bulk-verification', [PostController::class, 'bulkVerification'])->name('bulk-verification');
|
||||
|
||||
@@ -16,28 +16,33 @@ class PostDataProcessor
|
||||
*/
|
||||
public function processCreate(array $data): array
|
||||
{
|
||||
// Конвертируем статус в enum, если необходимо
|
||||
if (isset($data['status']) && !$data['status'] instanceof PostStatus) {
|
||||
$data['status'] = PostStatus::tryFrom($data['status']) ?? PostStatus::DRAFT;
|
||||
}
|
||||
|
||||
// Удаляем ненужные данные
|
||||
unset($data['publication']);
|
||||
|
||||
// Устанавливаем текст для предпросмотра
|
||||
$data['preview_text'] = $this->setPreviewText($data);
|
||||
|
||||
if (($data['status']->value === PostStatus::PUBLISHED->value)) {
|
||||
if ($data['status'] === PostStatus::PUBLISHED) {
|
||||
$data['publish_at'] = $this->setPublishDateTime();
|
||||
}
|
||||
|
||||
// Устанавливаем время публикации
|
||||
if ($data['publish_setting']['publish_after'] === true) {
|
||||
if (isset($data['publish_setting']['publish_after']) && $data['publish_setting']['publish_after'] === true) {
|
||||
$data['publish_at'] = $this->setPublishDateTimeInFuture($data['publish_setting']);
|
||||
}
|
||||
|
||||
unset($data['publish_setting']);
|
||||
|
||||
// Генерируем данные для поиска
|
||||
$data['search_data'] = $this->generateSearchData($data['content']);
|
||||
|
||||
// Рассчитываем время чтения
|
||||
$data['reading_time'] = $this->calculateReadingTime($data['search_data']);
|
||||
// Генерируем данные для поиска только если content - массив
|
||||
if (isset($data['content']) && is_array($data['content'])) {
|
||||
$data['search_data'] = $this->generateSearchData($data['content']);
|
||||
$data['reading_time'] = $this->calculateReadingTime($data['search_data']);
|
||||
}
|
||||
|
||||
// Устанавливаем ID текущего пользователя
|
||||
$data['user_id'] = auth()->id();
|
||||
@@ -48,32 +53,36 @@ class PostDataProcessor
|
||||
|
||||
public function processUpdate(array $data): array
|
||||
{
|
||||
// Конвертируем статус в enum, если необходимо
|
||||
if (isset($data['status']) && !$data['status'] instanceof PostStatus) {
|
||||
$data['status'] = PostStatus::tryFrom($data['status']);
|
||||
$data['status'] = PostStatus::tryFrom($data['status']) ?? PostStatus::DRAFT;
|
||||
}
|
||||
|
||||
// Если статус не установлен, используем черновик
|
||||
if (!isset($data['status'])) {
|
||||
$data['status'] = PostStatus::DRAFT;
|
||||
}
|
||||
|
||||
unset($data['publication']);
|
||||
|
||||
// Устанавливаем текст для предпросмотра
|
||||
$data['preview_text'] = $this->setPreviewText($data);
|
||||
|
||||
|
||||
if (!$data['publish_at'] && ($data['status']->value === PostStatus::PUBLISHED->value)) {
|
||||
// Обновляем publish_at из publish_setting если дата передана
|
||||
if (isset($data['publish_setting']['publish_at']) && $data['publish_setting']['publish_at']) {
|
||||
$data['publish_at'] = Carbon::parse($data['publish_setting']['publish_at']);
|
||||
} elseif (!isset($data['publish_at']) && $data['status'] === PostStatus::PUBLISHED) {
|
||||
// Если дата не передана и пост публикуется — ставим текущее время
|
||||
$data['publish_at'] = $this->setPublishDateTime();
|
||||
}
|
||||
|
||||
// Устанавливаем время публикации
|
||||
if ((isset($data['publish_setting']['publish_after']) && $data['publish_setting']['publish_after']) ||
|
||||
(isset($data['publish_setting']['publish_at']) && $data['publish_setting']['publish_at'])) {
|
||||
$data['publish_at'] = $this->setPublishDateTimeInFuture($data['publish_setting']);
|
||||
}
|
||||
|
||||
unset($data['publish_setting']);
|
||||
|
||||
// Генерируем данные для поиска
|
||||
$data['search_data'] = $this->generateSearchData($data['content']);
|
||||
|
||||
// Рассчитываем время чтения
|
||||
$data['reading_time'] = $this->calculateReadingTime($data['search_data']);
|
||||
// Генерируем данные для поиска только если content - массив
|
||||
if (isset($data['content']) && is_array($data['content'])) {
|
||||
$data['search_data'] = $this->generateSearchData($data['content']);
|
||||
$data['reading_time'] = $this->calculateReadingTime($data['search_data']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
@@ -86,6 +95,11 @@ class PostDataProcessor
|
||||
*/
|
||||
private function setPreviewText(array $data): string
|
||||
{
|
||||
// Если content не массив или пустой, возвращаем пустую строку
|
||||
if (!isset($data['content']) || !is_array($data['content']) || empty($data['content'])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$rowData = $this->getBlockBySeoActiveState('paragraph', $data['content']);
|
||||
if ($rowData === null) {
|
||||
$rowData = $this->getFirstBlockByName('paragraph', $data['content']);
|
||||
|
||||
@@ -29,7 +29,7 @@ class VkPostPublisher
|
||||
$imagesFromPost = $post->images ?? [];
|
||||
$imagesFromContent = $this->extractImagesFromContent($post->content);
|
||||
$allImages = array_merge($imagesFromPost, $imagesFromContent);
|
||||
$imageLinks = $this->generateImageLinksForVk($allImages);
|
||||
$imageLinks = array_filter($this->generateImageLinksForVk($allImages)); // Удаляем null
|
||||
$videos = $this->extractVideosFromContent($post->content);
|
||||
|
||||
$publishDate = $post->publish_at > now() ? $post->publish_at->timestamp : null;
|
||||
@@ -54,7 +54,7 @@ class VkPostPublisher
|
||||
$imagesFromPost = $post->images ?? [];
|
||||
$imagesFromContent = $this->extractImagesFromContent($post->content);
|
||||
$allImages = array_merge($imagesFromPost, $imagesFromContent);
|
||||
$imageLinks = $this->generateImageLinksForVk($allImages);
|
||||
$imageLinks = array_filter($this->generateImageLinksForVk($allImages)); // Удаляем null
|
||||
$videos = $this->extractVideosFromContent($post->content);
|
||||
|
||||
$publishDate = $post->publish_at > now() ? $post->publish_at->timestamp : null;
|
||||
@@ -183,7 +183,11 @@ class VkPostPublisher
|
||||
private function generateImageLinksForVk(array $images): array
|
||||
{
|
||||
return array_map(function ($file) {
|
||||
return Storage::url($file); // Генерируем полный URL для изображения
|
||||
// Пропускаем невалидные данные (пустые массивы, null и т.д.)
|
||||
if (!is_string($file) || empty($file)) {
|
||||
return null;
|
||||
}
|
||||
return Storage::url($file);
|
||||
}, $images);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user