Gogs 3 долоо хоног өмнө
parent
commit
3c8ca026b7

+ 20 - 37
README.md

@@ -794,58 +794,41 @@ coffee
 {
     _id: "page_about_borbad"
     type: "page"
-    domain: ["borbad.s5l.ru", "concert-hall.tj"]
+    domain: ["borbad.s5l.ru", "global"]
     language: ["ru", "en", "tj"]
-    title: ["О нас - Кохи Борбад", "About Us - Borbad Concert Hall", "Дар бораи мо - Ҳолли Борбад"]
-    slug: ["about", "about", "dar-borai-mo"]
+    title: ["О нас - Кохи Борбад", "About Us - Borbad", "Дар бораи мо - Борбад"]
     content: [
-        "# О концертном зале Борбад\n\n[class:bg-blue-50 p-6 rounded-lg shadow-md]Мы рады приветствовать вас в нашем концертном зале.[/class] \n\n app-link(:to="/") Главная",
-        "# About Borbad Concert Hall\n\n[class:bg-blue-50 p-6 rounded-lg shadow-md]We are pleased to welcome you to our concert hall.[/class] \n\n app-link(:to="/") Main",
-        "# Дар бораи ҳолли консертии Борбад\n\n[class:bg-blue-50 p-6 rounded-lg shadow-md]Мо шодем, ки шуморо дар ҳолли консертии мо пазируфтаем.[/class] \n\n app-link(:to="/") Главная"
+        "# О нашем концертном зале\n\n[component name=\"HeroSection\" props='{\"title\": \"Добро пожаловать в Борбад\", \"image\": \"/assets/borbad.s5l.ru/about-hero.jpg\"}']\n\nИстория нашего зала начинается с 2010 года...\n\n[component name=\"ImageGallery\" props='{\"images\": [\"/assets/gallery/1.jpg\", \"/assets/gallery/2.jpg\"]}']\n\n## Наша миссия\n\nМы создаем пространство для культуры и искусства.",
+        "# About Our Concert Hall\n\n[component name=\"HeroSection\" props='{\"title\": \"Welcome to Borbad\", \"image\": \"/assets/borbad.s5l.ru/about-hero.jpg\"}']\n\nOur hall's history began in 2010...\n\n[component name=\"ImageGallery\" props='{\"images\": [\"/assets/gallery/1.jpg\", \"/assets/gallery/2.jpg\"]}']\n\n## Our Mission\n\nWe create space for culture and art."
     ]
-    excerpt: ["Информация о концертном зале Борбад в Душанбе", "Information about Borbad Concert Hall in Dushanbe", "Маълумот дар бораи ҳолли консертии Борбад дар Душанбе"]
+    excerpt: ["Информация о концертном зале Борбад", "Information about Borbad Concert Hall", "Маълумот дар бораи ҳолли консертии Борбад"]
+    seo: {
+        description: ["Концертный зал Борбад - история и миссия", "Borbad Concert Hall - history and mission", "Ҳолли консертии Борбад - таърих ва вазифа"]
+        keywords: [["о нас", "история", "миссия"], ["about", "history", "mission"], ["дар бораи мо", "таърих", "вазифа"]]
+        title: ["О нас - Кохи Борбад", "About Us - Borbad", "Дар бораи мо - Борбад"]
+    }
     image: [
         "/assets/borbad.s5l.ru/pages/about.jpg"
         "/assets/borbad.s5l.ru/pages/about.jpg"
         "/assets/borbad.s5l.ru/pages/about.jpg"
     ]
-    gallery: [
-        [
-            "/assets/borbad.s5l.ru/gallery/about1.jpg"
-            "/assets/borbad.s5l.ru/gallery/about2.jpg"
-        ],
-        [
-            "/assets/borbad.s5l.ru/gallery/about1.jpg"
-            "/assets/borbad.s5l.ru/gallery/about2.jpg"
-        ],
-        [
-            "/assets/borbad.s5l.ru/gallery/about1.jpg"
-            "/assets/borbad.s5l.ru/gallery/about2.jpg"
-        ]
-    ]
-    seo: {
-        description: ["Узнайте больше о концертном зале Борбад в Душанбе", "Learn more about Borbad Concert Hall in Dushanbe", "Маълумоти бештар дар бораи ҳолли консертии Борбад дар Душанбе"]
-        keywords: [
-            ["концертный зал", "Борбад", "Душанбе", "культура"],
-            ["concert hall", "Borbad", "Dushanbe", "culture"],
-            ["ҳолли консертӣ", "Борбад", "Душанбе", "фарҳанг"]
-        ]
-        title: ["О нас - Концертный зал Борбад", "About Us - Borbad Concert Hall", "Дар бораи мо - Ҳолли Борбад"]
-    }
-    components: {
-            app-link: 'AppLink'
-            formvalidator: 'FormValidator'
-    }
+    gallery: [[], [], []]
+    tags: [["страница", "о нас"], ["page", "about"], ["саҳифа", "дар бораи мо"]]
+    category_id: "category_pages_borbad"
+    category_path: ["category_pages_borbad"]
+    author: ["Администрация Борбад", "Borbad Administration", "Маъмурияти Борбад"]
+    status: "published"
+    featured: false
+    template: "default"
     parent_id: null
-    parent_path: []
     order: 1
-    status: "published"
     protected: false
     show_in_sitemap: true
     allow_comments: false
-    featured: false
     created_at: "2024-01-15T10:00:00.000Z"
     updated_at: "2024-01-15T10:00:00.000Z"
+    published_at: "2024-01-15T10:00:00.000Z"
+    views: 0
 }
 ```
 Наследник "Событие" (event) - расширяет blog_post

+ 122 - 168
vue/app/pages/Page/index.coffee

@@ -1,202 +1,156 @@
 # Загрузка стилей компонента
 document.head.insertAdjacentHTML('beforeend','<style type="text/tailwindcss"  page="Page">'+stylFns['app/pages/Page/index.styl']+'</style>')
 
-# Подключение markdown парсера
-marked = require 'marked'
-
 module.exports =
-    name: 'Page'
+    name: 'PageView'
     render: (new Function '_ctx', '_cache', renderFns['app/pages/Page/index.pug'])()
     
     data: ->
         page: null
         loading: true
         error: null
-        notFound: false
-        pageSettings: {}
-    
-    computed:
-        # Текущий язык из глобального состояния
-        currentLanguage: ->
-            return _.currentLanguage || 'ru'
-        
-        # Настройки сайта из глобального состояния
-        siteSettings: ->
-            return _.appState?.siteSettings || {}
-        
-        # Обработанный markdown контент
-        processedContent: ->
-            if not @page?.content
-                return ''
-            
-            # Получаем контент для текущего языка
-            content = @getMultilingualText(@page.content, '')
-            return @processMarkdown(content)
-        
-        # Мета-данные для SEO
-        pageMeta: ->
-            if not @page
-                return {}
-            
-            return {
-                title: @getMultilingualText(@page.seo?.title, @getMultilingualText(@page.title, '')),
-                description: @getMultilingualText(@page.seo?.description, @getMultilingualText(@page.excerpt, '')),
-                keywords: @getMultilingualText(@page.seo?.keywords, []).join(', ')
-            }
+        processedContent: ''
+        components: {}
     
     beforeMount: ->
-        @loadPageSettings()
         @loadPageData()
     
-    watch:
-        '$route.params.slug': ->
-            @loadPageData()
-        
-        'currentLanguage': ->
-            @loadPageData()
-    
     methods:
-        # Загрузка настроек страниц
-        loadPageSettings: ->
-            @pageSettings = _.appState?.siteSettings?.pages || {
-                urls: {
-                    about: '/about'
-                    contacts: '/contacts' 
-                    privacy: '/privacy'
-                    terms: '/terms'
-                }
-                components: {
-                    about: 'AboutPage'
-                    contacts: 'ContactsPage'
-                }
-                strings: {
-                    not_found: ['Страница не найдена', 'Page not found', 'Саҳифа ёфт нашуд']
-                    back_to_home: ['Вернуться на главную', 'Back to home', 'Бозгашт ба саҳифаи асосӣ']
-                    loading: ['Загрузка...', 'Loading...', 'Бор шуда истодааст...']
-                }
-            }
-        
         # Загрузка данных страницы
         loadPageData: ->
-            @loading = true
-            @error = null
-            @notFound = false
-            
-            slug = @$route.params.slug
-            if not slug
-                @error = "Не указан slug страницы"
-                @loading = false
-                return
-            
-            @loadPageBySlug(slug).then (page) =>
-                if page
-                    @.page = page
-                    @updatePageMeta()
+            try
+                @loading = true
+                pageId = @$route.params.id
+                
+                if pageId
+                    # Загрузка конкретной страницы по ID
+                    @page = await AppDB.getDocumentById(pageId)
+                else
+                    # Загрузка по slug из пути
+                    slug = @$route.path.replace(/^\//, '')
+                    @page = await @findPageBySlug(slug)
+                
+                if @page
+                    @processPageContent()
                 else
-                    @.notFound = true
+                    @error = "Страница не найдена"
+                
                 @loading = false
-            .catch (error) =>
+                
+            catch error
                 @error = "Ошибка загрузки страницы: "+error
                 @loading = false
         
         # Поиск страницы по slug
-        loadPageBySlug: (slug) ->
-            return new Promise (resolve, reject) =>
-                try
-                    # Ищем в кэше глобального состояния
-                    cachedPages = _.appState?.pages || []
-                    cachedPage = cachedPages.find (page) =>
-                        slugs = @getMultilingualText(page.slug, [])
-                        return slugs.includes(slug)
-                    
-                    if cachedPage
-                        resolve(cachedPage)
-                        return
-                    
-                    # Если нет в кэше, загружаем из базы
-                    AppDB.db.query('pages/by_slug_multilingual', {
-                        key: ['borbad.s5l.ru', @currentLanguage, slug]
-                        include_docs: true
-                    }).then (result) =>
-                        if result.rows.length > 0
-                            page = AppDB.processMultilingualDocument(result.rows[0].doc)
-                            # Сохраняем в кэш
-                            if not _.appState.pages
-                                _.appState.pages = []
-                            _.appState.pages.push(page)
-                            resolve(page)
-                        else
-                            resolve(null)
-                    .catch (error) ->
-                        reject(error)
-                        
-                catch error
-                    reject(error)
+        findPageBySlug: (slug) ->
+            try
+                result = await AppDB.db.query('multilingual_content/published_by_domain_language', {
+                    startkey: [_.currentDomain, _.currentLanguage, 'page']
+                    endkey: [_.currentDomain, _.currentLanguage, 'page', {}]
+                    include_docs: true
+                })
+                
+                for row in result.rows
+                    doc = row.doc
+                    pageSlug = AppDB.multilingual.getText(doc.slug, '')
+                    if pageSlug == slug
+                        return AppDB.processMultilingualDocument(doc)
+                
+                return null
+            catch error
+                debug.log "Ошибка поиска страницы по slug: "+error
+                return null
         
-        # Обработка markdown с поддержкой кастомных классов
-        processMarkdown: (content) ->
-            if not content
-                return ''
-            
-            # Обрабатываем кастомные теги для классов
-            processedContent = content.replace(/\[class:([^\]]+)\]/g, '<div class="$1">')
-                                     .replace(/\[\/class\]/g, '</div>')
+        # Обработка контента с компонентами
+        processPageContent: ->
+            if not @page?.content
+                @processedContent = ''
+                return
             
-            # Настройка marked
-            marked.setOptions({
-                breaks: true
-                gfm: true
-                sanitize: false  # Разрешаем HTML для кастомных классов
-            })
+            # Получаем контент для текущего языка
+            content = AppDB.multilingual.getText(@page.content, '')
             
-            return marked(processedContent)
-        
-        # Получение мультиязычного текста
-        getMultilingualText: (textArray, fallback = '') ->
-            return AppDB.multilingual.getText(textArray, fallback)
+            # Обрабатываем компоненты в Markdown
+            @processedContent = @parseComponents(content)
         
-        # Получение локализованной строки из настроек
-        getLocalizedString: (key, fallback = '') ->
-            strings = @pageSettings.strings?[key] || []
-            return @getMultilingualText(strings, fallback)
-        
-        # Обновление мета-данных страницы
-        updatePageMeta: ->
-            if @pageMeta.title
-                document.title = @pageMeta.title + ' - Кохи Борбад'
+        # Парсинг компонентов в Markdown
+        parseComponents: (content) ->
+            # Регулярное выражение для поиска компонентов
+            componentPattern = /\[component\s+name="([^"]+)"\s*(?:props='([^']*)')?\s*\]/g
             
-            # Обновляем meta теги
-            metaDescription = document.querySelector('meta[name="description"]')
-            if not metaDescription
-                metaDescription = document.createElement('meta')
-                metaDescription.name = 'description'
-                document.head.appendChild(metaDescription)
-            metaDescription.content = @pageMeta.description
+            # Заменяем компоненты на HTML-разметку
+            processedContent = content.replace componentPattern, (match, componentName, propsJson = '{}') =>
+                try
+                    props = JSON.parse(propsJson)
+                catch
+                    props = {}
+                
+                # Создаем уникальный ID для компонента
+                componentId = "component_"+Math.random().toString(36).substr(2, 9)
+                
+                # Регистрируем компонент для рендеринга
+                @components[componentId] = {
+                    name: componentName
+                    props: props
+                }
+                
+                # Возвращаем placeholder для Vue компонента
+                return '<div class="dynamic-component" data-component-id="'+componentId+'"></div>'
             
-            metaKeywords = document.querySelector('meta[name="keywords"]')
-            if not metaKeywords
-                metaKeywords = document.createElement('meta')
-                metaKeywords.name = 'keywords'
-                document.head.appendChild(metaKeywords)
-            metaKeywords.content = @pageMeta.keywords
+            return processedContent
         
-        # Рендер компонента указанного в настройках страницы
-        renderCustomComponent: ->
-            if not @page?.settings?.component
-                return null
-            
-            componentName = @page.settings.component
+        # Рендеринг динамических компонентов
+        renderDynamicComponents: ->
+            elements = document.querySelectorAll('.dynamic-component')
+            for element in elements
+                componentId = element.getAttribute('data-component-id')
+                componentInfo = @components[componentId]
+                
+                if componentInfo
+                    try
+                        # Загружаем компонент динамически
+                        componentModule = await @loadComponent(componentInfo.name)
+                        if componentModule
+                            # Создаем и монтируем компонент
+                            componentInstance = Vue.createAppComponent(
+                                componentModule.default or componentModule
+                                componentInfo.props
+                            )
+                            componentInstance.mount(element)
+                    catch error
+                        debug.log "Ошибка загрузки компонента "+componentInfo.name+": "+error
+        
+        # Динамическая загрузка компонента
+        loadComponent: (componentName) ->
+            # Преобразуем имя компонента в путь
+            componentPath = @getComponentPath(componentName)
             try
-                component = require('app/pages/' + componentName)
-                return component
+                return await import(componentPath)
             catch error
-                debug.log "Компонент "+componentName+" не найден: "+error
+                debug.log "Компонент не найден: "+componentPath
                 return null
         
-        # Обработка клика по внутренним ссылкам в контенте
-        handleContentClick: (event) ->
-            if event.target.tagName == 'A'
-                href = event.target.getAttribute('href')
-                if href and href.startsWith('/')
-                    event.preventDefault()
-                    _.$router.push(href)
+        # Получение пути к компоненту
+        getComponentPath: (componentName) ->
+            # Базовая карта компонентов
+            componentMap =
+                'HeroSection': '@/app/shared/HeroSection'
+                'ImageGallery': '@/app/shared/ImageGallery'
+                'ContactForm': '@/app/shared/ContactForm'
+                'EventList': '@/app/shared/EventList'
+                'Testimonials': '@/app/shared/Testimonials'
+            
+            return componentMap[componentName] or '@/app/shared/'+componentName
+        
+        # Получение текста с учетом языка
+        getText: (textArray) ->
+            return AppDB.multilingual.getText(textArray, '')
+    
+    mounted: ->
+        # Рендерим динамические компоненты после монтирования
+        @$nextTick =>
+            @renderDynamicComponents()
+    
+    watch:
+        '$route': 'loadPageData'
+        '_.currentLanguage': 'loadPageData'

+ 52 - 85
vue/app/pages/Page/index.pug

@@ -1,96 +1,63 @@
-div(class="page-container")
+div(class="page-view")
     //- Состояние загрузки
-    div(v-if="loading" class="container mx-auto px-4 py-16 text-center")
-        div(class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto")
-        p(class="mt-4 text-gray-600") {{ getLocalizedString('loading') }}
+    div(v-if="loading" class="container mx-auto px-4 py-8")
+        div(class="flex justify-center items-center py-12")
+            div(class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600")
     
     //- Состояние ошибки
-    div(v-else-if="error" class="container mx-auto px-4 py-16")
-        div(class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded max-w-2xl mx-auto")
+    div(v-else-if="error" class="container mx-auto px-4 py-8")
+        div(class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded")
             p {{ error }}
-            app-link(
-                to="/" 
-                class="mt-4 inline-block bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 transition-colors"
-            ) {{ getLocalizedString('back_to_home') }}
+            app-link(to="/" class="text-blue-600 hover:text-blue-800 mt-2 inline-block") Вернуться на главную
     
-    //- Страница не найдена
-    div(v-else-if="notFound" class="container mx-auto px-4 py-16 text-center")
-        div(class="max-w-2xl mx-auto")
-            h1(class="text-6xl font-bold text-gray-300 mb-4") 404
-            h2(class="text-2xl font-semibold text-gray-800 mb-4") {{ getLocalizedString('not_found') }}
-            p(class="text-gray-600 mb-8") {{ getLocalizedString('page_not_found_description', 'Запрашиваемая страница не существует или была перемещена') }}
-            app-link(
-                to="/" 
-                class="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors inline-block"
-            ) {{ getLocalizedString('back_to_home') }}
-    
-    //- Отображение страницы
+    //- Контент страницы
     div(v-else-if="page" class="page-content")
-        //- Кастомный компонент если указан
-        component(
-            v-if="renderCustomComponent()"
-            :is="renderCustomComponent()"
-            :page="page"
-            :settings="pageSettings"
-        )
+        //- Хидер страницы
+        header(class="bg-white dark:bg-gray-800 shadow-sm")
+            div(class="container mx-auto px-4 py-6")
+                div(class="flex items-center justify-between")
+                    div
+                        h1(class="text-3xl font-bold text-gray-800 dark:text-white") {{ getText(page.title) }}
+                        p(v-if="page.excerpt" class="text-gray-600 dark:text-gray-300 mt-2") {{ getText(page.excerpt) }}
+                    
+                    div(class="flex items-center space-x-4")
+                        span(class="text-sm text-gray-500") {{ formatDate(page.created_at) }}
         
-        //- Стандартное отображение
-        div(v-else)
-            //- Hero секция если есть изображение
-            section(v-if="page.image" class="page-hero relative bg-gray-900")
+        //- Основной контент
+        main(class="container mx-auto px-4 py-8")
+            div(v-if="page.image" class="mb-8")
                 img(
-                    :src="getMultilingualText(page.image)" 
-                    :alt="getMultilingualText(page.title)"
-                    class="w-full h-64 md:h-96 object-cover opacity-70"
+                    :src="getText(page.image)" 
+                    :alt="getText(page.title)"
+                    class="w-full h-64 object-cover rounded-lg shadow-md"
                 )
-                div(class="absolute inset-0 flex items-center justify-center")
-                    div(class="text-center text-white")
-                        h1(class="text-4xl md:text-6xl font-bold mb-4") {{ getMultilingualText(page.title) }}
-                        p(v-if="getMultilingualText(page.excerpt)" class="text-xl opacity-90 max-w-2xl mx-auto") {{ getMultilingualText(page.excerpt) }}
             
-            //- Контент страницы
-            div(:class="{ 'container mx-auto px-4 py-16': !page.image }")
-                //- Заголовок если нет hero изображения
-                div(v-if="!page.image" class="text-center mb-12")
-                    h1(class="text-4xl font-bold text-gray-800 mb-4") {{ getMultilingualText(page.title) }}
-                    p(v-if="getMultilingualText(page.excerpt)" class="text-xl text-gray-600 max-w-2xl mx-auto") {{ getMultilingualText(page.excerpt) }}
-                
-                //- Основной контент
-                article(
-                    class="prose prose-lg max-w-none prose-headings:text-gray-800 prose-p:text-gray-600 prose-a:text-blue-600 prose-a:no-underline hover:prose-a:underline prose-ul:text-gray-600 prose-ol:text-gray-600 prose-strong:text-gray-800 prose-blockquote:border-blue-600 prose-blockquote:text-gray-600"
-                    v-html="processedContent"
-                    @click="handleContentClick"
-                )
-                
-                //- Галерея если есть
-                div(v-if="page.gallery && page.gallery[0] && page.gallery[0].length > 0" class="mt-12")
-                    h2(class="text-2xl font-semibold text-gray-800 mb-6") {{ getLocalizedString('gallery', 'Галерея') }}
-                    div(class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4")
-                        img(
-                            v-for="(image, index) in getMultilingualText(page.gallery, [])" 
-                            :key="index"
-                            :src="image"
-                            :alt="getMultilingualText(page.title) + ' - изображение ' + (index + 1)"
-                            class="w-full h-48 object-cover rounded-lg shadow-md hover:shadow-lg transition-shadow cursor-pointer"
-                            @click="openImageGallery(index)"
-                        )
-    
-    //- Модальное окно для галереи
-    div(v-if="showGalleryModal" class="fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center z-50 p-4")
-        div(class="relative max-w-4xl max-h-full")
-            button(
-                @click="showGalleryModal = false"
-                class="absolute -top-12 right-0 text-white text-2xl hover:text-gray-300 transition-colors"
-            ) ×
-            img(
-                :src="currentGalleryImage"
-                class="max-w-full max-h-full object-contain"
+            //- Динамический контент с компонентами
+            div(
+                class="prose prose-lg max-w-none dark:prose-invert"
+                v-html="processedContent"
             )
-            div(v-if="getMultilingualText(page.gallery, []).length > 1" class="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex space-x-2")
-                button(
-                    v-for="(image, index) in getMultilingualText(page.gallery, [])"
-                    :key="index"
-                    @click="currentGalleryIndex = index"
-                    :class="{ 'bg-blue-600': currentGalleryIndex === index, 'bg-white': currentGalleryIndex !== index }"
-                    class="w-3 h-3 rounded-full transition-colors"
-                )
+        
+        //- Дополнительная информация
+        footer(v-if="page.tags || page.author" class="border-t border-gray-200 dark:border-gray-700 mt-8 pt-8")
+            div(class="container mx-auto px-4")
+                div(class="flex flex-wrap gap-4 items-center justify-between")
+                    div(v-if="page.tags" class="flex flex-wrap gap-2")
+                        span(
+                            v-for="tag in getText(page.tags)"
+                            :key="tag"
+                            class="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded"
+                        ) {{ tag }}
+                    
+                    div(v-if="page.author" class="text-sm text-gray-500")
+                        | Автор: {{ getText(page.author) }}
+    
+    //- Страница не найдена
+    div(v-else class="container mx-auto px-4 py-8")
+        div(class="text-center")
+            h2(class="text-2xl font-bold text-gray-800 dark:text-white mb-4") Страница не найдена
+            p(class="text-gray-600 dark:text-gray-300 mb-6") Запрошенная страница не существует или была перемещена
+            app-link(
+                to="/" 
+                class="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors"
+            ) Вернуться на главную