changes
This commit is contained in:
@@ -14,6 +14,23 @@ class Page extends Model
|
|||||||
|
|
||||||
protected $guarded = false;
|
protected $guarded = false;
|
||||||
|
|
||||||
|
protected static function boot(): void
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
|
||||||
|
// Auto-generate path when page is created
|
||||||
|
static::creating(function (Page $page) {
|
||||||
|
$page->generatePath();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only regenerate path if slug or parent section changed
|
||||||
|
static::updating(function (Page $page) {
|
||||||
|
if ($page->isDirty(['slug', 'sub_section_id'])) {
|
||||||
|
$page->generatePath();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public function section(): BelongsTo
|
public function section(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(SubSection::class, 'sub_section_id');
|
return $this->belongsTo(SubSection::class, 'sub_section_id');
|
||||||
@@ -28,4 +45,45 @@ class Page extends Model
|
|||||||
'content' => 'array',
|
'content' => 'array',
|
||||||
'settings' => 'array',
|
'settings' => 'array',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate path for the page based on slug and parent section
|
||||||
|
*/
|
||||||
|
protected function generatePath(): void
|
||||||
|
{
|
||||||
|
// Don't regenerate path for registered pages (system routes)
|
||||||
|
if ($this->exists && $this->is_registered) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only generate path if slug is present
|
||||||
|
if (empty($this->slug)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get sub_section_id from model attributes
|
||||||
|
$subSectionId = $this->sub_section_id;
|
||||||
|
|
||||||
|
if ($subSectionId === null) {
|
||||||
|
$this->path = $this->slug;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch subSection with mainSection relationship
|
||||||
|
$subSection = SubSection::with('mainSection')->find($subSectionId);
|
||||||
|
|
||||||
|
if ($subSection === null) {
|
||||||
|
$this->path = $this->slug;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mainSection = $subSection->mainSection;
|
||||||
|
|
||||||
|
if ($mainSection === null) {
|
||||||
|
$this->path = $subSection->slug . '/' . $this->slug;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->path = $mainSection->slug . '/' . $subSection->slug . '/' . $this->slug;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ use App\Containers\Dashboard\Tasks\Content\GenerateSearchDataTask;
|
|||||||
class CreatePageAction
|
class CreatePageAction
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly GeneratePagePathAction $generatePagePathAction,
|
|
||||||
private readonly GenerateSearchDataTask $generateSearchDataTask,
|
private readonly GenerateSearchDataTask $generateSearchDataTask,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -20,19 +19,11 @@ class CreatePageAction
|
|||||||
*/
|
*/
|
||||||
public function run(array $data): Page
|
public function run(array $data): Page
|
||||||
{
|
{
|
||||||
$subSectionId = $data['sub_section_id'] ?? null;
|
|
||||||
|
|
||||||
// Генерируем path на основе subSection
|
|
||||||
$data['path'] = $this->generatePagePathAction->run($data['slug'], $subSectionId);
|
|
||||||
|
|
||||||
// Генерируем search_data из контента
|
// Генерируем search_data из контента
|
||||||
if (!empty($data['content'])) {
|
if (!empty($data['content'])) {
|
||||||
$data['search_data'] = $this->generateSearchDataTask->run($data['content']);
|
$data['search_data'] = $this->generateSearchDataTask->run($data['content']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Удаляем sub_section_id — он не является полем модели Page
|
|
||||||
unset($data['sub_section_id']);
|
|
||||||
|
|
||||||
return Page::create($data);
|
return Page::create($data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Containers\Dashboard\Actions\Pages;
|
|
||||||
|
|
||||||
use App\Containers\AppStructure\Models\SubSection;
|
|
||||||
|
|
||||||
class GeneratePagePathAction
|
|
||||||
{
|
|
||||||
public function run(string $slug, ?int $subSectionId = null): string
|
|
||||||
{
|
|
||||||
if ($subSectionId === null) {
|
|
||||||
return $slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
$subSection = SubSection::with('mainSection')->find($subSectionId);
|
|
||||||
|
|
||||||
if ($subSection === null) {
|
|
||||||
return $slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($subSection->mainSection === null) {
|
|
||||||
return $subSection->slug . '/' . $slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $subSection->mainSection->slug . '/' . $subSection->slug . '/' . $slug;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@ use App\Containers\Dashboard\Tasks\Content\GenerateSearchDataTask;
|
|||||||
class UpdatePageAction
|
class UpdatePageAction
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly GeneratePagePathAction $generatePagePathAction,
|
|
||||||
private readonly GenerateSearchDataTask $generateSearchDataTask,
|
private readonly GenerateSearchDataTask $generateSearchDataTask,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -26,17 +25,6 @@ class UpdatePageAction
|
|||||||
$data['search_data'] = $this->generateSearchDataTask->run($data['content']);
|
$data['search_data'] = $this->generateSearchDataTask->run($data['content']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если страница не зарегистрирована, перегенерируем path
|
|
||||||
if ($page->is_registered == false) {
|
|
||||||
$subSectionId = $data['sub_section_id'] ?? $page->sub_section_id;
|
|
||||||
$slug = $data['slug'] ?? $page->slug;
|
|
||||||
|
|
||||||
$data['path'] = $this->generatePagePathAction->run($slug, $subSectionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Удаляем sub_section_id — он не является полем модели Page
|
|
||||||
unset($data['sub_section_id']);
|
|
||||||
|
|
||||||
$page->update($data);
|
$page->update($data);
|
||||||
|
|
||||||
return $page->fresh();
|
return $page->fresh();
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Containers\Dashboard\Actions\Posts;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class DeploySiteAction
|
||||||
|
{
|
||||||
|
private string $lockFile = '/var/www/_deploy/deploy.lock';
|
||||||
|
private string $logFile = '/var/www/_deploy/deploy.log';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создаёт файл-триггер для запуска деплоя
|
||||||
|
*
|
||||||
|
* @return array ['success' => bool, 'message' => string]
|
||||||
|
*/
|
||||||
|
public function run(): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Проверяем, не запущен ли уже деплой
|
||||||
|
if (file_exists($this->lockFile)) {
|
||||||
|
$lockContent = file_get_contents($this->lockFile);
|
||||||
|
$lockData = json_decode($lockContent, true);
|
||||||
|
|
||||||
|
if ($lockData && isset($lockData['status']) && $lockData['status'] === 'running') {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Деплой уже запущен! Подождите завершения текущего процесса.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём файл-триггер
|
||||||
|
$lockData = [
|
||||||
|
'status' => 'pending',
|
||||||
|
'started_at' => now()->toDateTimeString(),
|
||||||
|
'started_by' => auth()->id(),
|
||||||
|
];
|
||||||
|
|
||||||
|
file_put_contents($this->lockFile, json_encode($lockData, JSON_PRETTY_PRINT));
|
||||||
|
|
||||||
|
Log::info('Deploy triggered', ['user_id' => auth()->id()]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Деплой запущен! Процесс обновления сайта начнётся в течение 1 минуты.',
|
||||||
|
];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Deploy trigger failed', ['exception' => $e->getMessage()]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Ошибка при запуске деплоя: ' . $e->getMessage(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет статус деплоя
|
||||||
|
*/
|
||||||
|
public function getStatus(): array
|
||||||
|
{
|
||||||
|
if (!file_exists($this->lockFile)) {
|
||||||
|
return ['status' => 'idle', 'message' => 'Деплой не запущен'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($this->lockFile);
|
||||||
|
$data = json_decode($content, true);
|
||||||
|
|
||||||
|
if (!$data) {
|
||||||
|
return ['status' => 'error', 'message' => 'Ошибка чтения статуса'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Читаем лог если есть
|
||||||
|
$log = '';
|
||||||
|
if (file_exists($this->logFile)) {
|
||||||
|
$log = file_get_contents($this->logFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => $data['status'] ?? 'unknown',
|
||||||
|
'started_at' => $data['started_at'] ?? null,
|
||||||
|
'completed_at' => $data['completed_at'] ?? null,
|
||||||
|
'message' => $data['message'] ?? '',
|
||||||
|
'log' => $log,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Очищает статус деплоя
|
||||||
|
*/
|
||||||
|
public function clearStatus(): void
|
||||||
|
{
|
||||||
|
if (file_exists($this->lockFile)) {
|
||||||
|
unlink($this->lockFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Containers\Dashboard\UI\WEB\Controllers;
|
||||||
|
|
||||||
|
use App\Containers\Dashboard\Actions\Posts\DeploySiteAction;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
class DeployController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly DeploySiteAction $deploySiteAction,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запускает деплой (создаёт файл-триггер)
|
||||||
|
*/
|
||||||
|
public function deploy(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
if (app()->environment() !== 'production') {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Деплой доступен только на production',
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$request->user()->hasRole('admin')) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Недостаточно прав для выполнения этой операции',
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->deploySiteAction->run();
|
||||||
|
|
||||||
|
return response()->json($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет статус деплоя
|
||||||
|
*/
|
||||||
|
public function status(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
if (app()->environment() !== 'production') {
|
||||||
|
return response()->json(['status' => 'disabled'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$request->user()->hasRole('admin')) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Недостаточно прав',
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = $this->deploySiteAction->getStatus();
|
||||||
|
|
||||||
|
return response()->json($status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Очищает статус деплоя
|
||||||
|
*/
|
||||||
|
public function clear(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
if (!$request->user()->hasRole('admin')) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Недостаточно прав',
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->deploySiteAction->clearStatus();
|
||||||
|
|
||||||
|
return response()->json(['success' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ use App\Containers\Dashboard\UI\WEB\Controllers\CategoryController as NewsCatego
|
|||||||
use App\Containers\Dashboard\UI\WEB\Controllers\ContactWidgetController;
|
use App\Containers\Dashboard\UI\WEB\Controllers\ContactWidgetController;
|
||||||
use App\Containers\Dashboard\UI\WEB\Controllers\CreateSliderController;
|
use App\Containers\Dashboard\UI\WEB\Controllers\CreateSliderController;
|
||||||
use App\Containers\Dashboard\UI\WEB\Controllers\CustomFormController;
|
use App\Containers\Dashboard\UI\WEB\Controllers\CustomFormController;
|
||||||
|
use App\Containers\Dashboard\UI\WEB\Controllers\DeployController;
|
||||||
use App\Containers\Dashboard\UI\WEB\Controllers\DepartmentController;
|
use App\Containers\Dashboard\UI\WEB\Controllers\DepartmentController;
|
||||||
use App\Containers\Dashboard\UI\WEB\Controllers\PageReferenceListController;
|
use App\Containers\Dashboard\UI\WEB\Controllers\PageReferenceListController;
|
||||||
use App\Containers\Dashboard\UI\WEB\Controllers\DepartmentProgramController;
|
use App\Containers\Dashboard\UI\WEB\Controllers\DepartmentProgramController;
|
||||||
@@ -60,6 +61,9 @@ Route::post('/dashboard/logout', [AuthenticatedSessionController::class, 'destro
|
|||||||
// Authenticated dashboard routes
|
// Authenticated dashboard routes
|
||||||
Route::middleware(['access-check', 'dashboard.auth'])->group(function () {
|
Route::middleware(['access-check', 'dashboard.auth'])->group(function () {
|
||||||
Route::get('/dashboard', IndexDashboardController::class)->name('dashboard.index');
|
Route::get('/dashboard', IndexDashboardController::class)->name('dashboard.index');
|
||||||
|
Route::post('/dashboard/deploy', [DeployController::class, 'deploy'])->name('dashboard.deploy');
|
||||||
|
Route::get('/dashboard/deploy/status', [DeployController::class, 'status'])->name('dashboard.deploy.status');
|
||||||
|
Route::post('/dashboard/deploy/clear', [DeployController::class, 'clear'])->name('dashboard.deploy.clear');
|
||||||
|
|
||||||
// CRUD постов
|
// CRUD постов
|
||||||
Route::prefix('/dashboard/posts')->name('dashboard.posts.')->group(function () {
|
Route::prefix('/dashboard/posts')->name('dashboard.posts.')->group(function () {
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ class HandleInertiaRequests extends Middleware
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
...parent::share($request),
|
...parent::share($request),
|
||||||
|
'app' => [
|
||||||
|
'env' => app()->environment(),
|
||||||
|
],
|
||||||
'auth' => [
|
'auth' => [
|
||||||
'user' => $request->user() ? $request->user()->only('id', 'name', 'email', 'created_at') : null,
|
'user' => $request->user() ? $request->user()->only('id', 'name', 'email', 'created_at') : null,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ class HandleInertiaRequests extends Middleware
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
...parent::share($request),
|
...parent::share($request),
|
||||||
|
'app' => [
|
||||||
|
'env' => app()->environment(),
|
||||||
|
],
|
||||||
'auth' => [
|
'auth' => [
|
||||||
'user' => $request->user() ? $request->user()->only('id', 'name', 'email', 'created_at') : null,
|
'user' => $request->user() ? $request->user()->only('id', 'name', 'email', 'created_at') : null,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="modelValue.settings.is_accordion"
|
:checked="modelValue?.settings?.is_accordion"
|
||||||
@change="updateSettings('is_accordion', $event.target.checked)"
|
@change="updateSettings('is_accordion', $event.target.checked)"
|
||||||
class="h-4 w-4 text-primary focus:ring-primary border-layer-line rounded"
|
class="h-4 w-4 text-primary focus:ring-primary border-layer-line rounded"
|
||||||
/>
|
/>
|
||||||
@@ -97,7 +97,7 @@ export default {
|
|||||||
this.$emit('update:modelValue', { ...this.modelValue, [field]: value });
|
this.$emit('update:modelValue', { ...this.modelValue, [field]: value });
|
||||||
},
|
},
|
||||||
updateSettings(field, value) {
|
updateSettings(field, value) {
|
||||||
this.update('settings', { ...this.modelValue.settings, [field]: value });
|
this.update('settings', { ...(this.modelValue.settings || {}), [field]: value });
|
||||||
},
|
},
|
||||||
updateTab(index, field, value) {
|
updateTab(index, field, value) {
|
||||||
const tabs = [...this.tabs];
|
const tabs = [...this.tabs];
|
||||||
|
|||||||
@@ -6,6 +6,43 @@
|
|||||||
<!-- Flash Messages (shared component) -->
|
<!-- Flash Messages (shared component) -->
|
||||||
<FlashMessages />
|
<FlashMessages />
|
||||||
|
|
||||||
|
<!-- Deploy Button -->
|
||||||
|
<div v-if="isProduction" class="mb-6 bg-gradient-to-r from-primary/10 to-info/10 border border-primary/20 rounded-lg p-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-10 h-10 rounded-lg bg-primary/20 flex items-center justify-center">
|
||||||
|
<DashboardIcon name="arrow-path" size="5" class="text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-foreground">Обновление сайта</h3>
|
||||||
|
<p class="text-xs text-muted-foreground-1">Запуск скрипта деплоя и пересборка сервера</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="deploySite"
|
||||||
|
:disabled="deploying"
|
||||||
|
class="inline-flex items-center gap-2 px-5 py-2.5 bg-primary text-white text-sm font-medium rounded-lg hover:bg-primary-hover transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-sm hover:shadow-md"
|
||||||
|
>
|
||||||
|
<svg v-if="deploying" class="animate-spin w-4 h-4" 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="rocket-launch" size="4" />
|
||||||
|
{{ deploying ? 'Обновление...' : 'Обновить сайт' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Deploy Output -->
|
||||||
|
<div v-if="deployOutput" class="mt-4 p-3 bg-surface border border-layer-line rounded-lg">
|
||||||
|
<div class="flex items-start gap-2 mb-2">
|
||||||
|
<DashboardIcon :name="deploySuccess ? 'check-circle' : 'exclamation-circle'" size="4" :class="deploySuccess ? 'text-success' : 'text-rose-500'" />
|
||||||
|
<span class="text-sm font-medium" :class="deploySuccess ? 'text-success' : 'text-rose-500'">{{ deployMessage }}</span>
|
||||||
|
</div>
|
||||||
|
<pre v-if="deployOutput" class="mt-2 p-2 bg-muted/30 rounded text-xs text-foreground overflow-auto max-h-48">{{ deployOutput }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Stats Overview -->
|
<!-- Stats Overview -->
|
||||||
<StatsOverview :stats="stats" class="mb-6" />
|
<StatsOverview :stats="stats" class="mb-6" />
|
||||||
|
|
||||||
@@ -87,6 +124,11 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
deploying: false,
|
||||||
|
deployOutput: null,
|
||||||
|
deploySuccess: false,
|
||||||
|
deployStatus: null,
|
||||||
|
deployPolling: null,
|
||||||
domainSections: [
|
domainSections: [
|
||||||
{
|
{
|
||||||
title: 'Контент сайта',
|
title: 'Контент сайта',
|
||||||
@@ -125,7 +167,80 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.SET_DOCUMENT_TITLE('Главная');
|
this.SET_DOCUMENT_TITLE('Главная');
|
||||||
},
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
if (this.deployPolling) {
|
||||||
|
clearInterval(this.deployPolling);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isProduction() {
|
||||||
|
return this.$page.props.app?.env === 'production';
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async deploySite() {
|
||||||
|
if (!confirm('Вы уверены, что хотите обновить сайт? Это запустит скрипт деплоя.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.deploying = true;
|
||||||
|
this.deployOutput = null;
|
||||||
|
this.deploySuccess = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(route('dashboard.deploy'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
this.deploySuccess = result.success;
|
||||||
|
this.deployOutput = result.message;
|
||||||
|
|
||||||
|
// Если деплой запущен — начинаем polling статуса
|
||||||
|
if (result.success) {
|
||||||
|
this.startDeployPolling();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.deploySuccess = false;
|
||||||
|
this.deployOutput = 'Ошибка при выполнении запроса: ' + error.message;
|
||||||
|
} finally {
|
||||||
|
this.deploying = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
startDeployPolling() {
|
||||||
|
// Проверяем статус каждые 3 секунды
|
||||||
|
this.deployPolling = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(route('dashboard.deploy.status'));
|
||||||
|
const status = await response.json();
|
||||||
|
|
||||||
|
this.deployStatus = status;
|
||||||
|
|
||||||
|
if (status.status === 'completed' || status.status === 'failed' || status.status === 'idle') {
|
||||||
|
clearInterval(this.deployPolling);
|
||||||
|
this.deployPolling = null;
|
||||||
|
this.deployOutput = status.message || 'Деплой завершён';
|
||||||
|
this.deploySuccess = status.status === 'completed';
|
||||||
|
|
||||||
|
if (status.log) {
|
||||||
|
this.deployOutput += '\n\n' + status.log;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error polling deploy status:', error);
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
},
|
||||||
|
get deployMessage() {
|
||||||
|
if (!this.deployOutput) return '';
|
||||||
|
return this.deploySuccess ? 'Сайт успешно обновлён!' : 'Ошибка при обновлении сайта';
|
||||||
|
},
|
||||||
getBgClass(color) {
|
getBgClass(color) {
|
||||||
const map = {
|
const map = {
|
||||||
primary: 'bg-primary/10',
|
primary: 'bg-primary/10',
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-background-2">
|
<div class="min-h-screen bg-background-2">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="border-b border-line-2 bg-layer/50 backdrop-blur-sm sticky top-0 z-10 h-16">
|
<div class="border-b border-line-2 bg-layer/50 backdrop-blur-sm sticky top-0 z-10">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-full">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div class="flex items-center h-full gap-3">
|
<div class="flex items-center justify-between h-16">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
<a
|
<a
|
||||||
:href="route('dashboard.pages.index')"
|
:href="route('dashboard.pages.index')"
|
||||||
class="p-2 text-muted-foreground-1 hover:text-foreground hover:bg-muted-hover rounded-lg transition-all"
|
class="p-2 text-muted-foreground-1 hover:text-foreground hover:bg-muted-hover rounded-lg transition-all"
|
||||||
@@ -20,6 +21,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<a
|
||||||
|
:href="route('dashboard.pages.index')"
|
||||||
|
class="inline-flex items-center gap-2 px-4 py-2 bg-surface border border-layer-line text-foreground text-sm font-medium rounded-lg hover:bg-muted-hover transition-all"
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="submit"
|
||||||
|
:disabled="processing"
|
||||||
|
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 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<svg v-if="processing" class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||||
|
<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" />
|
||||||
|
</svg>
|
||||||
|
<DashboardIcon v-else name="check" size="4" />
|
||||||
|
{{ processing ? 'Создание...' : 'Создать страницу' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -275,28 +298,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Submit Button -->
|
|
||||||
<div class="flex items-center justify-end gap-3 mt-8 pt-6 border-t border-line-2">
|
|
||||||
<a
|
|
||||||
:href="route('dashboard.pages.index')"
|
|
||||||
class="inline-flex items-center gap-2 px-4 py-2.5 bg-surface border border-layer-line text-foreground text-sm font-medium rounded-lg hover:bg-muted-hover transition-all"
|
|
||||||
>
|
|
||||||
Отмена
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
:disabled="processing"
|
|
||||||
class="inline-flex items-center gap-2 px-4 py-2.5 bg-primary text-white text-sm font-medium rounded-lg hover:bg-primary-hover transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
<svg v-if="processing" class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
|
||||||
<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" />
|
|
||||||
</svg>
|
|
||||||
<DashboardIcon v-else name="check" size="4" />
|
|
||||||
{{ processing ? 'Сохранение...' : 'Создать страницу' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-background-2">
|
<div class="min-h-screen bg-background-2">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="border-b border-line-2 bg-layer/50 backdrop-blur-sm sticky top-0 z-10 h-16">
|
<div class="border-b border-line-2 bg-layer/50 backdrop-blur-sm sticky top-0 z-10">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-full">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div class="flex items-center h-full gap-3">
|
<div class="flex items-center justify-between h-16">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
<a
|
<a
|
||||||
:href="route('dashboard.pages.index')"
|
:href="route('dashboard.pages.index')"
|
||||||
class="p-2 text-muted-foreground-1 hover:text-foreground hover:bg-muted-hover rounded-lg transition-all"
|
class="p-2 text-muted-foreground-1 hover:text-foreground hover:bg-muted-hover rounded-lg transition-all"
|
||||||
@@ -20,6 +21,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<a
|
||||||
|
:href="route('dashboard.pages.index')"
|
||||||
|
class="inline-flex items-center gap-2 px-4 py-2 bg-surface border border-layer-line text-foreground text-sm font-medium rounded-lg hover:bg-muted-hover transition-all"
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="submit"
|
||||||
|
:disabled="processing"
|
||||||
|
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 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<svg v-if="processing" class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||||
|
<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" />
|
||||||
|
</svg>
|
||||||
|
<DashboardIcon v-else name="check" size="4" />
|
||||||
|
{{ processing ? 'Сохранение...' : 'Сохранить' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -287,28 +310,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Submit Button -->
|
|
||||||
<div class="flex items-center justify-end gap-3 mt-8 pt-6 border-t border-line-2">
|
|
||||||
<a
|
|
||||||
:href="route('dashboard.pages.index')"
|
|
||||||
class="inline-flex items-center gap-2 px-4 py-2.5 bg-surface border border-layer-line text-foreground text-sm font-medium rounded-lg hover:bg-muted-hover transition-all"
|
|
||||||
>
|
|
||||||
Отмена
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
:disabled="processing"
|
|
||||||
class="inline-flex items-center gap-2 px-4 py-2.5 bg-primary text-white text-sm font-medium rounded-lg hover:bg-primary-hover transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
<svg v-if="processing" class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
|
||||||
<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" />
|
|
||||||
</svg>
|
|
||||||
<DashboardIcon v-else name="check" size="4" />
|
|
||||||
{{ processing ? 'Сохранение...' : 'Сохранить' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user