From d94baa78137daf5f0f375ccc18be0c22dbfe9807 Mon Sep 17 00:00:00 2001 From: Vladimir Fomichev Date: Tue, 2 Sep 2025 10:55:05 +0300 Subject: [PATCH] =?utf8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?utf8?q?=D0=B8=D0=B5=20=D0=BF=D0=B0=D1=80=D1=81=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- docker/nginx/conf/nginx.conf | 4 +- erp24/commands/MarketplaceController.php | 125 ++++++++++ erp24/controllers/MatrixErpController.php | 222 +++++++++++++++++- erp24/models/HtmlImportForm.php | 40 ++++ erp24/services/FileService.php | 84 +++++++ erp24/services/ProductParserService.php | 175 ++++++++++++++ .../views/matrix_erp/parse-flowwow-cards.php | 77 ++++++ .../index.php | 2 +- 8 files changed, 718 insertions(+), 11 deletions(-) create mode 100644 erp24/models/HtmlImportForm.php create mode 100644 erp24/services/ProductParserService.php create mode 100644 erp24/views/matrix_erp/parse-flowwow-cards.php diff --git a/docker/nginx/conf/nginx.conf b/docker/nginx/conf/nginx.conf index 9689c6d2..855353c0 100644 --- a/docker/nginx/conf/nginx.conf +++ b/docker/nginx/conf/nginx.conf @@ -13,7 +13,7 @@ events { http { include /etc/nginx/mime.types; default_type application/octet-stream; - + client_max_body_size 128m; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; @@ -54,7 +54,7 @@ http { server { listen 443 ssl; listen [::]:443 ssl; - + client_max_body_size 128m; ssl_certificate /etc/ssl/certs/nginx.crt; ssl_certificate_key /etc/ssl/private/nginx.key; diff --git a/erp24/commands/MarketplaceController.php b/erp24/commands/MarketplaceController.php index 1777a3bb..65b78802 100644 --- a/erp24/commands/MarketplaceController.php +++ b/erp24/commands/MarketplaceController.php @@ -2,7 +2,11 @@ namespace yii_app\commands; +use GuzzleHttp\Client; +use GuzzleHttp\Exception\RequestException; use OpenAPI\Client\ObjectSerializer; +use Symfony\Component\DomCrawler\Crawler; +use voku\helper\HtmlDomParser; use Yii; use yii\console\Controller; use yii\console\ExitCode; @@ -201,4 +205,125 @@ class MarketplaceController extends Controller return ExitCode::OK; } + + public function actionParseFlowwow() + { + { + $this->stdout("Начинаем парсинг Flowwow..." . PHP_EOL); + + // URL для парсинга + $baseUrl = 'https://flowwow.com/shop/baza-cvetov-24f-6167/'; + + $this->stdout("Загружаем основную страницу: {$baseUrl}" . PHP_EOL); + + // Контекст для обхода SSL + $context = stream_context_create([ + "ssl" => [ + "verify_peer" => false, + "verify_peer_name" => false, + ], + "http" => [ + "header" => "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + "timeout" => 30 + ] + ]); + + try { + // Загружаем основную страницу + $dom = HtmlDomParser::file_get_html($baseUrl, false, $context); + + $categoryWrappers = $dom->findMulti('div.category-wrapper') ?? []; + $categoryCount = count($categoryWrappers); + + foreach ($categoryWrappers as $index => $category) { + $categoryNameElement = $category->findOne('h2.category-name'); + $categoryName = $categoryNameElement ? trim((string)$categoryNameElement->text()) : 'Без категории'; + + $productCards = $category->findMulti('div.category-content-item a.product-card') ?? []; + $productCount = count($productCards); + + foreach ($productCards as $productIndex => $card) { + $productPath = $card->getAttribute('href') ?: null; + if (!$productPath) { continue; } + + $productUrl = 'https://flowwow.com' . $productPath; + + $nameElement = $card->findOne('div.name'); + $priceElement = $card->findOne('div.price span'); + $imgElement = $card->findOne('img'); + + $productName = $nameElement ? trim((string)$nameElement->text()) : ''; + $priceText = $priceElement ? trim((string)$priceElement->text()) : ''; + $price = (int)preg_replace('/[^\d]/u', '', $priceText); + $imageUrl = $imgElement ? (string)$imgElement->getAttribute('src') : ''; + + $productData = [ + 'category' => $categoryName, + 'name' => $productName, + 'price' => $price, + 'image_url' => $imageUrl, + 'product_url' => $productUrl + ]; + + try { + $this->stdout("Загружаем страницу товара..." . PHP_EOL); + + // Загружаем страницу товара + $productDom = HtmlDomParser::file_get_html($productUrl, false, $context); + if ($productDom !== false) { + $this->stdout("Страница товара загружена, извлекаем данные..." . PHP_EOL); + + // Извлечение описания + $descriptionElement = $productDom->findOne('div.product-card-content'); + if ($descriptionElement) { + $productData['description'] = trim((string)$descriptionElement->text()); + } + + $properties = []; + foreach ($productDom->findMulti('div.property-item') ?? [] as $item) { + $propName = $item->findOne('div.property-name')?->text(); + $propValue = $item->findOne('div.property-value')?->text(); + if ($propName !== null && $propValue !== null) { + $properties[trim((string)$propName)] = trim((string)$propValue); + } + } + $productData['properties'] = $properties; + + $this->stdout("Характеристики извлечены: " . count($properties) . " свойств" . PHP_EOL); + + // $productDom->close(); + } + } catch (\Exception $e) { + $this->stdout("Ошибка при загрузке страницы товара: " . $e->getMessage() . PHP_EOL); + $productData['error'] = 'Не удалось загрузить страницу товара: ' . $e->getMessage(); + } + + $products[] = $productData; + $this->stdout("Товар добавлен в результат: {$productName}" . PHP_EOL); + + // Задержка между запросами + $delay = 500000; // 0.5 секунды + $this->stdout("Ждем {$delay} микросекунд перед следующим запросом..." . PHP_EOL); + usleep($delay); + } + } + + // $dom->close(); + + $this->stdout("Парсинг завершен. Обработано товаров: " . count($products) . PHP_EOL); + + // Сохраняем результат в файл (опционально) + $filename = date('Y-m-d_H-i-s') . '_flowwow_products.json'; + file_put_contents($filename, json_encode($products, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); + $this->stdout("Результат сохранен в файл: {$filename}" . PHP_EOL); + + return ExitCode::OK; + + } catch (\Exception $e) { + $this->stdout("Произошла ошибка: " . $e->getMessage() . PHP_EOL); + return ExitCode::SOFTWARE; + } + } + } + } \ No newline at end of file diff --git a/erp24/controllers/MatrixErpController.php b/erp24/controllers/MatrixErpController.php index 8afa9cf1..a093b2f3 100644 --- a/erp24/controllers/MatrixErpController.php +++ b/erp24/controllers/MatrixErpController.php @@ -2,9 +2,19 @@ namespace app\controllers; +use app\models\HtmlImportForm; +use DOMDocument; +use DOMXPath; +use Exception; +use GuzzleHttp\Client; +use GuzzleHttp\Exception\RequestException; +use Symfony\Component\DomCrawler\Crawler; +use voku\helper\HtmlDomParser; use Yii; use yii\base\DynamicModel; use yii\helpers\Json; +use yii\web\UploadedFile; +use yii_app\records\Images; use yii_app\records\MatrixErp; use yii_app\records\MatrixErpMedia; use yii_app\records\MatrixErpProperty; @@ -13,13 +23,19 @@ use yii\web\Controller; use yii\web\NotFoundHttpException; use yii\filters\VerbFilter; use yii_app\records\Products1c; +use yii_app\services\FileService; use yii_app\services\MarketplaceService; +use yii_app\services\ProductParserService; /** * MatrixErpController implements the CRUD actions for MatrixErp model. */ class MatrixErpController extends Controller { + const OUT_DIR = +// __DIR__ . "/../json"; + "/www/api2/json"; + /** * @inheritDoc */ @@ -38,7 +54,8 @@ class MatrixErpController extends Controller ); } - public static function fillInGuidsFromMarketplaceAndAdditional() { + public static function fillInGuidsFromMarketplaceAndAdditional() + { $existings = MatrixErp::find()->all(); $existingMapGuidGroupNames = []; foreach ($existings as $existing) { @@ -112,11 +129,9 @@ class MatrixErpController extends Controller $query = MatrixErp::find() // ->select([]) ->joinWith('matrixProperty') - ->joinWith('matrixMedia') - - ; + ->joinWith('matrixMedia'); - if(Yii::$app->request->isPost && $filterModel->load(Yii::$app->request->post())){ + if (Yii::$app->request->isPost && $filterModel->load(Yii::$app->request->post())) { $nameFilter = $filterModel->nameFilter; $groupNameFilter = $filterModel->groupNameFilter; $activeFilter = $filterModel->activeFilter; @@ -140,7 +155,7 @@ class MatrixErpController extends Controller if (isset($nameFilter) && $nameFilter != "") { if (!empty($nameFilter)) { - $query->andFilterWhere(['like', 'matrix_erp.name', $nameFilter]);; + $query->andFilterWhere(['like', 'matrix_erp.name', $nameFilter]);; } } $test = 1; @@ -238,7 +253,8 @@ class MatrixErpController extends Controller ]); } - public function actionDelete($id) { + public function actionDelete($id) + { $matrixErp = MatrixErp::findOne($id); $matrixErp->active = 0; $matrixErp->deleted_at = date('Y-m-d H:i:s'); @@ -264,4 +280,194 @@ class MatrixErpController extends Controller throw new NotFoundHttpException('The requested page does not exist.'); } -} + + + public function actionParseFlowwowCards() + { + $model = new HtmlImportForm(); + $results = []; + + if ($model->load(Yii::$app->request->post())) { + $model->files = UploadedFile::getInstances($model, 'files'); + + if ($model->validate()) { + foreach ($model->files as $file) { + $results[] = $this->processFile($file, $model->category); + } + + Yii::$app->session->setFlash('importResults', $results); + return $this->refresh(); + } + } + + return $this->render('/matrix_erp/parse-flowwow-cards', [ + 'model' => $model, + 'results' => Yii::$app->session->getFlash('importResults', []), + ]); + } + + /** + * Обработка одного файла: парсинг → поиск MatrixErp по артикулу → upsert MatrixErpProperty по GUID. + */ + protected function processFile(UploadedFile $file, string $category): array + { + $res = [ + 'file' => $file->name, + 'status' => 'ok', + 'articule' => null, + 'guid' => null, + 'errors' => [], + ]; + + try { + $html = file_get_contents($file->tempName); + + $service = new ProductParserService(); + + $parsed = $service->parseProductHtml($html); + + $products = $this->normalizeParsed($parsed); + var_dump($products);die(); + if (empty($products)) { + throw new \RuntimeException('Парсер вернул пустой результат.'); + } + + foreach ($products as $p) { + $art = $this->extractArticule($p['name'] ?? ''); + $res['articule'] = $res['articule'] ?? $art; + + if (!$art) { + $res['status'] = 'error'; + $res['errors'][] = 'Артикул не найден в названии.'; + continue; + } + + $matrix = MatrixErp::find()->where(['articule' => $art])->one(); + if (!$matrix) { + $res['status'] = 'error'; + $res['errors'][] = "Товар с артикулом {$art} не найден в MatrixErp."; + continue; + } + + $res['guid'] = $matrix->guid; + + if (!$this->upsertProperty($matrix, $p, $category)) { + $res['status'] = 'error'; + $res['errors'][] = "Не удалось сохранить свойства для GUID {$matrix->guid}."; + } + } + } catch (\Throwable $e) { + $res['status'] = 'error'; + $res['errors'][] = $e->getMessage(); + Yii::error($e->getMessage() . ' in ' . $file->name, __METHOD__); + } + + return $res; + } + + + protected function normalizeParsed($parsed): array + { + if (!$parsed) return []; + if (is_array($parsed) && $this->isAssoc($parsed) && isset($parsed['name'])) { + return [$parsed]; + } + return array_values((array)$parsed); + } + + protected function isAssoc(array $arr): bool + { + // Аналог ArrayHelper::isAssociative для совместимости + if ([] === $arr) return false; + return array_keys($arr) !== range(0, count($arr) - 1); + } + + /** + * Артикул из названия: ищем (FW0125) или просто FW0125, либо "Артикул: FW0125". + */ + protected function extractArticule(string $name): ?string + { + $pattern = `/\\((?[A-Z]{2,4}\\d{3,6})\\)/u`; + if (preg_match($pattern, $name, $m)) { + return strtoupper($m['art']); + } + + return null; + } + + /** + * Upsert в matrix_erp_property по guid товара. + */ + protected function upsertProperty(MatrixErp $matrix, array $matrixProduct, string $category, string $subcategory): bool + { + $prop = MatrixErpProperty::find()->where(['guid' => $matrix->guid])->one(); + $nowDt = date('Y-m-d H:i:s'); + $nowD = date('Y-m-d'); + + if (!$prop) { + $prop = new MatrixErpProperty(); + $prop->guid = $matrix->guid; + $prop->matrix_erp_id = $matrix->guid; + $prop->date = $nowD; + $prop->created_admin_id = Yii::$app->user->id ?? null; + $prop->created_at = $nowDt; + } else { + $prop->updated_admin_id = Yii::$app->user->id ?? null; + $prop->updated_at = $nowDt; + } + + try { + $uploadImage = FileService::downloadAsUploadedFile($externalUrl, maxBytes: 8_000_000); + } catch (\Throwable $e) { + Yii::error("Ошибка загрузки изображения по ссылке: " . $e->getMessage()); + + \Yii::$app->response->format = \yii\web\Response::FORMAT_JSON; + return ['error' => 'Не удалось скачать файл по ссылке']; + } + + + if ($uploadImage) { + if (Images::isImageFile($uploadImage, ['png', 'jpg', 'jpeg', 'webp', 'gif'])) { + $image = new Images(); + $imageId = $image->loadImage($uploadImage); // ваш существующий метод (как при обычной загрузке) + + if (!empty($imageId)) { + $prop->image_id = $imageId; + $prop->external_image_url = MarketplaceService::getProductImageUrl($imageId); + if (!empty($oldFile)) { + $oldFile->delete(); + } + if (!$prop->save()) { + Yii::error("Ошибка сохранения ссылок на картинки " . json_encode($modelEdit->getErrors(), JSON_UNESCAPED_UNICODE)); + } + } + } else { + \Yii::$app->response->format = \yii\web\Response::FORMAT_JSON; + return ['error' => 'Файл не является изображением либо запрещённое расширение']; + } + } else { + \Yii::$app->response->format = \yii\web\Response::FORMAT_JSON; + return ['error' => 'Не передан файл и не указана ссылка']; + } + + + $prop->display_name = $matrixProduct['name'] ?? $prop->display_name; + $prop->description = $matrixProduct['description'] ?? $prop->description; + $prop->external_image_url = $matrixProduct['image_url'] ?? $prop->external_image_url; + $prop->product_url = $matrixProduct['product_url'] ?? $prop->product_url; + + // Категории + $prop->flowwow_category = $category; + $prop->flowwow_subcategory = $category; + $prop->flowwow_subcategory = $matrixProduct['properties']['subcategory'] ?? $prop->flowwow_subcategory; + $prop->yandex_category = $matrixProduct['properties']['yandex_category'] ?? $prop->yandex_category; + + + if (isset($matrixProduct['properties']['length'])) $prop->length = (float)$matrixProduct['properties']['length']; + if (isset($matrixProduct['properties']['width'])) $prop->width = (float)$matrixProduct['properties']['width']; + if (isset($matrixProduct['properties']['height'])) $prop->height = (float)$matrixProduct['properties']['height']; + if (isset($matrixProduct['properties']['weight'])) $prop->weight = (float)$matrixProduct['properties']['weight']; + + return $prop->save(); + } +} \ No newline at end of file diff --git a/erp24/models/HtmlImportForm.php b/erp24/models/HtmlImportForm.php new file mode 100644 index 00000000..e0f6f201 --- /dev/null +++ b/erp24/models/HtmlImportForm.php @@ -0,0 +1,40 @@ + 255], + + [['files'], 'required'], + [ + ['files'], + 'file', + 'skipOnEmpty' => false, + 'extensions' => ['html', 'htm'], + 'maxFiles' => 100, + ], + ]; + } + + public function attributeLabels(): array + { + return [ + 'category' => 'Название категории', + 'subcategory' => 'Название подкатегории', + 'files' => 'HTML файлы', + ]; + } +} + diff --git a/erp24/services/FileService.php b/erp24/services/FileService.php index 8e363a69..e0dca70c 100755 --- a/erp24/services/FileService.php +++ b/erp24/services/FileService.php @@ -2,9 +2,13 @@ namespace yii_app\services; +use GuzzleHttp\Client; use Yii; use yii\helpers\ArrayHelper; +use yii\helpers\FileHelper; use yii\helpers\Url; + +use yii\web\UploadedFile; use yii_app\helpers\ImageHelper; use yii_app\records\Files; use yii_app\records\Images; @@ -267,4 +271,84 @@ class FileService return $result; } + /** + * Скачивает файл по URL и возвращает "виртуальный" UploadedFile. + * Бросает исключение при ошибке/превышении лимита. + * + * @param string $url Ссылка на файл (http/https) + * @param int $maxBytes Максимальный размер, байт (например, 8 МБ) + * @param int $timeout Таймаут запроса, сек + */ + public static function downloadAsUploadedFile(string $url, int $maxBytes = 8_000_000, int $timeout = 20): UploadedFile + { + $scheme = parse_url($url, PHP_URL_SCHEME); + if (!in_array($scheme, ['http', 'https'], true)) { + throw new \RuntimeException('Недопустимая схема URL'); + } + + $client = new Client([ + 'timeout' => $timeout, + 'verify' => true, + 'headers' => [ + 'User-Agent' => 'Mozilla/5.0 (compatible; Yii2 Guzzle Downloader)', + 'Accept' => 'image/*,*/*;q=0.8', + ], + ]); + + $tmpDir = \Yii::getAlias('@runtime/http-downloads'); + FileHelper::createDirectory($tmpDir); + $tmpFile = tempnam($tmpDir, 'img_'); + + $downloaded = 0; + $progress = function ($downloadTotal, $downloadedBytes) use (&$downloaded, $maxBytes) { + $downloaded = $downloadedBytes; + if ($downloaded > $maxBytes) { + throw new \RuntimeException('Файл слишком большой'); + } + }; + + $response = $client->request('GET', $url, [ + 'sink' => $tmpFile, + 'progress' => $progress, + ]); + + if ($response->getStatusCode() !== 200) { + @unlink($tmpFile); + throw new \RuntimeException('HTTP ' . $response->getStatusCode()); + } + + $mime = FileHelper::getMimeType($tmpFile) ?: 'application/octet-stream'; + if (strncmp($mime, 'image/', 6) !== 0) { + @unlink($tmpFile); + throw new \RuntimeException('Загруженный файл не является изображением'); + } + + $pathFromUrl = parse_url($url, PHP_URL_PATH) ?: ''; + $basename = basename($pathFromUrl); + $name = $basename ?: ('image_' . uniqid()); + $ext = pathinfo($name, PATHINFO_EXTENSION); + + if ($ext === '') { + $map = [ + 'image/jpeg' => 'jpg', + 'image/png' => 'png', + 'image/webp' => 'webp', + 'image/gif' => 'gif', + ]; + $ext = $map[$mime] ?? 'jpg'; + $name = $name . '.' . $ext; + } + + /** @var UploadedFile $uploaded */ + $uploaded = new UploadedFile([ + 'name' => $name, + 'tempName' => $tmpFile, + 'type' => $mime, + 'size' => filesize($tmpFile), + 'error' => UPLOAD_ERR_OK, + ]); + + return $uploaded; + } + } diff --git a/erp24/services/ProductParserService.php b/erp24/services/ProductParserService.php new file mode 100644 index 00000000..7f7e6b93 --- /dev/null +++ b/erp24/services/ProductParserService.php @@ -0,0 +1,175 @@ +loadHTML($html); + libxml_clear_errors(); + + $xpath = new DOMXPath($dom); + + return [ + 'name' => $this->extractName($xpath), + 'price' => $this->extractPrice($xpath), + 'old_price' => $this->extractOldPrice($xpath), + 'rating' => $this->extractRating($xpath), + 'reviews_count' => $this->extractReviewsCount($xpath), + 'image_url' => $this->extractImageUrl($xpath), + 'description' => $this->extractDescription($xpath), + 'properties' => $this->extractProperties($xpath), + + ]; + } + + private + function extractName(DOMXPath $xpath): string + { + $node = $xpath->query("//h1")->item(0); + return $node ? trim($node->nodeValue) : ''; + } + + private + function extractPrice(DOMXPath $xpath): string + { + $node = $xpath->query("//span[@class='footer-price']")->item(0); + return $node ? trim($node->nodeValue) : ''; + } + + private + function extractOldPrice(DOMXPath $xpath): string + { + $node = $xpath->query("//div[@class='footer-old-price']")->item(0); + return $node ? trim($node->nodeValue) : ''; + } + + + private + function extractRating(DOMXPath $xpath): string + { + $node = $xpath->query("//div[@class='rating']//div[contains(@class, 'el-rate')]")->item(0); + return $node ? $node->getAttribute('data-score') : ''; + } + + private + function extractReviewsCount(DOMXPath $xpath): string + { + $node = $xpath->query("//div[@class='review-count']")->item(0); + return $node ? trim($node->nodeValue) : ''; + } + + private + function extractImageUrl(DOMXPath $xpath): string + { + $q1 = "//div[@id='js-detect-events']" . + "//div[contains(concat(' ', normalize-space(@class), ' '), ' swiper-slide-active ')]" . + "//img[contains(concat(' ', normalize-space(@class), ' '), ' main-image-content ')]"; + + $img = $xpath->query($q1)->item(0); + + if (!$img) { + $q2 = "//div[@id='js-detect-events']//img[contains(concat(' ', normalize-space(@class), ' '), ' main-image-content ')]"; + $img = $xpath->query($q2)->item(0); + } + + $src = null; + if ($img) { + $src = $img->getAttribute('data-src'); + } + + if (!$src) { + $zoom = $xpath->query("//div[@id='js-image-zoom']")->item(0); + if ($zoom && preg_match('/background-image:\s*url\([\"\']?(.*?)[\"\']?\)/i', $zoom->getAttribute('style'), $m)) { + $src = $m[1]; + } + } + return $src; + } + + private + function extractDescription(DOMXPath $xpath): string + { + $node = $xpath->query("//div[@class='product-properties-general']" . "//div[@class='property-item']" . "//p[@class='pre-line']" . "//span")->item(0); + return $node ? trim($node->nodeValue) : ''; + } + + function extractProperties(DOMXPath $xpath): array + { + $properties = []; + + $nodes = $xpath->query("//div[contains(concat(' ', normalize-space(@class), ' '), ' property-item ')]"); + + foreach ($nodes as $node) { + + $nameNode = $xpath->query(".//h3[contains(concat(' ', normalize-space(@class), ' '), ' property-name ')]", $node)->item(0); + if (!$nameNode) { + continue; + } + $propName = trim($nameNode->textContent); + + if (mb_stripos($propName, 'Размер') !== false) { + $sizeSpans = $xpath->query( + ".//li[contains(concat(' ', normalize-space(@class), ' '), ' size-item ')]" . + "//span[contains(concat(' ', normalize-space(@class), ' '), ' size-text ')]", + $node + ); + + $sizes = []; + foreach ($sizeSpans as $span) { + $text = trim(preg_replace('/\s+/u', ' ', $span->textContent)); + + if (preg_match('/(Ширина|Высота|Длина)\s*[-:–—]\s*(\d+(?:[.,]\d+)?)\s*см/ui', $text, $m)) { + $label = ucfirst(mb_strtolower($m[1])); + $value = (float) str_replace(',', '.', $m[2]); + $sizes[$label] = $value; + } + } + + if ($sizes) { + $properties['Размер'] = $sizes; + if (isset($sizes['Ширина'])) $properties['width'] = $sizes['Ширина']; + if (isset($sizes['Высота'])) $properties['height'] = $sizes['Высота']; + } + + continue; + } + + + $valueNode = $xpath->query( + ".//*[contains(concat(' ', normalize-space(@class), ' '), ' property-text ') " . + " or contains(concat(' ', normalize-space(@class), ' '), ' pre-line ')]", + $node + )->item(0); + + if ($valueNode) { + $properties[$propName] = trim(preg_replace('/\s+/u', ' ', $valueNode->textContent)); + } + } + + + if (!isset($properties['width']) || !isset($properties['height'])) { + $tagSpans = $xpath->query("//ul[contains(@class,'size-tag')]//li//span[not(contains(@class,'size-tag-icon'))]"); + $seen = []; + foreach ($tagSpans as $span) { + $text = trim($span->textContent); + if (preg_match('/(\d+(?:[.,]\d+)?)\s*см/ui', $text, $m)) { + $val = (float) str_replace(',', '.', $m[1]); + $seen[] = $val; + } + } + if (!empty($seen)) { + if (!isset($properties['width']) && isset($seen[0])) $properties['width'] = $seen[0]; + if (!isset($properties['height']) && isset($seen[1])) $properties['height'] = $seen[1]; + } + } + + return $properties; + } + +} \ No newline at end of file diff --git a/erp24/views/matrix_erp/parse-flowwow-cards.php b/erp24/views/matrix_erp/parse-flowwow-cards.php new file mode 100644 index 00000000..e1426f2d --- /dev/null +++ b/erp24/views/matrix_erp/parse-flowwow-cards.php @@ -0,0 +1,77 @@ +title = 'Импорт свойств из HTML'; +?> +
+
+

title) ?>

+ + ['enctype' => 'multipart/form-data']]); ?> + + field($model, 'category')->textInput(['maxlength' => true, 'placeholder' => 'Например: Цветы']) ?> + field($model, 'subcategory')->textInput(['maxlength' => true, 'placeholder' => 'Например: Монобукеты']) ?> + + field($model, 'files[]')->fileInput([ + 'multiple' => true, + 'accept' => '.html,.htm', + ]) ?> + +
+ 'btn btn-primary']) ?> +
+ + +
+
+ + +
+
+
Результаты импорта
+
+ + + + + + + + + + + + + + + + + + + + + +
ФайлАртикулGUIDСтатусОшибки
+ + ok + + error + + + +
    + +
  • + +
+ +
+
+
+
+ diff --git a/erp24/views/products1c-nomenclature-actuality/index.php b/erp24/views/products1c-nomenclature-actuality/index.php index 6aa70cef..1f95de89 100644 --- a/erp24/views/products1c-nomenclature-actuality/index.php +++ b/erp24/views/products1c-nomenclature-actuality/index.php @@ -25,7 +25,7 @@ $this->registerJsFile('/js/products1cNomenclatureActuality/index.js', ['position function monthList() { $list = []; - $start = new DateTime('2025-01'); + $start = new DateTime('2024-01'); $end = new DateTime('2026-12'); while ($start <= $end) { $key = $start->format('Y-m'); -- 2.39.5