## 🎯 ПОЛНЫЙ ПРОМТ ДЛЯ РАЗРАБОТКИ ИНТЕРНЕТ-МАГАЗИНА "БРАЕР-КОЛОР" ### 📋 ТЕХНИЧЕСКОЕ ЗАДАНИЕ **ВАЖНО:** `debug` является глобально объявленной переменной во всем приложении. НЕ используйте `debug = require 'debug'` в компонентах. ### 🎯 КОНТЕКСТ ПРОЕКТА **Название:** Интернет-магазин лакокрасочной продукции "Браер-Колор" **Тип:** SPA (Single Page Application) с современным минималистичным дизайном **Аналоги:** Функциональность m-kraski.ru с дизайном https://braer-color.ru/ **Архитектура:** Мультидоменная, мультиязычная PWA с офлайн-режимом **GIT репозитарий:** https://gogs.osvoj.ru/oleg/s5l.ru-crm **Текущая версия промта** https://gogs.osvoj.ru/oleg/s5l.ru-crm/raw/master/README.md ## 🛠 ТЕХНИЧЕСКИЙ СТЕК - **Шаблонизатор:** Pug с Vue компонентами - **Стилизация:** Stylus + CSS переменные + BEM методология - **Логика:** CoffeeScript + Vue.js 3 (runtime) - **Важно** Vuejs в runtime режиме, не использовать template, шаблоны СТРОГО через "render: (new Function '_ctx', '_cache', renderFns['app/index.pug'])()" во всех компанентах - **Маршрутизация:** 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 # Глобальная инициализация debug globalThis.log = debug.log # Главный файл приложения log '🚀 Инициализация приложения Браер-Колор' # Загрузка конфигурации config = require 'app/config' DataTypes = require 'app/types/data' EventTypes = require 'app/types/events' # Инициализация глобальных переменных globalThis.renderFns = require 'pug.json' globalThis.stylFns = require 'styl.json' # Сервисы (пока заглушки) PouchDBService = init: -> Promise.resolve() getDocument: -> Promise.resolve(null) saveToRemote: -> Promise.resolve() DomainService = init: -> Promise.resolve() loadDomainSettings: -> Promise.resolve(null) getAvailableDomains: -> [] # Мета-теги document.head.insertAdjacentHTML 'beforeend', '' document.head.insertAdjacentHTML 'beforeend', '' document.head.insertAdjacentHTML 'beforeend', 'Браер-Колор - Интернет-магазин лакокрасочной продукции' # Добавление глобальных стилей if stylFns['app/index.styl'] styleElement = document.createElement('style') styleElement.type = 'text/css' styleElement.textContent = stylFns['app/index.styl'] document.head.appendChild(styleElement) else log '⚠️ Глобальные стили не найдены' # Создание Vue приложения app = Vue.createApp({ data: -> { theme: localStorage.getItem('theme') or config.defaultTheme companyName: config.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) or {} methods: # Управление темой toggleTheme: -> @theme = if @theme == 'light' then 'dark' else 'light' localStorage.setItem 'theme', @theme document.documentElement.classList.toggle 'dark' @$emit EventTypes.THEME_CHANGED, @theme log '🎨 Тема изменена:', @theme # Управление языком setLanguage: (lang) -> if @languages.includes lang @currentLanguage = lang localStorage.setItem 'language', @currentLanguage @$emit EventTypes.LANGUAGE_CHANGED, lang log '🌐 Язык изменен:', lang else log '⚠️ Язык не поддерживается:', lang # Смена домена changeDomain: (domain) -> log '🌐 Смена домена на:', domain @currentDomain = domain @loadDomainData() # Переход в корзину goToCart: -> @$router.push '/cart' log '🛒 Переход в корзину' # Загрузка настроек домена loadDomainData: -> log '📡 Загрузка настроек домена:', @currentDomain DomainService.loadDomainSettings(@currentDomain) .then (settings) => @currentDomainSettings = settings document.title = settings?.companyName or @companyName log '✅ Настройки домена загружены', settings .catch (error) => log '⚠️ Настройки домена не найдены, используются значения по умолчанию' @currentDomainSettings = new DataTypes.DomainSettings() @currentDomainSettings.companyName = @companyName # Управление корзиной updateCart: (items) -> @cartItems = items localStorage.setItem 'cart', JSON.stringify(items) @$emit EventTypes.CART_UPDATE, items log '🛒 Корзина обновлена:', items.length, 'товаров' # Уведомления showNotification: (message, type = 'info') -> notification = { id: Date.now(), message, type, visible: true, timestamp: new Date() } @notifications.push notification log '📢 Показано уведомление:', message setTimeout (=> notification.visible = false setTimeout (=> @notifications = @notifications.filter (n) -> n.id != notification.id ), 300 ), 5000 # Закрытие уведомления closeNotification: (id) -> @notifications = @notifications.filter (notification) -> notification.id != id log '📢 Уведомление закрыто:', id # Загрузка пользователя loadUserData: -> userData = localStorage.getItem 'user' if userData try @user = JSON.parse userData log '👤 Пользователь загружен:', @user.username catch error log '❌ Ошибка загрузки пользователя:', error @user = null else @user = null # Загрузка корзины loadCartData: -> cartData = localStorage.getItem 'cart' if cartData try @cartItems = JSON.parse cartData log '🛒 Корзина загружена:', @cartItems.length, 'товаров' catch error log '❌ Ошибка загрузки корзины:', error @cartItems = [] else @cartItems = [] # Инициализация приложения initializeApp: -> log '🔧 Начало инициализации приложения' @loading = true # Инициализация темы if @theme == 'dark' document.documentElement.classList.add 'dark' log '🌙 Темная тема активирована' else log '☀️ Светлая тема активирована' # Последовательная инициализация сервисов Promise.resolve() .then => log '📦 Инициализация PouchDB...' PouchDBService.init() .then => log '🌐 Инициализация DomainService...' DomainService.init() .then => log '📡 Получение доступных доменов...' @availableDomains = DomainService.getAvailableDomains() .then => @loadDomainData() .then => @loadUserData() .then => @loadCartData() .then => log '✅ Приложение успешно инициализировано' @showNotification('Приложение готово к работе', 'success') .catch (error) => log '❌ Ошибка инициализации приложения:', error @showNotification('Ошибка загрузки приложения', 'error') .finally => @loading = false async mounted: -> await @initializeApp() # Рендер функция из Pug render: (new Function '_ctx', '_cache', globalThis.renderFns['app/index.pug'])() }) Router = require 'app/router/index.coffee' # Регистрация глобальных компонентов app.component('ui-button', require 'app/components/UI/Button/index.coffee') app.component('notification-container', require 'app/components/UI/Notification/index.coffee') app.component('app-loader', require 'app/components/UI/AppLoader/index.coffee') app.component('multilevel-menu', require 'app/components/Domain/MultilevelMenu/index.coffee') app.component('theme-toggle', require 'app/components/UI/ThemeToggle/index.coffee') app.component('language-toggle', require 'app/components/UI/LanguageToggle/index.coffee') app.component('cart-widget', require 'app/components/Domain/CartWidget/index.coffee') # Подключение роутера app.use Router # Глобальная обработка ошибок Vue app.config.errorHandler = (err, vm, info) -> log '💥 Vue ошибка:', err, info console.error('Vue ошибка:', err, info) # Глобальная обработка предупреждений app.config.warnHandler = (msg, vm, trace) -> log '⚠️ Vue предупреждение:', msg, trace # Монтирование приложения try app.mount('body') log '✅ Приложение успешно смонтировано' catch error log '❌ Ошибка монтирования приложения:', error console.error('Ошибка монтирования:', error) ``` Пример файла компонента, или страницы.Пиши стилевые файлы к элементам **Важно** не нужно делать Vue = require 'vue' или require 'VueRouter' они уже подключены глобально доступны ** app/pages/Home/index.coffee** ``` # app/pages/Home/index.coffee # Добавление стилей страницы if globalThis.stylFns and globalThis.stylFns['app/pages/Home/index.styl'] styleElement = document.createElement('style') styleElement.type = 'text/css' styleElement.textContent = globalThis.stylFns['app/pages/Home/index.styl'] document.head.appendChild(styleElement) else log '⚠️ Стили главной страницы не найдены' module.exports = { # Импорт компонентов components: { 'product-grid': require 'app/components/Domain/ProductGrid/index.coffee' } props: domainSettings: type: Object default: -> {} language: type: String default: 'ru' data: -> { welcomeText: 'Добро пожаловать в Браер-Колор' loading: false productsLoading: false featuredProducts: [] features: [ { title: 'Качественные материалы' description: 'Широкий ассортимент красок, грунтовок и лакокрасочных материалов от проверенных производителей' } { title: 'Доставка по России' description: 'Быстрая и надежная доставка в любой регион страны. Работаем с ведущими транспортными компаниями' } { title: 'Профессиональные консультации' description: 'Наши специалисты помогут подобрать оптимальные материалы для ваших задач и бюджета' } ] } methods: goToCatalog: -> @$router.push '/catalog' contactSupport: -> @$emit 'show-notification', 'Форма обратной связи будет добавлена позже', 'info' viewProduct: (productId) -> @$router.push "/product/#{productId}" loadFeaturedProducts: -> @productsLoading = true # Заглушка для загрузки товаров setTimeout (=> @featuredProducts = [ { id: 1, name: 'Грунтовка глубокого проникновения', price: 528, image: '' }, { id: 2, name: 'Краска акриловая белая', price: 890, image: '' }, { id: 3, name: 'Эмаль для металла', price: 670, image: '' } ] @productsLoading = false ), 1000 mounted: -> log 'Главная страница загружена' @loadFeaturedProducts() render: (new Function '_ctx', '_cache', globalThis.renderFns['app/pages/Home/index.pug'])() } ``` ``` # app/components/UI/Button/index.coffee # Добавление стилей компонента if globalThis.stylFns and globalThis.stylFns['app/components/UI/Button/index.styl'] styleElement = document.createElement('style') styleElement.type = 'text/css' styleElement.textContent = globalThis.stylFns['app/components/UI/Button/index.styl'] document.head.appendChild(styleElement) else log '⚠️ Стили кнопки не найдены' module.exports = name: 'ui-button' props: type: type: String default: 'primary' validator: (value) -> ['primary', 'secondary', 'success', 'danger', 'outline'].includes(value) size: type: String default: 'medium' validator: (value) -> ['small', 'medium', 'large'].includes(value) disabled: type: Boolean default: false loading: type: Boolean default: false methods: handleClick: (event) -> if not @disabled and not @loading @$emit 'click', event render: (new Function '_ctx', '_cache', globalThis.renderFns['app/components/UI/Button/index.pug'])() ``` **app/router/index.coffee** пример, VueRouter определён глобально вызывать отдельно не нужно ```coffee # app/router/index.coffee config = require 'app/config' # Middleware для проверки прав доступа authGuard = (to, from, next) -> log 'Проверка прав доступа для route:', to.path # Здесь будет логика проверки пользователя из глобального состояния if to.matched.some (record) -> record.meta.requiresAuth # Проверка авторизации next() # Временная заглушка - всегда разрешаем доступ else next() domainMiddleware = (to, from, next) -> log 'Обработка динамического домена для route' # Логика обработки домена будет интегрирована позже next() router = VueRouter.createRouter({ history: VueRouter.createWebHistory() routes: [ { path: '/' name: 'Home' component: require 'app/pages/Home/index.coffee' beforeEnter: [domainMiddleware] } { path: '/catalog' name: 'Catalog' component: require 'app/pages/Catalog/index.coffee' beforeEnter: [domainMiddleware] } { path: '/catalog/:category?' name: 'CatalogCategory' component: require 'app/pages/Catalog/index.coffee' beforeEnter: [domainMiddleware] } { path: '/product/:id' name: 'Product' component: require 'app/pages/Product/index.coffee' beforeEnter: [domainMiddleware] } { path: '/cart' name: 'Cart' component: require 'app/pages/Cart/index.coffee' beforeEnter: [domainMiddleware] } { path: '/admin' name: 'Admin' component: require 'app/pages/Admin/index.coffee' meta: { requiresAuth: true } beforeEnter: [domainMiddleware, authGuard] } { path: '/:pathMatch(.*)*' name: 'NotFound' component: require 'app/pages/NotFound/index.coffee' beforeEnter: [domainMiddleware] } ] }) # Глобальные обработчики роутера router.beforeEach (to, from, next) -> log "Переход с "+ from.path +" на "+to.path+"" next() router.afterEach (to, from) -> log "Навигация завершена на "+to.path+"" module.exports = router ## 🚨 КРИТИЧЕСКИЕ ПРАВИЛА РАЗРАБОТКИ ### ❌ **ЗАПРЕЩЕНО:** 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 **Разделитель:** `;` (точка с запятой) **Кодировка:** UTF-8 **Обязательные поля:** `Артикул*`, `Название товара`, `Цена, руб.*` ### 🎯 ПРИМЕР CSV СТРОКИ ```csv №;Артикул*;Название товара;Цена, руб.*;Цена до скидки, руб.;НДС, %*;Рассрочка;Баллы за отзывы;SKU;Штрихкод (Серийный номер / EAN);Вес в упаковке, г*;Ширина упаковки, мм*;Высота упаковки, мм*;Длина упаковки, мм*;Ссылка на главное фото*;Ссылки на дополнительные фото;Ссылки на фото 360;Артикул фото;Бренд*;Название модели (для объединения в одну карточку)*;Единиц в одном товаре;Цвет товара;Название цвета;Тип*;Класс опасности товара*;Степень блеска покрытия;Работы;Вес товара, г;Количество товара в УЕИ;#Хештеги;Аннотация;Rich-контент JSON;Название группы;Образец цвета;Партномер;Гарантия;Страна-изготовитель;Комплектация;ТН ВЭД коды ЕАЭС;Срок годности в днях;Количество заводских упаковок;Вид краски;Объем, л;Время высыхания, часов;Вес, кг;Расход, л/м2;Назначение грунтовки;Рекомендуемое количество слоев;Расход, кг/м2;Область применения состава;Количество компонентов;Особенности ЛКМ;Макс. температура эксплуатации, С°;Материал основания;Основа краски;Основа грунтовки;Способ нанесения;Форма выпуска средства;Назначение;Тип помещения;Возможность колеровки;Вид выпуска товара;Тип растворителя;Эффект краски;Марка эмали;Можно мыть;Базис;Аэрозоль;Помещение;Название модели для шаблона наименования;Ошибка;Предупреждение 1;4673764201943;Грунтовка глубокого проникновения для стен под обои и покраску ЭкоКрас 1кг;528,00;1 056,00;20;Да;Нет;;4673764201943;1000;100;55;250;https://cdn1.ozone.ru/s3/multimedia-1-o/7663357104.jpg;"https://cdn1.ozone.ru/s3/multimedia-1-8/7663357124.jpg https://cdn1.ozone.ru/s3/multimedia-1-w/7663352504.jpg https://cdn1.ozone.ru/s3/multimedia-1-r/7663357179.jpg https://cdn1.ozone.ru/s3/multimedia-1-b/7663352483.jpg https://cdn1.ozone.ru/s3/multimedia-1-c/7663352556.jpg https://cdn1.ozone.ru/s3/multimedia-1-t/7663352537.jpg https://cdn1.ozone.ru/s3/multimedia-1-o/7663352496.jpg https://cdn1.ozone.ru/s3/multimedia-1-k/7663352492.jpg https://cdn1.ozone.ru/s3/multimedia-1-y/7663365970.jpg https://cdn1.ozone.ru/s3/multimedia-1-m/7663365922.jpg https://cdn1.ozone.ru/s3/multimedia-1-1/7663365937.jpg https://cdn1.ozone.ru/s3/multimedia-1-p/7663352533.jpg";;;ЭкоКрас;4673764201943;1;прозрачный;;Грунтовка;Не опасен;;"Внутренние;Наружные";;;#Грунтовка #ГрунтДляСтен #АкриловаяГрунтовка #СтроительныеМатериалы #ГлубокогоПроникновения #УниверсальнаяГрунтовка #АдгезионнаяГрунтовка #АнтисептическаяГрунтовка #ДляВнутреннихРабот #ДляНаружныхРабот #ДляБетона #ДляГипсокартона #ДляДерева #Быстросохнущая #Водостойкая #Укрепляющая #Противогрибковая #БелаяГрунтовка #ПодОбои #ПодШтукатурку #ПодПокраску #ПодПлитку #ДляРовныхСтен #РемонтДома #ОтделкаПомещений #СтроительныеРаботы #КачественныйРемонт #СтроительныеТовары #профессиональная_грунтовка #грунтовка_концентрат_премиум;"Премиум грунтовка глубокого проникновения для подготовки поверхностей перед финишной отделкой...";"{""content"": [{""widgetName"": ""raTextBlock"", ""title"": {""items"": [{""type"": ""text"", ""content"": ""Грунтовка глубокого проникновения PRIMER 1:4 концентрат ImPasto 1кг""}], ""size"": ""size5"", ""color"": ""color1""}, ""theme"": ""primary"", ""padding"": ""type2"", ""gapSize"": ""m"", ""text"": {""size"": ""size2"", ""align"": ""left"", ""color"": ""color1"", ""items"": [{""type"": ""text"", ""content"": ""Премиум грунтовка глубокого проникновения для подготовки поверхностей перед финишной отделкой...""}]}}], ""version"": 0.3}";экокрас;https://cdn1.ozone.ru/s3/multimedia-1-q/7218190898.jpg;;2 года;Россия;Грунтовка глубокого проникновения ЭкоКрас 1кг- 1шт;;990;1;;1;24;1;0,05;"Глубокого проникновения;Обеспыливающая;Пропиточная;Укрепляющая;Универсальная";;;"По бетону;Для фасадов;Для хобби и творчества;Для швов;По кирпичу;По штукатурке;Универсальная";;;;"Бетон;Газобетон;Кирпич;Пенобетон;Штукатурка";;Акриловая;"Валик;Кисть;Краскопульт;Пистолет";Готовый раствор;;"С повышенной влажностью;С умеренной влажностью;Сухое";;;;;;;;;;;; ``` ### 🔧 ОБРАБОТКА НЕОБЯЗАТЕЛЬНЫХ ПОЛЕЙ **ВАЖНО:** Система должна обрабатывать ЛЮБЫЕ необязательные поля, даже с неизвестными именами. Все дополнительные поля сохраняются в атрибуты товара. ```coffee # Пример обработки произвольных полей extractAllAttributes: (csvRow) -> attributes = {} # Обязательные технические поля requiredFields = [ 'Вес в упаковке, г*', 'Ширина упаковки, мм*', 'Высота упаковки, мм*', 'Длина упаковки, мм*', 'Объем, л', 'Время высыхания, часов' ] # Обрабатываем все поля CSV for key, value of csvRow if value and value.toString().trim() != '' # Сохраняем все поля, кроме системных if not key in ['№', 'Артикул*', 'Название товара', 'Цена, руб.*', 'Ссылка на главное фото*', 'Ссылки на дополнительные фото'] attributes[key] = value.toString().trim() log "Извлечено атрибутов: #{Object.keys(attributes).length}" return attributes ``` ## 🎯 ПОДРОБНОЕ ЛОГГИРОВАНИЕ ### 📝 ТРЕБОВАНИЯ К ЛОГГИРОВАНИЮ **Обязательно логировать:** - Начало и завершение всех основных операций - Ошибки с полным стектрейсом - Прогресс длительных операций (импорт, синхронизация) - Пользовательские действия (добавление в корзину, авторизация) - Изменения состояния приложения - Запросы к базе данных **Пример детального логгирования:** ```coffee importFromCSV: (file, domain, onProgress) -> log "🚀 Начало импорта CSV файла: #{file.name}" log "📊 Домен для импорта: #{domain}" log "📁 Размер файла: #{file.size} байт" return new Promise (resolve, reject) => reader = new FileReader() reader.onload = (e) => try log "✅ Файл успешно прочитан, начало парсинга CSV" results = Papa.parse(e.target.result, { header: true delimiter: ';' skipEmptyLines: true encoding: 'UTF-8' }) log "📈 CSV распарсен: #{results.data.length} строк найдено" validProducts = results.data.filter (row) => isValid = row and row['Артикул*'] and row['Название товара'] and row['Цена, руб.*'] if not isValid log "⚠️ Пропущена строка из-за отсутствия обязательных полей: #{JSON.stringify(row)}" return isValid log "✅ Валидных товаров для импорта: #{validProducts.length}" this.processProductsInBatches(validProducts, domain, onProgress) .then (results) => log "🎉 Импорт успешно завершен: #{results.length} товаров обработано" resolve(results) .catch (error) => log "❌ Ошибка в процессе импорта: #{error.message}" reject(error) catch error log "💥 Критическая ошибка парсинга CSV: #{error.message}" log "🔍 Stack trace:", error.stack reject(new Error("Ошибка парсинга CSV: #{error.message}")) reader.onerror = (error) -> log "❌ Ошибка чтения файла: #{error}" reject(new Error('Ошибка чтения файла')) reader.readAsText(file, 'UTF-8') ``` ## 🚨 КРИТИЧЕСКИЕ ПРАВИЛА РАЗРАБОТКИ ### ❌ **ЗАПРЕЩЕНО:** 1. Использование rgba() функций в стилях 2. Прямое использование console.log (только глобальный log) 3. Многострочные атрибуты в Pug 4. @import в стилях компонентов 5. JavaScript операторы в CoffeeScript 6. Объявление debug в компонентах ### ✅ **ОБЯЗАТЕЛЬНО:** 1. Полные листинги файлов компонентов 2. Адаптивная верстка (mobile-first) 3. BEM методология именования классов 4. CSS переменные для цветов и прозрачностей 5. Подробное логгирование всех операций 6. Обработка всех необязательных полей CSV ## 📅 ПОЭТАПНЫЙ ПЛАН РАЗРАБОТКИ ### 🎯 ЭТАП 1: БАЗОВАЯ АРХИТЕКТУРА ✅ - [ ] Настройка сборки и конфигурации - [ ] Базовая структура Vue приложения - [ ] PouchDB сервис и дизайн-документы - [ ] Главный layout и система стилей - [ ] Система типов и интерфейсов ### 🎯 ЭТАП 2: АДМИН-ПАНЕЛЬ ⚠️ В РАБОТЕ - [ ] Layout админ-панели - [ ] Компонент управления товарами - [ ] **ТЕКУЩАЯ ЗАДАЧА: Система импорта товаров** - [ ] Редактор категорий с загрузкой изображений - [ ] Управление слайдами главной страницы - [ ] Управление статьями блога - [ ] Настройки доменов и мультиязычности ### 🎯 ЭТАП 3: КЛИЕНТСКАЯ ЧАСТЬ - [ ] Главная страница с адаптивным дизайном - [ ] Каталог товаров с фильтрацией - [ ] Страница товара с галереей - [ ] Корзина и оформление заказа - [ ] Блог и система статей ### 🎯 ЭТАП 4: ДОПОЛНИТЕЛЬНЫЕ ФУНКЦИИ - [ ] Поиск по товарам - [ ] Система отзывов и рейтингов - [ ] Личный кабинет пользователя - [ ] Система скидок и промокодов - [ ] Интеграция с платежными системами ### 🎯 ЭТАП 5: ОПТИМИЗАЦИЯ И ТЕСТИРОВАНИЕ - [ ] PWA функциональность (офлайн-режим) - [ ] Оптимизация производительности - [ ] Тестирование на разных устройствах - [ ] Деплой и настройка продакшн-окружения ## 📊 ТЕКУЩОЕ СОСТОЯНИЕ ПРОЕКТА ### ✅ ВЫПОЛНЕНО 1. **Базовая архитектура** - Напиши базовые файлы системы - Система роутинга - Глобальная конфигурация Добавлено подключение компонентов во все объекты где они используются: Главное приложение подключает все UI и Domain компоненты Страницы подключают используемые компоненты через components Роутер подключает все страницы приложения Создана полная структура компонентов: UI компоненты: Button, Notification, AppLoader, ThemeToggle, LanguageToggle Domain компоненты: MultilevelMenu, CartWidget, ProductGrid, ProductCard Страницы: Home, Catalog, Product, Cart, Admin, NotFound Настроена правильная иерархия зависимостей: Каждый компонент самостоятельно подключает свои стили Родительские компоненты регистрируют дочерние через components Главное приложение регистрирует глобальные компоненты Созданы все необходимые заглушки для полнофункционального приложения Полноценный PouchDB сервис: Подключение к CouchDB с системой аутентификации Двусторонняя синхронизация с фильтрацией по доменам Обработка ошибок и автоматические повторы Локальное кэширование и офлайн-работа Дизайн-документы CouchDB: Views для товаров, категорий, заказов с индексацией Validate_doc_update функции для валидации данных Система поисковых индексов для быстрого поиска Сервисы данных: ProductService: работа с товарами, поиск, фильтрация CategoryService: управление категориями, иерархия DomainService: мультидоменность, настройки доменов Интеграция в приложение: Правильная инициализация всех сервисов Обработка состояний загрузки и ошибок Подробное логгирование всех операций Layout администратора с навигацией: Боковая панель с навигационным меню Хедер с хлебными крошками и информацией пользователя Адаптивный дизайн для мобильных устройств Компонент DataTable: Сортировка по колонкам Поиск и фильтрация данных Пагинация Выбор элементов Слоты для кастомных действий Компонент загрузки файлов: Drag & drop интерфейс Валидация типов и размера файлов Индикатор прогресса загрузки Поддержка множественного выбора ### 🎯 БЛИЖАЙШИЕ ЗАДАЧИ 2. **Работа с данными** - PouchDB сервис с синхронизацией - Дизайн-документы для CouchDB - Система типов данных 3. **Базовые компоненты** - Главный layout приложения - Layout админ-панели - Система уведомлений 4. **Стили и дизайн** - CSS переменные и дизайн-система - Адаптивная верстка - Темная/светлая темы 5. **Система импорта товаров** - Компонент загрузки CSV файлов - Парсинг и валидация данных - Пакетная обработка товаров - Создание категорий на лету 5.1. Завершить компонент импорта товаров 5.2. Реализовать редактор категорий 5.3. Создать компонент управления слайдами 5.4. Разработать главную страницу магазина 6. **Админ-панель** - Управление товарами - Система прогресса операций - Обработка ошибок импорта ### 🔄 В РАБОТЕ СЕЙЧАС отвечай на русском Анализировать реализованный код, по git репозитарию https://gogs.osvoj.ru/oleg/s5l.ru-crm Проверяй промт и изменения в нём по адресу https://gogs.osvoj.ru/oleg/s5l.ru-crm/raw/master/README.md после выполнения задачи, напиши - что сделано. - Напиши следующую задачу на выполнение (после завершения отладки, созданных файлов) напиши файлы реализующие следующую задачу. ⚠️ ПРИОРИТЕТ ЭТАП 1.6: РЕАЛИЗАЦИЯ СИСТЕМЫ ИМПОРТА И МЕДИА-МЕНЕДЖЕРА Страница импорта товаров: Интеграция компонента FileUpload Парсинг CSV файлов с валидацией Преобразование данных в структуру PouchDB Пакетная обработка и прогресс-бар Редактор категорий: Древовидная структура категорий Загрузка изображений для категорий Drag & drop для изменения иерархии Редактирование метаданных Медиа-менеджер: Загрузка изображений товаров Прикрепление файлов к документам CouchDB Система кэширования и оптимизации Управление версиями файлов Приоритет: Критический 🚨 (необходимо для наполнения магазина товарами и контентом) Приоритет: Высокий ⚠️ (необходимо для наполнения магазина товарами)