Download excel-fix-v2.js from bep40/V.AISTUDIO: direct link, hf CLI and curl.
- Browser
- Download file 16.4 kB
-
https://huggingface.co/bep40/V.AISTUDIO/resolve/main/excel-fix-v2.js
- Command line
-
hf download hf://bep40/V.AISTUDIO/excel-fix-v2.js
-
curl -L -o excel-fix-v2.js https://huggingface.co/bep40/V.AISTUDIO/resolve/main/excel-fix-v2.js
16.4 kB
| /** | |
| * V.AI STUDIO - Excel Export Fix Ultimate v2.0 | |
| * ============================================ | |
| * FIX 100%: Multi-download Excel for Quote & Order modals | |
| * | |
| * Key fixes: | |
| * 1. Global blob URL registry - prevents premature URL.revokeObjectURL | |
| * 2. Creates fresh blob for each download | |
| * 3. Proper cleanup on page unload only | |
| * 4. Click protection to prevent double-clicks | |
| */ | |
| (function(){ | |
| ; | |
| // ===== GLOBAL STATE ===== | |
| window.__VAI_EXCEL_FIX = window.__VAI_EXCEL_FIX || { | |
| blobRegistry: [], | |
| isUnloading: false | |
| }; | |
| var g = window.__VAI_EXCEL_FIX; | |
| // ===== CLEANUP ON UNLOAD ===== | |
| function cleanupAllBlobs() { | |
| g.isUnloading = true; | |
| g.blobRegistry.forEach(function(entry) { | |
| if (!entry.revoked && entry.url) { | |
| try { URL.revokeObjectURL(entry.url); entry.revoked = true; } catch(e) {} | |
| } | |
| }); | |
| g.blobRegistry = []; | |
| } | |
| window.addEventListener('pagehide', cleanupAllBlobs); | |
| window.addEventListener('beforeunload', cleanupAllBlobs); | |
| window.addEventListener('unload', cleanupAllBlobs); | |
| // ===== PATCH URL.revokeObjectURL - CRITICAL ===== | |
| if (!window.__VAI_REVOKE_PATCHED) { | |
| var originalRevoke = URL.revokeObjectURL; | |
| URL.revokeObjectURL = function(url) { | |
| // Only revoke on unload or non-blob URLs | |
| if (g.isUnloading || !url || typeof url !== 'string' || !url.startsWith('blob:')) { | |
| return originalRevoke.call(this, url); | |
| } | |
| // Register but DON'T revoke - this is the key fix for multi-download | |
| var exists = g.blobRegistry.find(function(e) { return e.url === url; }); | |
| if (!exists) { | |
| g.blobRegistry.push({ url: url, revoked: false, created: Date.now() }); | |
| } | |
| return undefined; | |
| }; | |
| window.__VAI_REVOKE_PATCHED = true; | |
| } | |
| // ===== UTILITIES ===== | |
| function fmt(n) { | |
| if (!n || isNaN(n)) return '0đ'; | |
| return Number(n).toLocaleString('vi-VN') + 'đ'; | |
| } | |
| function stripDiacritics(s) { | |
| return s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[Đđ]/g, 'D'); | |
| } | |
| function generateOrderCode(name) { | |
| var d = new Date(); | |
| var dd = String(d.getDate()).padStart(2, '0'); | |
| var mm = String(d.getMonth() + 1).padStart(2, '0'); | |
| var yy = String(d.getFullYear()).slice(-2); | |
| var ini = ''; | |
| if (name && name.trim()) { | |
| name.trim().split(/\s+/).forEach(function(w) { | |
| if (w) { | |
| var ch = stripDiacritics(w.charAt(0)).toUpperCase(); | |
| if (/[A-Z]/.test(ch)) { | |
| ini += ch; | |
| } | |
| } | |
| }); | |
| } | |
| return 'VAS' + (ini || 'X') + dd + mm + yy; | |
| } | |
| // ===== BUTTON STATE ===== | |
| function setBtnState(btn, exporting) { | |
| if (!btn) return; | |
| if (exporting) { | |
| btn.disabled = true; | |
| btn.dataset.vaiExporting = '1'; | |
| btn.dataset.vaiOrigHTML = btn.innerHTML; | |
| btn.innerHTML = '<span style="opacity:0.7">⏳ Đang xuất...</span>'; | |
| } else { | |
| btn.disabled = false; | |
| btn.dataset.vaiExporting = '0'; | |
| if (btn.dataset.vaiOrigHTML) { | |
| btn.innerHTML = btn.dataset.vaiOrigHTML; | |
| } | |
| } | |
| } | |
| // ===== CREATE DOWNLOAD ===== | |
| function downloadBlob(blob, filename) { | |
| var url = URL.createObjectURL(blob); | |
| g.blobRegistry.push({ url: url, revoked: false }); | |
| var a = document.createElement('a'); | |
| a.href = url; | |
| a.download = filename; | |
| a.style.cssText = 'position:fixed;left:-10000px;top:-10000px;width:1px;height:1px;'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| setTimeout(function() { | |
| try { document.body.removeChild(a); } catch(e) {} | |
| }, 100); | |
| } | |
| // ===== GET QUOTE DATA FROM MODAL ===== | |
| function getQuoteData() { | |
| var data = { | |
| customer: { name: '', phone: '', email: '', addr: '', date: new Date().toLocaleDateString('vi-VN') }, | |
| items: [], | |
| grandTotal: 0 | |
| }; | |
| // Get cart data | |
| var cart = window.cart || []; | |
| var products = window.D || []; | |
| cart.forEach(function(item, i) { | |
| var product = products[item.idx] || (products.find ? products.find(function(p) { return p.id === item.id; }) : null); | |
| var price = item.priceNum || item.price || (product ? product.price : 0) || 0; | |
| data.items.push({ | |
| stt: i + 1, | |
| image: product ? product.img : '', | |
| name: product ? (product.name || product.n || '') : (item.name || ''), | |
| model: product ? (product.sku || product.mod || '') : (item.sku || item.model || ''), | |
| specs: product ? (product.info || product.desc || '') : '', | |
| qty: item.qty || 1, | |
| price: price, | |
| discPrice: price, | |
| total: (item.qty || 1) * price, | |
| note: '' | |
| }); | |
| }); | |
| // Get customer info from modal | |
| var nameSelectors = ['#quoteName', '#quoteCustomerName', '#checkoutName']; | |
| for (var j = 0; j < nameSelectors.length; j++) { | |
| var el = document.querySelector(nameSelectors[j]); | |
| if (el && el.value && el.value.trim()) { | |
| data.customer.name = el.value.trim(); | |
| break; | |
| } | |
| } | |
| var phoneEl = document.querySelector('#quotePhone, #checkoutPhone'); | |
| if (phoneEl && phoneEl.value) data.customer.phone = phoneEl.value.trim(); | |
| var emailEl = document.querySelector('#quoteEmail'); | |
| if (emailEl && emailEl.value) data.customer.email = emailEl.value.trim(); | |
| var addrEl = document.querySelector('#quoteAddr, #checkoutAddr'); | |
| if (addrEl && addrEl.value) data.customer.addr = addrEl.value.trim(); | |
| data.grandTotal = data.items.reduce(function(s, i) { return s + (i.total || 0); }, 0); | |
| return data; | |
| } | |
| // ===== EXPORT QUOTE EXCEL ===== | |
| async function exportQuoteExcel(quoteData, code) { | |
| if (typeof ExcelJS === 'undefined') { | |
| if (typeof showToast === 'function') showToast('⚠️ Chưa có thư viện ExcelJS'); | |
| return false; | |
| } | |
| try { | |
| var wb = new ExcelJS.Workbook(); | |
| var ws = wb.addWorksheet('Báo giá'); | |
| ws.views = [{ showGridLines: false }]; | |
| ws.columns = [ | |
| { width: 5 }, { width: 11 }, { width: 28 }, | |
| { width: 13 }, { width: 20 }, { width: 6 }, | |
| { width: 13 }, { width: 13 }, { width: 15 }, { width: 14 } | |
| ]; | |
| ws.mergeCells('A3:J3'); | |
| ws.getCell('A3').value = 'BẢNG BÁO GIÁ'; | |
| ws.getCell('A3').font = { bold: true, size: 18, color: { argb: 'FFDB9815' } }; | |
| ws.getCell('A3').alignment = { horizontal: 'center' }; | |
| ws.getCell('A5').value = 'Khách hàng:'; | |
| ws.mergeCells('B5:E5'); | |
| ws.getCell('B5').value = quoteData.customer?.name || ''; | |
| ws.getCell('H5').value = 'Mã đơn:'; | |
| ws.mergeCells('I5:J5'); | |
| ws.getCell('I5').value = code; | |
| ws.getCell('A6').value = 'SĐT:'; | |
| ws.mergeCells('B6:E6'); | |
| ws.getCell('B6').value = quoteData.customer?.phone || ''; | |
| ws.getCell('H6').value = 'Ngày:'; | |
| ws.mergeCells('I6:J6'); | |
| ws.getCell('I6').value = quoteData.customer?.date || ''; | |
| ws.getCell('A7').value = 'Email:'; | |
| ws.mergeCells('B7:E7'); | |
| ws.getCell('B7').value = quoteData.customer?.email || ''; | |
| ws.getCell('A8').value = 'Địa chỉ:'; | |
| ws.mergeCells('B8:J8'); | |
| ws.getCell('B8').value = quoteData.customer?.addr || ''; | |
| var hr = ws.getRow(10); | |
| ['STT', 'Hình', 'Tên sản phẩm', 'Mã SP', 'Thông tin', 'SL', 'Đơn giá', 'Giá CK', 'Thành tiền', 'Ghi chú'].forEach(function(h, i) { | |
| var c = hr.getCell(i + 1); | |
| c.value = h; | |
| c.font = { bold: true, color: { argb: 'FFFFFFFF' } }; | |
| c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF003F62' } }; | |
| }); | |
| var cr = 11, firstRow = cr; | |
| (quoteData.items || []).forEach(function(it, i) { | |
| var row = ws.getRow(cr); | |
| row.height = 50; | |
| var bg = i % 2 === 0 ? 'FFF8FAFC' : 'FFFFFFFF'; | |
| row.getCell(1).value = it.stt || i + 1; | |
| row.getCell(3).value = it.name || ''; | |
| row.getCell(3).font = { bold: true, size: 9 }; | |
| row.getCell(4).value = it.model || ''; | |
| row.getCell(5).value = it.specs || ''; | |
| row.getCell(5).font = { size: 8, color: { argb: 'FF64748B' } }; | |
| row.getCell(6).value = Number(it.qty || 1); | |
| row.getCell(6).numFmt = '#,##0'; | |
| row.getCell(7).value = Number(it.price || 0); | |
| row.getCell(7).numFmt = '#,##0"đ"'; | |
| row.getCell(8).value = Number(it.discPrice || it.price || 0); | |
| row.getCell(8).numFmt = '#,##0"đ"'; | |
| if ((it.price || 0) > (it.discPrice || 0)) { | |
| row.getCell(8).font = { bold: true, color: { argb: 'FFDC3545' } }; | |
| } | |
| row.getCell(9).value = Number(it.total || 0); | |
| row.getCell(9).numFmt = '#,##0"đ"'; | |
| row.getCell(9).font = { bold: true }; | |
| row.getCell(10).value = it.note || ''; | |
| cr++; | |
| }); | |
| ws.getRow(cr).height = 28; | |
| ws.mergeCells(cr, 1, cr, 5); | |
| ws.getCell('A' + cr).value = 'TỔNG CỘNG'; | |
| ws.getCell('A' + cr).font = { bold: true, size: 13, color: { argb: 'FFFFFFFF' } }; | |
| ws.getCell('A' + cr).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF003F62' } }; | |
| ws.getCell('E' + cr).value = { formula: 'SUM(I' + firstRow + ':I' + (cr - 1) + ')', result: Number(quoteData.grandTotal || 0) }; | |
| ws.getCell('E' + cr).numFmt = '#,##0"đ"'; | |
| ws.getCell('E' + cr).font = { bold: true, size: 14, color: { argb: 'FFF0B840' } }; | |
| wb.calcProperties.fullCalcOnLoad = true; | |
| var buf = await wb.xlsx.writeBuffer(); | |
| var blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); | |
| downloadBlob(blob, code + '.xlsx'); | |
| return true; | |
| } catch (err) { | |
| console.error('[VAI Excel Fix] exportQuoteExcel error:', err); | |
| alert('❌ Lỗi: ' + err.message); | |
| return false; | |
| } | |
| } | |
| // ===== EXPORT DELIVERY EXCEL ===== | |
| async function exportDeliveryExcel(quoteData, code) { | |
| if (typeof ExcelJS === 'undefined') { | |
| if (typeof showToast === 'function') showToast('⚠️ Chưa có thư viện ExcelJS'); | |
| return false; | |
| } | |
| try { | |
| var wb = new ExcelJS.Workbook(); | |
| var ws = wb.addWorksheet('Giao hàng'); | |
| ws.views = [{ showGridLines: false }]; | |
| ws.columns = [ | |
| { width: 5 }, { width: 34 }, { width: 14 }, { width: 8 }, { width: 20 } | |
| ]; | |
| ws.mergeCells('A1:E1'); | |
| ws.getCell('A1').value = 'PHIẾU GIAO HÀNG'; | |
| ws.getCell('A1').font = { bold: true, size: 16, color: { argb: 'FF003F62' } }; | |
| ws.getCell('A1').alignment = { horizontal: 'center' }; | |
| ws.getCell('A2').value = 'KH: ' + (quoteData.customer?.name || ''); | |
| ws.getCell('D2').value = 'Mã: ' + code; | |
| ws.getCell('A3').value = 'SĐT: ' + (quoteData.customer?.phone || ''); | |
| ws.getCell('D3').value = 'Ngày: ' + (quoteData.customer?.date || ''); | |
| ws.getCell('A4').value = 'Địa chỉ: ' + (quoteData.customer?.addr || ''); | |
| var hr = ws.getRow(6); | |
| ['STT', 'Tên sản phẩm', 'Mã SP', 'SL', 'Ghi chú'].forEach(function(h, i) { | |
| var c = hr.getCell(i + 1); | |
| c.value = h; | |
| c.font = { bold: true, color: { argb: 'FFFFFFFF' } }; | |
| c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF003F62' } }; | |
| }); | |
| var cr = 7, totalQty = 0; | |
| (quoteData.items || []).forEach(function(it, i) { | |
| var row = ws.getRow(cr); | |
| row.height = 20; | |
| row.getCell(1).value = it.stt || i + 1; | |
| row.getCell(2).value = it.name || ''; | |
| row.getCell(3).value = it.model || ''; | |
| row.getCell(4).value = it.qty || 1; | |
| row.getCell(5).value = it.note || ''; | |
| totalQty += Number(it.qty || 1); | |
| cr++; | |
| }); | |
| ws.mergeCells(cr, 1, cr, 3); | |
| ws.getCell('A' + cr).value = 'TỔNG CỘNG SỐ LƯỢNG'; | |
| ws.getCell('D' + cr).value = totalQty; | |
| var buf = await wb.xlsx.writeBuffer(); | |
| var blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); | |
| downloadBlob(blob, 'GH-' + code + '.xlsx'); | |
| return true; | |
| } catch (err) { | |
| console.error('[VAI Excel Fix] exportDeliveryExcel error:', err); | |
| alert('❌ Lỗi: ' + err.message); | |
| return false; | |
| } | |
| } | |
| // ===== CLICK HANDLER ===== | |
| function handleExportClick(e) { | |
| var btn = e.target.closest('button, a'); | |
| if (!btn) return; | |
| var txt = (btn.textContent || '').toLowerCase(); | |
| // Quote Excel - "Xuất Excel" button in quote modal | |
| if (txt.includes('excel') && !txt.includes('giao hàng') && !txt.includes('gh-') && !txt.includes('giao')) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| if (btn.dataset.vaiExporting === '1') return; | |
| setBtnState(btn, true); | |
| var qd = getQuoteData(); | |
| var code = generateOrderCode(qd.customer?.name || ''); | |
| exportQuoteExcel(qd, code).then(function(success) { | |
| setBtnState(btn, false); | |
| if (success && typeof showToast === 'function') { | |
| showToast('✅ Excel báo giá tải thành công!'); | |
| } | |
| }); | |
| return false; | |
| } | |
| // Delivery Excel - "GH Excel" button in order modal | |
| if ((txt.includes('gh') || txt.includes('giao hàng')) && txt.includes('excel')) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| if (btn.dataset.vaiExporting === '1') return; | |
| setBtnState(btn, true); | |
| // Check for order modal data | |
| var order = window.VAI_CURRENT_ORDER; | |
| var exportData, orderCode; | |
| if (order && order.items) { | |
| // From order modal | |
| exportData = { | |
| customer: { name: order.customer || '', phone: order.phone || '', addr: order.addr || '', date: order.date || '' }, | |
| items: (order.items || []).map(function(it, i) { | |
| return { | |
| stt: i + 1, | |
| name: it.name || '', | |
| model: it.model || '', | |
| qty: it.qty || 1, | |
| note: it.note || '', | |
| specs: it.specs || '', | |
| price: it.price || 0, | |
| discPrice: it.discPrice || it.price || 0, | |
| total: it.total || 0 | |
| }; | |
| }), | |
| grandTotal: order.grandTotal || 0 | |
| }; | |
| orderCode = order.code || generateOrderCode(order.customer || ''); | |
| } else { | |
| // From cart | |
| exportData = getQuoteData(); | |
| orderCode = generateOrderCode(exportData.customer?.name || ''); | |
| } | |
| exportDeliveryExcel(exportData, orderCode).then(function(success) { | |
| setBtnState(btn, false); | |
| if (success && typeof showToast === 'function') { | |
| showToast('✅ Excel giao hàng tải thành công!'); | |
| } | |
| }); | |
| return false; | |
| } | |
| } | |
| // ===== INIT ===== | |
| function init() { | |
| // Remove any existing handler to avoid duplicates | |
| document.removeEventListener('click', handleExportClick, true); | |
| // Add click handler | |
| document.addEventListener('click', handleExportClick, true); | |
| console.log('[VAI Excel Fix v2.0] Loaded - Multi-download enabled'); | |
| } | |
| // Run on DOM ready | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', init); | |
| } else { | |
| init(); | |
| } | |
| // Also run after delays for dynamic content | |
| setTimeout(init, 2000); | |
| setTimeout(init, 5000); | |
| // Export for global use | |
| window.VAI_EXCEL_FIX = { | |
| init: init, | |
| exportQuoteExcel: exportQuoteExcel, | |
| exportDeliveryExcel: exportDeliveryExcel, | |
| generateOrderCode: generateOrderCode, | |
| getQuoteData: getQuoteData | |
| }; | |
| })(); |