V.AISTUDIO / product-editor.js
bep40's picture
v7: Monkey-patch showDetail() to guarantee edit icons. Simple prompt-based edit. 100% reliable.
72240ba verified
/**
* V.AI STUDIO — Product Editor v7
*
* Approach: Monkey-patch showDetail() để CHẮC CHẮN inject edit icons
* sau khi detail content được render. Không dùng MutationObserver.
*
* Edit bằng prompt() — đơn giản, hoạt động mọi browser.
*/
(function(){
'use strict';
var AUTH_USER='V.AISTUDIO',AUTH_PASS='Khongbiet';
var EDITS_KEY='vai_product_edits',CUSTOM_KEY='vai_custom_products';
function getEdits(){try{return JSON.parse(localStorage.getItem(EDITS_KEY)||'{}');}catch(e){return {};}}
function saveEdits(e){localStorage.setItem(EDITS_KEY,JSON.stringify(e));}
function getCustom(){try{return JSON.parse(localStorage.getItem(CUSTOM_KEY)||'[]');}catch(e){return [];}}
function saveCustom(a){localStorage.setItem(CUSTOM_KEY,JSON.stringify(a));}
function getD(){return window.D||[];}
function checkAuth(){return sessionStorage.getItem('vai_editor_auth')==='1';}
function doLogin(cb){
if(checkAuth()){cb();return;}
var pw=prompt('🔐 Nhập mật khẩu quản trị:');
if(pw===AUTH_PASS){sessionStorage.setItem('vai_editor_auth','1');cb();}
else if(pw!==null)alert('Sai mật khẩu');
}
// === Apply saved edits to D[] on load ===
function applyEdits(){
var D=getD();if(!D.length)return;
var edits=getEdits(),applied=0;
for(var i=0;i<D.length;i++){
var p=D[i],edit=edits[p.slug];if(!edit)continue;
if(edit.price!==undefined){p.priceNum=edit.price;p.price=edit.price.toLocaleString('vi-VN')+'đ';}
if(edit.image){p.image=edit.image;if(p.images&&p.images.length)p.images[0]=edit.image;else p.images=[edit.image];}
if(edit.name)p.name=edit.name;
if(edit.model)p.model=edit.model;
applied++;
}
var customs=getCustom();
if(customs.length){
var slugs={};D.forEach(function(p){if(p.slug)slugs[p.slug]=true;});
customs.forEach(function(cp){if(!slugs[cp.slug]){D.push(cp);slugs[cp.slug]=true;}});
}
if(applied||customs.length)console.log('[Editor] Applied '+applied+' edits, '+customs.length+' custom');
}
// === MONKEY-PATCH showDetail — inject edit icons after render ===
function patchShowDetail(){
if(typeof window.showDetail!=='function')return false;
if(window._showDetailPatched)return true;
var original=window.showDetail;
window.showDetail=function(idx){
original.apply(this,arguments);
// After original sets innerHTML, inject edit icons
setTimeout(function(){injectIcons(idx);},100);
};
window._showDetailPatched=true;
console.log('[Editor] showDetail patched ✓');
return true;
}
// === Inject edit icons into current detail view ===
function injectIcons(idx){
var dv=document.getElementById('detailView');
if(!dv)return;
var p=getD()[idx];
if(!p)return;
var ICON='<span class="vai-eic" style="display:inline-block;margin-left:8px;cursor:pointer;font-size:13px;opacity:0.5;transition:opacity .15s" onmouseenter="this.style.opacity=1" onmouseleave="this.style.opacity=0.5">✏️</span>';
// Add icon to price
var priceEl=dv.querySelector('.detail-price');
if(priceEl&&!priceEl.querySelector('.vai-eic')){
priceEl.insertAdjacentHTML('beforeend',ICON);
priceEl.querySelector('.vai-eic').onclick=function(e){
e.stopPropagation();
doLogin(function(){
var val=prompt('💰 Nhập giá mới (số):',String(p.priceNum||0));
if(val===null)return;
var num=parseInt(val.replace(/\D/g,''))||0;
if(num>0){
p.priceNum=num;p.price=num.toLocaleString('vi-VN')+'đ';
var edits=getEdits();if(!edits[p.slug])edits[p.slug]={};
edits[p.slug].price=num;saveEdits(edits);
priceEl.firstChild.textContent=p.price;
}
});
};
}
// Add icon to name
var nameEl=dv.querySelector('.detail-name');
if(nameEl&&!nameEl.querySelector('.vai-eic')){
nameEl.insertAdjacentHTML('beforeend',ICON);
nameEl.querySelector('.vai-eic').onclick=function(e){
e.stopPropagation();
doLogin(function(){
var val=prompt('📝 Tên sản phẩm:',p.name||'');
if(val===null||!val.trim())return;
p.name=val.trim();
var edits=getEdits();if(!edits[p.slug])edits[p.slug]={};
edits[p.slug].name=val.trim();saveEdits(edits);
nameEl.firstChild.textContent=val.trim();
});
};
}
// Add icon on gallery (for image edit)
var gallery=dv.querySelector('.gallery,.gallery-carousel');
if(gallery&&!gallery.querySelector('.vai-eic')){
var imgBtn=document.createElement('span');
imgBtn.className='vai-eic';
imgBtn.textContent='✏️';
imgBtn.style.cssText='position:absolute;top:10px;right:10px;z-index:50;background:rgba(255,255,255,.92);padding:5px 8px;border-radius:8px;cursor:pointer;font-size:15px;box-shadow:0 2px 8px rgba(0,0,0,.15);opacity:0.7';
imgBtn.onmouseenter=function(){this.style.opacity='1';};
imgBtn.onmouseleave=function(){this.style.opacity='0.7';};
imgBtn.onclick=function(e){
e.stopPropagation();
doLogin(function(){
var val=prompt('🖼️ URL hình ảnh mới:',p.image||'');
if(val===null||!val.trim())return;
p.image=val.trim();
if(p.images&&p.images.length)p.images[0]=val.trim();
var edits=getEdits();if(!edits[p.slug])edits[p.slug]={};
edits[p.slug].image=val.trim();saveEdits(edits);
var img=dv.querySelector('.gallery-slide img,img');
if(img)img.src=val.trim();
});
};
gallery.style.position='relative';
gallery.appendChild(imgBtn);
}
}
// === Thêm SP ===
var PROXIES=[function(u){return 'https://api.allorigins.win/get?url='+encodeURIComponent(u);}];
function openAddUrl(){
var o=document.getElementById('vai-addurl');if(o)o.remove();
var d=document.createElement('div');d.id='vai-addurl';
d.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:99999;display:flex;align-items:center;justify-content:center;padding:16px';
d.onclick=function(e){if(e.target===d)d.remove();};
d.innerHTML='<div style="background:#fff;border-radius:14px;max-width:420px;width:100%;overflow:hidden"><div style="padding:12px 16px;background:#0369a1;color:#fff;font-weight:800;font-size:13px">➕ Thêm SP</div><div style="padding:14px"><div style="display:flex;gap:4px;margin-bottom:8px"><input id="va-url" placeholder="URL sản phẩm (tuỳ chọn)" style="flex:1;padding:7px;border:1px solid #ddd;border-radius:6px;font-size:11px"><button id="va-fetch" style="padding:7px 10px;background:#0369a1;color:#fff;border:none;border-radius:6px;font-size:11px;cursor:pointer">Crawl</button></div><div id="va-st" style="font-size:10px;color:#64748b;margin-bottom:6px"></div><input id="va-name" placeholder="Tên SP *" style="width:100%;padding:7px;border:1px solid #ddd;border-radius:6px;font-size:11px;margin-bottom:5px;box-sizing:border-box"><input id="va-model" placeholder="Model/Mã" style="width:100%;padding:7px;border:1px solid #ddd;border-radius:6px;font-size:11px;margin-bottom:5px;box-sizing:border-box"><input id="va-price" placeholder="Giá (VNĐ)" style="width:100%;padding:7px;border:1px solid #ddd;border-radius:6px;font-size:11px;margin-bottom:5px;box-sizing:border-box"><input id="va-img" placeholder="URL hình ảnh" style="width:100%;padding:7px;border:1px solid #ddd;border-radius:6px;font-size:11px;margin-bottom:5px;box-sizing:border-box"><input id="va-brand" placeholder="Thương hiệu" style="width:100%;padding:7px;border:1px solid #ddd;border-radius:6px;font-size:11px;margin-bottom:10px;box-sizing:border-box"><div style="display:flex;gap:6px"><button id="va-save" style="flex:1;padding:9px;background:#059669;color:#fff;border:none;border-radius:7px;font-weight:700;cursor:pointer">Thêm vào kho</button><button id="va-x" style="padding:9px 14px;background:#e2e8f0;border:none;border-radius:7px;cursor:pointer">✕</button></div></div></div>';
document.body.appendChild(d);
document.getElementById('va-x').onclick=function(){d.remove();};
document.getElementById('va-fetch').onclick=function(){
var url=document.getElementById('va-url').value.trim();if(!url)return;
document.getElementById('va-st').textContent='Đang crawl...';
fetch(PROXIES[0](url)).then(function(r){return r.json();}).then(function(j){
var html=j.contents||'';if(!html){document.getElementById('va-st').textContent='Lỗi';return;}
var doc=(new DOMParser()).parseFromString(html,'text/html');
var h1=doc.querySelector('h1');if(h1)document.getElementById('va-name').value=h1.textContent.trim();
var og=doc.querySelector('meta[property="og:image"]');if(og)document.getElementById('va-img').value=og.content||'';
var pr=doc.querySelector('[itemprop="price"],.price,[class*="price"]');if(pr){var n=(pr.content||pr.textContent||'').replace(/[^\d]/g,'');if(n&&parseInt(n)>1000)document.getElementById('va-price').value=n;}
document.getElementById('va-st').textContent='✓ OK';
}).catch(function(){document.getElementById('va-st').textContent='Lỗi crawl';});
};
document.getElementById('va-save').onclick=function(){
var name=document.getElementById('va-name').value.trim();if(!name){alert('Nhập tên SP');return;}
var model=document.getElementById('va-model').value.trim();
var price=parseInt((document.getElementById('va-price').value||'').replace(/\D/g,''))||0;
var image=document.getElementById('va-img').value.trim();
var brand=document.getElementById('va-brand').value.trim();
var slug='c'+Date.now();
var _idx=(model+' '+brand+' '+name).toLowerCase();
if(typeof buildSearchIndex==='function')try{_idx=buildSearchIndex({name:name,model:model,sku:model,brand:brand,cat:'custom',specs:{}});}catch(e){}
var np={name:name,model:model,sku:model,slug:slug,price:price?price.toLocaleString('vi-VN')+'đ':'Liên hệ',priceNum:price,discPrice:price,image:image,images:image?[image]:[],link:document.getElementById('va-url').value.trim(),cat:'custom',catSlug:'custom',brand:brand,_idx:_idx};
getD().push(np);var c=getCustom();c.push(np);saveCustom(c);
d.remove();
if(typeof init==='function')try{init();}catch(e){}
alert('✅ Đã thêm: '+name);
};
}
// === FAB button ===
function injectFAB(){
if(document.getElementById('vai-fab'))return;
var b=document.createElement('button');b.id='vai-fab';
b.style.cssText='position:fixed;bottom:20px;left:20px;z-index:9000;width:46px;height:46px;border-radius:50%;background:linear-gradient(135deg,#0369a1,#059669);color:#fff;border:none;font-size:22px;cursor:pointer;box-shadow:0 4px 14px rgba(0,0,0,.25);transition:transform .2s';
b.textContent='+';
b.title='Thêm sản phẩm';
b.onmouseenter=function(){b.style.transform='scale(1.1)';};
b.onmouseleave=function(){b.style.transform='';};
b.onclick=function(){doLogin(openAddUrl);};
document.body.appendChild(b);
}
// === INIT ===
// Wait for D[] and showDetail to exist, then patch
var _patchInterval=setInterval(function(){
if(getD().length>50&&typeof window.showDetail==='function'){
clearInterval(_patchInterval);
applyEdits();
patchShowDetail();
}
},300);
setTimeout(function(){clearInterval(_patchInterval);},30000);
setTimeout(injectFAB,1000);
window.VAI_EDITOR={add:openAddUrl,patch:patchShowDetail};
console.log('[Editor v7] ready');
})();