|
@@ -12,6 +12,737 @@
|
|
|
**GIT репозитарий:** https://gogs.osvoj.ru/oleg/s5l.ru-crm
|
|
**GIT репозитарий:** https://gogs.osvoj.ru/oleg/s5l.ru-crm
|
|
|
**Текущая версия промта** https://gogs.osvoj.ru/oleg/s5l.ru-crm/raw/master/README.md
|
|
**Текущая версия промта** https://gogs.osvoj.ru/oleg/s5l.ru-crm/raw/master/README.md
|
|
|
|
|
|
|
|
|
|
+## 🛠 ТЕХНИЧЕСКИЙ СТЕК
|
|
|
|
|
+- **Шаблонизатор:** Pug с Vue компонентами
|
|
|
|
|
+- **Стилизация:** Stylus + CSS переменные + BEM методология
|
|
|
|
|
+- **Логика:** CoffeeScript + Vue.js 3 (runtime)
|
|
|
|
|
+- **Маршрутизация:** Vue Router
|
|
|
|
|
+- **База данных:** PouchDB (клиент) + CouchDB (сервер)
|
|
|
|
|
+- **Анимации:** CSS transitions/transforms + Vue transitions
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+## 📁 СТРУКТУРА ПРОЕКТА
|
|
|
|
|
+
|
|
|
|
|
+### 🏗️ АРХИТЕКТУРА ФАЙЛОВ
|
|
|
|
|
+```
|
|
|
|
|
+app/
|
|
|
|
|
+├── index.pug # Главный layout
|
|
|
|
|
+├── index.coffee # Инициализация Vue и роутера
|
|
|
|
|
+├── index.styl # Глобальные стили и CSS переменные
|
|
|
|
|
+├── config.coffee # Конфигурация приложения
|
|
|
|
|
+├── types/ # Интерфейсы данных
|
|
|
|
|
+│ ├── data.coffee
|
|
|
|
|
+│ ├── events.coffee
|
|
|
|
|
+│ └── api.coffee
|
|
|
|
|
+├── services/ # Бизнес-логика
|
|
|
|
|
+│ ├── DomainService.coffee
|
|
|
|
|
+│ ├── ProductService.coffee
|
|
|
|
|
+│ ├── ImportService.coffee
|
|
|
|
|
+│ └── CategoryService.coffee
|
|
|
|
|
+├── utils/ # Утилиты
|
|
|
|
|
+│ └── pouch.coffee # PouchDB сервис
|
|
|
|
|
+├── design/ # Дизайн-документы CouchDB
|
|
|
|
|
+│ ├── admin.coffee
|
|
|
|
|
+│ └── site.coffee
|
|
|
|
|
+├── components/ # Переиспользуемые компоненты
|
|
|
|
|
+│ ├── UI/
|
|
|
|
|
+│ │ ├── Button/
|
|
|
|
|
+│ │ ├── Modal/
|
|
|
|
|
+│ │ └── Notification/
|
|
|
|
|
+│ ├── Domain/
|
|
|
|
|
+│ │ ├── ProductCard/
|
|
|
|
|
+│ │ ├── CategoryMenu/
|
|
|
|
|
+│ │ └── CartWidget/
|
|
|
|
|
+│ └── Admin/
|
|
|
|
|
+│ ├── DataTable/
|
|
|
|
|
+│ └── FileUpload/
|
|
|
|
|
+└── pages/ # Страницы приложения
|
|
|
|
|
+ ├── Home/ # Главная страница
|
|
|
|
|
+ │ ├── index.pug
|
|
|
|
|
+ │ ├── index.coffee
|
|
|
|
|
+ │ └── index.styl
|
|
|
|
|
+ ├── Catalog/ # Каталог товаров
|
|
|
|
|
+ │ ├── index.pug
|
|
|
|
|
+ │ ├── index.coffee
|
|
|
|
|
+ │ └── index.styl
|
|
|
|
|
+ ├── Product/ # Страница товара
|
|
|
|
|
+ │ ├── index.pug
|
|
|
|
|
+ │ ├── index.coffee
|
|
|
|
|
+ │ └── index.styl
|
|
|
|
|
+ ├── Cart/ # Корзина
|
|
|
|
|
+ │ ├── index.pug
|
|
|
|
|
+ │ ├── index.coffee
|
|
|
|
|
+ │ └── index.styl
|
|
|
|
|
+ ├── Blog/ # Блог
|
|
|
|
|
+ │ ├── index.pug
|
|
|
|
|
+ │ ├── index.coffee
|
|
|
|
|
+ │ └── index.styl
|
|
|
|
|
+ ├── Article/ # Статья блога
|
|
|
|
|
+ │ ├── index.pug
|
|
|
|
|
+ │ ├── index.coffee
|
|
|
|
|
+ │ └── index.styl
|
|
|
|
|
+ └── Admin/ # Админ-панель
|
|
|
|
|
+ ├── index.pug
|
|
|
|
|
+ ├── index.coffee
|
|
|
|
|
+ ├── index.styl
|
|
|
|
|
+ ├── Dashboard/ # Дашборд
|
|
|
|
|
+ ├── Products/ # Управление товарами
|
|
|
|
|
+ │ ├── index.pug
|
|
|
|
|
+ │ ├── index.coffee
|
|
|
|
|
+ │ ├── index.styl
|
|
|
|
|
+ │ └── Import/ # Импорт товаров
|
|
|
|
|
+ │ ├── index.pug
|
|
|
|
|
+ │ ├── index.coffee
|
|
|
|
|
+ │ └── index.styl
|
|
|
|
|
+ ├── Categories/ # Управление категориями
|
|
|
|
|
+ ├── Blog/ # Управление блогом
|
|
|
|
|
+ ├── Slider/ # Управление слайдами
|
|
|
|
|
+ ├── Clients/ # Управление клиентами
|
|
|
|
|
+ ├── Orders/ # Управление заказами
|
|
|
|
|
+ ├── Routes/ # Управление маршрутами
|
|
|
|
|
+ └── Settings/ # Настройки системы
|
|
|
|
|
+```
|
|
|
|
|
+## 💻 ПРИМЕРЫ КОДА
|
|
|
|
|
+
|
|
|
|
|
+### 🎨 СИСТЕМА СТИЛЕЙ
|
|
|
|
|
+
|
|
|
|
|
+**app/index.styl**
|
|
|
|
|
+здесь храняться базовые стили темы. которые используются во всех остальных стилевых файлах.
|
|
|
|
|
+отдельно его подключать в них не нужно, он глобально доступен.
|
|
|
|
|
+
|
|
|
|
|
+```stylus
|
|
|
|
|
+// CSS переменные вместо rgba функций
|
|
|
|
|
+:root
|
|
|
|
|
+ // Основные цвета
|
|
|
|
|
+ --color-primary: #2c5aa0
|
|
|
|
|
+ --color-secondary: #6c757d
|
|
|
|
|
+ --color-success: #28a745
|
|
|
|
|
+ --color-danger: #dc3545
|
|
|
|
|
+ --color-warning: #ffc107
|
|
|
|
|
+ --color-light: #f8f9fa
|
|
|
|
|
+ --color-dark: #343a40
|
|
|
|
|
+
|
|
|
|
|
+ // Прозрачные варианты
|
|
|
|
|
+ --color-primary-10: #2c5aa01a
|
|
|
|
|
+ --color-primary-20: #2c5aa033
|
|
|
|
|
+ --color-primary-50: #2c5aa080
|
|
|
|
|
+ --color-dark-10: #343a401a
|
|
|
|
|
+ --color-dark-50: #343a4080
|
|
|
|
|
+ --color-light-10: #f8f9fa1a
|
|
|
|
|
+ --color-light-50: #f8f9fa80
|
|
|
|
|
+
|
|
|
|
|
+ // Тени
|
|
|
|
|
+ --shadow-sm: 0 1px 2px var(--color-dark-10)
|
|
|
|
|
+ --shadow-md: 0 4px 6px var(--color-dark-10)
|
|
|
|
|
+ --shadow-lg: 0 10px 15px var(--color-dark-10)
|
|
|
|
|
+
|
|
|
|
|
+ // Прочие переменные
|
|
|
|
|
+ --border-radius: 8px
|
|
|
|
|
+ --transition: all 0.3s ease
|
|
|
|
|
+
|
|
|
|
|
+// Базовые стили
|
|
|
|
|
+.app
|
|
|
|
|
+ min-height: 100vh
|
|
|
|
|
+ transition: var(--transition)
|
|
|
|
|
+ background: var(--color-light)
|
|
|
|
|
+ color: var(--color-dark)
|
|
|
|
|
+
|
|
|
|
|
+ &.theme-dark
|
|
|
|
|
+ background: var(--color-dark)
|
|
|
|
|
+ color: var(--color-light)
|
|
|
|
|
+
|
|
|
|
|
+.header
|
|
|
|
|
+ display: flex
|
|
|
|
|
+ align-items: center
|
|
|
|
|
+ padding: 1rem 2rem
|
|
|
|
|
+ background: var(--color-light)
|
|
|
|
|
+ box-shadow: var(--shadow-sm)
|
|
|
|
|
+ border-bottom: 1px solid var(--color-primary-10)
|
|
|
|
|
+
|
|
|
|
|
+ .theme-dark &
|
|
|
|
|
+ background: var(--color-dark)
|
|
|
|
|
+ box-shadow: var(--shadow-md)
|
|
|
|
|
+
|
|
|
|
|
+// Адаптивность
|
|
|
|
|
+@media (max-width: 768px)
|
|
|
|
|
+ .header
|
|
|
|
|
+ padding: 1rem
|
|
|
|
|
+ flex-direction: column
|
|
|
|
|
+ gap: 1rem
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**app/index.pug**
|
|
|
|
|
+```pug
|
|
|
|
|
+div(class="app" :class="{'theme-dark': theme === 'dark'}")
|
|
|
|
|
+ header(class="header")
|
|
|
|
|
+ nav(class="header-nav")
|
|
|
|
|
+ div(class="header-nav-block")
|
|
|
|
|
+ div(class="header-nav--name") {{ currentDomainSettings?.companyName || companyName }}
|
|
|
|
|
+ div(class="header-nav--menu")
|
|
|
|
|
+ multilevelmenu(:domains="availableDomains" :current-domain="currentDomain")
|
|
|
|
|
+ themetoggle(:theme="theme" @theme-changed="toggleTheme")
|
|
|
|
|
+ languagetoggle(:languages="languages" :current-language="currentLanguage" @language-changed="setLanguage")
|
|
|
|
|
+ cartwidget(:items="cartItems" @update-cart="updateCart")
|
|
|
|
|
+
|
|
|
|
|
+ main(class="main-content")
|
|
|
|
|
+ router-view(v-slot="{ Component, route }")
|
|
|
|
|
+ transition(name="page-slide" mode="out-in")
|
|
|
|
|
+ component(
|
|
|
|
|
+ :is="Component"
|
|
|
|
|
+ :key="route.fullPath"
|
|
|
|
|
+ :domain-settings="currentDomainSettings"
|
|
|
|
|
+ :language="currentLanguage"
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ notification-container(:notifications="notifications")
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**app/index.coffee**
|
|
|
|
|
+```coffee
|
|
|
|
|
+# Загрузка конфигурации
|
|
|
|
|
+config = require 'app/config'
|
|
|
|
|
+DataTypes = require 'app/types/data'
|
|
|
|
|
+EventTypes = require 'app/types/events'
|
|
|
|
|
+
|
|
|
|
|
+# Инициализация глобальных переменных
|
|
|
|
|
+globalThis.renderFns = require 'pug.json'
|
|
|
|
|
+globalThis.stylFns = require 'styl.json'
|
|
|
|
|
+
|
|
|
|
|
+# Глобальная инициализация debug
|
|
|
|
|
+globalThis.debug = require 'debug'
|
|
|
|
|
+globalThis.log = debug.log
|
|
|
|
|
+
|
|
|
|
|
+# Сервисы
|
|
|
|
|
+PouchDBService = require 'app/utils/pouch'
|
|
|
|
|
+DomainService = require 'app/services/DomainService'
|
|
|
|
|
+
|
|
|
|
|
+# Мета-теги
|
|
|
|
|
+document.head.insertAdjacentHTML 'beforeend', '<meta charset="UTF-8">'
|
|
|
|
|
+document.head.insertAdjacentHTML 'beforeend', '<meta name="viewport" content="width=device-width, initial-scale=1.0">'
|
|
|
|
|
+document.head.insertAdjacentHTML 'beforeend', '<style type="text/css">' + stylFns['app/index.styl'] + '</style>'
|
|
|
|
|
+
|
|
|
|
|
+# Главное приложение Vue
|
|
|
|
|
+app = Vue.createApp({
|
|
|
|
|
+ data: ->
|
|
|
|
|
+ {
|
|
|
|
|
+ theme: localStorage.getItem('theme') or 'light'
|
|
|
|
|
+ companyName: 'Браер-Колор'
|
|
|
|
|
+ loading: false
|
|
|
|
|
+ currentDomain: window.location.hostname
|
|
|
|
|
+ currentDomainSettings: null
|
|
|
|
|
+ availableDomains: []
|
|
|
|
|
+ languages: config.languages
|
|
|
|
|
+ currentLanguage: localStorage.getItem('language') or config.defaultLanguage
|
|
|
|
|
+ user: null
|
|
|
|
|
+ cartItems: []
|
|
|
|
|
+ notifications: []
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ computed:
|
|
|
|
|
+ isAdmin: -> @user?.role == 'admin'
|
|
|
|
|
+ domainConfig: ->
|
|
|
|
|
+ DomainService.getDomainConfig(@currentDomain)
|
|
|
|
|
+
|
|
|
|
|
+ methods:
|
|
|
|
|
+ toggleTheme: ->
|
|
|
|
|
+ @theme = if @theme == 'light' then 'dark' else 'light'
|
|
|
|
|
+ localStorage.setItem 'theme', @theme
|
|
|
|
|
+ document.documentElement.classList.toggle 'dark'
|
|
|
|
|
+ @$emit EventTypes.THEME_CHANGED, @theme
|
|
|
|
|
+
|
|
|
|
|
+ setLanguage: (lang) ->
|
|
|
|
|
+ if @languages.includes lang
|
|
|
|
|
+ @currentLanguage = lang
|
|
|
|
|
+ localStorage.setItem 'language', @lang
|
|
|
|
|
+ @$emit EventTypes.LANGUAGE_CHANGED, lang
|
|
|
|
|
+
|
|
|
|
|
+ loadDomainData: ->
|
|
|
|
|
+ DomainService.loadDomainSettings(@currentDomain)
|
|
|
|
|
+ .then (settings) =>
|
|
|
|
|
+ @currentDomainSettings = settings
|
|
|
|
|
+ document.title = settings?.companyName or @companyName
|
|
|
|
|
+ log 'Настройки домена загружены', settings
|
|
|
|
|
+ .catch (error) =>
|
|
|
|
|
+ log 'Настройки домена не найдены, используются значения по умолчанию'
|
|
|
|
|
+ @currentDomainSettings = new DataTypes.DomainSettings()
|
|
|
|
|
+
|
|
|
|
|
+ updateCart: (items) ->
|
|
|
|
|
+ @cartItems = items
|
|
|
|
|
+ localStorage.setItem 'cart', JSON.stringify(items)
|
|
|
|
|
+
|
|
|
|
|
+ showNotification: (message, type = 'success') ->
|
|
|
|
|
+ notification = { id: Date.now(), message, type, visible: true }
|
|
|
|
|
+ @notifications.push notification
|
|
|
|
|
+ setTimeout (=>
|
|
|
|
|
+ notification.visible = false
|
|
|
|
|
+ setTimeout (=>
|
|
|
|
|
+ @notifications = @notifications.filter (n) -> n.id != notification.id
|
|
|
|
|
+ ), 300
|
|
|
|
|
+ ), 5000
|
|
|
|
|
+
|
|
|
|
|
+ async mounted: ->
|
|
|
|
|
+ # Инициализация темы
|
|
|
|
|
+ if @theme == 'dark'
|
|
|
|
|
+ document.documentElement.classList.add 'dark'
|
|
|
|
|
+
|
|
|
|
|
+ # Инициализация сервисов
|
|
|
|
|
+ try
|
|
|
|
|
+ await PouchDBService.init()
|
|
|
|
|
+ log 'PouchDB инициализирован'
|
|
|
|
|
+
|
|
|
|
|
+ await DomainService.init()
|
|
|
|
|
+ @availableDomains = DomainService.getAvailableDomains()
|
|
|
|
|
+
|
|
|
|
|
+ await @loadDomainData()
|
|
|
|
|
+ catch error
|
|
|
|
|
+ log 'Ошибка инициализации:', error
|
|
|
|
|
+
|
|
|
|
|
+ # Загрузка пользователя и корзины
|
|
|
|
|
+ @loadUserData()
|
|
|
|
|
+ @loadCartData()
|
|
|
|
|
+
|
|
|
|
|
+ loadUserData: ->
|
|
|
|
|
+ userData = localStorage.getItem 'user'
|
|
|
|
|
+ if userData
|
|
|
|
|
+ try
|
|
|
|
|
+ @user = JSON.parse userData
|
|
|
|
|
+ catch
|
|
|
|
|
+ @user = null
|
|
|
|
|
+
|
|
|
|
|
+ loadCartData: ->
|
|
|
|
|
+ cartData = localStorage.getItem 'cart'
|
|
|
|
|
+ if cartData
|
|
|
|
|
+ try
|
|
|
|
|
+ @cartItems = JSON.parse cartData
|
|
|
|
|
+ catch
|
|
|
|
|
+ @cartItems = []
|
|
|
|
|
+
|
|
|
|
|
+ render: (new Function '_ctx', '_cache', renderFns['app/index.pug'])()
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+# Глобальная обработка ошибок
|
|
|
|
|
+app.config.errorHandler = (err, vm, info) ->
|
|
|
|
|
+ log 'Vue ошибка:', err, info
|
|
|
|
|
+
|
|
|
|
|
+app.mount('body')
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 🚨 КРИТИЧЕСКИЕ ПРАВИЛА РАЗРАБОТКИ
|
|
|
|
|
+
|
|
|
|
|
+### ❌ **ЗАПРЕЩЕНО:**
|
|
|
|
|
+
|
|
|
|
|
+1. **Использование rgba() функций в стилях**
|
|
|
|
|
+ ```stylus
|
|
|
|
|
+ // ❌ НЕПРАВИЛЬНО
|
|
|
|
|
+ background: rgba(44, 90, 160, 0.1)
|
|
|
|
|
+
|
|
|
|
|
+ // ✅ ПРАВИЛЬНО
|
|
|
|
|
+ background: var(--color-primary-10)
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+2. **Прямое использование console.log**
|
|
|
|
|
+ ```coffee
|
|
|
|
|
+ # ❌ НЕПРАВИЛЬНО
|
|
|
|
|
+ console.log 'Ошибка'
|
|
|
|
|
+
|
|
|
|
|
+ # ✅ ПРАВИЛЬНО
|
|
|
|
|
+ log 'Ошибка'
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+3. **Многострочные атрибуты в Pug**
|
|
|
|
|
+ ```pug
|
|
|
|
|
+ // ❌ НЕПРАВИЛЬНО
|
|
|
|
|
+ div(
|
|
|
|
|
+ class="class"
|
|
|
|
|
+ data-attr="value"
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ // ✅ ПРАВИЛЬНО
|
|
|
|
|
+ div(class="class" data-attr="value")
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+4. **@import в стилях компонентов**
|
|
|
|
|
+ ```stylus
|
|
|
|
|
+ // ❌ НЕПРАВИЛЬНО
|
|
|
|
|
+ @import '../../index.styl'
|
|
|
|
|
+
|
|
|
|
|
+ // ✅ ПРАВИЛЬНО
|
|
|
|
|
+ .my-component
|
|
|
|
|
+ background: var(--color-primary-10)
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+5. **JavaScript операторы в CoffeeScript**
|
|
|
|
|
+ ```coffee
|
|
|
|
|
+ # ❌ НЕПРАВИЛЬНО
|
|
|
|
|
+ if user && user.role
|
|
|
|
|
+ condition ? 'yes' : 'no'
|
|
|
|
|
+
|
|
|
|
|
+ # ✅ ПРАВИЛЬНО
|
|
|
|
|
+ if user and user.role
|
|
|
|
|
+ if condition then 'yes' else 'no'
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+6. **HTML/HEAD/BODY теги в компонентах**
|
|
|
|
|
+ ```pug
|
|
|
|
|
+ // ❌ НЕПРАВИЛЬНО
|
|
|
|
|
+ html
|
|
|
|
|
+ body
|
|
|
|
|
+ div.app
|
|
|
|
|
+
|
|
|
|
|
+ // ✅ ПРАВИЛЬНО
|
|
|
|
|
+ div(class="app")
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+7. **Объявление debug в компонентах**
|
|
|
|
|
+ ```coffee
|
|
|
|
|
+ # ❌ НЕПРАВИЛЬНО
|
|
|
|
|
+ debug = require 'debug'
|
|
|
|
|
+ log = debug.log
|
|
|
|
|
+
|
|
|
|
|
+ # ✅ ПРАВИЛЬНО - debug глобальный
|
|
|
|
|
+ log 'Сообщение'
|
|
|
|
|
+ ```
|
|
|
|
|
+
|
|
|
|
|
+### ✅ **ОБЯЗАТЕЛЬНО:**
|
|
|
|
|
+
|
|
|
|
|
+1. **Полные листинги файлов** - всегда приводи все три файла компонента
|
|
|
|
|
+2. **Адаптивная верстка** - mobile-first для всех компонентов
|
|
|
|
|
+3. **BEM методология** - `block__element--modifier`
|
|
|
|
|
+4. **CSS переменные** - только через var(--variable-name)
|
|
|
|
|
+5. **Обработка ошибок** - try/catch для всех асинхронных операций
|
|
|
|
|
+6. **Валидация данных** - проверка обязательных полей
|
|
|
|
|
+7. **Прогресс операций** - индикаторы для длительных процессов
|
|
|
|
|
+8. **Мультиязычность** - все тексты из настроек домена
|
|
|
|
|
+9. **Использование глобального debug** - только `log 'сообщение'`
|
|
|
|
|
+
|
|
|
|
|
+## 🗂️ СТРУКТУРА ДАННЫХ
|
|
|
|
|
+
|
|
|
|
|
+### 📊 ТИПЫ ДОКУМЕНТОВ
|
|
|
|
|
+
|
|
|
|
|
+**app/types/data.coffee**
|
|
|
|
|
+```coffee
|
|
|
|
|
+class DomainEntity
|
|
|
|
|
+ constructor: ->
|
|
|
|
|
+ @_id = ''
|
|
|
|
|
+ @type = ''
|
|
|
|
|
+ @domains = []
|
|
|
|
|
+ @createdAt = new Date().toISOString()
|
|
|
|
|
+ @updatedAt = new Date().toISOString()
|
|
|
|
|
+ @active = true
|
|
|
|
|
+
|
|
|
|
|
+class Product extends DomainEntity
|
|
|
|
|
+ constructor: ->
|
|
|
|
|
+ super()
|
|
|
|
|
+ @type = 'product'
|
|
|
|
|
+ @name = ''
|
|
|
|
|
+ @sku = ''
|
|
|
|
|
+ @price = 0
|
|
|
|
|
+ @oldPrice = null
|
|
|
|
|
+ @category = ''
|
|
|
|
|
+ @brand = ''
|
|
|
|
|
+ @description = ''
|
|
|
|
|
+ @attributes = {}
|
|
|
|
|
+ @images = []
|
|
|
|
|
+ @richContent = null
|
|
|
|
|
+ @inStock = true
|
|
|
|
|
+ @weight = 0
|
|
|
|
|
+ @volume = 0
|
|
|
|
|
+
|
|
|
|
|
+class Category extends DomainEntity
|
|
|
|
|
+ constructor: ->
|
|
|
|
|
+ super()
|
|
|
|
|
+ @type = 'category'
|
|
|
|
|
+ @name = ''
|
|
|
|
|
+ @slug = ''
|
|
|
|
|
+ @parent = null
|
|
|
|
|
+ @order = 0
|
|
|
|
|
+ @image = ''
|
|
|
|
|
+ @description = ''
|
|
|
|
|
+
|
|
|
|
|
+class HeroSlide extends DomainEntity
|
|
|
|
|
+ constructor: ->
|
|
|
|
|
+ super()
|
|
|
|
|
+ @type = 'hero_slide'
|
|
|
|
|
+ @title = ''
|
|
|
|
|
+ @subtitle = ''
|
|
|
|
|
+ @image = ''
|
|
|
|
|
+ @buttonText = 'В каталог'
|
|
|
|
|
+ @buttonLink = '/catalog'
|
|
|
|
|
+ @order = 0
|
|
|
|
|
+
|
|
|
|
|
+class DomainSettings extends DomainEntity
|
|
|
|
|
+ constructor: ->
|
|
|
|
|
+ super()
|
|
|
|
|
+ @type = 'domain_settings'
|
|
|
|
|
+ @domain = ''
|
|
|
|
|
+ @companyName = ''
|
|
|
|
|
+ @languages = ['ru']
|
|
|
|
|
+ @theme = 'light'
|
|
|
|
|
+ @contacts = {}
|
|
|
|
|
+ @seo = {}
|
|
|
|
|
+ @social = {}
|
|
|
|
|
+
|
|
|
|
|
+module.exports = {Product, Category, HeroSlide, DomainSettings, DomainEntity}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 🔧 СЕРВИСЫ И УТИЛИТЫ
|
|
|
|
|
+
|
|
|
|
|
+### 🗃️ POUCHDB СЕРВИС
|
|
|
|
|
+
|
|
|
|
|
+**app/utils/pouch.coffee**
|
|
|
|
|
+```coffee
|
|
|
|
|
+class PouchDBService
|
|
|
|
|
+ constructor: (options = {}) ->
|
|
|
|
|
+ {@localDbName, @remoteDbUrl, @userFilter, @appVersion} = options
|
|
|
|
|
+ @localDb = null
|
|
|
|
|
+ @remoteDb = null
|
|
|
|
|
+ @initialized = false
|
|
|
|
|
+
|
|
|
|
|
+ init: ->
|
|
|
|
|
+ return Promise.resolve() if @initialized
|
|
|
|
|
+
|
|
|
|
|
+ try
|
|
|
|
|
+ @localDb = new PouchDB(@localDbName or 'braer_color_cache')
|
|
|
|
|
+ await @ensureRemoteDatabase()
|
|
|
|
|
+ await @loadDesignDocs()
|
|
|
|
|
+ await @ensureDesignDocs()
|
|
|
|
|
+
|
|
|
|
|
+ # Настройка селективной синхронизации
|
|
|
|
|
+ PouchDB.sync(@remoteDb, @localDb, {
|
|
|
|
|
+ live: true,
|
|
|
|
|
+ retry: true,
|
|
|
|
|
+ filter: (doc) => @shouldSyncDocument(doc)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ @initialized = true
|
|
|
|
|
+ return Promise.resolve()
|
|
|
|
|
+ catch error
|
|
|
|
|
+ log 'Ошибка инициализации PouchDB:', error
|
|
|
|
|
+ return Promise.reject(error)
|
|
|
|
|
+
|
|
|
|
|
+ getDocument: (docId) ->
|
|
|
|
|
+ @ensureInit()
|
|
|
|
|
+
|
|
|
|
|
+ try
|
|
|
|
|
+ return await @localDb.get(docId)
|
|
|
|
|
+ catch localError
|
|
|
|
|
+ if localError.status == 404
|
|
|
|
|
+ try
|
|
|
|
|
+ doc = await @remoteDb.get(docId)
|
|
|
|
|
+ await @localDb.put(doc)
|
|
|
|
|
+ return doc
|
|
|
|
|
+ catch remoteError
|
|
|
|
|
+ throw remoteError
|
|
|
|
|
+ else
|
|
|
|
|
+ throw localError
|
|
|
|
|
+
|
|
|
|
|
+ saveToRemote: (doc) ->
|
|
|
|
|
+ @ensureInit()
|
|
|
|
|
+
|
|
|
|
|
+ try
|
|
|
|
|
+ existingDoc = await @remoteDb.get(doc._id)
|
|
|
|
|
+ doc._rev = existingDoc._rev
|
|
|
|
|
+ return await @remoteDb.put(doc)
|
|
|
|
|
+ catch error
|
|
|
|
|
+ if error.status == 404
|
|
|
|
|
+ return await @remoteDb.put(doc)
|
|
|
|
|
+ else
|
|
|
|
|
+ throw error
|
|
|
|
|
+
|
|
|
|
|
+ shouldSyncDocument: (doc) ->
|
|
|
|
|
+ if doc.type in ['product', 'category', 'settings', 'hero_slide', 'blog_article', 'route']
|
|
|
|
|
+ return true
|
|
|
|
|
+ if doc.type in ['order', 'user_data', 'cart']
|
|
|
|
|
+ return doc.userId == @userFilter?.userId
|
|
|
|
|
+ return false
|
|
|
|
|
+
|
|
|
|
|
+module.exports = new PouchDBService({
|
|
|
|
|
+ localDbName: 'braer_color_cache'
|
|
|
|
|
+ remoteDbUrl: 'http://localhost:5984/braer_color_shop'
|
|
|
|
|
+ userFilter: { userId: 'current_user_id' }
|
|
|
|
|
+ appVersion: '1.0.0'
|
|
|
|
|
+})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 📦 СЕРВИС ИМПОРТА ТОВАРОВ
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+Важно: при полной реализации используй пример csv файла, При этом учти что количество полей в документе может меняться. выдели из них основные, остальные загружай по факту как свойства товара.
|
|
|
|
|
+Индивидуальное сохранение товаров
|
|
|
|
|
+Каждый товар сохраняется как отдельный документ в PouchDB
|
|
|
|
|
+Используется стабильный _id на основе артикула: product:#{sku}
|
|
|
|
|
+Автоматическое обновление существующих товаров при повторном импорте
|
|
|
|
|
+Улучшенная обработка изображений
|
|
|
|
|
+Загрузка основного и дополнительных изображений как attachments
|
|
|
|
|
+Формат ссылок: /d/braer_color_shop/{doc_id}/{filename}
|
|
|
|
|
+Все фото из csv должны быть загружены как attachment в документ товара
|
|
|
|
|
+Объеденяй товары в группы по полю из csv файла, в списке товаров выводи главное изображение.
|
|
|
|
|
+в редакторе нужно иметь возможность управлять всеми данными товара. некоторые атрибуты иметь возможность отметить как скрытые, по из названи. (нужно упаравление атрибутами в категории)
|
|
|
|
|
+Прогресс импорта
|
|
|
|
|
+Визуальный индикатор прогресса обработки
|
|
|
|
|
+Отслеживание количества обработанных товаров
|
|
|
|
|
+Детальная статистика по завершении импорта
|
|
|
|
|
+Обработка Rich-контента
|
|
|
|
|
+Преобразование JSON в Markdown для описаний товаров
|
|
|
|
|
+Сохранение оригинальной структуры для возможного редактирования
|
|
|
|
|
+Создать редактор товаров
|
|
|
|
|
+
|
|
|
|
|
+Создание объектов катигорий при появлении новых значений в поле "Тип*", сделать редактор категорий, с возможностью загрузки фото
|
|
|
|
|
+При обработке csv загружай изображения в базу данных как attachment, также сохраняй ричконтент преобразовывая в markdown, и выводи его в качестве описания товара при его наличии.
|
|
|
|
|
+формат ссылок на attachment файлы "/d/[ИМЯ БД]/[id doc]/[имя файла]"
|
|
|
|
|
+Важно: каждый товар должен быть привязан к домену/доменам (может одновременно быть доступен на разных доменах), тоже касается статей блога.
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+**app/services/ImportService.coffee**
|
|
|
|
|
+```coffee
|
|
|
|
|
+{ Product, Category } = require 'app/types/data'
|
|
|
|
|
+
|
|
|
|
|
+class ImportService
|
|
|
|
|
+ constructor: ->
|
|
|
|
|
+ @batchSize = 50
|
|
|
|
|
+ @maxImages = 5
|
|
|
|
|
+
|
|
|
|
|
+ transformProductData: (csvRow, index, domain) ->
|
|
|
|
|
+ product = new Product()
|
|
|
|
|
+
|
|
|
|
|
+ product._id = "product:#{csvRow['Артикул*']}"
|
|
|
|
|
+ product.name = csvRow['Название товара']
|
|
|
|
|
+ product.sku = csvRow['Артикул*']
|
|
|
|
|
+ product.price = parseFloat(csvRow['Цена, руб.*'].replace(',', '.')) or 0
|
|
|
|
|
+ product.oldPrice = if csvRow['Цена до скидки, руб.'] then parseFloat(csvRow['Цена до скидки, руб.'].replace(',', '.')) else null
|
|
|
|
|
+ product.brand = csvRow['Бренд*']
|
|
|
|
|
+ product.category = csvRow['Тип*']
|
|
|
|
|
+ product.domains = [domain]
|
|
|
|
|
+
|
|
|
|
|
+ product.images = @processImages(csvRow, product._id)
|
|
|
|
|
+
|
|
|
|
|
+ if csvRow['Rich-контент JSON']
|
|
|
|
|
+ try
|
|
|
|
|
+ product.richContent = @jsonToMarkdown(JSON.parse(csvRow['Rich-контент JSON']))
|
|
|
|
|
+ catch error
|
|
|
|
|
+ log 'Ошибка парсинга rich-контента:', error
|
|
|
|
|
+
|
|
|
|
|
+ product.attributes = @extractAttributes(csvRow)
|
|
|
|
|
+
|
|
|
|
|
+ return product
|
|
|
|
|
+
|
|
|
|
|
+ processImages: (csvRow, docId) ->
|
|
|
|
|
+ images = []
|
|
|
|
|
+
|
|
|
|
|
+ if csvRow['Ссылка на главное фото*']
|
|
|
|
|
+ images.push {
|
|
|
|
|
+ url: csvRow['Ссылка на главное фото*']
|
|
|
|
|
+ type: 'main'
|
|
|
|
|
+ order: 0
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if csvRow['Ссылки на дополнительные фото']
|
|
|
|
|
+ additionalImages = csvRow['Ссылки на дополнительные фото'].split('\n').slice(0, @maxImages)
|
|
|
|
|
+ additionalImages.forEach (imgUrl, index) ->
|
|
|
|
|
+ if imgUrl.trim()
|
|
|
|
|
+ images.push {
|
|
|
|
|
+ url: imgUrl.trim()
|
|
|
|
|
+ type: 'additional'
|
|
|
|
|
+ order: index + 1
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return images
|
|
|
|
|
+
|
|
|
|
|
+ jsonToMarkdown: (richContent) ->
|
|
|
|
|
+ markdown = ''
|
|
|
|
|
+
|
|
|
|
|
+ if richContent?.content
|
|
|
|
|
+ richContent.content.forEach (block) ->
|
|
|
|
|
+ if block.widgetName == 'raTextBlock' and block.text?.items
|
|
|
|
|
+ block.text.items.forEach (item) ->
|
|
|
|
|
+ if item.type == 'text' and item.content
|
|
|
|
|
+ markdown += item.content + '\n\n'
|
|
|
|
|
+ else if item.type == 'br'
|
|
|
|
|
+ markdown += '\n'
|
|
|
|
|
+
|
|
|
|
|
+ return markdown.trim()
|
|
|
|
|
+
|
|
|
|
|
+ extractAttributes: (csvRow) ->
|
|
|
|
|
+ attributes = {}
|
|
|
|
|
+
|
|
|
|
|
+ technicalFields = [
|
|
|
|
|
+ 'Вес в упаковке, г*', 'Ширина упаковки, мм*', 'Высота упаковки, мм*',
|
|
|
|
|
+ 'Длина упаковки, мм*', 'Объем, л', 'Время высыхания, часов', 'Расход, л/м2',
|
|
|
|
|
+ 'Макс. температура эксплуатации, С°', 'Количество компонентов'
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ technicalFields.forEach (field) ->
|
|
|
|
|
+ if csvRow[field]
|
|
|
|
|
+ attributes[field] = csvRow[field]
|
|
|
|
|
+
|
|
|
|
|
+ categoryFields = [
|
|
|
|
|
+ 'Вид краски', 'Назначение грунтовки', 'Материал основания', 'Основа краски',
|
|
|
|
|
+ 'Способ нанесения', 'Назначение', 'Тип помещения', 'Возможность колеровки'
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ categoryFields.forEach (field) ->
|
|
|
|
|
+ if csvRow[field]
|
|
|
|
|
+ attributes[field] = csvRow[field]
|
|
|
|
|
+
|
|
|
|
|
+ return attributes
|
|
|
|
|
+
|
|
|
|
|
+ importFromCSV: (file, domain, onProgress) ->
|
|
|
|
|
+ return new Promise (resolve, reject) =>
|
|
|
|
|
+ reader = new FileReader()
|
|
|
|
|
+
|
|
|
|
|
+ reader.onload = (e) =>
|
|
|
|
|
+ try
|
|
|
|
|
+ results = Papa.parse(e.target.result, {
|
|
|
|
|
+ header: true
|
|
|
|
|
+ delimiter: ';'
|
|
|
|
|
+ skipEmptyLines: true
|
|
|
|
|
+ encoding: 'UTF-8'
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ validProducts = results.data.filter (row) =>
|
|
|
|
|
+ row and row['Артикул*'] and row['Название товара'] and row['Цена, руб.*']
|
|
|
|
|
+
|
|
|
|
|
+ this.processProductsInBatches(validProducts, domain, onProgress)
|
|
|
|
|
+ .then(resolve)
|
|
|
|
|
+ .catch(reject)
|
|
|
|
|
+
|
|
|
|
|
+ catch error
|
|
|
|
|
+ reject(new Error("Ошибка парсинга CSV: #{error.message}"))
|
|
|
|
|
+
|
|
|
|
|
+ reader.onerror = -> reject(new Error('Ошибка чтения файла'))
|
|
|
|
|
+ reader.readAsText(file, 'UTF-8')
|
|
|
|
|
+
|
|
|
|
|
+ processProductsInBatches: (products, domain, onProgress) ->
|
|
|
|
|
+ batches = []
|
|
|
|
|
+ for i in [0...products.length] by @batchSize
|
|
|
|
|
+ batches.push(products.slice(i, i + @batchSize))
|
|
|
|
|
+
|
|
|
|
|
+ processed = 0
|
|
|
|
|
+ total = products.length
|
|
|
|
|
+
|
|
|
|
|
+ processBatch = (batch) =>
|
|
|
|
|
+ transformedProducts = batch.map (product, index) =>
|
|
|
|
|
+ @transformProductData(product, processed + index, domain)
|
|
|
|
|
+
|
|
|
|
|
+ return PouchDBService.bulkDocs(transformedProducts)
|
|
|
|
|
+ .then (results) =>
|
|
|
|
|
+ processed += batch.length
|
|
|
|
|
+ if onProgress
|
|
|
|
|
+ onProgress({
|
|
|
|
|
+ processed: processed
|
|
|
|
|
+ total: total
|
|
|
|
|
+ percentage: Math.round((processed / total) * 100)
|
|
|
|
|
+ })
|
|
|
|
|
+ return results
|
|
|
|
|
+
|
|
|
|
|
+ return batches.reduce (promise, batch) =>
|
|
|
|
|
+ promise.then -> processBatch(batch)
|
|
|
|
|
+ , Promise.resolve()
|
|
|
|
|
+
|
|
|
|
|
+module.exports = new ImportService()
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
## 📊 ОБРАЗЕЦ CSV ФАЙЛА ДЛЯ ИМПОРТА
|
|
## 📊 ОБРАЗЕЦ CSV ФАЙЛА ДЛЯ ИМПОРТА
|
|
|
|
|
|
|
|
### 📝 СПЕЦИФИКАЦИЯ CSV
|
|
### 📝 СПЕЦИФИКАЦИЯ CSV
|
|
@@ -123,13 +854,7 @@ importFromCSV: (file, domain, onProgress) ->
|
|
|
|
|
|
|
|
reader.readAsText(file, 'UTF-8')
|
|
reader.readAsText(file, 'UTF-8')
|
|
|
```
|
|
```
|
|
|
-## 🛠 ТЕХНИЧЕСКИЙ СТЕК
|
|
|
|
|
-- **Шаблонизатор:** Pug с Vue компонентами
|
|
|
|
|
-- **Стилизация:** Stylus + CSS переменные + BEM методология
|
|
|
|
|
-- **Логика:** CoffeeScript + Vue.js 3 (runtime + compiler)
|
|
|
|
|
-- **Маршрутизация:** Vue Router
|
|
|
|
|
-- **База данных:** PouchDB (клиент) + CouchDB (сервер)
|
|
|
|
|
-- **Анимации:** CSS transitions/transforms + Vue transitions
|
|
|
|
|
|
|
+
|
|
|
|
|
|
|
|
## 🚨 КРИТИЧЕСКИЕ ПРАВИЛА РАЗРАБОТКИ
|
|
## 🚨 КРИТИЧЕСКИЕ ПРАВИЛА РАЗРАБОТКИ
|
|
|
|
|
|
|
@@ -212,28 +937,31 @@ importFromCSV: (file, domain, onProgress) ->
|
|
|
- Темная/светлая темы
|
|
- Темная/светлая темы
|
|
|
|
|
|
|
|
|
|
|
|
|
-1. **Система импорта товаров** ⚠️ ПРИОРИТЕТ
|
|
|
|
|
|
|
+5. **Система импорта товаров**
|
|
|
- Компонент загрузки CSV файлов
|
|
- Компонент загрузки CSV файлов
|
|
|
- Парсинг и валидация данных
|
|
- Парсинг и валидация данных
|
|
|
- Пакетная обработка товаров
|
|
- Пакетная обработка товаров
|
|
|
- Создание категорий на лету
|
|
- Создание категорий на лету
|
|
|
|
|
+
|
|
|
|
|
+ 5.1. Завершить компонент импорта товаров
|
|
|
|
|
+ 5.2. Реализовать редактор категорий
|
|
|
|
|
+ 5.3. Создать компонент управления слайдами
|
|
|
|
|
+ 5.4. Разработать главную страницу магазина
|
|
|
|
|
+
|
|
|
|
|
|
|
|
-2. **Админ-панель**
|
|
|
|
|
|
|
+6. **Админ-панель**
|
|
|
- Управление товарами
|
|
- Управление товарами
|
|
|
- Система прогресса операций
|
|
- Система прогресса операций
|
|
|
- Обработка ошибок импорта
|
|
- Обработка ошибок импорта
|
|
|
|
|
|
|
|
-1. Завершить компонент импорта товаров
|
|
|
|
|
-2. Реализовать редактор категорий
|
|
|
|
|
-3. Создать компонент управления слайдами
|
|
|
|
|
-4. Разработать главную страницу магазина
|
|
|
|
|
|
|
+
|
|
|
|
|
|
|
|
|
|
|
|
|
### 🔄 В РАБОТЕ СЕЙЧАС
|
|
### 🔄 В РАБОТЕ СЕЙЧАС
|
|
|
|
|
|
|
|
отвечай на русском Анализировать реализованный код, по git репозитарию https://gogs.osvoj.ru/oleg/s5l.ru-crm.git Проверяй промт и изменения в нём по адресу https://gogs.osvoj.ru/oleg/s5l.ru-crm/raw/master/README.md
|
|
отвечай на русском Анализировать реализованный код, по git репозитарию https://gogs.osvoj.ru/oleg/s5l.ru-crm.git Проверяй промт и изменения в нём по адресу https://gogs.osvoj.ru/oleg/s5l.ru-crm/raw/master/README.md
|
|
|
|
|
|
|
|
-1. **Базовая архитектура**
|
|
|
|
|
|
|
+1. **Базовая архитектура** ⚠️ ПРИОРИТЕТ
|
|
|
- Напиши базовые файлы системы
|
|
- Напиши базовые файлы системы
|
|
|
- Система роутинга
|
|
- Система роутинга
|
|
|
- Глобальная конфигурация
|
|
- Глобальная конфигурация
|