changes
This commit is contained in:
@@ -0,0 +1,791 @@
|
||||
# Визуальная кастомизация страниц — Анализ и план реализации
|
||||
|
||||
> **Дата:** 7 апреля 2026 г.
|
||||
> **Контекст:** Запрос на функционал изменения визуала страниц через админку (отступы, шрифты, размеры, цвета и т.д.)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Текущая архитектура страниц
|
||||
|
||||
В проекте существуют **два типа страниц**:
|
||||
|
||||
### Тип A: Database-driven страницы (Page Builder)
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| **Создаются** | Через Filament админку |
|
||||
| **Хранение** | JSON в колонке `content` таблицы `pages` |
|
||||
| **Рендеринг** | Универсальный `Page.vue` → `Builder` компонент → динамический рендер блоков |
|
||||
| **Стилизация** | Hardcoded Tailwind классы в блоках (`HeadingBlock.vue`, `ParagraphBlock.vue`, и т.д.) |
|
||||
| **Настройки** | Колонка `settings` (JSON) — только **логические флаги** (скрыть breadcrumbs, навигацию и т.д.) |
|
||||
|
||||
**Ключевые файлы:**
|
||||
- Model: `app/Containers/AppStructure/Models/Page.php`
|
||||
- Controller: `app/Containers/AppStructure/UI/WEB/Controllers/PageController.php`
|
||||
- Action: `app/Containers/AppStructure/Actions/RenderPageAction.php`
|
||||
- Vue: `resources/js/Pages/Page.vue`
|
||||
- Builder: `resources/js/componentss/shared/builder/pageBuilder/Builder.vue`
|
||||
- Filament Form: `app/Filament/Components/Forms/PageForm.php`
|
||||
- Filament Builder: `app/Filament/Components/Forms/ItemForm/Pages/ContentBuilderItem.php`
|
||||
|
||||
### Тип B: Code-driven страницы (Hardcoded Vue компоненты)
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| **Создаются** | Как `.vue` файлы в `resources/js/Pages/` |
|
||||
| **Примеры** | `Main.vue` (главная), страницы Dashboard |
|
||||
| **Стилизация** | Tailwind классы напрямую в template |
|
||||
| **Авто-регистрация** | `RegisterApplicationRoutesTask` создаёт запись в `pages` с `is_registered = true` |
|
||||
|
||||
**Ключевые файлы:**
|
||||
- Task: `app/Containers/AppStructure/Tasks/RegisterApplicationRoutesTask.php`
|
||||
- Middleware: `app/Ship/Middleware/AccessCheck.php`
|
||||
- Пример: `resources/js/Pages/Main.vue`
|
||||
|
||||
### Текущая структура `pages.settings`
|
||||
|
||||
```json
|
||||
{
|
||||
"hide_page_sub_section_links": false,
|
||||
"hide_page_navigate_links": false,
|
||||
"hide_breadcrumbs": false,
|
||||
"form": {
|
||||
"id": "form_id",
|
||||
"title": "Заголовок",
|
||||
"description": "Описание",
|
||||
"button": "Текст кнопки"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Текущая структура `pages` таблицы
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---------|-----|----------|
|
||||
| `id` | bigint | Primary key |
|
||||
| `title` | string(255) | Заголовок страницы |
|
||||
| `content` | longText (JSON) | Builder блоки |
|
||||
| `slug` | string(255) | URL сегмент |
|
||||
| `path` | string | Полный URL путь |
|
||||
| `is_registered` | boolean | true = авто-регистрация из кода |
|
||||
| `is_visible` | boolean | Видимость |
|
||||
| `searchable` | boolean | Индексация в поиске |
|
||||
| `is_url` | boolean | Редирект на внешний URL |
|
||||
| `code` | integer | HTTP статус (200, 404, 500) |
|
||||
| `sub_section_id` | bigint FK | Связь с подразделом |
|
||||
| `settings` | longText (JSON) | Настройки отображения |
|
||||
| `icon` | string | Heroicon для навигации |
|
||||
| `search_data` | longText | Текст для поиска |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Проблема
|
||||
|
||||
Сейчас **нет возможности** через админку менять визуальные параметры страниц:
|
||||
- ❌ Отступы (padding, margin)
|
||||
- ❌ Размер шрифта
|
||||
- ❌ Шрифт (font-family)
|
||||
- ❌ Цвета
|
||||
- ❌ Максимальная ширина контента
|
||||
- ❌ И другие CSS-свойства
|
||||
|
||||
Все стили **захардкожены** в Vue компонентах и блоках билдера.
|
||||
|
||||
---
|
||||
|
||||
## 💡 Решения (от простого к сложному)
|
||||
|
||||
### Решение 1: Расширение `settings` JSON (Рекомендуемое)
|
||||
|
||||
**Концепция:** Добавить в существующую колонку `pages.settings` секцию `visual` с визуальными настройками.
|
||||
|
||||
**Структура данных:**
|
||||
```json
|
||||
{
|
||||
"hide_page_sub_section_links": false,
|
||||
"hide_page_navigate_links": false,
|
||||
"hide_breadcrumbs": false,
|
||||
"form": { ... },
|
||||
"visual": {
|
||||
"typography": {
|
||||
"font_family": "Inter",
|
||||
"title_size": "2xl",
|
||||
"body_size": "base",
|
||||
"line_height": "normal"
|
||||
},
|
||||
"spacing": {
|
||||
"container_padding_top": "10",
|
||||
"container_padding_bottom": "10",
|
||||
"container_padding_x": "4",
|
||||
"content_gap": "5"
|
||||
},
|
||||
"layout": {
|
||||
"max_width": "screen-xl",
|
||||
"sidebar_position": "left"
|
||||
},
|
||||
"colors": {
|
||||
"background": "white",
|
||||
"text": "gray-900"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Backend (Filament Form) — пример добавления в PageForm.php:**
|
||||
```php
|
||||
Section::make('Визуальные настройки')
|
||||
->description('Настройка внешнего вида страницы')
|
||||
->collapsible()
|
||||
->schema([
|
||||
Select::make('visual.typography.font_family')
|
||||
->label('Шрифт')
|
||||
->options([
|
||||
'Inter' => 'Inter (по умолчанию)',
|
||||
'Roboto' => 'Roboto',
|
||||
'Open Sans' => 'Open Sans',
|
||||
'Montserrat' => 'Montserrat',
|
||||
])
|
||||
->default('Inter'),
|
||||
|
||||
Select::make('visual.typography.title_size')
|
||||
->label('Размер заголовка')
|
||||
->options([
|
||||
'xl' => 'XL (маленький)',
|
||||
'2xl' => '2XL (стандарт)',
|
||||
'3xl' => '3XL (большой)',
|
||||
'4xl' => '4XL (очень большой)',
|
||||
])
|
||||
->default('2xl'),
|
||||
|
||||
Select::make('visual.spacing.container_padding_top')
|
||||
->label('Отступ сверху')
|
||||
->options([
|
||||
'0' => '0',
|
||||
'4' => '16px',
|
||||
'6' => '24px',
|
||||
'10' => '40px',
|
||||
])
|
||||
->default('10'),
|
||||
]);
|
||||
```
|
||||
|
||||
**Frontend (Page.vue) — пример применения:**
|
||||
```vue
|
||||
<template>
|
||||
<BasicPageWrapper>
|
||||
<div
|
||||
:class="[
|
||||
'relative mx-auto max-w-screen-xl w-full px-4 md:flex md:flex-row',
|
||||
visualClasses.containerPadding,
|
||||
visualClasses.backgroundColor
|
||||
]"
|
||||
:style="customFontStyle"
|
||||
>
|
||||
<BasicTitle
|
||||
:header="page.data.title"
|
||||
:size="settings?.visual?.typography?.title_size || '2xl'"
|
||||
/>
|
||||
<div id="page-area" :class="contentGapClass">
|
||||
<Builder :blocks="page.data.content" />
|
||||
</div>
|
||||
</div>
|
||||
</BasicPageWrapper>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
computed: {
|
||||
visualClasses() {
|
||||
const v = this.settings?.visual || {};
|
||||
return {
|
||||
containerPadding: `pt-${v.spacing?.container_padding_top || 10} pb-${v.spacing?.container_padding_bottom || 10}`,
|
||||
backgroundColor: `bg-${v.colors?.background || 'white'}`,
|
||||
};
|
||||
},
|
||||
customFontStyle() {
|
||||
const typography = this.settings?.visual?.typography || {};
|
||||
return {
|
||||
fontFamily: typography.font_family || 'Inter',
|
||||
fontSize: typography.body_size || '16px',
|
||||
lineHeight: typography.line_height === 'relaxed' ? '1.75' : 'normal',
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
**Плюсы:**
|
||||
- ✅ Минимальные изменения в архитектуре
|
||||
- ✅ Использует существующую инфраструктуру `settings`
|
||||
- ✅ Легко масштабировать (добавить новые поля в JSON)
|
||||
- ✅ Не требует миграций БД
|
||||
- ✅ Работает для **обоих типов страниц** (нужно только передать `settings` в Inertia)
|
||||
|
||||
**Минусы:**
|
||||
- ❌ Ограниченный набор стилей (только то, что предусмотрено в UI)
|
||||
- ❌ Нужно менять все Builder-блоки для поддержки наследования стилей
|
||||
|
||||
---
|
||||
|
||||
### Решение 2: CSS Custom Properties (CSS Variables)
|
||||
|
||||
**Концепция:** Хранить CSS-переменные в `settings`, применять через inline `<style>` тег.
|
||||
|
||||
**Структура данных:**
|
||||
```json
|
||||
{
|
||||
"visual": {
|
||||
"css_variables": {
|
||||
"--page-font-family": "Inter",
|
||||
"--page-title-size": "2rem",
|
||||
"--page-content-padding-top": "40px",
|
||||
"--page-max-width": "1280px",
|
||||
"--page-background": "#FFFFFF",
|
||||
"--page-text-color": "#0F172A"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Frontend (Page.vue):**
|
||||
```vue
|
||||
<template>
|
||||
<BasicPageWrapper>
|
||||
<style v-if="hasCustomStyles" scoped>
|
||||
#page-area {
|
||||
font-family: var(--page-font-family, Inter);
|
||||
font-size: var(--page-body-size, 16px);
|
||||
}
|
||||
h1 {
|
||||
font-size: var(--page-title-size, 2rem);
|
||||
}
|
||||
.page-container {
|
||||
padding-top: var(--page-content-padding-top, 40px);
|
||||
padding-bottom: var(--page-content-padding-bottom, 40px);
|
||||
max-width: var(--page-max-width, 1280px);
|
||||
background-color: var(--page-background, #FFFFFF);
|
||||
color: var(--page-text-color, #0F172A);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="page-container">
|
||||
<!-- ... -->
|
||||
</div>
|
||||
</BasicPageWrapper>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
computed: {
|
||||
cssVariables() {
|
||||
return this.settings?.visual?.css_variables || {};
|
||||
},
|
||||
hasCustomStyles() {
|
||||
return Object.keys(this.cssVariables).length > 0;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
**Плюсы:**
|
||||
- ✅ Гибкость — можно задать **любое** CSS-свойство
|
||||
- ✅ Каскадное применение — переменные наследуются вниз
|
||||
- ✅ Не нужно менять все Builder-блоки (применяются глобально)
|
||||
- ✅ Легко реализовать в админке (ключ-значение форма)
|
||||
|
||||
**Минусы:**
|
||||
- ❌ Требует знания CSS у контент-менеджеров (или ограниченный набор в UI)
|
||||
- ❌ Могут быть конфликты с Tailwind классами
|
||||
- ❌ Сложнее валидировать значения
|
||||
|
||||
---
|
||||
|
||||
### Решение 3: Page Themes / Templates System
|
||||
|
||||
**Концепция:** Создать систему **тем** — предустановленных наборов стилей.
|
||||
|
||||
**Структура данных:**
|
||||
|
||||
**Таблица `page_themes`:**
|
||||
```sql
|
||||
id | name | description | styles (JSON) | is_active
|
||||
1 | Default | Стандартная тема | {...} | true
|
||||
2 | Compact | Компактная | {...} | false
|
||||
3 | Spacious | Просторная | {...} | false
|
||||
```
|
||||
|
||||
**`pages` таблица — новые колонки:**
|
||||
```sql
|
||||
theme_id (FK -> page_themes)
|
||||
custom_overrides (JSON) — индивидуальные переопределения
|
||||
```
|
||||
|
||||
**Пример `styles` в теме:**
|
||||
```json
|
||||
{
|
||||
"typography": {
|
||||
"font_family": "Inter",
|
||||
"title_sizes": { "h1": "3xl", "h2": "2xl", "h3": "xl" },
|
||||
"body_size": "base",
|
||||
"line_height": "relaxed"
|
||||
},
|
||||
"spacing": {
|
||||
"container": { "max_width": "screen-xl", "px": "4", "py": "10" },
|
||||
"content_gap": "space-y-5"
|
||||
},
|
||||
"colors": {
|
||||
"background": "white",
|
||||
"text": "gray-900",
|
||||
"accent": "primary"
|
||||
},
|
||||
"borders": {
|
||||
"radius": "rounded-lg",
|
||||
"shadow": "shadow-sm"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Filament админка:**
|
||||
- CRUD для `PageTheme` (создание/редактирование тем)
|
||||
- В `PageForm`: `Select::make('theme_id')->relationship('theme', 'name')`
|
||||
- Опционально: overrides для конкретной страницы
|
||||
|
||||
**Frontend:**
|
||||
```vue
|
||||
<script>
|
||||
export default {
|
||||
computed: {
|
||||
theme() {
|
||||
return this.page.data.theme?.styles || {};
|
||||
},
|
||||
overrides() {
|
||||
return this.page.data.custom_overrides || {};
|
||||
},
|
||||
mergedSettings() {
|
||||
return _.merge({}, this.theme, this.overrides);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
**Плюсы:**
|
||||
- ✅ **Масштабируемость** — одна тема применяется к множеству страниц
|
||||
- ✅ **Безопасность** — админ не сломает стили (выбирает из готовых)
|
||||
- ✅ **A/B тестирование** — легко менять темы
|
||||
- ✅ Разделение ответственности: дизайнер создаёт темы, контент-менеджер выбирает
|
||||
|
||||
**Минусы:**
|
||||
- ❌ **Сложность реализации** — новая таблица, CRUD, UI для тем
|
||||
- ❌ Требует больше времени на разработку
|
||||
- ❌ Нужно менять `PageResource`, `RenderPageAction`, `Page.vue`
|
||||
|
||||
---
|
||||
|
||||
### Решение 4: Custom CSS per Page (Полная свобода)
|
||||
|
||||
**Концепция:** Позволить админу писать **произвольный CSS** для страницы.
|
||||
|
||||
**Структура данных:**
|
||||
```json
|
||||
{
|
||||
"visual": {
|
||||
"custom_css": "#page-area h1 { font-size: 2.5rem; color: #2D4191; }\n#page-area p { line-height: 1.8; }"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Frontend:**
|
||||
```vue
|
||||
<template>
|
||||
<BasicPageWrapper>
|
||||
<style v-if="settings?.visual?.custom_css" v-html="settings.visual.custom_css" />
|
||||
<!-- ... -->
|
||||
</BasicPageWrapper>
|
||||
</template>
|
||||
```
|
||||
|
||||
**Filament Form:**
|
||||
```php
|
||||
Textarea::make('visual.custom_css')
|
||||
->label('Custom CSS')
|
||||
->rows(10)
|
||||
->monospace()
|
||||
->helperText('Произвольный CSS для страницы. Используйте с осторожностью!')
|
||||
```
|
||||
|
||||
**Плюсы:**
|
||||
- ✅ **Полная гибкость** — можно сделать **что угодно**
|
||||
- ✅ Минимальная реализация (одно поле + `<style>` тег)
|
||||
|
||||
**Минусы:**
|
||||
- ❌ **Опасно** — можно сломать layout, нужны ограничения
|
||||
- ❌ Требует знаний CSS у админа
|
||||
- ❌ Сложно валидировать
|
||||
- ❌ Потенциальные XSS (нужен sanitize)
|
||||
- ❌ **Не рекомендуется** для не-технических пользователей
|
||||
|
||||
---
|
||||
|
||||
## 🏆 Рекомендация (Консенсус комитета)
|
||||
|
||||
**Используй комбинацию Решений 1 + 2:**
|
||||
|
||||
1. **Основное:** Расширение `settings.visual` с **типизированными настройками** (шрифты, отступы, цвета через select/dropdown)
|
||||
2. **Дополнительное:** CSS Custom Variables для **тонкой настройки** (для продвинутых пользователей)
|
||||
|
||||
**Почему:**
|
||||
- ✅ Покрывает 95% use cases (типичные настройки визуала)
|
||||
- ✅ Безопасно (админ выбирает из пресетов, не пишет CSS)
|
||||
- ✅ CSS variables дают гибкость для edge cases
|
||||
- ✅ Минимальные изменения в архитектуре (используем существующий `settings`)
|
||||
- ✅ Работает для **обоих типов страниц** (нужно только передавать `settings` в Inertia для hardcoded страниц)
|
||||
|
||||
---
|
||||
|
||||
## 📋 План реализации
|
||||
|
||||
### Шаг 1: Backend — Filament Form
|
||||
|
||||
**Файл:** `app/Filament/Components/Forms/PageForm.php`
|
||||
|
||||
Добавить в Tab `'Дополнительные настройки'` новую секцию:
|
||||
|
||||
```php
|
||||
Section::make('Визуальные настройки')
|
||||
->description('Настройка типографики, отступов и цветов страницы')
|
||||
->collapsible()
|
||||
->schema([
|
||||
// Типографика
|
||||
Select::make('visual.typography.font_family')
|
||||
->label('Шрифт')
|
||||
->options([
|
||||
'Inter' => 'Inter (по умолчанию)',
|
||||
'Roboto' => 'Roboto',
|
||||
'Open Sans' => 'Open Sans',
|
||||
'Montserrat' => 'Montserrat',
|
||||
])
|
||||
->default('Inter')
|
||||
->columnSpan(1),
|
||||
|
||||
Select::make('visual.typography.title_size')
|
||||
->label('Размер заголовка H1')
|
||||
->options([
|
||||
'xl' => 'XL (маленький)',
|
||||
'2xl' => '2XL (стандарт)',
|
||||
'3xl' => '3XL (большой)',
|
||||
'4xl' => '4XL (очень большой)',
|
||||
])
|
||||
->default('2xl')
|
||||
->columnSpan(1),
|
||||
|
||||
Select::make('visual.typography.body_size')
|
||||
->label('Размер текста')
|
||||
->options([
|
||||
'sm' => 'SM (маленький)',
|
||||
'base' => 'Base (стандарт)',
|
||||
'lg' => 'LG (большой)',
|
||||
])
|
||||
->default('base')
|
||||
->columnSpan(1),
|
||||
|
||||
Select::make('visual.typography.line_height')
|
||||
->label('Межстрочный интервал')
|
||||
->options([
|
||||
'tight' => 'Tight (плотный)',
|
||||
'normal' => 'Normal (стандарт)',
|
||||
'relaxed' => 'Relaxed (просторный)',
|
||||
])
|
||||
->default('normal')
|
||||
->columnSpan(1),
|
||||
|
||||
// Отступы
|
||||
Select::make('visual.spacing.container_padding_top')
|
||||
->label('Отступ сверху')
|
||||
->options([
|
||||
'0' => '0',
|
||||
'4' => '16px',
|
||||
'6' => '24px',
|
||||
'10' => '40px',
|
||||
'16' => '64px',
|
||||
])
|
||||
->default('10')
|
||||
->columnSpan(1),
|
||||
|
||||
Select::make('visual.spacing.container_padding_bottom')
|
||||
->label('Отступ снизу')
|
||||
->options([
|
||||
'0' => '0',
|
||||
'4' => '16px',
|
||||
'6' => '24px',
|
||||
'10' => '40px',
|
||||
'16' => '64px',
|
||||
])
|
||||
->default('10')
|
||||
->columnSpan(1),
|
||||
|
||||
Select::make('visual.spacing.content_gap')
|
||||
->label('Расстояние между блоками')
|
||||
->options([
|
||||
'2' => '8px',
|
||||
'5' => '20px (стандарт)',
|
||||
'8' => '32px',
|
||||
'10' => '40px',
|
||||
])
|
||||
->default('5')
|
||||
->columnSpan(1),
|
||||
|
||||
// Layout
|
||||
Select::make('visual.layout.max_width')
|
||||
->label('Максимальная ширина контента')
|
||||
->options([
|
||||
'screen-md' => '768px',
|
||||
'screen-lg' => '1024px',
|
||||
'screen-xl' => '1280px (стандарт)',
|
||||
'screen-2xl' => '1536px',
|
||||
'full' => '100%',
|
||||
])
|
||||
->default('screen-xl')
|
||||
->columnSpan(1),
|
||||
])
|
||||
->columns(4),
|
||||
```
|
||||
|
||||
### Шаг 2: Frontend — Page.vue
|
||||
|
||||
**Файл:** `resources/js/Pages/Page.vue`
|
||||
|
||||
Изменения:
|
||||
|
||||
1. Добавить computed properties для визуальных настроек
|
||||
2. Применить классы к контейнеру
|
||||
3. Передать настройки в `BasicTitle` и `Builder`
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<MetaTags :seo="seo" />
|
||||
<MainPageNavBar :sections="$page.props.navigation" />
|
||||
|
||||
<BasicPageWrapper>
|
||||
<div
|
||||
:class="[
|
||||
'relative mx-auto w-full px-4 md:flex md:flex-row',
|
||||
`max-w-${visualSettings.layout?.max_width || 'screen-xl'}`,
|
||||
`pt-${visualSettings.spacing?.container_padding_top || '10'}`,
|
||||
`pb-${visualSettings.spacing?.container_padding_bottom || '10'}`,
|
||||
`bg-${visualSettings.colors?.background || 'white'}`
|
||||
]"
|
||||
:style="typographyStyle"
|
||||
>
|
||||
<PageSubSectionLinks
|
||||
v-if="!settings?.hide_page_sub_section_links"
|
||||
:sub-section-pages="subSectionPages"
|
||||
:current-section="page.data.section"
|
||||
/>
|
||||
<NavigateLinks
|
||||
v-if="!settings?.hide_page_navigate_links"
|
||||
:header-navs="headerNavs"
|
||||
/>
|
||||
<div class="w-full min-w-0 mt-1 max-w-6xl px-1 md:px-6">
|
||||
<div :class="`space-y-${visualSettings.spacing?.content_gap || '5'}`">
|
||||
<BaseBreadcrumbs
|
||||
v-if="!settings?.hide_breadcrumbs"
|
||||
:breadcrumbs="breadcrumbs"
|
||||
>
|
||||
<BreadcrumbsItem
|
||||
:title="breadcrumbs.page.data.title"
|
||||
:url="route('page.view', breadcrumbs.page.data.path)"
|
||||
/>
|
||||
</BaseBreadcrumbs>
|
||||
|
||||
<BasicTitle
|
||||
:header="page.data.title"
|
||||
:size="visualSettings.typography?.title_size || '2xl'"
|
||||
/>
|
||||
|
||||
<div id="page-area">
|
||||
<Builder
|
||||
:blocks="page.data.content"
|
||||
:settings="visualSettings"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BasicFooter />
|
||||
</BasicPageWrapper>
|
||||
|
||||
<!-- Floating form (без изменений) -->
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Page",
|
||||
data() {
|
||||
return {
|
||||
headerNavs: (this.page?.data?.content || [])
|
||||
.filter((block) => block?.type === "heading")
|
||||
.map((block) => ({
|
||||
id: block?.data?.id,
|
||||
text: block?.data?.content,
|
||||
})),
|
||||
settings: this.page?.data?.settings || {},
|
||||
};
|
||||
},
|
||||
props: {
|
||||
page: { type: Object },
|
||||
subSectionPages: { type: Object },
|
||||
breadcrumbs: { type: Object },
|
||||
seo: { type: Object },
|
||||
},
|
||||
computed: {
|
||||
visualSettings() {
|
||||
return this.settings?.visual || {};
|
||||
},
|
||||
typographyStyle() {
|
||||
const t = this.visualSettings.typography || {};
|
||||
return {
|
||||
fontFamily: t.font_family || 'Inter',
|
||||
fontSize: this.bodySizeMap[t.body_size] || '16px',
|
||||
lineHeight: this.lineHeightMap[t.line_height] || 'normal',
|
||||
};
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
bodySizeMap: {
|
||||
'sm': '14px',
|
||||
'base': '16px',
|
||||
'lg': '18px',
|
||||
},
|
||||
lineHeightMap: {
|
||||
'tight': '1.25',
|
||||
'normal': '1.5',
|
||||
'relaxed': '1.75',
|
||||
},
|
||||
};
|
||||
},
|
||||
// ... components, methods (без изменений)
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
### Шаг 3: Обновить Builder-блоки
|
||||
|
||||
**Файл:** `resources/js/componentss/shared/builder/pageBuilder/Builder.vue`
|
||||
|
||||
Передать `settings` в дочерние блоки:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<component
|
||||
v-for="(block, index) in blocks"
|
||||
:key="index"
|
||||
:is="blockComponents[block.type]"
|
||||
:data="block.data"
|
||||
:settings="settings"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
blocks: { type: Array, required: true },
|
||||
settings: { type: Object, default: () => ({}) },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
### Шаг 4: Обновить BasicTitle
|
||||
|
||||
**Файл:** `resources/js/componentss/ui/titles/BasicTitle.vue`
|
||||
|
||||
Добавить prop для размера:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<h1 :class="titleClass">{{ header }}</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "BasicTitle",
|
||||
props: {
|
||||
header: { type: String, required: true },
|
||||
size: { type: String, default: '2xl' },
|
||||
},
|
||||
computed: {
|
||||
titleClass() {
|
||||
const sizeMap = {
|
||||
'xl': 'text-xl font-bold mb-5',
|
||||
'2xl': 'text-2xl font-bold mb-5 md:text-3xl',
|
||||
'3xl': 'text-3xl font-bold mb-5 md:text-4xl',
|
||||
'4xl': 'text-4xl font-bold mb-5 md:text-5xl',
|
||||
};
|
||||
return sizeMap[this.size] || sizeMap['2xl'];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
### Шаг 5: Hardcoded страницы
|
||||
|
||||
Для страниц типа `Main.vue` — нужно передавать `settings` из `pages` записи через Inertia.
|
||||
|
||||
**Backend:** В контроллере главной страницы найти страницу и передать settings:
|
||||
```php
|
||||
$mainPage = Page::where('path', '/')->first();
|
||||
return inertia('Main', [
|
||||
'posts' => $posts,
|
||||
'pageSettings' => $mainPage?->settings,
|
||||
]);
|
||||
```
|
||||
|
||||
**Frontend:** В `Main.vue` использовать аналогичный подход.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Чеклист реализации
|
||||
|
||||
- [ ] **Шаг 1:** Расширить `PageForm.php` — секция "Визуальные настройки"
|
||||
- [ ] **Шаг 2:** Обновить `Page.vue` — применение стилей из `settings.visual`
|
||||
- [ ] **Шаг 3:** Обновить `Builder.vue` — передача `settings` в блоки
|
||||
- [ ] **Шаг 4:** Обновить `BasicTitle.vue` — поддержка размера заголовка
|
||||
- [ ] **Шаг 5:** Обновить hardcoded страницы (`Main.vue` и др.) — передача `settings`
|
||||
- [ ] **Тестирование:** Проверить все комбинации настроек
|
||||
- [ ] **Документация:** Описать доступные настройки для контент-менеджеров
|
||||
|
||||
---
|
||||
|
||||
## 🔮 Будущие улучшения (Phase 2)
|
||||
|
||||
- [ ] CSS Custom Variables для продвинутой настройки (Решение 2)
|
||||
- [ ] Система тем (Решение 3) — если потребуется масштабирование
|
||||
- [ ] Превью изменений в реальном времени (live preview в админке)
|
||||
- [ ] Наследование настроек от родительской страницы
|
||||
- [ ] Глобальные настройки сайта (дефолтная тема для всех страниц)
|
||||
|
||||
---
|
||||
|
||||
## 📚 Ссылки на ключевые файлы
|
||||
|
||||
| Файл | Путь |
|
||||
|------|------|
|
||||
| Page Model | `app/Containers/AppStructure/Models/Page.php` |
|
||||
| PageForm | `app/Filament/Components/Forms/PageForm.php` |
|
||||
| PageController | `app/Containers/AppStructure/UI/WEB/Controllers/PageController.php` |
|
||||
| RenderPageAction | `app/Containers/AppStructure/Actions/RenderPageAction.php` |
|
||||
| Page.vue | `resources/js/Pages/Page.vue` |
|
||||
| Builder.vue | `resources/js/componentss/shared/builder/pageBuilder/Builder.vue` |
|
||||
| BasicTitle.vue | `resources/js/componentss/ui/titles/BasicTitle.vue` |
|
||||
| Main.vue | `resources/js/Pages/Main.vue` |
|
||||
| Tailwind Config | `tailwind.config.js` |
|
||||
Reference in New Issue
Block a user