|
|
4 nedēļas atpakaļ | |
|---|---|---|
| app | 4 nedēļas atpakaļ | |
| README.md | 4 nedēļas atpakaļ |
Название: Интернет-магазин лакокрасочной продукции "Браер-Колор" с админ панелью, мудьтидоменной и мультиязычной структурой Тип: SPA (Single Page Application) с современным минималистичным дизайном Аналоги: Функциональность m-kraski.ru с дизайном https://braer-color.ru/
app/
├── index.pug (основной layout)
├── index.coffee (инициализация Vue и роутера)
├── index.styl (файл хранения описания классов по системе BEM )
├── utils/
│ └── pouch.coffee (сервис работы с PouchDB)
├── design/
│ ├── admin.coffee (design документы админки)
│ └── site.coffee (design документы сайта)
└── pages/
├── Admin/ (админ-панель)
│ ├── Layout/ (общий layout админки)
│ ├── Slider/ (управление слайдами)
│ ├── Products/ (управление товарами)
│ ├── Clients/ (управление клиентами)
│ ├── Blog/ (управление блогом с Markdown)
│ ├── Routes/ (управление маршрутами)
│ └── Settings/ (настройки системы)
└── [пользовательские страницы]
Организация стилей (.styl файлы):
использовать @import '../../index.styl' не нужно, приложение одностраничное.
Основной layout (app/index.pug):
div(class="")
header(class="header")
nav(class="header-nav")
div(class="header-nav-blok")
div(class="header-nav--name") {{ kompname }}
div(class="header-nav--menu")
multilevelmenu
themetoggle
main
router-view(v-slot='{ Component }')
transition(name='page-slide' mode='out-in')
component(:is='Component')
Важно: не используй многострочные вычисляемые атрибуты, приводит к ошибке. Важно: мета данные добавляются через app/index.coffe базовым тегоьм для vuejs является body, app/index.pug начинается с div, теги html, head, body ЗАПРЕЩЕНО использовать. Важно: все тексты в клиенской части беруться их документа настройки домена. в которых должна быть предусмотрена мультиязычность, документы могут иметь произвольную структуру, создаваемую в админке.
Инициализация приложения (app/index.coffee):
# Инициализация глобальных переменных
globalThis.renderFns = require 'pug.json'
globalThis.stylFns = require 'styl.json'
PouchDBService = require 'app/utils/pouch.coffee'
# Инициализация сервиса данных
pouchService = PouchDBService
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>')
document.head.insertAdjacentHTML('beforeend','<title></title>')
# Создание главного приложения Vue
app = Vue.createApp({
data: ->
return {
theme: localStorage.getItem('theme') || 'light'
companyName: 'Браер-Колор'
loading: false
cartItems: []
user: null
currentDomain: window.location.hostname
languages: ['ru', 'en']
currentLanguage: 'ru'
currentDomainSettings: null
}
render: (new Function '_ctx', '_cache', renderFns['app/index.pug'])()
computed:
isAdmin: -> @user?.role == 'admin'
methods:
toggleTheme: ->
@theme = if @theme == 'light' then 'dark' else 'light'
localStorage.setItem 'theme', @theme
document.documentElement.classList.toggle 'dark'
setLanguage: (lang) ->
if @languages.includes lang
@currentLanguage = lang
localStorage.setItem 'language', @lang
loadDomainSettings: ->
pouchService.getDocument("domain_settings:#{@currentDomain}")
.then (settings) =>
@currentDomainSettings = settings
# Устанавливаем заголовок страницы
if settings?.companyName
document.title = settings.companyName
.catch (error) =>
console.log 'Настройки домена не найдены, используются значения по умолчанию'
@currentDomainSettings = null
showNotification: (message, type = 'success') ->
# Реализация системы уведомлений
console.log "#{type.toUpperCase()}: #{message}"
mounted: ->
# Инициализация темы
if @theme == 'dark'
document.documentElement.classList.add 'dark'
# Инициализация PouchDB
try
await pouchService.init()
console.log 'PouchDB инициализирован'
# Загружаем настройки домена
await @loadDomainSettings()
catch error
console.error 'Ошибка инициализации PouchDB:', error
# Загрузка пользователя (если авторизован)
@loadUserData()
loadUserData: ->
# Загрузка данных пользователя из localStorage или PouchDB
userData = localStorage.getItem 'user'
if userData
try
@user = JSON.parse userData
catch
@user = null
})
PouchDBService (app/utils/pouch.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 || '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
console.error('Ошибка инициализации 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'
})
Выжно: при реализации кода, проверяй наличие баз данных, если их нет то создай, также проверяй наличие _desing документов, если их нет создай, если нет документа настройки домена тоже создай его.
Учти возможность многодоменной разработки, и ограничения доступа в зависимости от используемого домена на клиенской части, и в админке, в зависимости от прав администратора.
Layout админки (app/pages/Admin/index.pug):
div(class="mainadmin-block")
.admin-sidebar
.p-4
h1(class="mainadmin-block--h1") Панель администратора
nav(class="mainadmin-block--nav")
a(
v-for="item in menuItems"
:key="item.id"
:href="item.path"
@click.prevent="navigateTo(item.path)"
:class="getMenuItemClass(item.id)"
)
.flex.items-center.space-x-3
component(:is="item.icon" class="w-5 h-5")
span {{ item.name }}
.admin-content
router-view
Пример компонента админки (app/pages/Admin/Slider/index.coffee):
document.head.insertAdjacentHTML('beforeend','<style type="text/tailwindcss">'+stylFns['app/pages/Admin/Slider/index.styl']+'</style>')
PouchDB = require 'app/utils/pouch'
module.exports =
name: 'AdminSlider'
render: (new Function '_ctx', '_cache', renderFns['app/pages/Admin/Slider/index.pug'])()
data: ->
slides: []
showSlideModal: false
currentSlide: {
title: ''
subtitle: ''
image: ''
buttonText: 'В каталог'
buttonLink: '/catalog'
active: true
order: 0
}
mounted: ->
@loadSlides()
methods:
loadSlides: ->
PouchDB.queryView('admin', 'slides', { descending: false })
.then (result) =>
@slides = result.rows.map (row) -> row.doc
.catch (error) =>
console.error 'Ошибка загрузки слайдов:', error
@showNotification 'Ошибка загрузки слайдов', 'error'
saveSlide: ->
slideData = {
type: 'hero_slide'
...@currentSlide
updatedAt: new Date().toISOString()
}
if !slideData._id
slideData._id = "hero_slide:#{Date.now()}"
slideData.createdAt = new Date().toISOString()
slideData.order = @slides.length
PouchDB.saveToRemote(slideData)
.then (result) =>
@loadSlides() # Перезагружаем через view
@showSlideModal = false
@resetCurrentSlide()
@showNotification 'Слайд успешно сохранен'
.catch (error) =>
console.error 'Ошибка сохранения слайда:', error
@showNotification 'Ошибка сохранения слайда', 'error'
admin.coffee:
module.exports = {
_id: '_design/admin',
version: '1.1.0',
appVersion: '1.0.0',
hash: 'generated_hash_here',
views: {
slides: {
map: ((doc) ->
if doc.type == 'hero_slide'
emit(doc.order, {
_id: doc._id,
title: doc.title,
active: doc.active,
order: doc.order
})
).toString()
},
products: {
map: ((doc) ->
if doc.type == 'product'
emit([doc.category, doc.createdAt], {
_id: doc._id,
name: doc.name,
price: doc.price,
active: doc.active
})
).toString()
}
}
}
Метод импорта (app/pages/Admin/Products/index.coffee):
importProducts: ->
if !@selectedFile
@showNotification 'Выберите файл для импорта', 'error'
return
@importing = true
@importResults = null
@readFile(@selectedFile)
.then (text) =>
results = Papa.parse(text, {
header: true
delimiter: ';'
skipEmptyLines: true
encoding: 'UTF-8'
})
products = results.data.filter (row) =>
row && row['Артикул*'] && row['Название товара'] && row['Цена, руб.*']
couchProducts = products.map (product, index) =>
@transformProductData(product, index)
# Пакетное сохранение в удаленную БД
return PouchDB.bulkDocs(couchProducts)
.then (result) =>
@importResults = { success: true, processed: couchProducts.length }
@showNotification "Импортировано #{couchProducts.length} товаров"
@loadProducts() # Перезагружаем список
.catch (error) =>
console.error 'Ошибка импорта:', error
@importResults = { success: false, error: error.message }
@showNotification "Ошибка импорта: #{error.message}", 'error'
.finally =>
@importing = false
Важно: при полной реализации используй пример 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;прозрачный;;Грунтовка;Не опасен;;"Внутренние;Наружные";;;#Грунтовка #ГрунтДляСтен #АкриловаяГрунтовка #СтроительныеМатериалы #ГлубокогоПроникновения #УниверсальнаяГрунтовка #АдгезионнаяГрунтовка #АнтисептическаяГрунтовка #ДляВнутреннихРабот #ДляНаружныхРабот #ДляБетона #ДляГипсокартона #ДляДерева #Быстросохнущая #Водостойкая #Укрепляющая #Противогрибковая #БелаяГрунтовка #ПодОбои #ПодШтукатурку #ПодПокраску #ПодПлитку #ДляРовныхСтен #РемонтДома #ОтделкаПомещений #СтроительныеРаботы #КачественныйРемонт #СтроительныеТовары #профессиональная_грунтовка #грунтовка_концентрат_премиум;"Премиум грунтовка глубокого проникновения для подготовки поверхностей перед финишной отделкой. Грунтовка глубокого проникновения – это универсальное средство, она проникает глубоко в структуру бетона, кирпича, штукатурки, дерева и других материалов, укрепляя их и улучшая адгезию для последующего нанесения краски, обоев или плитки.
Грунтовка для стен применяется для укрепления штукатурки и шпатлевок, препятствует появлению высолов создавая щелочестойкий слой. Идеально подходит для стен, потолков и полов в любых помещениях – от ванной и кухни до жилых комнат. Благодаря жидкой консистенции и акриловой основе, грунтовка быстро впитывается, связывая пыль и предотвращая осыпание старых покрытий.
Средство подходит для наружных работ, так как устойчива к перепадам температур и влажности. Может использоваться под наливные полы, фактурную штукатурку и кафель. Белый или прозрачный состав не оставляет разводов и не влияет на цвет финишного покрытия.
Высокое содержание полимеров позволяет получить слабо впитывающие поверхности, даже в случае осыпающихся, осмеливающихся поверхностей (известковые покрытия, песчанно-цементные штукатурки слабого качества, давно окрашенные водоэмульсионными красками поверхности). Расход 0,05-0,15 кг на 1 м 2 в один раз. Наносить в один или два слоя в зависимости от состояния подложки. Второй слой наносить непосредственно за первым для более глубокого проникновения или же после полного высыхания первого слоя. Быстро высыхает, не имеет резкого запаха и безопасна для внутренних работ.
Применяется перед покраской, поклейкой обоев или укладкой плитки, обеспечивая долговечность и качество ремонта. Незаменима для обработки бетонных, известковых и гипсовых оснований, а также при работе с волма слоем.
Используйте грунтовку глубокого проникновения – и ваши отделочные работы будут выполнены на профессиональном уровне! ";"{ ""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"": ""Премиум грунтовка глубокого проникновения для подготовки поверхностей перед финишной отделкой. Грунтовка глубокого проникновения – это универсальное средство, она проникает глубоко в структуру бетона, кирпича, штукатурки, дерева и других материалов, укрепляя их и улучшая адгезию для последующего нанесения краски, обоев или плитки.\r""
},
{
""type"": ""br""
},
{
""type"": ""text"",
""content"": ""\r""
},
{
""type"": ""br""
},
{
""type"": ""text"",
""content"": ""Грунтовка для стен применяется для укрепления штукатурки и шпатлевок, препятствует появлению высолов создавая щелочестойкий слой. Идеально подходит для стен, потолков и полов в любых помещениях – от ванной и кухни до жилых комнат. Благодаря жидкой консистенции и акриловой основе, грунтовка быстро впитывается, связывая пыль и предотвращая осыпание старых покрытий. \r""
},
{
""type"": ""br""
},
{
""type"": ""text"",
""content"": ""\r""
},
{
""type"": ""br""
},
{
""type"": ""text"",
""content"": ""Средство подходит для наружных работ, так как устойчива к перепадам температур и влажности. Может использоваться под наливные полы, фактурную штукатурку и кафель. Белый или прозрачный состав не оставляет разводов и не влияет на цвет финишного покрытия.\r""
},
{
""type"": ""br""
},
{
""type"": ""text"",
""content"": ""\r""
},
{
""type"": ""br""
},
{
""type"": ""text"",
""content"": ""Высокое содержание полимеров позволяет получить слабо впитывающие поверхности, даже в случае осыпающихся, осмеливающихся поверхностей (известковые покрытия, песчанно-цементные штукатурки слабого качества, давно окрашенные водоэмульсионными красками поверхности). Расход 0,05-0,15 кг на 1 м 2 в один раз. Наносить в один или два слоя в зависимости от состояния подложки. Второй слой наносить непосредственно за первым для более глубокого проникновения или же после полного высыхания первого слоя. Быстро высыхает, не имеет резкого запаха и безопасна для внутренних работ.""
}
]
}
}
], ""version"": 0.3 }";экокрас;https://cdn1.ozone.ru/s3/multimedia-1-q/7218190898.jpg;;2 года;Россия;Грунтовка глубокого проникновения ЭкоКрас 1кг- 1шт;;990;1;;1;24;1;0,05;"Глубокого проникновения;Обеспыливающая;Пропиточная;Укрепляющая;Универсальная";;;"По бетону;Для фасадов;Для хобби и творчества;Для швов;По кирпичу;По штукатурке;Универсальная";;;;"Бетон;Газобетон;Кирпич;Пенобетон;Штукатурка";;Акриловая;"Валик;Кисть;Краскопульт;Пистолет";Готовый раствор;;"С повышенной влажностью;С умеренной влажностью;Сухое";;;;;;;;;;;;
При обработке csv загружай изображения в базу данных как attachment, также сохраняй ричконтент преобразовывая в markdown, и выводи его в качестве описания товара при его наличии. формат ссылок на attachment файлы "/d/[ИМЯ БД]/[id doc]/[имя файла]" Важно: каждый товар должен быть привязан к домену/доменам (может одновременно быть доступен на разных доменах), тоже касается статей блога.
index.pug, index.coffee, index.styl(new Function '_ctx', '_cache', renderFns[...])()theme.config.coffeeАнализировать реализованный код, по git репозитарию https://gogs.osvoj.ru/oleg/s5l.ru-crm.git Проверяй промт и изменения в нём по адресу https://gogs.osvoj.ru/oleg/s5l.ru-crm/raw/master/README.md Важно Всегда приводи только полные листинги файлов в coffee файлах используй debug.log вместо console.log в стил файлах не используй @import '../../index.styl', только стили текущего элемента, общие переменные определяй только в app/index.styl
исправь ошибку в файле app/pages/Admin/Products/index.coffee:
reason: Error: Parsing file /app/pages/Admin/Products/index.coffee: Unexpected token (505:45)
at Deps.parseDeps (/mnt/shared/oleg/works/siteScript/node_modules/module-deps/index.js:519:15)
at getDeps (/mnt/shared/oleg/works/siteScript/node_modules/module-deps/index.js:447:44)
at /mnt/shared/oleg/works/siteScript/node_modules/module-deps/index.js:430:38
at ConcatStream.<anonymous> (/mnt/shared/oleg/works/siteScript/node_modules/concat-stream/index.js:37:43)
at ConcatStream.emit (node:events:519:35)
at finishMaybe (/mnt/shared/oleg/works/siteScript/node_modules/concat-stream/node_modules/readable-stream/lib/_stream_writable.js:630:14)
at endWritable (/mnt/shared/oleg/works/siteScript/node_modules/concat-stream/node_modules/readable-stream/lib/_stream_writable.js:638:3)
at Writable.end (/mnt/shared/oleg/works/siteScript/node_modules/concat-stream/node_modules/readable-stream/lib/_stream_writable.js:594:22)
at DuplexWrapper.onend (/mnt/shared/oleg/works/siteScript/node_modules/duplexer2/node_modules/readable-stream/lib/_stream_readable.js:577:10)
at Object.onceWrapper (node:events:621:28)
at DuplexWrapper.emit (node:events:519:35)
at endReadableNT (/mnt/shared/oleg/works/siteScript/node_modules/duplexer2/node_modules/readable-stream/lib/_stream_readable.js:1010:12)
at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {