changes
This commit is contained in:
@@ -8,6 +8,7 @@ use Filament\Support\Contracts\HasColor;
|
|||||||
enum PostStatus: string implements HasLabel, HasColor
|
enum PostStatus: string implements HasLabel, HasColor
|
||||||
{
|
{
|
||||||
|
|
||||||
|
case DRAFT = 'draft';
|
||||||
case VERIFICATION = 'verification';
|
case VERIFICATION = 'verification';
|
||||||
case PUBLISHED = 'published';
|
case PUBLISHED = 'published';
|
||||||
case REJECTED = 'rejected';
|
case REJECTED = 'rejected';
|
||||||
@@ -15,6 +16,7 @@ enum PostStatus: string implements HasLabel, HasColor
|
|||||||
public function getLabel(): ?string
|
public function getLabel(): ?string
|
||||||
{
|
{
|
||||||
return match ($this) {
|
return match ($this) {
|
||||||
|
self::DRAFT => 'Черновик',
|
||||||
self::VERIFICATION => 'На рассмотрении',
|
self::VERIFICATION => 'На рассмотрении',
|
||||||
self::PUBLISHED => 'Опубликовано',
|
self::PUBLISHED => 'Опубликовано',
|
||||||
self::REJECTED => 'Отклонено',
|
self::REJECTED => 'Отклонено',
|
||||||
@@ -24,6 +26,7 @@ enum PostStatus: string implements HasLabel, HasColor
|
|||||||
public function getColor(): string|array|null
|
public function getColor(): string|array|null
|
||||||
{
|
{
|
||||||
return match ($this) {
|
return match ($this) {
|
||||||
|
self::DRAFT => 'gray',
|
||||||
self::VERIFICATION => 'warning',
|
self::VERIFICATION => 'warning',
|
||||||
self::PUBLISHED => 'success',
|
self::PUBLISHED => 'success',
|
||||||
self::REJECTED => 'gray',
|
self::REJECTED => 'gray',
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ namespace App\Containers\Dashboard\Actions\Posts;
|
|||||||
use App\Containers\Article\Models\Post;
|
use App\Containers\Article\Models\Post;
|
||||||
use App\Containers\Dashboard\Tasks\Posts\CreatePostTask;
|
use App\Containers\Dashboard\Tasks\Posts\CreatePostTask;
|
||||||
use App\Containers\Dashboard\Tasks\Posts\HandlePostSliderTask;
|
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\SendPostNotificationTask;
|
||||||
|
|
||||||
class CreatePostAction
|
class CreatePostAction
|
||||||
@@ -14,7 +13,6 @@ class CreatePostAction
|
|||||||
private readonly CreatePostTask $createPostTask,
|
private readonly CreatePostTask $createPostTask,
|
||||||
private readonly HandlePostSliderTask $handleSliderTask,
|
private readonly HandlePostSliderTask $handleSliderTask,
|
||||||
private readonly SendPostNotificationTask $sendNotificationTask,
|
private readonly SendPostNotificationTask $sendNotificationTask,
|
||||||
private readonly PublishPostToVkTask $publishToVkTask,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,7 +25,6 @@ class CreatePostAction
|
|||||||
{
|
{
|
||||||
// Извлекаем данные слайдера и публикации
|
// Извлекаем данные слайдера и публикации
|
||||||
$slideData = $data['slide'] ?? [];
|
$slideData = $data['slide'] ?? [];
|
||||||
$shouldPublishToVk = $data['publication']['vk'] ?? false;
|
|
||||||
|
|
||||||
// Удаляем служебные данные перед созданием
|
// Удаляем служебные данные перед созданием
|
||||||
unset($data['slide'], $data['publication']);
|
unset($data['slide'], $data['publication']);
|
||||||
@@ -41,9 +38,6 @@ class CreatePostAction
|
|||||||
// Отправляем уведомления (Task)
|
// Отправляем уведомления (Task)
|
||||||
$this->sendNotificationTask->run($post, null, true);
|
$this->sendNotificationTask->run($post, null, true);
|
||||||
|
|
||||||
// Публикуем в VK (Task)
|
|
||||||
$this->publishToVkTask->run($post, $shouldPublishToVk, false);
|
|
||||||
|
|
||||||
return $post;
|
return $post;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ namespace App\Containers\Dashboard\Actions\Posts;
|
|||||||
|
|
||||||
use App\Containers\Article\Models\Post;
|
use App\Containers\Article\Models\Post;
|
||||||
use App\Containers\Dashboard\Tasks\Posts\HandlePostSliderTask;
|
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\SendPostNotificationTask;
|
||||||
use App\Containers\Dashboard\Tasks\Posts\UpdatePostTask;
|
use App\Containers\Dashboard\Tasks\Posts\UpdatePostTask;
|
||||||
|
|
||||||
@@ -14,7 +13,6 @@ class UpdatePostAction
|
|||||||
private readonly UpdatePostTask $updatePostTask,
|
private readonly UpdatePostTask $updatePostTask,
|
||||||
private readonly HandlePostSliderTask $handleSliderTask,
|
private readonly HandlePostSliderTask $handleSliderTask,
|
||||||
private readonly SendPostNotificationTask $sendNotificationTask,
|
private readonly SendPostNotificationTask $sendNotificationTask,
|
||||||
private readonly PublishPostToVkTask $publishToVkTask,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,8 +26,6 @@ class UpdatePostAction
|
|||||||
{
|
{
|
||||||
// Извлекаем данные слайдера и публикации
|
// Извлекаем данные слайдера и публикации
|
||||||
$slideData = $data['slide'] ?? [];
|
$slideData = $data['slide'] ?? [];
|
||||||
$shouldPublishToVk = $data['publication']['vk'] ?? false;
|
|
||||||
$newStatus = $data['status'] ?? null;
|
|
||||||
|
|
||||||
// Удаляем служебные данные перед обновлением
|
// Удаляем служебные данные перед обновлением
|
||||||
unset($data['slide'], $data['publication']);
|
unset($data['slide'], $data['publication']);
|
||||||
@@ -41,10 +37,7 @@ class UpdatePostAction
|
|||||||
$this->handleSliderTask->run($post, $slideData, false);
|
$this->handleSliderTask->run($post, $slideData, false);
|
||||||
|
|
||||||
// Отправляем уведомления (Task)
|
// Отправляем уведомления (Task)
|
||||||
$this->sendNotificationTask->run($post, $newStatus, false);
|
$this->sendNotificationTask->run($post, $data['status'] ?? null, false);
|
||||||
|
|
||||||
// Публикуем в VK (Task)
|
|
||||||
$this->publishToVkTask->run($post, $shouldPublishToVk, true);
|
|
||||||
|
|
||||||
return $post;
|
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\Enums\PostStatus;
|
||||||
use App\Containers\Article\Models\Category;
|
use App\Containers\Article\Models\Category;
|
||||||
use App\Containers\Widget\Models\Slider;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
|
|
||||||
class GetPostFormDataTask
|
class GetPostFormDataTask
|
||||||
{
|
{
|
||||||
@@ -13,7 +11,6 @@ class GetPostFormDataTask
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'categories' => Category::all(['id', 'title']),
|
'categories' => Category::all(['id', 'title']),
|
||||||
'sliders' => Slider::where('is_active', true)->get(['id', 'title']),
|
|
||||||
'statuses' => PostStatus::cases(),
|
'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\ListAiPreparedPostsAction;
|
||||||
use App\Containers\Dashboard\Actions\Posts\ListPostsAction;
|
use App\Containers\Dashboard\Actions\Posts\ListPostsAction;
|
||||||
use App\Containers\Dashboard\Actions\Posts\UpdatePostAction;
|
use App\Containers\Dashboard\Actions\Posts\UpdatePostAction;
|
||||||
|
use App\Containers\Dashboard\Actions\Posts\UploadPostImagesAction;
|
||||||
use App\Containers\Dashboard\UI\WEB\Requests\StorePostRequest;
|
use App\Containers\Dashboard\UI\WEB\Requests\StorePostRequest;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
@@ -31,6 +32,7 @@ class PostController extends Controller
|
|||||||
private readonly BulkDeletePostsAction $bulkDeletePostsAction,
|
private readonly BulkDeletePostsAction $bulkDeletePostsAction,
|
||||||
private readonly BulkPublishPostsAction $bulkPublishPostsAction,
|
private readonly BulkPublishPostsAction $bulkPublishPostsAction,
|
||||||
private readonly BulkVerificationPostsAction $bulkVerificationPostsAction,
|
private readonly BulkVerificationPostsAction $bulkVerificationPostsAction,
|
||||||
|
private readonly UploadPostImagesAction $uploadPostImagesAction,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,7 +51,7 @@ class PostController extends Controller
|
|||||||
public function store(StorePostRequest $request): RedirectResponse
|
public function store(StorePostRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->createPostAction->run($request->validated());
|
$post = $this->createPostAction->run($request->validated());
|
||||||
|
|
||||||
return redirect()->route('dashboard.posts.index')
|
return redirect()->route('dashboard.posts.index')
|
||||||
->with('success', 'Новость успешно создана!');
|
->with('success', 'Новость успешно создана!');
|
||||||
@@ -71,6 +73,7 @@ class PostController extends Controller
|
|||||||
return Inertia::render('Dashboard/Posts/Edit', [
|
return Inertia::render('Dashboard/Posts/Edit', [
|
||||||
'post' => [
|
'post' => [
|
||||||
...$post->toArray(),
|
...$post->toArray(),
|
||||||
|
'status' => $post->status?->value ?? $post->status,
|
||||||
'publish_setting' => [
|
'publish_setting' => [
|
||||||
'publish_after' => $post->publish_at !== null,
|
'publish_after' => $post->publish_at !== null,
|
||||||
'publish_at' => $post->publish_at,
|
'publish_at' => $post->publish_at,
|
||||||
@@ -215,4 +218,29 @@ class PostController extends Controller
|
|||||||
->with('error', 'Ошибка: ' . $e->getMessage());
|
->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;
|
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
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'title' => ['required', 'string', 'max:255'],
|
'title' => ['required', 'string', 'max:255'],
|
||||||
'slug' => ['required', 'string', 'max:255', Rule::unique('posts', 'slug')->ignore($this->route('post'))],
|
'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'],
|
'category_id' => ['nullable', 'integer', 'exists:categories,id'],
|
||||||
'tags' => ['nullable', 'array'],
|
'tags' => ['nullable', 'array'],
|
||||||
'authors' => ['nullable', 'array'],
|
'authors' => ['nullable', 'array'],
|
||||||
@@ -26,7 +81,7 @@ class StorePostRequest extends FormRequest
|
|||||||
'images' => ['nullable', 'array'],
|
'images' => ['nullable', 'array'],
|
||||||
'publish_setting' => ['nullable', 'array'],
|
'publish_setting' => ['nullable', 'array'],
|
||||||
'publish_setting.publish_after' => ['nullable', 'boolean'],
|
'publish_setting.publish_after' => ['nullable', 'boolean'],
|
||||||
'publish_setting.publish_at' => ['nullable', 'date', 'after:now'],
|
'publish_setting.publish_at' => ['nullable', 'date'],
|
||||||
'publication' => ['nullable', 'array'],
|
'publication' => ['nullable', 'array'],
|
||||||
'publication.vk' => ['nullable', 'boolean'],
|
'publication.vk' => ['nullable', 'boolean'],
|
||||||
'publication.telegram' => ['nullable', 'boolean'],
|
'publication.telegram' => ['nullable', 'boolean'],
|
||||||
@@ -54,7 +109,7 @@ class StorePostRequest extends FormRequest
|
|||||||
'slug.required' => 'URL-адрес обязателен',
|
'slug.required' => 'URL-адрес обязателен',
|
||||||
'slug.unique' => 'Такой URL-адрес уже используется',
|
'slug.unique' => 'Такой URL-адрес уже используется',
|
||||||
'status.required' => 'Статус публикации обязателен',
|
'status.required' => 'Статус публикации обязателен',
|
||||||
'status.in' => 'Некорректный статус публикации',
|
'status.in' => 'Некорректный статус публикации. Допустимые значения: draft, published, verification, rejected',
|
||||||
'category_id.exists' => 'Выбранная категория не существует',
|
'category_id.exists' => 'Выбранная категория не существует',
|
||||||
'content.required' => 'Содержание новости обязательно',
|
'content.required' => 'Содержание новости обязательно',
|
||||||
'publish_setting.publish_at.after' => 'Дата публикации должна быть в будущем',
|
'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::post('/ai-prepared/parse-email', [ParseEmailNewsController::class, '__invoke'])->name('ai-prepared.parse-email');
|
||||||
Route::get('/create', [PostController::class, 'create'])->name('create');
|
Route::get('/create', [PostController::class, 'create'])->name('create');
|
||||||
Route::post('/', [PostController::class, 'store'])->name('store');
|
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::delete('/bulk-destroy', [PostController::class, 'bulkDestroy'])->name('bulk-destroy');
|
||||||
Route::post('/bulk-publish', [PostController::class, 'bulkPublish'])->name('bulk-publish');
|
Route::post('/bulk-publish', [PostController::class, 'bulkPublish'])->name('bulk-publish');
|
||||||
Route::post('/bulk-verification', [PostController::class, 'bulkVerification'])->name('bulk-verification');
|
Route::post('/bulk-verification', [PostController::class, 'bulkVerification'])->name('bulk-verification');
|
||||||
|
|||||||
@@ -16,28 +16,33 @@ class PostDataProcessor
|
|||||||
*/
|
*/
|
||||||
public function processCreate(array $data): array
|
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']);
|
unset($data['publication']);
|
||||||
|
|
||||||
// Устанавливаем текст для предпросмотра
|
// Устанавливаем текст для предпросмотра
|
||||||
$data['preview_text'] = $this->setPreviewText($data);
|
$data['preview_text'] = $this->setPreviewText($data);
|
||||||
|
|
||||||
if (($data['status']->value === PostStatus::PUBLISHED->value)) {
|
if ($data['status'] === PostStatus::PUBLISHED) {
|
||||||
$data['publish_at'] = $this->setPublishDateTime();
|
$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']);
|
$data['publish_at'] = $this->setPublishDateTimeInFuture($data['publish_setting']);
|
||||||
}
|
}
|
||||||
|
|
||||||
unset($data['publish_setting']);
|
unset($data['publish_setting']);
|
||||||
|
|
||||||
// Генерируем данные для поиска
|
// Генерируем данные для поиска только если content - массив
|
||||||
$data['search_data'] = $this->generateSearchData($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']);
|
||||||
$data['reading_time'] = $this->calculateReadingTime($data['search_data']);
|
}
|
||||||
|
|
||||||
// Устанавливаем ID текущего пользователя
|
// Устанавливаем ID текущего пользователя
|
||||||
$data['user_id'] = auth()->id();
|
$data['user_id'] = auth()->id();
|
||||||
@@ -48,32 +53,36 @@ class PostDataProcessor
|
|||||||
|
|
||||||
public function processUpdate(array $data): array
|
public function processUpdate(array $data): array
|
||||||
{
|
{
|
||||||
|
// Конвертируем статус в enum, если необходимо
|
||||||
if (isset($data['status']) && !$data['status'] instanceof PostStatus) {
|
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']);
|
unset($data['publication']);
|
||||||
|
|
||||||
// Устанавливаем текст для предпросмотра
|
// Устанавливаем текст для предпросмотра
|
||||||
$data['preview_text'] = $this->setPreviewText($data);
|
$data['preview_text'] = $this->setPreviewText($data);
|
||||||
|
|
||||||
|
// Обновляем publish_at из publish_setting если дата передана
|
||||||
if (!$data['publish_at'] && ($data['status']->value === PostStatus::PUBLISHED->value)) {
|
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();
|
$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']);
|
unset($data['publish_setting']);
|
||||||
|
|
||||||
// Генерируем данные для поиска
|
// Генерируем данные для поиска только если content - массив
|
||||||
$data['search_data'] = $this->generateSearchData($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']);
|
||||||
$data['reading_time'] = $this->calculateReadingTime($data['search_data']);
|
}
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
@@ -86,6 +95,11 @@ class PostDataProcessor
|
|||||||
*/
|
*/
|
||||||
private function setPreviewText(array $data): string
|
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']);
|
$rowData = $this->getBlockBySeoActiveState('paragraph', $data['content']);
|
||||||
if ($rowData === null) {
|
if ($rowData === null) {
|
||||||
$rowData = $this->getFirstBlockByName('paragraph', $data['content']);
|
$rowData = $this->getFirstBlockByName('paragraph', $data['content']);
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class VkPostPublisher
|
|||||||
$imagesFromPost = $post->images ?? [];
|
$imagesFromPost = $post->images ?? [];
|
||||||
$imagesFromContent = $this->extractImagesFromContent($post->content);
|
$imagesFromContent = $this->extractImagesFromContent($post->content);
|
||||||
$allImages = array_merge($imagesFromPost, $imagesFromContent);
|
$allImages = array_merge($imagesFromPost, $imagesFromContent);
|
||||||
$imageLinks = $this->generateImageLinksForVk($allImages);
|
$imageLinks = array_filter($this->generateImageLinksForVk($allImages)); // Удаляем null
|
||||||
$videos = $this->extractVideosFromContent($post->content);
|
$videos = $this->extractVideosFromContent($post->content);
|
||||||
|
|
||||||
$publishDate = $post->publish_at > now() ? $post->publish_at->timestamp : null;
|
$publishDate = $post->publish_at > now() ? $post->publish_at->timestamp : null;
|
||||||
@@ -54,7 +54,7 @@ class VkPostPublisher
|
|||||||
$imagesFromPost = $post->images ?? [];
|
$imagesFromPost = $post->images ?? [];
|
||||||
$imagesFromContent = $this->extractImagesFromContent($post->content);
|
$imagesFromContent = $this->extractImagesFromContent($post->content);
|
||||||
$allImages = array_merge($imagesFromPost, $imagesFromContent);
|
$allImages = array_merge($imagesFromPost, $imagesFromContent);
|
||||||
$imageLinks = $this->generateImageLinksForVk($allImages);
|
$imageLinks = array_filter($this->generateImageLinksForVk($allImages)); // Удаляем null
|
||||||
$videos = $this->extractVideosFromContent($post->content);
|
$videos = $this->extractVideosFromContent($post->content);
|
||||||
|
|
||||||
$publishDate = $post->publish_at > now() ? $post->publish_at->timestamp : null;
|
$publishDate = $post->publish_at > now() ? $post->publish_at->timestamp : null;
|
||||||
@@ -183,7 +183,11 @@ class VkPostPublisher
|
|||||||
private function generateImageLinksForVk(array $images): array
|
private function generateImageLinksForVk(array $images): array
|
||||||
{
|
{
|
||||||
return array_map(function ($file) {
|
return array_map(function ($file) {
|
||||||
return Storage::url($file); // Генерируем полный URL для изображения
|
// Пропускаем невалидные данные (пустые массивы, null и т.д.)
|
||||||
|
if (!is_string($file) || empty($file)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Storage::url($file);
|
||||||
}, $images);
|
}, $images);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<div
|
<div
|
||||||
v-for="(block, index) in blocks"
|
v-for="(block, index) in blocks"
|
||||||
:key="block._uid"
|
:key="block._uid"
|
||||||
class="group relative bg-white border border-layer-line rounded-lg overflow-hidden"
|
class="group relative bg-white border border-layer-line rounded-lg"
|
||||||
>
|
>
|
||||||
<!-- Block Header -->
|
<!-- Block Header -->
|
||||||
<div class="flex items-center gap-2 px-4 py-3 bg-muted/30 border-b border-layer-line">
|
<div class="flex items-center gap-2 px-4 py-3 bg-muted/30 border-b border-layer-line">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3" style="overflow: visible;">
|
||||||
<div class="flex items-center gap-2 mb-2">
|
<div class="flex items-center gap-2 mb-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div style="overflow: visible;">
|
||||||
<label class="block text-sm font-medium text-foreground mb-1">
|
<label class="block text-sm font-medium text-foreground mb-1">
|
||||||
Текст <span class="text-danger">*</span>
|
Текст <span class="text-danger">*</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -90,9 +90,9 @@ export default {
|
|||||||
tinymce.init({
|
tinymce.init({
|
||||||
selector: `#${this.editorId}`,
|
selector: `#${this.editorId}`,
|
||||||
license_key: 'gpl',
|
license_key: 'gpl',
|
||||||
autoresize_bottom_margin: 20,
|
min_height: 400,
|
||||||
autoresize_overflow_padding: 20,
|
autoresize_bottom_margin: 30,
|
||||||
max_height: 600,
|
autoresize_overflow_padding: 30,
|
||||||
menubar: false,
|
menubar: false,
|
||||||
statusbar: true,
|
statusbar: true,
|
||||||
branding: false,
|
branding: false,
|
||||||
@@ -105,14 +105,13 @@ export default {
|
|||||||
toolbar: 'undo redo | blocks | ' +
|
toolbar: 'undo redo | blocks | ' +
|
||||||
'bold italic forecolor | alignleft aligncenter ' +
|
'bold italic forecolor | alignleft aligncenter ' +
|
||||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
'alignright alignjustify | bullist numlist outdent indent | ' +
|
||||||
'removeformat | help',
|
'removeformat | help | fullscreen',
|
||||||
content_style: 'body { font-family: Inter, -apple-system, sans-serif; font-size: 14px; }',
|
content_style: 'body { font-family: Inter, -apple-system, sans-serif; font-size: 14px; }',
|
||||||
setup: (editor) => {
|
setup: (editor) => {
|
||||||
this.editor = editor;
|
this.editor = editor;
|
||||||
editor.on('init', () => {
|
editor.on('init', () => {
|
||||||
editor.setContent(this.modelValue.content || '');
|
editor.setContent(this.modelValue.content || '');
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
// Emit initial content to ensure parent has correct value
|
|
||||||
this.update('content', editor.getContent());
|
this.update('content', editor.getContent());
|
||||||
});
|
});
|
||||||
editor.on('change', () => {
|
editor.on('change', () => {
|
||||||
|
|||||||
@@ -96,7 +96,6 @@
|
|||||||
<DashboardIcon v-if="tab.id === 'main'" name="information-circle" size="4" :class="activeTab === tab.id ? 'text-primary' : 'text-muted-foreground-2'" />
|
<DashboardIcon v-if="tab.id === 'main'" name="information-circle" size="4" :class="activeTab === tab.id ? 'text-primary' : 'text-muted-foreground-2'" />
|
||||||
<DashboardIcon v-if="tab.id === 'content'" name="document-text" size="4" :class="activeTab === tab.id ? 'text-primary' : 'text-muted-foreground-2'" />
|
<DashboardIcon v-if="tab.id === 'content'" name="document-text" size="4" :class="activeTab === tab.id ? 'text-primary' : 'text-muted-foreground-2'" />
|
||||||
<DashboardIcon v-if="tab.id === 'media'" name="photo" size="4" :class="activeTab === tab.id ? 'text-primary' : 'text-muted-foreground-2'" />
|
<DashboardIcon v-if="tab.id === 'media'" name="photo" size="4" :class="activeTab === tab.id ? 'text-primary' : 'text-muted-foreground-2'" />
|
||||||
<DashboardIcon v-if="tab.id === 'slider'" name="presentation-chart-bar" size="4" :class="activeTab === tab.id ? 'text-primary' : 'text-muted-foreground-2'" />
|
|
||||||
{{ tab.label }}
|
{{ tab.label }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -305,7 +304,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Social Media -->
|
<!-- Social Media — ВРЕМЕННО СКРЫТО -->
|
||||||
|
<!--
|
||||||
<div class="border border-layer-line rounded-lg overflow-hidden">
|
<div class="border border-layer-line rounded-lg overflow-hidden">
|
||||||
<div class="px-4 py-3 bg-surface/50 border-b border-line-2">
|
<div class="px-4 py-3 bg-surface/50 border-b border-line-2">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -325,9 +325,12 @@
|
|||||||
Опубликовать ВКонтакте
|
Опубликовать ВКонтакте
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-xs text-muted-foreground-1">Новость будет автоматически опубликована в VK</p>
|
<p class="mt-2 text-xs text-muted-foreground-1">
|
||||||
|
{{ isEdit ? 'Новость будет обновлена в VK' : 'Новость будет автоматически опубликована в VK' }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab: Content -->
|
<!-- Tab: Content -->
|
||||||
@@ -473,14 +476,19 @@
|
|||||||
<div class="space-y-3 text-center">
|
<div class="space-y-3 text-center">
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div :class="[isDraggingGallery ? 'bg-primary/20' : 'bg-primary/10', 'w-12 h-12 rounded-lg flex items-center justify-center transition-all']">
|
<div :class="[isDraggingGallery ? 'bg-primary/20' : 'bg-primary/10', 'w-12 h-12 rounded-lg flex items-center justify-center transition-all']">
|
||||||
<DashboardIcon name="cloud-arrow-up" size="6" class="text-primary" />
|
<svg v-if="uploadingImages" class="animate-spin w-6 h-6 text-primary" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
<DashboardIcon v-else name="cloud-arrow-up" size="6" class="text-primary" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-medium text-foreground">
|
<p class="text-sm font-medium text-foreground">
|
||||||
<span class="text-primary">Нажмите для выбора</span> или перетащите файлы
|
<span v-if="uploadingImages">Загрузка...</span>
|
||||||
|
<span v-else><span class="text-primary">Нажмите для выбора</span> или перетащите файлы</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-muted-foreground-1 mt-1">JPG, PNG (до 20MB каждый, макс. 200 файлов)</p>
|
<p class="text-xs text-muted-foreground-1 mt-1">JPG, PNG, WebP (до 20MB каждый, макс. 200 файлов)</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -546,14 +554,6 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab: Slider -->
|
|
||||||
<div v-if="activeTab === 'slider'" class="space-y-6">
|
|
||||||
<div class="text-center py-12">
|
|
||||||
<DashboardIcon name="information-circle" size="16" class="text-gray-400 mx-auto mb-4" />
|
|
||||||
<p class="text-sm text-gray-600">Управление слайдами доступно в разделе "Слайдеры" Dashboard</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -582,10 +582,6 @@ export default {
|
|||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
sliders: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
statuses: {
|
statuses: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
@@ -596,10 +592,10 @@ export default {
|
|||||||
return {
|
return {
|
||||||
activeTab: 'main',
|
activeTab: 'main',
|
||||||
isEdit: false,
|
isEdit: false,
|
||||||
isSliderEnabled: false,
|
|
||||||
activeButton: true,
|
activeButton: true,
|
||||||
isDraggingPreview: false,
|
isDraggingPreview: false,
|
||||||
isDraggingGallery: false,
|
isDraggingGallery: false,
|
||||||
|
uploadingImages: false,
|
||||||
newTag: '',
|
newTag: '',
|
||||||
newAuthor: '',
|
newAuthor: '',
|
||||||
PostStatus,
|
PostStatus,
|
||||||
@@ -642,8 +638,7 @@ export default {
|
|||||||
tabs: [
|
tabs: [
|
||||||
{ id: 'main', label: 'Основная информация' },
|
{ id: 'main', label: 'Основная информация' },
|
||||||
{ id: 'content', label: 'Содержание' },
|
{ id: 'content', label: 'Содержание' },
|
||||||
{ id: 'media', label: 'Медиа' },
|
{ id: 'media', label: 'Медиа' }
|
||||||
{ id: 'slider', label: 'Слайдер' }
|
|
||||||
],
|
],
|
||||||
slugGenerated: false
|
slugGenerated: false
|
||||||
}
|
}
|
||||||
@@ -685,25 +680,24 @@ export default {
|
|||||||
if (typeof this.form.preview === 'string') {
|
if (typeof this.form.preview === 'string') {
|
||||||
return `/storage/${this.form.preview}`;
|
return `/storage/${this.form.preview}`;
|
||||||
}
|
}
|
||||||
return URL.createObjectURL(this.form.preview);
|
if (this.form.preview instanceof File || this.form.preview instanceof Blob) {
|
||||||
|
return URL.createObjectURL(this.form.preview);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
galleryUrls() {
|
galleryUrls() {
|
||||||
return this.form.images.map(img => {
|
return this.form.images.map(img => {
|
||||||
|
if (!img) return null;
|
||||||
if (typeof img === 'string') {
|
if (typeof img === 'string') {
|
||||||
return `/storage/${img}`;
|
return `/storage/${img}`;
|
||||||
}
|
}
|
||||||
return URL.createObjectURL(img);
|
if (img instanceof File || img instanceof Blob) {
|
||||||
});
|
return URL.createObjectURL(img);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}).filter(url => url !== null);
|
||||||
},
|
},
|
||||||
|
|
||||||
slideImageUrl() {
|
|
||||||
if (!this.form.slide.image.url) return null;
|
|
||||||
if (typeof this.form.slide.image.url === 'string') {
|
|
||||||
return `/storage/${this.form.slide.image.url}`;
|
|
||||||
}
|
|
||||||
return URL.createObjectURL(this.form.slide.image.url);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
@@ -729,6 +723,7 @@ export default {
|
|||||||
|
|
||||||
initializeForm() {
|
initializeForm() {
|
||||||
const p = this.post;
|
const p = this.post;
|
||||||
|
|
||||||
this.form = {
|
this.form = {
|
||||||
...this.form,
|
...this.form,
|
||||||
title: p.title || '',
|
title: p.title || '',
|
||||||
@@ -739,13 +734,11 @@ export default {
|
|||||||
authors: p.authors || [],
|
authors: p.authors || [],
|
||||||
content: p.content || [],
|
content: p.content || [],
|
||||||
preview: p.preview || null,
|
preview: p.preview || null,
|
||||||
images: p.images || [],
|
images: Array.isArray(p.images) ? [...p.images] : [],
|
||||||
publish_setting: p.publish_setting || { publish_after: false, publish_at: null },
|
publish_setting: p.publish_setting || { publish_after: false, publish_at: null },
|
||||||
publication: p.publication || { vk: true, telegram: true },
|
publication: p.publication || { vk: true, telegram: true },
|
||||||
slide: p.slide || this.form.slide
|
slide: p.slide || this.form.slide
|
||||||
};
|
};
|
||||||
|
|
||||||
this.isSliderEnabled = !!(p.slide && p.slide.slider_id);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
inputClass(field) {
|
inputClass(field) {
|
||||||
@@ -793,14 +786,44 @@ export default {
|
|||||||
handlePreviewDrop(e) {
|
handlePreviewDrop(e) {
|
||||||
const files = e.dataTransfer.files;
|
const files = e.dataTransfer.files;
|
||||||
if (files.length > 0 && this.isImageFile(files[0])) {
|
if (files.length > 0 && this.isImageFile(files[0])) {
|
||||||
this.form.preview = files[0];
|
this.uploadPreviewToServer(files[0]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handlePreviewFileSelect(e) {
|
handlePreviewFileSelect(e) {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
if (file && this.isImageFile(file)) {
|
if (file && this.isImageFile(file)) {
|
||||||
this.form.preview = file;
|
this.uploadPreviewToServer(file);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async uploadPreviewToServer(file) {
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
this.uploadingImages = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('images[]', file);
|
||||||
|
|
||||||
|
const response = await fetch(route('dashboard.posts.upload-images'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success && result.paths && result.paths.length > 0) {
|
||||||
|
this.form.preview = result.paths[0];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Preview upload error:', error);
|
||||||
|
} finally {
|
||||||
|
this.uploadingImages = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -810,16 +833,48 @@ export default {
|
|||||||
|
|
||||||
handleGalleryDrop(e) {
|
handleGalleryDrop(e) {
|
||||||
const files = Array.from(e.dataTransfer.files).filter(f => this.isImageFile(f));
|
const files = Array.from(e.dataTransfer.files).filter(f => this.isImageFile(f));
|
||||||
files.forEach(file => {
|
this.uploadImagesToServer(files);
|
||||||
this.form.images.push(file);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
handleGalleryFileSelect(e) {
|
handleGalleryFileSelect(e) {
|
||||||
const files = Array.from(e.target.files).filter(f => this.isImageFile(f));
|
const files = Array.from(e.target.files).filter(f => this.isImageFile(f));
|
||||||
files.forEach(file => {
|
this.uploadImagesToServer(files);
|
||||||
this.form.images.push(file);
|
// Сбрасываем input чтобы можно было выбрать те же файлы снова
|
||||||
});
|
e.target.value = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
async uploadImagesToServer(files) {
|
||||||
|
if (files.length === 0) return;
|
||||||
|
|
||||||
|
this.uploadingImages = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
files.forEach(file => {
|
||||||
|
formData.append('images[]', file);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(route('dashboard.posts.upload-images'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success && result.paths) {
|
||||||
|
result.paths.forEach(path => {
|
||||||
|
this.form.images.push(path);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Image upload error:', error);
|
||||||
|
} finally {
|
||||||
|
this.uploadingImages = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
triggerGalleryInput() {
|
triggerGalleryInput() {
|
||||||
@@ -884,27 +939,36 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSlideImageSelect(e) {
|
|
||||||
const file = e.target.files[0];
|
|
||||||
if (file && this.isImageFile(file)) {
|
|
||||||
this.form.slide.image.url = file;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
isImageFile(file) {
|
|
||||||
return ['image/jpeg', 'image/png', 'image/webp'].includes(file.type);
|
|
||||||
},
|
|
||||||
|
|
||||||
submitForm() {
|
submitForm() {
|
||||||
this.form.processing = true;
|
this.form.processing = true;
|
||||||
this.form.errors = {};
|
this.form.errors = {};
|
||||||
|
|
||||||
|
// Фильтруем изображения - оставляем только строки (пути)
|
||||||
|
this.form.images = this.form.images.filter(img => typeof img === 'string' && img.length > 0);
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
Object.keys(this.form).forEach(key => {
|
Object.keys(this.form).forEach(key => {
|
||||||
if (key === 'errors' || key === 'processing') return;
|
if (key === 'errors' || key === 'processing') return;
|
||||||
|
|
||||||
const value = this.form[key];
|
let value = this.form[key];
|
||||||
|
|
||||||
|
// Пропускаем preview если null, пустая строка или пустой объект
|
||||||
|
if (key === 'preview') {
|
||||||
|
if (!value || value === '' || (typeof value === 'object' && Object.keys(value).length === 0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пропускаем images если массив пустой
|
||||||
|
if (key === 'images' && Array.isArray(value) && value.length === 0) return;
|
||||||
|
|
||||||
|
// Пропускаем category_id если null
|
||||||
|
if (key === 'category_id' && (value === null || value === '')) return;
|
||||||
|
|
||||||
|
// Пропускаем slide если slider_id null и нет данных
|
||||||
|
if (key === 'slide' && value && !value.slider_id && !value.title && !value.content) return;
|
||||||
|
|
||||||
if (typeof value === 'object' && value !== null) {
|
if (typeof value === 'object' && value !== null) {
|
||||||
formData.append(key, JSON.stringify(value));
|
formData.append(key, JSON.stringify(value));
|
||||||
} else {
|
} else {
|
||||||
@@ -916,15 +980,13 @@ export default {
|
|||||||
? route('dashboard.posts.update', this.post.id)
|
? route('dashboard.posts.update', this.post.id)
|
||||||
: route('dashboard.posts.store');
|
: route('dashboard.posts.store');
|
||||||
|
|
||||||
const method = this.isEdit ? 'POST' : 'POST';
|
|
||||||
|
|
||||||
if (this.isEdit) {
|
if (this.isEdit) {
|
||||||
formData.append('_method', 'PUT');
|
formData.append('_method', 'PUT');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$inertia.post(url, formData, {
|
this.$inertia.post(url, formData, {
|
||||||
preserveScroll: true,
|
preserveScroll: false,
|
||||||
onSuccess: () => {
|
onSuccess: (page) => {
|
||||||
this.form.processing = false;
|
this.form.processing = false;
|
||||||
},
|
},
|
||||||
onError: (errors) => {
|
onError: (errors) => {
|
||||||
|
|||||||
@@ -194,6 +194,13 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
<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">
|
<div class="flex items-center justify-end gap-1.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<Link
|
||||||
|
:href="route('dashboard.pages.edit', page.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" />
|
||||||
|
</Link>
|
||||||
<button
|
<button
|
||||||
@click.prevent="detachPage(page)"
|
@click.prevent="detachPage(page)"
|
||||||
class="p-2 text-muted-foreground-1 hover:text-rose-600 hover:bg-rose-500/10 rounded-lg transition-all"
|
class="p-2 text-muted-foreground-1 hover:text-rose-600 hover:bg-rose-500/10 rounded-lg transition-all"
|
||||||
@@ -224,6 +231,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { Link } from '@inertiajs/vue3';
|
||||||
import DashboardIcon from '../Components/DashboardIcon.vue';
|
import DashboardIcon from '../Components/DashboardIcon.vue';
|
||||||
import FlashMessages from '../Components/shared/FlashMessages.vue';
|
import FlashMessages from '../Components/shared/FlashMessages.vue';
|
||||||
import EmptyState from '../Components/shared/EmptyState.vue';
|
import EmptyState from '../Components/shared/EmptyState.vue';
|
||||||
@@ -231,6 +239,7 @@ import EmptyState from '../Components/shared/EmptyState.vue';
|
|||||||
export default {
|
export default {
|
||||||
name: 'SubSectionEdit',
|
name: 'SubSectionEdit',
|
||||||
components: {
|
components: {
|
||||||
|
Link,
|
||||||
DashboardIcon,
|
DashboardIcon,
|
||||||
FlashMessages,
|
FlashMessages,
|
||||||
EmptyState
|
EmptyState
|
||||||
|
|||||||
Reference in New Issue
Block a user