Spaces:
Running
Running
Fix root cause mất ảnh khi thêm 2+ SP qua URL: server upsert chỉ match l link khi cả 2 không rỗng (tránh ghi đè SP trước); client gửi l/link/url; restore Hitachi 357869 record bị ghi đè. Fix treo modal FLASHSALE (.show CSS đè inline display:none). Hiển thị đủ ảnh detail BIGSALE/FLASHSALE/COMBO qua proxy primaryImgSrc. Cải thiện parser WooCommerce/bossvn (JSON-LD @graph , giá, gallery, specs, brand).
Browse files- .gitignore +1 -0
- index.html +4 -4
- index.ts +12 -2
- src/vaix-add-product.js +103 -12
- src/vaix-combo.js +4 -1
- src/vaix-flashsale.js +28 -6
- src/vaix-rag.js +7 -1
- src/version.js +1 -1
.gitignore
CHANGED
|
@@ -32,3 +32,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
|
| 32 |
|
| 33 |
# Finder (MacOS) folder config
|
| 34 |
.DS_Store
|
|
|
|
|
|
| 32 |
|
| 33 |
# Finder (MacOS) folder config
|
| 34 |
.DS_Store
|
| 35 |
+
env/
|
index.html
CHANGED
|
@@ -837,7 +837,7 @@
|
|
| 837 |
first paint — the welcome/avatar-picker modal renders immediately. -->
|
| 838 |
<script defer src="./src/cart-quote.js?gc=37"></script> <!-- Cart + Quote module (Giỏ hàng / Báo giá) -->
|
| 839 |
<script defer src="./src/customers.js?gc=19"></script> <!-- Customer list (Danh sách khách hàng) -->
|
| 840 |
-
<script defer src="./src/vaix-rag.js?gc=
|
| 841 |
<script defer src="./src/greeting-news.js?gc=42"></script> <!-- Greeting HOT news cards + source links -->
|
| 842 |
<script defer src="./src/order-sync.js?gc=17"></script> <!-- Order sync with V.AISTUDIO backend -->
|
| 843 |
<script type="module" src="./src/app.js?gc=61"></script>
|
|
@@ -847,8 +847,8 @@
|
|
| 847 |
<script defer src="./big_sale.js?v=15"></script>
|
| 848 |
<!-- V.AI STUDIO Promo CMS (admin editor + BIGSALE/COMBO manager) -->
|
| 849 |
<script defer src="./src/vaix-promo-cms.js?v=4"></script>
|
| 850 |
-
<script defer src="./src/vaix-combo.js?v=
|
| 851 |
-
<script defer src="./src/vaix-flashsale.js?v=
|
| 852 |
|
| 853 |
<script>
|
| 854 |
// Wire the "Đơn hàng" buttons to the order modal
|
|
@@ -869,7 +869,7 @@
|
|
| 869 |
|
| 870 |
<!-- ── Thêm SP (Add Product) module: 100% port from V.AISTUDIO, gated by the
|
| 871 |
homepage V.AISTUDIO access code (window.isVaAdmin()). ── -->
|
| 872 |
-
<script defer src="./src/vaix-add-product.js?v=
|
| 873 |
<script defer src="./src/vaix-bulk-import.js?v=10"></script>
|
| 874 |
</body>
|
| 875 |
</html>
|
|
|
|
| 837 |
first paint — the welcome/avatar-picker modal renders immediately. -->
|
| 838 |
<script defer src="./src/cart-quote.js?gc=37"></script> <!-- Cart + Quote module (Giỏ hàng / Báo giá) -->
|
| 839 |
<script defer src="./src/customers.js?gc=19"></script> <!-- Customer list (Danh sách khách hàng) -->
|
| 840 |
+
<script defer src="./src/vaix-rag.js?gc=67"></script> <!-- V.AI STUDIO RAG Module - loads FASTER via light index; lazy galleries; spec-aware search -->
|
| 841 |
<script defer src="./src/greeting-news.js?gc=42"></script> <!-- Greeting HOT news cards + source links -->
|
| 842 |
<script defer src="./src/order-sync.js?gc=17"></script> <!-- Order sync with V.AISTUDIO backend -->
|
| 843 |
<script type="module" src="./src/app.js?gc=61"></script>
|
|
|
|
| 847 |
<script defer src="./big_sale.js?v=15"></script>
|
| 848 |
<!-- V.AI STUDIO Promo CMS (admin editor + BIGSALE/COMBO manager) -->
|
| 849 |
<script defer src="./src/vaix-promo-cms.js?v=4"></script>
|
| 850 |
+
<script defer src="./src/vaix-combo.js?v=6"></script>
|
| 851 |
+
<script defer src="./src/vaix-flashsale.js?v=4"></script>
|
| 852 |
|
| 853 |
<script>
|
| 854 |
// Wire the "Đơn hàng" buttons to the order modal
|
|
|
|
| 869 |
|
| 870 |
<!-- ── Thêm SP (Add Product) module: 100% port from V.AISTUDIO, gated by the
|
| 871 |
homepage V.AISTUDIO access code (window.isVaAdmin()). ── -->
|
| 872 |
+
<script defer src="./src/vaix-add-product.js?v=18"></script>
|
| 873 |
<script defer src="./src/vaix-bulk-import.js?v=10"></script>
|
| 874 |
</body>
|
| 875 |
</html>
|
index.ts
CHANGED
|
@@ -1665,11 +1665,21 @@ const server = Bun.serve({
|
|
| 1665 |
if (ra.ok) { const d = await ra.json(); if (Array.isArray(d)) list = d; }
|
| 1666 |
} catch (_e) {}
|
| 1667 |
// Upsert by slug or by link, mirroring zalobot add_product_from_url().
|
| 1668 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1669 |
let replaced = false;
|
| 1670 |
for (let i = 0; i < list.length; i++) {
|
| 1671 |
const x = list[i];
|
| 1672 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1673 |
list[i] = rec; replaced = true; break;
|
| 1674 |
}
|
| 1675 |
}
|
|
|
|
| 1665 |
if (ra.ok) { const d = await ra.json(); if (Array.isArray(d)) list = d; }
|
| 1666 |
} catch (_e) {}
|
| 1667 |
// Upsert by slug or by link, mirroring zalobot add_product_from_url().
|
| 1668 |
+
// CRITICAL FIX: only match an existing record on `x.l === rec.l` when
|
| 1669 |
+
// BOTH links are non-empty. Previously an empty rec.l ("") matched the
|
| 1670 |
+
// first record with an empty l in the list, so adding a SECOND product
|
| 1671 |
+
// via URL silently overwrote the FIRST product's full record (images,
|
| 1672 |
+
// description, specs all wiped → product kept only a name in promos).
|
| 1673 |
+
const key = (rec.slug || "").toLowerCase();
|
| 1674 |
+
const keyL = (rec.l || "").toLowerCase();
|
| 1675 |
let replaced = false;
|
| 1676 |
for (let i = 0; i < list.length; i++) {
|
| 1677 |
const x = list[i];
|
| 1678 |
+
const xSlug = (x && (x.slug || "")).toLowerCase();
|
| 1679 |
+
const xLink = (x && (x.l || "")).toLowerCase();
|
| 1680 |
+
const slugMatch = !!key && xSlug === key;
|
| 1681 |
+
const linkMatch = !!keyL && !!xLink && xLink === keyL;
|
| 1682 |
+
if (slugMatch || linkMatch) {
|
| 1683 |
list[i] = rec; replaced = true; break;
|
| 1684 |
}
|
| 1685 |
}
|
src/vaix-add-product.js
CHANGED
|
@@ -800,24 +800,34 @@
|
|
| 800 |
function extractJsonLd(html) {
|
| 801 |
var re = /<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/gi;
|
| 802 |
var m, best = null;
|
|
|
|
|
|
|
|
|
|
| 803 |
while ((m = re.exec(html))) {
|
| 804 |
try {
|
| 805 |
var data = JSON.parse(m[1]);
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
if (
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
var price = item.offers && (item.offers.price != null ? item.offers.price : (item.offers.lowPrice != null ? item.offers.lowPrice : null));
|
| 814 |
-
best = { name: item.name, sku: item.sku, brand: (item.brand && (item.brand.name || item.brand)) || '', image: Array.isArray(item.image) ? item.image[0] : item.image, price: price, priceCurrency: item.offers && item.offers.priceCurrency, description: item.description };
|
| 815 |
-
break;
|
| 816 |
}
|
| 817 |
-
}
|
| 818 |
-
if (best) break;
|
| 819 |
} catch (e) {}
|
| 820 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 821 |
return best;
|
| 822 |
}
|
| 823 |
function extractPrices(html, ld) {
|
|
@@ -1009,6 +1019,86 @@
|
|
| 1009 |
var brand = getMeta(doc, 'og:site_name') || (ld && ld.brand) || '';
|
| 1010 |
if (!brand) { try { var host = new URL(pageUrl).hostname; brand = host.replace(/^www\./, '').split('.')[0]; } catch (e) {} }
|
| 1011 |
brand = String(brand || '').replace(/^www\./i, '');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1012 |
// ── dienmayxanh.com override (v2024 layout) ──
|
| 1013 |
if (/dienmayxanh\.com/i.test(pageUrl || '')) {
|
| 1014 |
// NAME: h1 direct text only
|
|
@@ -1311,6 +1401,7 @@
|
|
| 1311 |
cs: 'san-pham-them-moi', ci: 'fa-box',
|
| 1312 |
sum: r.desc, summary: r.desc, desc: r.desc,
|
| 1313 |
specs: r.specs, feats: featsArr, brand: brand,
|
|
|
|
| 1314 |
_sourceUrl: prev.pageUrl, _url_added: true
|
| 1315 |
};
|
| 1316 |
// 1) local durable list + live catalog injection (always works)
|
|
|
|
| 800 |
function extractJsonLd(html) {
|
| 801 |
var re = /<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/gi;
|
| 802 |
var m, best = null;
|
| 803 |
+
// Collect every JSON-LD node (handles @graph wrappers, nested arrays, and
|
| 804 |
+
// single objects) into a flat list, then pick the first Product.
|
| 805 |
+
var nodes = [];
|
| 806 |
while ((m = re.exec(html))) {
|
| 807 |
try {
|
| 808 |
var data = JSON.parse(m[1]);
|
| 809 |
+
(function collect(node) {
|
| 810 |
+
if (!node) return;
|
| 811 |
+
if (Array.isArray(node)) { node.forEach(collect); return; }
|
| 812 |
+
if (typeof node === 'object') {
|
| 813 |
+
nodes.push(node);
|
| 814 |
+
if (Array.isArray(node['@graph'])) node['@graph'].forEach(collect);
|
| 815 |
+
if (Array.isArray(node.itemListElement)) node.itemListElement.forEach(collect);
|
|
|
|
|
|
|
|
|
|
| 816 |
}
|
| 817 |
+
})(data);
|
|
|
|
| 818 |
} catch (e) {}
|
| 819 |
}
|
| 820 |
+
for (var k = 0; k < nodes.length; k++) {
|
| 821 |
+
var item = nodes[k];
|
| 822 |
+
if (!item) continue;
|
| 823 |
+
var typ = String(item['@type'] || '').toLowerCase();
|
| 824 |
+
var isProduct = typ.indexOf('product') >= 0 || (item.name && (item.offers || item.image));
|
| 825 |
+
if (isProduct) {
|
| 826 |
+
var price = item.offers && (item.offers.price != null ? item.offers.price : (item.offers.lowPrice != null ? item.offers.lowPrice : null));
|
| 827 |
+
best = { name: item.name, sku: item.sku, brand: (item.brand && (item.brand.name || item.brand)) || '', image: Array.isArray(item.image) ? item.image[0] : item.image, price: price, priceCurrency: item.offers && item.offers.priceCurrency, description: item.description, category: item.category || '' };
|
| 828 |
+
break;
|
| 829 |
+
}
|
| 830 |
+
}
|
| 831 |
return best;
|
| 832 |
}
|
| 833 |
function extractPrices(html, ld) {
|
|
|
|
| 1019 |
var brand = getMeta(doc, 'og:site_name') || (ld && ld.brand) || '';
|
| 1020 |
if (!brand) { try { var host = new URL(pageUrl).hostname; brand = host.replace(/^www\./, '').split('.')[0]; } catch (e) {} }
|
| 1021 |
brand = String(brand || '').replace(/^www\./i, '');
|
| 1022 |
+
// ── WooCommerce generic override (bossvn.vn, thegioididong-style shops) ──
|
| 1023 |
+
if (/woocommerce|bossvn\.vn|wp-content\/uploads/i.test(html) || /(bossvn|giadungninhbinhweb)/i.test(pageUrl || '')) {
|
| 1024 |
+
// BRAND: prefer the WordPress site hostname ("bossvn") over og:site_name's
|
| 1025 |
+
// template string ("Mẫu web công ty gia dụng - NinhBinhWeb").
|
| 1026 |
+
try {
|
| 1027 |
+
var wHost = (pageUrl || '').replace(/^https?:\/\//i, '').replace(/^www\./i, '').split(/[/:]/)[0] || '';
|
| 1028 |
+
var wHostBrand = wHost.replace(/\..+$/, '');
|
| 1029 |
+
if (/(web|mau|cong ty|gia dung|template|blog|demo)/i.test(String(brand)) || /https?:|www\./i.test(String(brand))) {
|
| 1030 |
+
brand = wHostBrand ? (wHostBrand.charAt(0).toUpperCase() + wHostBrand.slice(1)) : brand;
|
| 1031 |
+
}
|
| 1032 |
+
} catch (_wb) {}
|
| 1033 |
+
// NAME: prefer the WooCommerce <h1 class="product_title"> (strips the site
|
| 1034 |
+
// suffix " - Thiết bị nhà bếp Boss").
|
| 1035 |
+
var wH1 = doc.querySelector('h1.product_title, h1.product_title.entry-title, div.summary.entry-summary h1');
|
| 1036 |
+
if (wH1) {
|
| 1037 |
+
var wNm = (wH1.textContent || '').replace(/\s+/g, ' ').trim();
|
| 1038 |
+
if (wNm && wNm !== 'Sản phẩm') name = wNm.split(/\s*[-–—|]\s*/)[0].trim();
|
| 1039 |
+
}
|
| 1040 |
+
// PRICE: WooCommerce sale price (.price.product-page-price .woocommerce-Price-amount bdi).
|
| 1041 |
+
// Use sale first, then the plain .price block; prefers JSON-LD list price when present.
|
| 1042 |
+
var wSaleEl = doc.querySelector('p.price.product-page-price .woocommerce-Price-amount bdi, .price .woocommerce-Price-amount bdi');
|
| 1043 |
+
var wAmounts = [];
|
| 1044 |
+
if (wSaleEl) wAmounts.push(wSaleEl.textContent);
|
| 1045 |
+
doc.querySelectorAll('p.price .woocommerce-Price-amount bdi, .summary .price .woocommerce-Price-amount bdi').forEach(function (b) { wAmounts.push(b.textContent); });
|
| 1046 |
+
var wNums = [];
|
| 1047 |
+
for (var wAi = 0; wAi < wAmounts.length; wAi++) { var wN = parseInt(String(wAmounts[wAi]).replace(/[^\d]/g, ''), 10); if (wN && wN > 1000) wNums.push(wN); }
|
| 1048 |
+
if (wNums.length) {
|
| 1049 |
+
// Multiple prices: the LOWER is almost always the sale (đang KM), HIGHER is list.
|
| 1050 |
+
var wMin = Math.min.apply(null, wNums), wMax = Math.max.apply(null, wNums);
|
| 1051 |
+
priceInfo.salePn = wMin;
|
| 1052 |
+
priceInfo.listPn = (wMax !== wMin) ? wMax : 0;
|
| 1053 |
+
priceInfo.pn = wMin;
|
| 1054 |
+
priceInfo.salePrice = money(wMin);
|
| 1055 |
+
priceInfo.price = money(wMin);
|
| 1056 |
+
priceInfo.listPrice = wMax !== wMin ? money(wMax) : '';
|
| 1057 |
+
}
|
| 1058 |
+
// SKU: WooCommerce button data-product_sku / variation, else data-product_id,
|
| 1059 |
+
// else match "Mã BN7546" / "Mã số" text on the page.
|
| 1060 |
+
var wSkuEl = doc.querySelector('[data-product_sku][data-product_sku!=""]');
|
| 1061 |
+
if (wSkuEl && wSkuEl.getAttribute('data-product_sku')) sku = wSkuEl.getAttribute('data-product_sku');
|
| 1062 |
+
if (!sku) { var wPid = doc.querySelector('[data-product_id]'); if (wPid && wPid.getAttribute('data-product_id') && !wPid.getAttribute('data-product_sku')) sku = wPid.getAttribute('data-product_id'); }
|
| 1063 |
+
if (!sku) { var wMsku = html.match(/Mã\s+(BN\s*[\w-]+|[\w]{3,})/i); if (wMsku) sku = String(wMsku[1]).replace(/\s+/g, '').toUpperCase(); }
|
| 1064 |
+
// GALLERY: WooCommerce main image + gallery thumbs (data-src/src), all webp/jpg/png under /wp-content/uploads/.
|
| 1065 |
+
var wGal = [];
|
| 1066 |
+
var wMainImg = doc.querySelector('div.woocommerce-product-gallery__wrapper img, figure.woocommerce-product-gallery__wrapper img, .woocommerce-product-gallery img.wp-post-image');
|
| 1067 |
+
var wPrimary = '';
|
| 1068 |
+
if (wMainImg) { wPrimary = cleanUrl(wMainImg.getAttribute('src') || wMainImg.getAttribute('data-src') || '', pageUrl); }
|
| 1069 |
+
if (wPrimary && wPrimary.indexOf('wp-content/uploads') >= 0) { if (wGal.indexOf(wPrimary) < 0) wGal.push(wPrimary); }
|
| 1070 |
+
doc.querySelectorAll('div.woocommerce-product-gallery__wrapper img, figure.woocommerce-product-gallery__wrapper img').forEach(function (im) {
|
| 1071 |
+
var s = im.getAttribute('data-src') || im.getAttribute('src') || '';
|
| 1072 |
+
var cu = cleanUrl(s, pageUrl);
|
| 1073 |
+
if (cu && cu.indexOf('wp-content/uploads') >= 0 && wGal.indexOf(cu) < 0) wGal.push(cu);
|
| 1074 |
+
});
|
| 1075 |
+
// Only adopt the WooCommerce gallery if it found at least one product image.
|
| 1076 |
+
if (wGal.length) { gallery = wGal; if (wPrimary && gallery.indexOf(wPrimary) < 0) gallery.unshift(wPrimary); primaryImg = wPrimary || gallery[0]; }
|
| 1077 |
+
// DESCRIPTION: WooCommerce tab "description" panel text.
|
| 1078 |
+
var wDescEl = doc.querySelector('.woocommerce-Tabs-panel--description, #tab-description, #tab-description .panel');
|
| 1079 |
+
if (wDescEl) {
|
| 1080 |
+
var wDt = (wDescEl.textContent || '').replace(/\s+/g, ' ').trim();
|
| 1081 |
+
if (wDt.length > 60) desc = wDt.slice(0, 8000);
|
| 1082 |
+
}
|
| 1083 |
+
// SPECS: WooCommerce "additional_information" table.
|
| 1084 |
+
var wAdd = doc.querySelector('.woocommerce-Tabs-panel--additional_information table.shop_attributes, #tab-additional_information table.shop_attributes, table.shop_attributes');
|
| 1085 |
+
if (wAdd) {
|
| 1086 |
+
var wSp = {};
|
| 1087 |
+
wAdd.querySelectorAll('tr').forEach(function (tr) {
|
| 1088 |
+
var tds = tr.querySelectorAll('th, td');
|
| 1089 |
+
if (tds.length >= 2) {
|
| 1090 |
+
var wk = (tds[0].textContent || '').replace(/\s+/g, ' ').trim().replace(/:$/, '');
|
| 1091 |
+
var wv = (tds[1].textContent || '').replace(/\s+/g, ' ').trim();
|
| 1092 |
+
if (wk && wv && ['Mã sản phẩm', 'Thương hiệu', 'Giá'].indexOf(wk) < 0 && !wSp[wk]) wSp[wk] = wv;
|
| 1093 |
+
}
|
| 1094 |
+
});
|
| 1095 |
+
if (Object.keys(wSp).length) specs = wSp;
|
| 1096 |
+
}
|
| 1097 |
+
// CATEGORY: JSON-LD category, else breadcrumb.
|
| 1098 |
+
if (ld && ld.category && String(ld.category).trim() && String(ld.category).trim().length <= 80) {
|
| 1099 |
+
// prefer breadcrumb "Chậu rửa bát" as it's cleaner than JSON-LD "Chậu rửa bát" (same here)
|
| 1100 |
+
}
|
| 1101 |
+
}
|
| 1102 |
// ── dienmayxanh.com override (v2024 layout) ──
|
| 1103 |
if (/dienmayxanh\.com/i.test(pageUrl || '')) {
|
| 1104 |
// NAME: h1 direct text only
|
|
|
|
| 1401 |
cs: 'san-pham-them-moi', ci: 'fa-box',
|
| 1402 |
sum: r.desc, summary: r.desc, desc: r.desc,
|
| 1403 |
specs: r.specs, feats: featsArr, brand: brand,
|
| 1404 |
+
l: prev.pageUrl || '', link: prev.pageUrl || '', url: prev.pageUrl || '',
|
| 1405 |
_sourceUrl: prev.pageUrl, _url_added: true
|
| 1406 |
};
|
| 1407 |
// 1) local durable list + live catalog injection (always works)
|
src/vaix-combo.js
CHANGED
|
@@ -69,11 +69,14 @@
|
|
| 69 |
var pEl = document.getElementById("detail-price");
|
| 70 |
if (pEl) pEl.innerHTML = '<span style="color:#1d4ed8;font-size:1.3rem;font-weight:800">' + fmt(priceNum) + '</span>';
|
| 71 |
var ie = document.getElementById("detail-images");
|
|
|
|
|
|
|
|
|
|
| 72 |
if (ie && c.items && c.items.length) {
|
| 73 |
ie.innerHTML = c.items.map(function(it) {
|
| 74 |
var _iu = it.image || "";
|
| 75 |
if (!_iu) { try { var _pr = window.vaix && window.vaix.findProduct ? window.vaix.findProduct(String(it.sku || "")) : null; _iu = (_pr && _pr.image) || ""; } catch (e4) {} }
|
| 76 |
-
return '<div style="flex:1;min-width:90px;display:flex;flex-direction:column;align-items:center;gap:4px"><img src="' + (_iu
|
| 77 |
}).join("");
|
| 78 |
} else if (ie) {
|
| 79 |
ie.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;width:100%;padding:16px;background:linear-gradient(135deg,#eff6ff,#dbeafe);border-radius:12px;font-size:2rem">🧩</div>';
|
|
|
|
| 69 |
var pEl = document.getElementById("detail-price");
|
| 70 |
if (pEl) pEl.innerHTML = '<span style="color:#1d4ed8;font-size:1.3rem;font-weight:800">' + fmt(priceNum) + '</span>';
|
| 71 |
var ie = document.getElementById("detail-images");
|
| 72 |
+
// Route item images through the same-origin proxy (like the main list) so
|
| 73 |
+
// external CDN photos always render on the combo detail modal.
|
| 74 |
+
var _px = (typeof window.primaryImgSrc === 'function') ? window.primaryImgSrc : function(u){ return u; };
|
| 75 |
if (ie && c.items && c.items.length) {
|
| 76 |
ie.innerHTML = c.items.map(function(it) {
|
| 77 |
var _iu = it.image || "";
|
| 78 |
if (!_iu) { try { var _pr = window.vaix && window.vaix.findProduct ? window.vaix.findProduct(String(it.sku || "")) : null; _iu = (_pr && _pr.image) || ""; } catch (e4) {} }
|
| 79 |
+
return '<div style="flex:1;min-width:90px;display:flex;flex-direction:column;align-items:center;gap:4px"><img src="' + (_iu ? _px(_iu) : "https://huggingface.co/datasets/bep40/grob-products-updated/resolve/main/placeholder.png") + '" onerror="this.style.display=\'none\'" style="width:70px;height:70px;border-radius:10px;object-fit:cover;background:#f1f5f9"><div style="font-size:0.62rem;color:#475569;text-align:center;max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + (it.name || "") + '</div></div>';
|
| 80 |
}).join("");
|
| 81 |
} else if (ie) {
|
| 82 |
ie.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;width:100%;padding:16px;background:linear-gradient(135deg,#eff6ff,#dbeafe);border-radius:12px;font-size:2rem">🧩</div>';
|
src/vaix-flashsale.js
CHANGED
|
@@ -43,7 +43,7 @@
|
|
| 43 |
image: p.image || (p.images && p.images[0]) || "",
|
| 44 |
listPn: listN, salePn: saleN,
|
| 45 |
discountNum: disc, discount: disc + "%",
|
| 46 |
-
slug: p.slug || "", catalog: true
|
| 47 |
});
|
| 48 |
}
|
| 49 |
return out;
|
|
@@ -129,6 +129,26 @@
|
|
| 129 |
}
|
| 130 |
|
| 131 |
function openDetail(p) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
var overlay = document.getElementById("vaistudio-detail-overlay");
|
| 133 |
var modal = document.getElementById("vaistudio-detail-modal");
|
| 134 |
if (!overlay || !modal) return;
|
|
@@ -147,13 +167,15 @@
|
|
| 147 |
"</div>";
|
| 148 |
}
|
| 149 |
if (imagesEl) {
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
|
|
|
|
|
|
| 153 |
: '<div style="flex:1;min-width:120px;height:120px;background:linear-gradient(135deg,#fff7ed,#ffedd5);border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:2.5rem">⚡</div>';
|
| 154 |
}
|
| 155 |
-
if (overlay) overlay.classList.
|
| 156 |
-
if (modal) modal.classList.
|
| 157 |
if (window.VAI_modalTop) window.VAI_modalTop(overlay);
|
| 158 |
} catch (e) {}
|
| 159 |
}
|
|
|
|
| 43 |
image: p.image || (p.images && p.images[0]) || "",
|
| 44 |
listPn: listN, salePn: saleN,
|
| 45 |
discountNum: disc, discount: disc + "%",
|
| 46 |
+
slug: p.slug || "", catalog: true, _full: p
|
| 47 |
});
|
| 48 |
}
|
| 49 |
return out;
|
|
|
|
| 129 |
}
|
| 130 |
|
| 131 |
function openDetail(p) {
|
| 132 |
+
// Prefer the shared catalog detail renderer (full gallery, description,
|
| 133 |
+
// specs, features — identical to the "danh sách SP chính" modal) when the
|
| 134 |
+
// flashsale item came from the catalog (carries the full product in _full).
|
| 135 |
+
try {
|
| 136 |
+
if (p && p._full && typeof window.vaix === 'object' && window.vaix && typeof window.vaix.renderDetail === 'function') {
|
| 137 |
+
var _full = p._full;
|
| 138 |
+
// Honor listPn/salePn from the flashsale build in case price fields on
|
| 139 |
+
// the record differ, so the detail keeps the 2-giá price row.
|
| 140 |
+
var _p = Object.assign({}, _full);
|
| 141 |
+
if (_p.listPn === undefined || _p.salePn === undefined) {
|
| 142 |
+
_p.listPn = p.listPn; _p.salePn = p.salePn; _p.priceMode = 'both';
|
| 143 |
+
}
|
| 144 |
+
_p._flashsaleShow = true;
|
| 145 |
+
window.vaix.renderDetail(_p);
|
| 146 |
+
var ov = document.getElementById("vaistudio-detail-overlay");
|
| 147 |
+
if (ov) { ov.classList.remove('show'); ov.style.display = 'flex'; }
|
| 148 |
+
if (window.VAI_modalTop && ov) window.VAI_modalTop(ov);
|
| 149 |
+
return;
|
| 150 |
+
}
|
| 151 |
+
} catch (e) {}
|
| 152 |
var overlay = document.getElementById("vaistudio-detail-overlay");
|
| 153 |
var modal = document.getElementById("vaistudio-detail-modal");
|
| 154 |
if (!overlay || !modal) return;
|
|
|
|
| 167 |
"</div>";
|
| 168 |
}
|
| 169 |
if (imagesEl) {
|
| 170 |
+
// Full gallery, routed through the same-origin proxy like the main list.
|
| 171 |
+
var gal = (p.image ? [p.image] : []).concat(Array.isArray(p._full && p._full.images) ? p._full.images : (Array.isArray(p.images) ? p.images : []));
|
| 172 |
+
var uni = []; for (var gi = 0; gi < gal.length; gi++) { if (gal[gi] && uni.indexOf(gal[gi]) < 0) uni.push(gal[gi]); }
|
| 173 |
+
imagesEl.innerHTML = uni.length
|
| 174 |
+
? uni.map(function (u) { var pu = (typeof window.primaryImgSrc === 'function' ? window.primaryImgSrc(u) : u); return '<img src="' + pu + '" alt="' + (p.code || "") + '" style="flex:1;min-width:120px;height:120px;border-radius:10px;object-fit:cover;border:1px solid #e2e8f0;background:#f1f5f9" onerror="this.style.display=\'none\'">'; }).join("")
|
| 175 |
: '<div style="flex:1;min-width:120px;height:120px;background:linear-gradient(135deg,#fff7ed,#ffedd5);border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:2.5rem">⚡</div>';
|
| 176 |
}
|
| 177 |
+
if (overlay) { overlay.classList.remove("show"); overlay.style.display = "flex"; }
|
| 178 |
+
if (modal) { modal.classList.remove("show"); modal.style.display = "block"; }
|
| 179 |
if (window.VAI_modalTop) window.VAI_modalTop(overlay);
|
| 180 |
} catch (e) {}
|
| 181 |
}
|
src/vaix-rag.js
CHANGED
|
@@ -23,6 +23,12 @@ function primaryImgSrc(u) {
|
|
| 23 |
// Robust error handling for product <img>: on failure, hide the image (and an
|
| 24 |
// optional wrapping element) so an empty/broken box never shows. Guarantees no
|
| 25 |
// broken-image icon.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
function bindImgHideOnError(img, wrap) {
|
| 27 |
if (!img) return;
|
| 28 |
if (img.getAttribute("data-hide-bound")) return;
|
|
@@ -2182,7 +2188,7 @@ let src = p.image || (Array.isArray(p.images) && p.images[0] ? p.images[0] : '')
|
|
| 2182 |
}
|
| 2183 |
ic.innerHTML = "";
|
| 2184 |
if (!imgs.length) { const d = document.createElement("div"); d.style.cssText = "width:120px;height:120px;background:#f1f5f9;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:2rem;flex-shrink:0"; d.textContent = "📦"; ic.appendChild(d); }
|
| 2185 |
-
else { imgs.forEach(function(url, idx){ const wrapper = document.createElement("div"); wrapper.style.cssText = "position:relative;flex-shrink:0;scroll-snap-align:start;cursor:zoom-in"; wrapper.title = "Nhấn để xem ảnh toàn màn hình"; const img = document.createElement("img"); img.src = url; img.style.cssText = "width:120px;height:120px;border-radius:12px;object-fit:cover;border:1px solid #e2e8f0;transition:transform 0.2s, box-shadow 0.2s"; img.onerror = function(){ this.style.display = "none"; }; img.onmouseover = function(){ this.style.transform = "scale(1.05)"; this.style.boxShadow = "0 4px 12px rgba(0,0,0,0.15)"; }; img.onmouseout = function(){ this.style.transform = ""; this.style.boxShadow = ""; }; img.addEventListener("click", function(e){ e.stopPropagation(); var mImg = document.getElementById("detail-main-img"); if (mImg && url && url !== mImg.src) mImg.src = url; openImageViewer(url, p.title_clean, imgs); }); wrapper.appendChild(img); const zoomIcon = document.createElement("div"); zoomIcon.textContent = "🔍"; zoomIcon.style.cssText = "position:absolute;bottom:4px;right:4px;font-size:0.7rem;background:rgba(0,0,0,0.5);border-radius:50%;width:22px;height:22px;display:flex;align-items:center;justify-content:center;pointer-events:none;opacity:0.7"; wrapper.appendChild(zoomIcon); ic.appendChild(wrapper); }); }
|
| 2186 |
}
|
| 2187 |
|
| 2188 |
// Global handler: open the fullscreen image viewer from the detail modal's large
|
|
|
|
| 23 |
// Robust error handling for product <img>: on failure, hide the image (and an
|
| 24 |
// optional wrapping element) so an empty/broken box never shows. Guarantees no
|
| 25 |
// broken-image icon.
|
| 26 |
+
// Expose the proxy helpers globally so BIGSALE/FLASHSALE/COMBO panels and other
|
| 27 |
+
// modules can route every external CDN image through the same-origin proxy
|
| 28 |
+
// (identical behaviour to the main product list — fixes images disappearing on
|
| 29 |
+
// promo detail modals).
|
| 30 |
+
window.primaryImgSrc = primaryImgSrc;
|
| 31 |
+
window.proxyImgUrl = proxyImgUrl;
|
| 32 |
function bindImgHideOnError(img, wrap) {
|
| 33 |
if (!img) return;
|
| 34 |
if (img.getAttribute("data-hide-bound")) return;
|
|
|
|
| 2188 |
}
|
| 2189 |
ic.innerHTML = "";
|
| 2190 |
if (!imgs.length) { const d = document.createElement("div"); d.style.cssText = "width:120px;height:120px;background:#f1f5f9;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:2rem;flex-shrink:0"; d.textContent = "📦"; ic.appendChild(d); }
|
| 2191 |
+
else { imgs.forEach(function(url, idx){ const wrapper = document.createElement("div"); wrapper.style.cssText = "position:relative;flex-shrink:0;scroll-snap-align:start;cursor:zoom-in"; wrapper.title = "Nhấn để xem ảnh toàn màn hình"; const img = document.createElement("img"); img.src = primaryImgSrc(url); img.style.cssText = "width:120px;height:120px;border-radius:12px;object-fit:cover;border:1px solid #e2e8f0;transition:transform 0.2s, box-shadow 0.2s"; img.onerror = function(){ this.style.display = "none"; }; img.onmouseover = function(){ this.style.transform = "scale(1.05)"; this.style.boxShadow = "0 4px 12px rgba(0,0,0,0.15)"; }; img.onmouseout = function(){ this.style.transform = ""; this.style.boxShadow = ""; }; img.addEventListener("click", function(e){ e.stopPropagation(); var mImg = document.getElementById("detail-main-img"); if (mImg && url && url !== mImg.src) mImg.src = primaryImgSrc(url); openImageViewer(url, p.title_clean, imgs); }); wrapper.appendChild(img); const zoomIcon = document.createElement("div"); zoomIcon.textContent = "🔍"; zoomIcon.style.cssText = "position:absolute;bottom:4px;right:4px;font-size:0.7rem;background:rgba(0,0,0,0.5);border-radius:50%;width:22px;height:22px;display:flex;align-items:center;justify-content:center;pointer-events:none;opacity:0.7"; wrapper.appendChild(zoomIcon); ic.appendChild(wrapper); }); }
|
| 2192 |
}
|
| 2193 |
|
| 2194 |
// Global handler: open the fullscreen image viewer from the detail modal's large
|
src/version.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
// V.AI AVATAR — version banner (used for cache-busting / diagnostics)
|
| 2 |
-
window.VAI_AVATAR_VERSION = "
|
| 3 |
window.VAI_AVATAR_CACHE = "v27-gc-" + Date.now();
|
|
|
|
| 1 |
// V.AI AVATAR — version banner (used for cache-busting / diagnostics)
|
| 2 |
+
window.VAI_AVATAR_VERSION = "v32-promo-fix-live";
|
| 3 |
window.VAI_AVATAR_CACHE = "v27-gc-" + Date.now();
|