From e986940680a9b5a8b913412da8ba6ab0472270ca Mon Sep 17 00:00:00 2001 From: F4ilji Date: Thu, 9 Apr 2026 09:09:17 +0500 Subject: [PATCH] changes --- app/Containers/Article/Enums/PostStatus.php | 3 + .../Actions/Posts/CreatePostAction.php | 6 - .../Actions/Posts/UpdatePostAction.php | 9 +- .../Actions/Posts/UploadPostImagesAction.php | 41 ++++ .../Tasks/Posts/GetPostFormDataTask.php | 3 - .../UI/WEB/Controllers/PostController.php | 30 ++- .../UI/WEB/Requests/StorePostRequest.php | 61 +++++- .../Dashboard/UI/WEB/Routes/web.php | 1 + .../Domain/Posts/PostDataProcessor.php | 56 ++++-- .../Filament/Domain/Posts/VkPostPublisher.php | 10 +- .../ContentBuilder/ContentBuilder.vue | 2 +- .../ContentBuilder/blocks/ParagraphBlock.vue | 13 +- resources/js/Pages/Dashboard/Posts/Form.vue | 180 ++++++++++++------ .../js/Pages/Dashboard/SubSections/Edit.vue | 9 + 14 files changed, 312 insertions(+), 112 deletions(-) create mode 100644 app/Containers/Dashboard/Actions/Posts/UploadPostImagesAction.php diff --git a/app/Containers/Article/Enums/PostStatus.php b/app/Containers/Article/Enums/PostStatus.php index a4c06d2..47b3cea 100755 --- a/app/Containers/Article/Enums/PostStatus.php +++ b/app/Containers/Article/Enums/PostStatus.php @@ -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', diff --git a/app/Containers/Dashboard/Actions/Posts/CreatePostAction.php b/app/Containers/Dashboard/Actions/Posts/CreatePostAction.php index 96851f9..caa5276 100644 --- a/app/Containers/Dashboard/Actions/Posts/CreatePostAction.php +++ b/app/Containers/Dashboard/Actions/Posts/CreatePostAction.php @@ -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; } } diff --git a/app/Containers/Dashboard/Actions/Posts/UpdatePostAction.php b/app/Containers/Dashboard/Actions/Posts/UpdatePostAction.php index e67c458..5a42308 100644 --- a/app/Containers/Dashboard/Actions/Posts/UpdatePostAction.php +++ b/app/Containers/Dashboard/Actions/Posts/UpdatePostAction.php @@ -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; } diff --git a/app/Containers/Dashboard/Actions/Posts/UploadPostImagesAction.php b/app/Containers/Dashboard/Actions/Posts/UploadPostImagesAction.php new file mode 100644 index 0000000..f9106b6 --- /dev/null +++ b/app/Containers/Dashboard/Actions/Posts/UploadPostImagesAction.php @@ -0,0 +1,41 @@ +getMimeType(), 'image/')) { + continue; + } + + $result = $this->uploadFileTask->run($file); + $paths[] = $result['path']; + } + + return $paths; + } +} diff --git a/app/Containers/Dashboard/Tasks/Posts/GetPostFormDataTask.php b/app/Containers/Dashboard/Tasks/Posts/GetPostFormDataTask.php index 3063a55..9cb1cab 100644 --- a/app/Containers/Dashboard/Tasks/Posts/GetPostFormDataTask.php +++ b/app/Containers/Dashboard/Tasks/Posts/GetPostFormDataTask.php @@ -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(), ]; } diff --git a/app/Containers/Dashboard/UI/WEB/Controllers/PostController.php b/app/Containers/Dashboard/UI/WEB/Controllers/PostController.php index 2131911..59a1f0e 100644 --- a/app/Containers/Dashboard/UI/WEB/Controllers/PostController.php +++ b/app/Containers/Dashboard/UI/WEB/Controllers/PostController.php @@ -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); + } + } } diff --git a/app/Containers/Dashboard/UI/WEB/Requests/StorePostRequest.php b/app/Containers/Dashboard/UI/WEB/Requests/StorePostRequest.php index c994f3b..571373e 100644 --- a/app/Containers/Dashboard/UI/WEB/Requests/StorePostRequest.php +++ b/app/Containers/Dashboard/UI/WEB/Requests/StorePostRequest.php @@ -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' => 'Дата публикации должна быть в будущем', diff --git a/app/Containers/Dashboard/UI/WEB/Routes/web.php b/app/Containers/Dashboard/UI/WEB/Routes/web.php index 0fbc720..fc81e6a 100755 --- a/app/Containers/Dashboard/UI/WEB/Routes/web.php +++ b/app/Containers/Dashboard/UI/WEB/Routes/web.php @@ -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'); diff --git a/app/Services/Filament/Domain/Posts/PostDataProcessor.php b/app/Services/Filament/Domain/Posts/PostDataProcessor.php index 584f9cf..48d2b25 100755 --- a/app/Services/Filament/Domain/Posts/PostDataProcessor.php +++ b/app/Services/Filament/Domain/Posts/PostDataProcessor.php @@ -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']); diff --git a/app/Services/Filament/Domain/Posts/VkPostPublisher.php b/app/Services/Filament/Domain/Posts/VkPostPublisher.php index 2bb8f6a..f683edf 100755 --- a/app/Services/Filament/Domain/Posts/VkPostPublisher.php +++ b/app/Services/Filament/Domain/Posts/VkPostPublisher.php @@ -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); } } diff --git a/resources/js/Pages/Dashboard/Components/ContentBuilder/ContentBuilder.vue b/resources/js/Pages/Dashboard/Components/ContentBuilder/ContentBuilder.vue index f8d0b66..47f7a56 100644 --- a/resources/js/Pages/Dashboard/Components/ContentBuilder/ContentBuilder.vue +++ b/resources/js/Pages/Dashboard/Components/ContentBuilder/ContentBuilder.vue @@ -22,7 +22,7 @@
diff --git a/resources/js/Pages/Dashboard/Components/ContentBuilder/blocks/ParagraphBlock.vue b/resources/js/Pages/Dashboard/Components/ContentBuilder/blocks/ParagraphBlock.vue index 0294cdc..550da2e 100644 --- a/resources/js/Pages/Dashboard/Components/ContentBuilder/blocks/ParagraphBlock.vue +++ b/resources/js/Pages/Dashboard/Components/ContentBuilder/blocks/ParagraphBlock.vue @@ -1,5 +1,5 @@