|
|
vor 3 Wochen | |
|---|---|---|
| app | vor 3 Wochen | |
| .gitignore | vor 3 Wochen | |
| README.md | vor 3 Wochen |
ВАЖНО: 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
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 здесь храняться базовые стили темы. которые используются во всех остальных стилевых файлах. отдельно его подключать в них не нужно, он глобально доступен.
// 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
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
# Глобальная инициализация 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', '<meta charset="UTF-8">'
document.head.insertAdjacentHTML 'beforeend', '<meta name="viewport" content="width=device-width, initial-scale=1.0">'
document.head.insertAdjacentHTML 'beforeend', '<title>Браер-Колор - Интернет-магазин лакокрасочной продукции</title>'
# Добавление глобальных стилей
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 определён глобально вызывать отдельно не нужно
# 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')
```
Главное приложение подключает все 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 интерфейс
Валидация типов и размера файлов
Индикатор прогресса загрузки
Поддержка множественного выбора
Работа с данными
Базовые компоненты
Стили и дизайн
Система импорта товаров
5.1. Завершить компонент импорта товаров 5.2. Реализовать редактор категорий 5.3. Создать компонент управления слайдами 5.4. Разработать главную страницу магазина
Админ-панель
отвечай на русском Анализировать реализованный код, по 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
Система кэширования и оптимизации
Управление версиями файлов
Приоритет: Критический 🚨 (необходимо для наполнения магазина товарами и контентом)
Приоритет: Высокий ⚠️ (необходимо для наполнения магазина товарами)