index.pug 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. div(class="admin-products")
  2. div(class="admin-products__header")
  3. h1(class="admin-products__title") Управление товарами
  4. div(class="admin-products__actions")
  5. button(
  6. @click="createProduct"
  7. class="admin-products__button admin-products__button--primary"
  8. ) Добавить товар
  9. button(
  10. @click="showImportModal = true"
  11. class="admin-products__button admin-products__button--secondary"
  12. ) Импорт из CSV
  13. button(
  14. @click="showCategoriesManager"
  15. class="admin-products__button admin-products__button--secondary"
  16. ) Управление категориями
  17. button(
  18. v-if="selectedProducts.length > 0"
  19. @click="showMassActionsModal = true"
  20. class="admin-products__button admin-products__button--warning"
  21. ) Массовые действия ({{ selectedProducts.length }})
  22. div(class="admin-products__filters")
  23. div(class="admin-products__search")
  24. input(
  25. type="text"
  26. v-model="searchQuery"
  27. placeholder="Поиск по названию или артикулу..."
  28. class="admin-products__search-input"
  29. )
  30. div(class="admin-products__filter-group")
  31. select(v-model="selectedCategory" class="admin-products__select")
  32. option(value="") Все категории
  33. option(
  34. v-for="category in categories"
  35. :value="category._id"
  36. ) {{ category.name }} ({{ getCategoryProductCount(category._id) }})
  37. select(v-model="selectedStatus" class="admin-products__select")
  38. option(value="") Все статусы
  39. option(value="active") Активные
  40. option(value="inactive") Неактивные
  41. div(class="admin-products__content")
  42. div(class="admin-products__table-container")
  43. table(class="admin-products__table")
  44. thead
  45. tr
  46. th(class="admin-products__th admin-products__th--checkbox")
  47. input(
  48. type="checkbox"
  49. v-model="selectAll"
  50. @change="toggleSelectAll"
  51. class="admin-products__checkbox"
  52. )
  53. th(class="admin-products__th") Артикул
  54. th(class="admin-products__th") Название
  55. th(class="admin-products__th") Категория
  56. th(class="admin-products__th") Цена
  57. th(class="admin-products__th") Статус
  58. th(class="admin-products__th") Действия
  59. tbody
  60. tr(
  61. v-for="product in filteredProducts"
  62. :key="product._id"
  63. :class="['admin-products__tr', { 'admin-products__tr--inactive': !product.active }]"
  64. )
  65. td(class="admin-products__td admin-products__td--checkbox")
  66. input(
  67. type="checkbox"
  68. :checked="isProductSelected(product._id)"
  69. @change="toggleProductSelection(product._id)"
  70. class="admin-products__checkbox"
  71. )
  72. td(class="admin-products__td admin-products__td--sku") {{ product.sku }}
  73. td(class="admin-products__td admin-products__td--name") {{ product.name }}
  74. td(class="admin-products__td") {{ getCategoryName(product.category) }}
  75. td(class="admin-products__td admin-products__td--price")
  76. span(v-if="product.oldPrice" class="admin-products__old-price") {{ product.oldPrice }} ₽
  77. span(class="admin-products__current-price") {{ product.price }} ₽
  78. td(class="admin-products__td")
  79. span(
  80. :class="['admin-products__status', product.active ? 'admin-products__status--active' : 'admin-products__status--inactive']"
  81. ) {{ product.active ? 'Активен' : 'Неактивен' }}
  82. td(class="admin-products__td admin-products__td--actions")
  83. button(
  84. @click="editProduct(product)"
  85. class="admin-products__action-button admin-products__action-button--edit"
  86. ) Редактировать
  87. button(
  88. @click="toggleProductStatus(product)"
  89. :class="['admin-products__action-button', product.active ? 'admin-products__action-button--deactivate' : 'admin-products__action-button--activate']"
  90. ) {{ product.active ? 'Деактивировать' : 'Активировать' }}
  91. div(v-if="filteredProducts.length === 0" class="admin-products__empty")
  92. p(class="admin-products__empty-text") Товары не найдены
  93. //- Модальное окно редактирования/создания товара
  94. div(
  95. v-if="showProductModal"
  96. class="admin-products__modal"
  97. )
  98. div(class="admin-products__modal-content admin-products__modal-content--large")
  99. div(class="admin-products__modal-header")
  100. h2(class="admin-products__modal-title") {{ isEditing ? 'Редактирование товара' : 'Создание товара' }}
  101. button(
  102. @click="showProductModal = false"
  103. class="admin-products__modal-close"
  104. ) ×
  105. div(class="admin-products__modal-body")
  106. div(class="admin-products__form")
  107. div(class="admin-products__form-row")
  108. div(class="admin-products__form-group")
  109. label(class="admin-products__label") Название товара *
  110. input(
  111. type="text"
  112. v-model="productForm.name"
  113. class="admin-products__input"
  114. placeholder="Введите название товара"
  115. )
  116. div(class="admin-products__form-group")
  117. label(class="admin-products__label") Артикул *
  118. input(
  119. type="text"
  120. v-model="productForm.sku"
  121. class="admin-products__input"
  122. placeholder="Введите артикул"
  123. )
  124. div(class="admin-products__form-row")
  125. div(class="admin-products__form-group")
  126. label(class="admin-products__label") Категория
  127. select(v-model="productForm.category" class="admin-products__select")
  128. option(value="") Без категории
  129. option(
  130. v-for="category in categories"
  131. :value="category._id"
  132. ) {{ category.name }}
  133. div(class="admin-products__form-group")
  134. label(class="admin-products__label") Бренд
  135. input(
  136. type="text"
  137. v-model="productForm.brand"
  138. class="admin-products__input"
  139. placeholder="Введите бренд"
  140. )
  141. div(class="admin-products__form-row")
  142. div(class="admin-products__form-group")
  143. label(class="admin-products__label") Цена *
  144. input(
  145. type="number"
  146. v-model="productForm.price"
  147. class="admin-products__input"
  148. placeholder="0.00"
  149. step="0.01"
  150. min="0"
  151. )
  152. div(class="admin-products__form-group")
  153. label(class="admin-products__label") Старая цена
  154. input(
  155. type="number"
  156. v-model="productForm.oldPrice"
  157. class="admin-products__input"
  158. placeholder="0.00"
  159. step="0.01"
  160. min="0"
  161. )
  162. div(class="admin-products__form-group")
  163. label(class="admin-products__label") Описание
  164. textarea(
  165. v-model="productForm.description"
  166. class="admin-products__textarea"
  167. placeholder="Введите описание товара"
  168. rows="4"
  169. )
  170. //- Управление атрибутами
  171. div(class="admin-products__form-group")
  172. label(class="admin-products__label") Атрибуты
  173. div(class="admin-products__attributes")
  174. div(
  175. v-for="attr in attributesList"
  176. :key="attr.key"
  177. class="admin-products__attribute"
  178. )
  179. div(class="admin-products__attribute-key") {{ attr.key }}
  180. input(
  181. type="text"
  182. :value="attr.value"
  183. @input="updateAttribute(attr.key, $event.target.value)"
  184. class="admin-products__input admin-products__input--small"
  185. placeholder="Значение"
  186. )
  187. button(
  188. @click="removeAttribute(attr.key)"
  189. class="admin-products__attribute-remove"
  190. ) ×
  191. div(class="admin-products__attribute-add")
  192. input(
  193. type="text"
  194. v-model="newAttributeKey"
  195. class="admin-products__input admin-products__input--small"
  196. placeholder="Название атрибута"
  197. @keyup.enter="addAttribute"
  198. )
  199. input(
  200. type="text"
  201. v-model="newAttributeValue"
  202. class="admin-products__input admin-products__input--small"
  203. placeholder="Значение"
  204. @keyup.enter="addAttribute"
  205. )
  206. button(
  207. @click="addAttribute"
  208. class="admin-products__button admin-products__button--secondary"
  209. ) Добавить
  210. //- Управление тегами
  211. div(class="admin-products__form-group")
  212. label(class="admin-products__label") Теги
  213. div(class="admin-products__tags")
  214. span(
  215. v-for="(tag, index) in productForm.tags"
  216. :key="index"
  217. class="admin-products__tag"
  218. )
  219. | {{ tag }}
  220. button(
  221. @click="removeTag(index)"
  222. class="admin-products__tag-remove"
  223. ) ×
  224. div(class="admin-products__tag-add")
  225. input(
  226. type="text"
  227. v-model="newTag"
  228. class="admin-products__input admin-products__input--small"
  229. placeholder="Новый тег"
  230. @keyup.enter="addTag"
  231. )
  232. button(
  233. @click="addTag"
  234. class="admin-products__button admin-products__button--secondary"
  235. ) Добавить
  236. //- Загрузка основного изображения
  237. div(class="admin-products__form-group")
  238. label(class="admin-products__label") Основное изображение
  239. div(class="admin-products__image-upload")
  240. div(v-if="productForm.image" class="admin-products__image-preview")
  241. img(:src="productForm.image" class="admin-products__preview-img")
  242. div(class="admin-products__image-info") Основное изображение
  243. div(class="admin-products__upload-controls")
  244. input(
  245. type="text"
  246. v-model="newImageUrl"
  247. class="admin-products__input"
  248. placeholder="Введите URL изображения"
  249. )
  250. button(
  251. @click="uploadMainImage"
  252. :disabled="uploadingImages || !newImageUrl"
  253. class="admin-products__button admin-products__button--secondary"
  254. ) {{ uploadingImages ? 'Загрузка...' : 'Загрузить' }}
  255. //- Загрузка дополнительных изображений
  256. div(class="admin-products__form-group")
  257. label(class="admin-products__label") Дополнительные изображения
  258. div(class="admin-products__additional-images")
  259. div(
  260. v-for="(image, index) in productForm.additionalImages"
  261. :key="index"
  262. class="admin-products__additional-image"
  263. )
  264. img(:src="image" class="admin-products__preview-img")
  265. button(
  266. @click="removeAdditionalImage(index)"
  267. class="admin-products__image-remove"
  268. ) ×
  269. div(class="admin-products__upload-controls")
  270. input(
  271. type="text"
  272. v-model="newAdditionalImageUrl"
  273. class="admin-products__input"
  274. placeholder="Введите URL дополнительного изображения"
  275. )
  276. button(
  277. @click="uploadAdditionalImage"
  278. :disabled="uploadingImages || !newAdditionalImageUrl"
  279. class="admin-products__button admin-products__button--secondary"
  280. ) {{ uploadingImages ? 'Загрузка...' : 'Добавить' }}
  281. div(class="admin-products__form-row")
  282. div(class="admin-products__form-group")
  283. label(class="admin-products__label admin-products__label--checkbox")
  284. input(
  285. type="checkbox"
  286. v-model="productForm.active"
  287. class="admin-products__checkbox"
  288. )
  289. span Активный товар
  290. div(class="admin-products__modal-footer")
  291. button(
  292. @click="saveProduct"
  293. :disabled="!productForm.name || !productForm.sku || !productForm.price"
  294. class="admin-products__button admin-products__button--primary"
  295. ) {{ isEditing ? 'Обновить' : 'Создать' }}
  296. button(
  297. @click="showProductModal = false"
  298. class="admin-products__button admin-products__button--secondary"
  299. ) Отмена
  300. //- Модальное окно управления категориями
  301. div(
  302. v-if="showCategoriesModal"
  303. class="admin-products__modal"
  304. )
  305. div(class="admin-products__modal-content admin-products__modal-content--large")
  306. div(class="admin-products__modal-header")
  307. h2(class="admin-products__modal-title") Управление категориями
  308. button(
  309. @click="showCategoriesModal = false"
  310. class="admin-products__modal-close"
  311. ) ×
  312. div(class="admin-products__modal-body")
  313. div(class="admin-products__categories-header")
  314. button(
  315. @click="createCategory"
  316. class="admin-products__button admin-products__button--primary"
  317. ) Создать категорию
  318. div(class="admin-products__categories-list")
  319. div(
  320. v-for="category in categories"
  321. :key="category._id"
  322. class="admin-products__category-item"
  323. )
  324. div(class="admin-products__category-info")
  325. div(class="admin-products__category-name") {{ category.name }}
  326. div(class="admin-products__category-meta")
  327. span Товаров: {{ getCategoryProductCount(category._id) }}
  328. span(v-if="category.slug") Slug: {{ category.slug }}
  329. div(class="admin-products__category-actions")
  330. button(
  331. @click="editCategory(category)"
  332. class="admin-products__action-button admin-products__action-button--edit"
  333. ) Редактировать
  334. button(
  335. @click="deleteCategory(category)"
  336. class="admin-products__action-button admin-products__action-button--danger"
  337. ) Удалить
  338. //- Модальное окно редактирования/создания категории
  339. div(
  340. v-if="showCategoryModal"
  341. class="admin-products__modal"
  342. )
  343. div(class="admin-products__modal-content")
  344. div(class="admin-products__modal-header")
  345. h2(class="admin-products__modal-title") {{ editingCategory ? 'Редактирование категории' : 'Создание категории' }}
  346. button(
  347. @click="showCategoryModal = false"
  348. class="admin-products__modal-close"
  349. ) ×
  350. div(class="admin-products__modal-body")
  351. div(class="admin-products__form")
  352. div(class="admin-products__form-group")
  353. label(class="admin-products__label") Название категории *
  354. input(
  355. type="text"
  356. v-model="categoryForm.name"
  357. class="admin-products__input"
  358. placeholder="Введите название категории"
  359. )
  360. div(class="admin-products__form-group")
  361. label(class="admin-products__label") Slug
  362. input(
  363. type="text"
  364. v-model="categoryForm.slug"
  365. class="admin-products__input"
  366. placeholder="Автоматически сгенерируется из названия"
  367. )
  368. div(class="admin-products__form-group")
  369. label(class="admin-products__label") Описание
  370. textarea(
  371. v-model="categoryForm.description"
  372. class="admin-products__textarea"
  373. placeholder="Введите описание категории"
  374. rows="3"
  375. )
  376. div(class="admin-products__form-row")
  377. div(class="admin-products__form-group")
  378. label(class="admin-products__label") Родительская категория
  379. select(v-model="categoryForm.parentCategory" class="admin-products__select")
  380. option(value="") Нет
  381. option(
  382. v-for="category in categories"
  383. :value="category._id"
  384. v-if="category._id != editingCategory?._id"
  385. ) {{ category.name }}
  386. div(class="admin-products__form-group")
  387. label(class="admin-products__label") Порядок сортировки
  388. input(
  389. type="number"
  390. v-model="categoryForm.sortOrder"
  391. class="admin-products__input"
  392. placeholder="0"
  393. )
  394. div(class="admin-products__form-group")
  395. label(class="admin-products__label admin-products__label--checkbox")
  396. input(
  397. type="checkbox"
  398. v-model="categoryForm.active"
  399. class="admin-products__checkbox"
  400. )
  401. span Активная категория
  402. div(class="admin-products__modal-footer")
  403. button(
  404. @click="saveCategory"
  405. :disabled="!categoryForm.name"
  406. class="admin-products__button admin-products__button--primary"
  407. ) {{ editingCategory ? 'Обновить' : 'Создать' }}
  408. button(
  409. @click="showCategoryModal = false"
  410. class="admin-products__button admin-products__button--secondary"
  411. ) Отмена
  412. //- Модальное окно импорта
  413. div(
  414. v-if="showImportModal"
  415. class="admin-products__modal"
  416. )
  417. div(class="admin-products__modal-content")
  418. div(class="admin-products__modal-header")
  419. h2(class="admin-products__modal-title") Импорт товаров из CSV
  420. button(
  421. @click="showImportModal = false"
  422. class="admin-products__modal-close"
  423. ) ×
  424. div(class="admin-products__modal-body")
  425. div(v-if="importing" class="admin-products__import-progress")
  426. div(class="admin-products__progress-bar")
  427. div(
  428. :style="{ width: importProgress + '%' }"
  429. class="admin-products__progress-fill"
  430. )
  431. p(class="admin-products__progress-text") Обработано: {{ processedCount }} из {{ totalCount }} ({{ importProgress }}%)
  432. div(v-else class="admin-products__import-form")
  433. input(
  434. type="file"
  435. @change="onFileSelect"
  436. accept=".csv"
  437. class="admin-products__file-input"
  438. )
  439. p(class="admin-products__help-text") Поддерживается формат CSV с разделителем ";" и кодировкой UTF-8
  440. div(v-if="importResults" class="admin-products__import-results")
  441. div(
  442. v-if="importResults.success"
  443. class="admin-products__result admin-products__result--success"
  444. )
  445. h3 Успешно импортировано!
  446. p Обработано товаров: {{ importResults.processed }} из {{ importResults.total }}
  447. ul(v-if="importResults.errors.length > 0")
  448. li(v-for="error in importResults.errors") {{ error }}
  449. div(
  450. v-else
  451. class="admin-products__result admin-products__result--error"
  452. )
  453. h3 Ошибка импорта!
  454. p {{ importResults.error }}
  455. div(class="admin-products__modal-footer")
  456. button(
  457. @click="importProducts"
  458. :disabled="!selectedFile || importing"
  459. class="admin-products__button admin-products__button--primary"
  460. ) {{ importing ? 'Импорт...' : 'Начать импорт' }}
  461. button(
  462. @click="showImportModal = false"
  463. class="admin-products__button admin-products__button--secondary"
  464. ) Отмена
  465. //- Модальное окно массовых действий
  466. div(
  467. v-if="showMassActionsModal"
  468. class="admin-products__modal"
  469. )
  470. div(class="admin-products__modal-content")
  471. div(class="admin-products__modal-header")
  472. h2(class="admin-products__modal-title") Массовые действия ({{ selectedProducts.length }} товаров)
  473. button(
  474. @click="showMassActionsModal = false"
  475. class="admin-products__modal-close"
  476. ) ×
  477. div(class="admin-products__modal-body")
  478. div(class="admin-products__mass-actions")
  479. button(
  480. @click="activateSelected"
  481. class="admin-products__button admin-products__button--success"
  482. ) Активировать
  483. button(
  484. @click="deactivateSelected"
  485. class="admin-products__button admin-products__button--warning"
  486. ) Деактивировать
  487. button(
  488. @click="deleteSelected"
  489. class="admin-products__button admin-products__button--danger"
  490. ) Удалить
  491. div(class="admin-products__modal-footer")
  492. button(
  493. @click="showMassActionsModal = false"
  494. class="admin-products__button admin-products__button--secondary"
  495. ) Закрыть