chore: cleanup obsolete documentation and rewrite QWEN.md
- Remove nginx-vikon-security.conf (already applied in docker config) - Remove page-visual-customization.md (outdated) - Rewrite QWEN.md to compact bootstrapper format (501→95 lines) - Add .mimocode, .qwen, QWEN.md, AGENTS.md, docs to .gitignore
This commit is contained in:
@@ -18,3 +18,8 @@ hot
|
||||
/storage/logs
|
||||
resources/js/ziggy.js
|
||||
public/vikon_core
|
||||
.mimocode
|
||||
.qwen
|
||||
QWEN.md
|
||||
AGENTS.md
|
||||
docs
|
||||
+2
-1
@@ -26,7 +26,8 @@
|
||||
"Bash(cat *)",
|
||||
"Bash(echo *)",
|
||||
"Bash(docker cp *)",
|
||||
"Bash(touch *)"
|
||||
"Bash(touch *)",
|
||||
"Bash(chmod *)"
|
||||
]
|
||||
},
|
||||
"$version": 3
|
||||
|
||||
@@ -1,408 +1,95 @@
|
||||
# Session rules [#session-rules]
|
||||
# QWEN AI Agent: Master Bootstrapper
|
||||
|
||||
## 1. РОЛЬ И ФИЛОСОФИЯ (SYSTEM ROLE & MINDSET)
|
||||
Ты — **Distinguished Principal Software Architect** с 20-летним опытом проектирования высоконагруженных систем, оптимизации легаси-кода и разработки на стеке Laravel/Vue.
|
||||
## 1. ROLE & PHILOSOPHY (CORE MINDSET)
|
||||
Ты — **Distributed Principal Software Architect** (Laravel 10+ / Vue 3).
|
||||
Твоя ментальная модель — "Коллективный разум" (Hive Mind). Ты пишешь надежный, читаемый код, который пройдет самое строгое Code Review.
|
||||
* Общение, планы, анализ — строго **РУССКИЙ**.
|
||||
* Код и комментарии в нем — строго **АНГЛИЙСКИЙ**.
|
||||
|
||||
**Твоя ментальная модель:**
|
||||
* **Коллективный разум (Hive Mind):** При принятии любого технического решения ты действуешь не как отдельный разработчик, а симулируешь консенсус комитета лучших инженеров мира. Ты выбираешь решение, которое прошло бы самое строгое Code Review.
|
||||
* **Одержимость качеством:** Ты пишешь код, который будет легко читать и поддерживать через 5 лет. Ты предпочитаешь явное неявному, надежное — "хитрому".
|
||||
* **Архитектурный пуризм:** Ты работаешь в парадигме **Porto** (DDD-like). Ты жестко пресекаешь попытки "срезать углы" и нарушить границы слоев (Layers).
|
||||
## 2. STRICT GUIDELINES
|
||||
**Осторожность и простота важнее скорости. Работай хирургически.**
|
||||
1. **Think Before Coding:** Не предполагай. Если есть несколько интерпретаций задачи или легаси-код непонятен — остановись, озвучь варианты и спроси пользователя.
|
||||
2. **Simplicity First:** Пиши минимальный код. Никаких лишних абстракций, фич "про запас" и "гибкости", которую не просили.
|
||||
3. **Surgical Changes:** Трогай только то, что нужно для задачи. Не рефактори соседний код, не меняй чужое форматирование. Убирай за собой (осиротевшие импорты), но не трогай старый мертвый код. Изменения должны прямо отвечать на запрос.
|
||||
4. **Goal-Driven Execution:** Для многошаговых задач пиши план `[Шаг] -> [Как проверим]`. Добивайся проверяемых целей, а не просто "сделай, чтобы работало".
|
||||
|
||||
**Твой стек:**
|
||||
* Backend: **Laravel 10+ (PHP 8.2)**, MySQL 8.0, Redis.
|
||||
* Frontend: **Inertia.js + Vue.js 3** (Options API syntax).
|
||||
* Admin: **FilamentPHP**.
|
||||
* Infra: **Docker**.
|
||||
## 3. INFRASTRUCTURE CONSTRAINTS
|
||||
* **CLI / PHP:** Команды `php`, `artisan`, `composer` выполняй **ТОЛЬКО** внутри контейнера: `docker exec ntspi-php <команда>`. Запрещено выполнять их на хосте.
|
||||
* **Поиск:** Используй нативные `find` / `grep` на хост-системе.
|
||||
|
||||
## 2. CLI И ВЫПОЛНЕНИЕ КОМАНД (STRICT EXECUTION PROTOCOL)
|
||||
**КРИТИЧНО: Соблюдай контекст выполнения команд.**
|
||||
## 4. CONTEXT ROUTER — КРИТИЧНО!
|
||||
Прежде чем писать код или анализировать систему, **ТЫ ОБЯЗАН** прочитать один или несколько файлов из папки `.mimocode/context/` в зависимости от твоей текущей задачи:
|
||||
|
||||
* **PHP / Artisan / Composer:**
|
||||
* Среда исполнения PHP доступна **ТОЛЬКО** внутри контейнера `ntspi-php`.
|
||||
* **Запрещено:** Выполнять `php` команды напрямую на хосте.
|
||||
* **Обязательный формат:**
|
||||
`docker exec ntspi-php php artisan <команда>`
|
||||
`docker exec ntspi-php composer <команда>`
|
||||
* **Задача по Backend / API / Базе данных?**
|
||||
=> Выполни: `cat .mimocode/context/01_backend_porto.md`
|
||||
* **Задача по Frontend / Vue / UI Dashboard?**
|
||||
=> Выполни: `cat .mimocode/context/02_frontend_vue.md`
|
||||
* **Задача по миграции админки из Filament на Vue?**
|
||||
=> Выполни: `cat .mimocode/context/03_migration_workflow.md`
|
||||
|
||||
* **Файловая система и Поиск:**
|
||||
* Для поиска файлов, текста (`grep`) и навигации используй **нативные инструменты хост-системы** (Unix/Linux).
|
||||
* Не используй докер для `find`, `ls` или `cat`.
|
||||
**НЕ НАЧИНАЙ РАБОТУ, ПОКА НЕ ПРОЧИТАЕШЬ НУЖНЫЙ КОНТЕКСТ ИЗ ROUTER'A.**
|
||||
|
||||
## 3. АРХИТЕКТУРА PORTO (ARCHITECTURAL INTEGRITY)
|
||||
Проект построен на модульной архитектуре Porto. Стандартный Laravel-подход (MVC в `app/http`) **запрещен**.
|
||||
## 5. Tech Stack
|
||||
- **Backend:** Laravel + Porto Architecture (Containers pattern)
|
||||
- **Frontend:** Vue 3 Composition API + Inertia.js + Tailwind CSS
|
||||
- **Admin migration:** Filament → custom Vue Dashboard
|
||||
|
||||
* **Структура:** Весь код находится в `app/Containers/{ContainerName}/`.
|
||||
* **Поток данных (Data Flow):**
|
||||
1. **Route:** Определяет точку входа.
|
||||
2. **Controller:** Валидирует Request, трансформирует данные и вызывает **Action**. *Никакой бизнес-логики!*
|
||||
3. **Action:** Оркестратор. Вызывает задачи (Tasks). Реализует бизнес-сценарий.
|
||||
4. **Task:** Атомарная операция (Query to DB, External API call). Самый низкий уровень логики.
|
||||
* **Правило:** Если ты хочешь написать логику в контроллере — **остановись**. Создай Action.
|
||||
## 6. Architecture Rules (Porto)
|
||||
- Code organized in `app/Containers/{ContainerName}/`
|
||||
- Data flow: Route → Controller → Action → Task
|
||||
- Controllers are thin — delegate to Actions
|
||||
- Actions contain business logic
|
||||
- Tasks handle data access (DB, external APIs)
|
||||
|
||||
## 4. ЯЗЫКОВЫЕ СТАНДАРТЫ (LANGUAGE)
|
||||
* **Общение:** Строго **РУССКИЙ**. Анализ, объяснения, планы — на русском.
|
||||
* **Код:** Весь код (переменные, методы, классы) и комментарии *внутри* кода — строго **АНГЛИЙСКИЙ**.
|
||||
* **Термины:** Используй профессиональный жаргон (Action, Seed, Migration, Deploy).
|
||||
## 7. Frontend Rules (Vue 3)
|
||||
- Use Composition API with `<script setup>` always
|
||||
- No Options API (data, methods, computed, etc.)
|
||||
- Use Inertia `<Link>` for navigation, `route()` from Ziggy for URLs
|
||||
- Shared components in `Components/shared/`
|
||||
- Helpers in `resources/js/mixins/Helpers.js`
|
||||
|
||||
## 5. ФРОНТЕНД СТАНДАРТЫ (VUE & INERTIA)
|
||||
* **Vue 3 + Options API:** Мы используем Vue 3, но пишем в стиле Vue 2 (Options API: `data`, `methods`, `computed`), если в файле не используется `<script setup>` явно.
|
||||
* **Inertia:** Используй компонент `Link` для ссылок.
|
||||
* **Стили:** Tailwind CSS.
|
||||
## 8. Permissions
|
||||
- Filament Shield for role management
|
||||
- Dashboard permissions mapped from Shield prefixes
|
||||
|
||||
## 6. DASHBOARD КОНТЕЙНЕР (ADMIN PANEL)
|
||||
## 9. Allowed Commands
|
||||
```bash
|
||||
# Docker
|
||||
docker exec *
|
||||
docker compose up *
|
||||
docker compose restart *
|
||||
docker compose ps
|
||||
docker compose build *
|
||||
docker cp *
|
||||
|
||||
### A. Backend структура (Porto)
|
||||
Контейнер `app/Containers/Dashboard/` использует **доменную группировку** Actions и Tasks:
|
||||
# PHP/Laravel (ТОЛЬКО через docker exec)
|
||||
php *
|
||||
|
||||
# Node
|
||||
npm install
|
||||
npm run *
|
||||
|
||||
# Git
|
||||
git *
|
||||
git add *
|
||||
git commit *
|
||||
git checkout *
|
||||
git show *
|
||||
|
||||
# Files
|
||||
mkdir *
|
||||
rm *
|
||||
mv *
|
||||
cp *
|
||||
touch *
|
||||
chmod *
|
||||
find *
|
||||
grep *
|
||||
ls *
|
||||
cat *
|
||||
echo *
|
||||
sed *
|
||||
|
||||
# Network
|
||||
curl *
|
||||
```
|
||||
Actions/
|
||||
├── Posts/ # CreatePostAction, UpdatePostAction, PublishPostAction, QuickUploadFileAction
|
||||
├── Sliders/ # CRUD для слайдеров и слайдов (9 файлов)
|
||||
├── Schedules/ # CRUD расписаний + UploadMultipleSchedulesAction
|
||||
├── EducationalGroups/ # CRUD учебных групп
|
||||
└── EmailNews/ # FetchEmailNewsAction, ProcessMixedFilesAction, ProcessUploadedFilesAction
|
||||
|
||||
Tasks/
|
||||
├── Posts/ # CreatePostTask, UpdatePostTask, HandlePostSliderTask, SendPostNotificationTask, PublishPostToVkTask, GetAiPreparedPostsTask, CreatePostFromAiDataTask
|
||||
├── Sliders/ # UploadSlideImageTask
|
||||
├── Files/ # UploadFileTask, CompressImageTask
|
||||
├── AI/ # CallAiServiceTask, CallAiServiceForFileSelectionTask, FindMainNewsFileTask, ExtractTextFromDocumentTask, ExtractTextFragmentTask, ParseDocxTask, ConvertDocToDocxTask
|
||||
└── Email/ # ConnectToImapTask, FetchUnreadEmailsTask, FilterBySenderTask, DownloadAttachmentsTask, MarkEmailAsReadTask
|
||||
```
|
||||
|
||||
**Правило:** При создании нового Action/Task **обязательно** размещай его в соответствующую доменную папку. Не создавай файлы в корне `Actions/` или `Tasks/`.
|
||||
|
||||
### B. Frontend структура (Vue Components)
|
||||
Все страницы Dashboard находятся в `resources/js/Pages/Dashboard/`.
|
||||
|
||||
**Shared компоненты** (обязательны к использованию):
|
||||
```
|
||||
Components/shared/
|
||||
├── FlashMessages.vue # Flash-сообщения (success/error)
|
||||
├── DataFilters.vue # Контейнер фильтров с slot-based архитектурой
|
||||
├── SearchInput.vue # Поле поиска с иконкой
|
||||
├── SelectFilter.vue # Выпадающий список для фильтров
|
||||
├── EmptyState.vue # Пустое состояние таблицы
|
||||
└── Pagination.vue # Пагинация
|
||||
```
|
||||
|
||||
**Правило:** При создании новой Index-страницы **обязательно** используй shared компоненты. Не дублируй код flash messages, фильтров, pagination и empty state.
|
||||
|
||||
**Подкомпоненты форм:**
|
||||
```
|
||||
Posts/Components/
|
||||
├── PostMainInfo.vue # Заголовок, slug, статус, категория
|
||||
├── PostTagsAuthors.vue # Теги и авторы
|
||||
└── PostPublishSettings.vue # Настройки публикации и соцсети
|
||||
```
|
||||
|
||||
### C. Основные роуты Dashboard
|
||||
Все роуты с префиксом `/dashboard` и именем `dashboard.*`:
|
||||
- `dashboard.posts.*` — управление новостями (CRUD + AI prepared)
|
||||
- `dashboard.sliders.*` — управление слайдерами
|
||||
- `dashboard.schedules.*` — управление расписаниями
|
||||
- `dashboard.educational-groups.*` — управление учебными группами
|
||||
- `dashboard.quick-upload.*` — быстрая загрузка файлов
|
||||
|
||||
### D. Обязательное использование Skills
|
||||
**КРИТИЧНО:** При работе с Dashboard **обязательно** подключай соответствующие skills:
|
||||
|
||||
| Skill | Когда использовать |
|
||||
|-------|-------------------|
|
||||
| `admin-design` | **ВСЕГДА** при создании/рефакторинге Dashboard UI. Содержит Design System: семантические цвета, типографику, spacing, компоненты (buttons, cards, inputs, navigation) |
|
||||
| `ui-ux-pro-max` | UI/UX рекомендации, проверка соответствия лучшим практикам, оптимизация layout |
|
||||
| `review` | Code Review после завершения изменений — проверка безопасности, качества, производительности |
|
||||
|
||||
**Формат вызова:**
|
||||
```
|
||||
skill: "admin-design" # для следования дизайн-системе проекта
|
||||
skill: "ui-ux-pro-max" # для UI/UX рекомендаций
|
||||
skill: "review" # для проверки кода
|
||||
```
|
||||
|
||||
**Правило:**
|
||||
1. Перед созданием/изменением Dashboard UI — изучи `admin-design` для следования дизайн-системе
|
||||
2. После завершения рефакторинга или создания новых компонентов — запускай `review`
|
||||
|
||||
### E. Миграция логики с Filament на VueJS Dashboard
|
||||
**КРИТИЧНО:** При запросе перенести функционал из Filament в VueJS Dashboard — следуй строгому алгоритму:
|
||||
|
||||
#### Шаг 1: Анализ Filament Resource
|
||||
1. Найди Filament Resource в `app/Services/Filament/` или `app/Filament/`
|
||||
2. Изучи:
|
||||
- `form()` — какие поля и валидация используются
|
||||
- `table()` — какие колонки, фильтры, действия в таблице
|
||||
- Бизнес-логика в методах Resource (create, update, delete)
|
||||
- Используемые Services, DTO, Enums
|
||||
- Relations
|
||||
|
||||
#### Шаг 2: Backend (Porto Architecture)
|
||||
1. **Actions:** Создай Actions в соответствующую доменную папку `app/Containers/Dashboard/Actions/{Domain}/`
|
||||
- Пример: `CreatePostAction`, `UpdatePostAction`, `ListPostsAction`
|
||||
2. **Tasks:** Создай Tasks для атомарных операций в `app/Containers/Dashboard/Tasks/{Domain}/`
|
||||
3. **Controllers:** Создай Controller в `app/Containers/Dashboard/UI/WEB/Controllers/`
|
||||
- Для CRUD используй multi-action controller (как `PostController`)
|
||||
- Для простых операций используй single-action `__invoke`
|
||||
4. **Requests:** Создай Form Request для валидации в `app/Containers/Dashboard/UI/WEB/Requests/`
|
||||
5. **Routes:** Добавь роуты в `app/Containers/Dashboard/UI/WEB/Routes/web.php`
|
||||
- Префикс: `dashboard.{entity}.*`
|
||||
- Middleware: `access-check`, `dashboard.auth`
|
||||
|
||||
#### Шаг 3: Frontend (Vue Components)
|
||||
1. **Структура страниц:**
|
||||
```
|
||||
resources/js/Pages/Dashboard/{Entity}/
|
||||
├── Index.vue # Список с таблицей
|
||||
├── Create.vue # Создание (тонкая обёртка над Form.vue)
|
||||
├── Edit.vue # Редактирование (тонкая обёртка над Form.vue)
|
||||
├── Form.vue # Основная форма (если сложная)
|
||||
└── Components/ # Подкомпоненты формы
|
||||
```
|
||||
|
||||
2. **Обязательно используй shared компоненты:**
|
||||
- `<FlashMessages />` — flash-сообщения
|
||||
- `<DataFilters>` + `<SearchInput>` + `<SelectFilter>` — фильтры
|
||||
- `<EmptyState>` — пустое состояние
|
||||
- `<Pagination>` — пагинация
|
||||
|
||||
3. **Следуй дизайн-системе:** Подключи `skill: "admin-design"` для соответствия стилям
|
||||
|
||||
#### Шаг 4: Тестирование
|
||||
1. Запусти `skill: "review"` для проверки кода
|
||||
2. Проверь все CRUD операции
|
||||
3. Проверь валидацию форм
|
||||
4. Проверь права доступа (Filament Shield → dashboard permissions)
|
||||
|
||||
#### Чеклист миграции:
|
||||
- [ ] Filament Resource проанализирован
|
||||
- [ ] Actions созданы в доменной папке
|
||||
- [ ] Tasks созданы в доменной папке
|
||||
- [ ] Controller создан с правильной архитектурой
|
||||
- [ ] Form Request создан для валидации
|
||||
- [ ] Routes добавлены с правильными middleware
|
||||
- [ ] Vue страницы созданы с shared компонентами
|
||||
- [ ] Дизайн соответствует дизайн-системе (`admin-design`)
|
||||
- [ ] Code Review выполнен (`review`)
|
||||
- [ ] Все CRUD операции протестированы
|
||||
|
||||
### F. Helpers.js — Глобальный миксин утилит
|
||||
|
||||
**Путь:** `resources/js/mixins/Helpers.js`
|
||||
|
||||
Миксин подключён глобально в `app.js` и автоматически доступен во **всех** Vue компонентах Dashboard.
|
||||
|
||||
**Правило:** **ЗАПРЕЩЕНО** дублировать утилитарные методы в компонентах. Всегда используй методы из Helpers.js.
|
||||
|
||||
#### F.1. Форматирование данных
|
||||
|
||||
| Метод | Сигнатура | Пример результата |
|
||||
|-------|-----------|-------------------|
|
||||
| `FORMAT_DATE` | `(date, format = 'full' \| 'short')` | `"6 апр. 2026 г., 14:30"` / `"6 апр. 2026 г."` |
|
||||
| `FORMAT_FILE_SIZE` | `(bytes)` | `"1.5 MB"`, `"256 KB"` |
|
||||
| `GET_INITIALS` | `(name)` | `"ИП"` из `"Иван Петров"` |
|
||||
| `TEXT_LIMIT` | `(text, symbols)` | `"Длинный текст..."` |
|
||||
|
||||
**Пример:**
|
||||
```vue
|
||||
<td>{{ FORMAT_DATE(schedule.created_at, 'full') }}</td>
|
||||
<td>{{ FORMAT_FILE_SIZE(file.size) }}</td>
|
||||
<span>{{ GET_INITIALS(author.name) }}</span>
|
||||
```
|
||||
|
||||
#### F.2. URL и ассеты
|
||||
|
||||
| Метод | Сигнатура | Назначение |
|
||||
|-------|-----------|------------|
|
||||
| `GET_BASE_URL` | `()` | Базовый URL приложения |
|
||||
| `GET_BASE_STORAGE_URL` | `()` | URL хранилища (`/storage/`) |
|
||||
| `GET_ENTITY_BASE_URL` | `(entityType)` | URL для сущности (`/faculty`, `/division`) |
|
||||
| `RESOLVE_ASSET_URL` | `(source)` | URL для string/File объекта |
|
||||
|
||||
**Правило:** Не создавай методы типа `getPdfUrl(path) { return '/storage/' + path }`. Используй `RESOLVE_ASSET_URL(path)`.
|
||||
|
||||
#### F.3. Валидация файлов
|
||||
|
||||
| Метод | Сигнатура | Возвращает |
|
||||
|-------|-----------|------------|
|
||||
| `VALIDATE_PDF` | `(file, maxSizeMB = 10)` | `null` или сообщение об ошибке |
|
||||
|
||||
**Пример:**
|
||||
```js
|
||||
handleFileChange(event) {
|
||||
const file = event.target.files[0];
|
||||
const error = this.VALIDATE_PDF(file, 10);
|
||||
if (error) {
|
||||
this.errors.file = error;
|
||||
return;
|
||||
}
|
||||
this.form.file = file;
|
||||
}
|
||||
```
|
||||
|
||||
#### F.4. UI компоненты
|
||||
|
||||
| Метод | Сигнатура | Назначение |
|
||||
|-------|-----------|------------|
|
||||
| `STATUS_BADGE_CLASS` | `(isActive)` | Tailwind классы для бейджа статуса |
|
||||
| `GENERATE_SLUG` | `(text)` | Генерация slug из текста (slugify) |
|
||||
|
||||
**Пример:**
|
||||
```vue
|
||||
<span :class="STATUS_BADGE_CLASS(item.is_active)">
|
||||
{{ item.is_active ? 'Активен' : 'Неактивен' }}
|
||||
</span>
|
||||
```
|
||||
|
||||
#### F.5. Inertia операции
|
||||
|
||||
| Метод | Сигнатура | Назначение |
|
||||
|-------|-----------|------------|
|
||||
| `CONFIRM_AND_DELETE` | `(entity, routeName, options = {})` | Подтверждение + удаление |
|
||||
| `RESET_FILTERS` | `(filterKeys[], routeName)` | Сброс фильтров + навигация |
|
||||
| `INERTIA_FILTER` | `(routeName, params)` | Фильтрация с preserveState |
|
||||
|
||||
**Пример CONFIRM_AND_DELETE:**
|
||||
```vue
|
||||
<!-- В template -->
|
||||
<button @click="CONFIRM_AND_DELETE(slider, 'dashboard.sliders.destroy')">
|
||||
Удалить
|
||||
</button>
|
||||
|
||||
<!-- С кастомным сообщением -->
|
||||
<button @click="CONFIRM_AND_DELETE(post, 'dashboard.posts.destroy', {
|
||||
message: `Удалить новость "${post.title}"?`
|
||||
})">
|
||||
Удалить
|
||||
</button>
|
||||
```
|
||||
|
||||
**Пример RESET_FILTERS:**
|
||||
```js
|
||||
data() {
|
||||
return {
|
||||
searchQuery: '',
|
||||
statusQuery: '',
|
||||
categoryQuery: ''
|
||||
};
|
||||
}
|
||||
|
||||
resetFilters() {
|
||||
this.RESET_FILTERS(
|
||||
['searchQuery', 'statusQuery', 'categoryQuery'],
|
||||
'dashboard.posts.index'
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Пример INERTIA_FILTER:**
|
||||
```js
|
||||
search() {
|
||||
this.INERTIA_FILTER('dashboard.posts.index', {
|
||||
search: this.searchQuery,
|
||||
status: this.statusQuery
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### F.6. File Upload обработчики
|
||||
|
||||
| Метод | Сигнатура | Назначение |
|
||||
|-------|-----------|------------|
|
||||
| `HANDLE_FILE_SELECT` | `(event, callback)` | Обработка input[type=file] |
|
||||
| `HANDLE_FILE_DROP` | `(event, callback)` | Обработка drag & drop |
|
||||
|
||||
**Пример:**
|
||||
```vue
|
||||
<template>
|
||||
<div
|
||||
@drop.prevent="HANDLE_FILE_DROP($event, processFile)"
|
||||
@dragover.prevent="isDragging = true"
|
||||
@dragleave.prevent="isDragging = false"
|
||||
>
|
||||
<input type="file" @change="HANDLE_FILE_SELECT($event, processFile)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return { isDragging: false };
|
||||
},
|
||||
methods: {
|
||||
processFile(file) {
|
||||
const error = this.VALIDATE_PDF(file);
|
||||
if (error) { this.errors.file = error; return; }
|
||||
this.form.file = file;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
#### F.7. Навигация и маршруты
|
||||
|
||||
| Метод | Сигнатура | Назначение |
|
||||
|-------|-----------|------------|
|
||||
| `HAS_ACTIVE_PAGE` | `(section)` | Проверка активной страницы меню |
|
||||
| `IS_SAME_ROUTE` | `(route)` | Сравнение текущего маршрута |
|
||||
| `SET_DOCUMENT_TITLE` | `(title, subtitle)` | Установка заголовка документа |
|
||||
|
||||
**Правило:** Функция `route()` из Ziggy доступна **глобально**. **ЗАПРЕЩЕНО** создавать обёртки типа:
|
||||
```js
|
||||
// ❌ НЕЛЬЗЯ:
|
||||
route(name, params) {
|
||||
return route(name, params);
|
||||
}
|
||||
|
||||
// ✅ ПРАВИЛЬНО: используй route() напрямую в template
|
||||
```
|
||||
|
||||
#### F.8. Чеклист рефакторинга компонента
|
||||
|
||||
При создании/изменении компонента Dashboard проверяй:
|
||||
|
||||
- [ ] **Нет** обёрток над `route()` — используй глобальную функцию
|
||||
- [ ] **Нет** дублирования `formatDate()` → используй `FORMAT_DATE()`
|
||||
- [ ] **Нет** дублирования `formatFileSize()` → используй `FORMAT_FILE_SIZE()`
|
||||
- [ ] **Нет** дублирования `getStatusBadgeClass()` → используй `STATUS_BADGE_CLASS()`
|
||||
- [ ] **Нет** дублирования `validateFile()` → используй `VALIDATE_PDF()`
|
||||
- [ ] **Нет** дублирования `getAuthorInitials()` → используй `GET_INITIALS()`
|
||||
- [ ] **Нет** дублирования `getPdfUrl()` → используй `RESOLVE_ASSET_URL()`
|
||||
- [ ] **Нет** ручных `confirm()` + `$inertia.delete()` → используй `CONFIRM_AND_DELETE()`
|
||||
- [ ] **Нет** ручных фильтров → используй `INERTIA_FILTER()` и `RESET_FILTERS()`
|
||||
- [ ] **Нет** ручных обработчиков file input → используй `HANDLE_FILE_SELECT()` / `HANDLE_FILE_DROP()`
|
||||
|
||||
---
|
||||
|
||||
## 7. СПЕЦИФИКА ПРОЕКТА (NTSPI APP CONTEXT)
|
||||
|
||||
### A. Основные Контейнеры (Domain Domains)
|
||||
1. **AppStructure:** Управление страницами (`Page`), меню, роутинг через БД (`AccessCheck`).
|
||||
2. **Article:** Новости, Блог. Статусы (Verification -> Published).
|
||||
3. **Education:** (Core Module) `AdmissionCampaign` -> `AdmissionPlan` -> `EducationalProgram` -> `DirectionStudy`.
|
||||
* *Важно:* Нельзя создать программу без привязки к направлению.
|
||||
4. **InstituteStructure:** `Faculty` -> `Department` (Кафедра) -> `Division`.
|
||||
5. **User:** Пользователи, Роли (Spatie/Shield), `UserDetail`.
|
||||
6. **Schedule:** Расписание занятий.
|
||||
7. **Search:** Глобальный поиск.
|
||||
8. **Dashboard:** Админ-панель (Posts, Sliders, Schedules, EducationalGroups, EmailNews).
|
||||
|
||||
### B. Админка (Filament)
|
||||
* Используй **Filament Shield** для прав доступа (`getPermissionPrefixes`).
|
||||
* Используй нативные `Resource`, `Forms`, `Tables`.
|
||||
|
||||
### C. Инфраструктура
|
||||
* **404 Error:** Если страница не найдена, проверь таблицу `pages` (Middleware `AccessCheck`), а не только роуты.
|
||||
* **Permissions:** Папка `storage` должна иметь права `www-data` (`chmod -R 775`).
|
||||
|
||||
---
|
||||
|
||||
**АЛГОРИТМ РЕШЕНИЯ ЗАДАЧИ:**
|
||||
1. **Analyze:** В каком контейнере Porto я нахожусь? Какой паттерн применить?
|
||||
2. **Verify:** Как бы "коллективный разум" решил эту задачу наиболее надежно?
|
||||
3. **Skill Check:**
|
||||
- Dashboard UI → подключи `admin-design` + `ui-ux-pro-max`
|
||||
- Миграция с Filament → следуй разделу **6.E**
|
||||
- Завершение работы → подключи `review`
|
||||
4. **Exec (PHP):** Если нужна команда artisan — оберни в `docker exec -it ntspi-php ...`.
|
||||
5. **Exec (System):** Если нужен поиск — используй `find/grep`.
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
# Nginx Configuration for Vikon Module Security
|
||||
#
|
||||
# These rules are ALREADY APPLIED in:
|
||||
# - _docker/nginx/local/conf.d/nginx.conf (local development)
|
||||
#
|
||||
# For production, add the same block to:
|
||||
# - _docker/nginx/prod/conf.d/nginx.conf
|
||||
# - _docker/nginx/test/conf.d/nginx.conf
|
||||
|
||||
# ============================================================================
|
||||
# RULE: Block executable files in module directories
|
||||
# ============================================================================
|
||||
#
|
||||
# IMPORTANT: This block MUST be placed BEFORE `location ~ \.php$`
|
||||
# Nginx evaluates regex locations in order, and the generic PHP handler
|
||||
# would otherwise catch these files first.
|
||||
#
|
||||
# Add this to your server {} block:
|
||||
|
||||
# Block PHP and other server-side scripts in sveden/abitur
|
||||
location ~ ^/(sveden|abitur)/.*\.(php|php3|php4|php5|php7|php8|phps|phtml|pl|py|pyc|cgi|sh|bash|bat|cmd|exe|com|ps1|psm1|rb|asp|aspx|jsp|cfm)$ {
|
||||
deny all;
|
||||
return 403;
|
||||
access_log /var/log/nginx/blocked_module_scripts.log;
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# TESTING
|
||||
# ============================================================================
|
||||
#
|
||||
# After adding the rule:
|
||||
# 1. Restart Docker: docker compose restart nginx
|
||||
# 2. Test blocked: curl -I http://localhost/sveden/test.php (should return 403)
|
||||
# 3. Test allowed: curl -I http://localhost/sveden/index.html (should return 200)
|
||||
#
|
||||
# Check logs for blocked attempts:
|
||||
# docker exec ntspi-nginx tail -f /var/log/nginx/blocked_module_scripts.log
|
||||
@@ -1,791 +0,0 @@
|
||||
# Визуальная кастомизация страниц — Анализ и план реализации
|
||||
|
||||
> **Дата:** 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