<style>
.invoice-wrapper {
max-width: 800px;
margin: 0 auto;
background: #fff;
box-shadow: 0 2px 15px rgba(0,0,0,0.1);
border-radius: 8px;
overflow: hidden;
}
.main-content {
padding: 0 20px;
}
.full-width-image {
width: 100%;
height: auto;
display: block;
object-fit: contain;
}
.invoice-header {
margin-top: 30px;
}
.invoice-table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
font-size: 12px;
page-break-inside: auto;
}
.invoice-table th {
background: #8f8f8f;
color: white;
padding: 8px 4px;
text-align: left;
font-weight: 400;
}
.invoice-table td {
padding: 8px 4px;
border-bottom: 1px solid #eee;
vertical-align: middle;
}
.invoice-table tr:nth-child(even) {
background: #f9f9f9;
}
.invoice-table tr {
page-break-inside: avoid;
}
.num-cell {
width: 25px;
text-align: center;
font-weight: 500;
}
.photo-cell {
width: 70px;
text-align: center;
}
.title-cell {
font-size: 13px;
line-height: 1.4;
}
.price-cell,
.total-cell {
text-align: right;
white-space: nowrap;
font-weight: 400;
}
.qty-cell {
text-align: center;
width: 60px;
}
.title-cell .t706__product-title {
font-size: 12px;
padding: 5px 0;
}
td.totals-cell .t706__cartwin-totalamount-wrap,
td.totals-cell .t706__cartpage-totals {
padding-top: 0;
font-size: 16px;
}
.uc-pdf-data{
display: none;
}
.invoice-table th:first-child,
.invoice-table td:last-child {
padding-left: 10px;
padding-right: 10px;
}
.product-thumb {
width: 50px;
height: 50px;
object-fit: cover;
border-radius: 4px;
border: 1px solid #ddd;
display: block;
margin: 0 auto;
}
.product-thumb.no-image {
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 18px;
width: 50px;
height: 50px;
border-radius: 4px;
border: 1px solid #ddd;
margin: 0 auto;
}
.total-row {
background: #ececec !important;
}
.invoice-footer {
color: #555;
margin-bottom: 35px;
line-height: 1.5;
padding-top: 8px;
}
.invoice-wrapper a,
.invoice-wrapper .t706__product-title a {
text-decoration: none !important;
color: inherit !important;
border-bottom: none !important;
}
.print-price-button {
position: relative;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
}
.print-price-button.is-generating {
opacity: 0.65;
pointer-events: none;
transform: scale(0.97);
background-color: #e8e8e8 !important;
border-color: #ccc !important;
}
.print-price-button.is-generating .button-text::after {
content: '...';
display: inline-block;
animation: dots 1.2s infinite steps(4, end);
width: 12px;
}
.t-rec .t706__cartpage-info .print-price-button {
margin-top: 15px;
}
.t706__cartpage-totals {
background: transparent;
border-radius: 0;
padding: 0;
margin-top: 0px;
margin-bottom: 0px;
bottom: 0;
}
span.t706__cartwin-discounts__description-wrapper {
display: none;
}
@keyframes dots {
0% { content: '.'; width: 6px; }
33% { content: '..'; width: 10px; }
66% { content: '...'; width: 14px; }
}
@media print {
body {
background: white;
padding: 0;
}
}
.t706 .t706__cartpage-totals {
padding-bottom: 0px;
}
.t706 .t706__cartwin-bottom+div {
margin-top: 0;
}
.uc-download-data {
display: none;
}
.load-time:after {
content: "";
opacity: 0.5;
background-image: url(https://tilda.ru/tpl/img/ajax-loader.gif);
background-size: 35px;
background-position: center;
background-repeat: no-repeat;
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
}
.load-time div {
opacity: 0.2;
}
.js-successbox.load-time.t-form__successbox {
opacity: 0.6;
}
.print-price-button.t-text {
border-radius: 0px;
margin-top: 5px;
font-weight: 600;
color: #000000;
border: 1px solid #dddddd;
cursor: pointer;
transition: all 0.2s;
width: 100%;
box-sizing: border-box;
}
.button-wrapper {
padding: 8px 0;
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
font-weight: 400;
}
.print-price-button.t-text:not(.load-time):hover {
background-color: #f5f5f5;
}
.button-icon {
background-image: url(https://static.tildacdn.com/tild6238-3065-4138-b936-303137363262/2849806-copy-interfa.svg);
width: 25px;
height: 25px;
background-position: center;
background-size: contain;
background-repeat: no-repeat;
margin-right: 12px;
}
@media screen and (max-width: 960px) {
.print-price-button.t-text {
width: calc(100% - 0px);
margin: auto;
}
.t706__cartpage-form .print-price-button.t-text {
width: 100%;
margin: auto;
}
}
@media screen and (max-width: 640px) {
.t706__cartpage-form .t-form__submit {
padding-bottom: 5px;
}
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function() {
t_onReady(function() {
setTimeout(function() {
t_onFuncLoad('tcart__init', function() {
function loadScript(src) {
return new Promise((resolve, reject) => {
if(document.querySelector(`script[src="${src}"]`)) return resolve();
const s = document.createElement('script');
s.src = src; s.async = true;
s.onload = resolve; s.onerror = reject;
document.head.appendChild(s);
});
}
async function ensureLibs() {
const promises = [];
if(typeof window.jspdf === 'undefined') promises.push(loadScript('https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js'));
if(typeof window.htmlToImage === 'undefined') promises.push(loadScript('https://cdn.jsdelivr.net/npm/html-to-image@1.11.11/dist/html-to-image.min.js'));
await Promise.all(promises);
}
function getCurrentDateFormatted() {
const now = new Date();
return [String(now.getDate()).padStart(2, '0'), String(now.getMonth() + 1).padStart(2, '0'), now.getFullYear()].join('.');
}
function replaceDateInHTML(html) {
return html.replace(/{date}/g, getCurrentDateFormatted());
}
function extractBgImageUrl(element) {
if(!element) return '';
const style = element.getAttribute('style') || '';
const match = style.match(/background-image:\s*url\(['"]?([^'")]+)['"]?\)/i);
return match ? match[1] : '';
}
function formatPrice(num) {
return String(num).replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
}
function getProjectFont() {
const selectors = ['.t-text', '.t-descr', '.t-title', '.t-body', 'body'];
for (let sel of selectors) {
const el = document.querySelector(sel);
if (!el) continue;
let fam = window.getComputedStyle(el).fontFamily;
let clean = fam.replace(/['"]/g, '').split(',')[0].trim();
if (clean && clean !== 'inherit' && clean !== 'initial') return clean;
}
return 'Arial';
}
function collectPdfMetaData() {
const pdfDataBlock = document.querySelector('.uc-pdf-data');
let headerImage = '', footerImage = '';
let pdfTitleHTML = '', pdfDescrHTML = '';
if(pdfDataBlock) {
const rowWrapper = pdfDataBlock.querySelector('.t667__row');
if(rowWrapper) {
const bgImages = rowWrapper.querySelectorAll('.t-bgimg');
if(bgImages[0]) headerImage = bgImages[0].getAttribute('data-original') || '';
if(bgImages[1]) footerImage = bgImages[1].getAttribute('data-original') || '';
}
const titleEl = pdfDataBlock.querySelector('.js-block-header-title');
const descrEl = pdfDataBlock.querySelector('.js-block-header-descr');
pdfTitleHTML = titleEl ? replaceDateInHTML(titleEl.outerHTML) : '';
pdfDescrHTML = descrEl ? replaceDateInHTML(descrEl.outerHTML) : '';
}
return { headerImage, footerImage, pdfTitleHTML, pdfDescrHTML };
}
function collectCartProducts() {
const products = [];
if(typeof tcart === 'undefined' || !tcart.products) return products;
const productsWrapper = document.querySelector('.t706__cartpage-products, .t706__cartwin-products');
if(!productsWrapper) return products;
const prodElements = productsWrapper.querySelectorAll('.t706__product');
prodElements.forEach((prodEl, i) => {
let productImage = '';
const thumb = prodEl.querySelector('.t706__product-thumb');
if(thumb) {
const imgDiv = thumb.querySelector('.t706__product-imgdiv');
if(imgDiv) productImage = extractBgImageUrl(imgDiv);
}
const titleEl = prodEl.querySelector('.t706__product-title');
const titleHTML = titleEl ? replaceDateInHTML(titleEl.outerHTML) : '<span>Без названия</span>';
const skuEl = prodEl.querySelector('.t706__product-sku, .t706__product-descr, .t706__product-options');
const skuHTML = skuEl ? replaceDateInHTML(skuEl.outerHTML) : '';
const productIndex = prodEl.getAttribute('data-cart-product-i');
let pricePerOne = 0, totalPrice = 0, quantity = 0;
if(productIndex !== null && tcart.products[productIndex]) {
const p = tcart.products[productIndex];
pricePerOne = p.price || 0;
totalPrice = p.amount || 0;
quantity = p.quantity || 0;
}
products.push({
index: productIndex !== null ? Number(productIndex) + 1 : i + 1,
image: productImage,
titleHTML,
skuHTML,
pricePerOne,
totalPrice,
quantity,
pricePerOneFormatted: formatPrice(pricePerOne),
totalPriceFormatted: formatPrice(totalPrice)
});
});
return products;
}
function buildCompactTotals(currency) {
if(typeof tcart === 'undefined') return '';
const prodamount = tcart.prodamount || 0;
const amount = tcart.amount || 0;
const discount = prodamount - amount;
let html = '<div class="compact-totals">';
html += `<div class="totals-line"><span class="totals-label">Сумма:</span><span class="totals-value">${formatPrice(prodamount)} ${currency}</span></div>`;
if(discount > 0) {
html += `<div class="totals-line"><span class="totals-label">Скидка:</span><span class="totals-value">${formatPrice(discount)} ${currency}</span></div>`;
}
html += `<div class="totals-line total-final"><span class="totals-label">Итоговая сумма:</span><span class="totals-value">${formatPrice(amount)} ${currency}</span></div>`;
html += '</div>';
return html;
}
function collectCartTotals() {
const el = document.querySelector('.t706__cartpage-totals, .t706__cartwin-totalamount-wrap');
return el ? replaceDateInHTML(el.outerHTML) : '';
}
function buildInvoiceHTML(orderData, fontFam) {
const { meta, products, currency, totalsHTML } = orderData;
const headerImg = meta.headerImage
? `<img src="${meta.headerImage}" alt="Header" class="full-width-image" crossorigin="anonymous" onerror="this.style.display='none'">`
: '';
const titleHTML = meta.pdfTitleHTML
? `<div class="header-title">${meta.pdfTitleHTML}</div>`
: '<div class="header-title"><h1>КОММЕРЧЕСКОЕ ПРЕДЛОЖЕНИЕ</h1></div>';
const rows = products.map((p, i) => {
const photo = p.image
? `<img src="${p.image}" class="product-thumb" crossorigin="anonymous" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex'"><div class="product-thumb no-image" style="display:none">—</div>`
: `<div class="product-thumb no-image">—</div>`;
const titleBlock = p.skuHTML
? `<div class="product-title">${p.titleHTML}</div><div class="product-sku">${p.skuHTML}</div>`
: `<div class="product-title">${p.titleHTML}</div>`;
return `<tr>
<td class="num-cell">${p.index}</td>
<td class="photo-cell">${photo}</td>
<td class="title-cell">${titleBlock}</td>
<td class="price-cell">${p.pricePerOneFormatted} ${currency}</td>
<td class="qty-cell">${p.quantity}</td>
<td class="total-cell">${p.totalPriceFormatted} ${currency}</td>
</tr>`;
}).join('');
const compactTotals = buildCompactTotals(currency);
const totalRow = compactTotals
? `<tr class="total-row"><td colspan="6" class="totals-cell">${compactTotals}</td></tr>`
: (totalsHTML ? `<tr class="total-row"><td colspan="6" class="totals-cell">${totalsHTML}</td></tr>` : '');
const footerDesc = meta.pdfDescrHTML
? `<div class="footer-desc">${meta.pdfDescrHTML}</div>`
: '';
const footerImg = meta.footerImage
? `<img src="${meta.footerImage}" alt="Footer" class="full-width-image padding-image" crossorigin="anonymous" onerror="this.style.display='none'">`
: '';
return `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: ${fontFam}, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
background: #fff;
color: #000;
line-height: 1.4;
font-size: 14px;
}
.invoice-wrapper { width: 800px; }
.full-width-image { width: 100%; display: block; }
.padding-image { margin-top: 20px; }
.main-content { padding: 20px 30px; }
.invoice-header { margin-bottom: 20px; }
.header-title h1 { font-size: 20px; font-weight: bold; margin: 0 0 10px 0; }
.header-title h2 { font-size: 16px; font-weight: normal; margin: 0 0 5px 0; }
.header-title p { font-size: 14px; margin: 0 0 3px 0; }
.invoice-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
margin-bottom: 0;
}
.invoice-table thead th {
background: #999;
color: #fff;
padding: 10px 8px;
text-align: left;
font-weight: normal;
border: none;
}
.invoice-table thead th.num-cell { width: 40px; text-align: center; }
.invoice-table thead th.photo-cell { width: 80px; }
.invoice-table thead th.price-cell { width: 110px; text-align: right; }
.invoice-table thead th.qty-cell { width: 60px; text-align: center; }
.invoice-table thead th.total-cell { width: 110px; text-align: right; }
.invoice-table tbody td {
padding: 12px 8px;
border-bottom: 1px solid #e0e0e0;
vertical-align: top;
}
.invoice-table tbody td.num-cell { text-align: center; width: 40px; }
.invoice-table tbody td.photo-cell { width: 80px; }
.invoice-table tbody td.price-cell { text-align: right; width: 110px; white-space: nowrap; }
.invoice-table tbody td.qty-cell { text-align: center; width: 60px; }
.invoice-table tbody td.total-cell { text-align: right; width: 110px; white-space: nowrap; }
.product-thumb {
width: 60px;
height: 60px;
object-fit: cover;
display: block;
border: 1px solid #ddd;
}
.product-thumb.no-image {
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
font-size: 18px;
color: #999;
}
.product-title { font-size: 13px; margin-bottom: 4px; }
.product-title * { font-size: 13px !important; margin: 0 !important; }
.product-sku { font-size: 11px; color: #666; }
.product-sku * { font-size: 11px !important; color: #666 !important; margin: 0 !important; }
/* === ИСПРАВЛЕНИЕ БЛОКА ИТОГОВ === */
.total-row td {
border-top: 2px solid #333 !important;
background: #f9f9f9;
padding: 0 !important;
}
.totals-cell {
text-align: right !important;
padding: 0 !important;
}
/* Компактные итоги */
.compact-totals {
display: inline-block;
text-align: right;
padding: 12px 20px;
min-width: 250px;
}
.compact-totals .totals-line {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 6px;
font-size: 13px;
white-space: nowrap;
}
.compact-totals .totals-line:last-child {
margin-bottom: 0;
}
.compact-totals .totals-label {
margin-right: 20px;
color: #333;
}
.compact-totals .totals-value {
font-weight: normal;
white-space: nowrap;
}
.compact-totals .total-final {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #ccc;
font-weight: bold;
font-size: 14px;
}
.compact-totals .total-final .totals-label,
.compact-totals .total-final .totals-value {
font-weight: bold;
}
/* Fallback для Tilda-HTML итогов */
.totals-cell > * { text-align: right !important; }
.totals-cell > * > * { text-align: right !important; }
.footer-desc { margin-top: 20px; font-size: 13px; }
.footer-desc * { font-size: 13px !important; }
</style>
<base target="_blank">
</head>
<body>
<div class="invoice-wrapper t-text">
${headerImg}
<div class="main-content">
<div class="invoice-header">${titleHTML}</div>
<table class="invoice-table">
<thead>
<tr>
<th class="num-cell">№</th>
<th class="photo-cell">Фото</th>
<th class="title-cell">Наименование товара / услуги</th>
<th class="price-cell">Цена</th>
<th class="qty-cell">Кол-во</th>
<th class="total-cell">Сумма</th>
</tr>
</thead>
<tbody>${rows}${totalRow}</tbody>
</table>
<div class="invoice-footer">${footerDesc}</div>
</div>
${footerImg}
</div>
</body>
</html>`;
}
async function renderAndCapture(fullHTML) {
return new Promise(async (resolve, reject) => {
const iframe = document.createElement('iframe');
iframe.style.cssText = 'position:fixed;left:-9999px;top:0;width:800px;height:100px;border:none;visibility:hidden;pointer-events:none;';
document.body.appendChild(iframe);
const doc = iframe.contentDocument || iframe.contentWindow.document;
doc.open();
doc.write(fullHTML);
doc.close();
const images = doc.querySelectorAll('img');
let loaded = 0;
const total = images.length;
const onReady = async () => {
try {
const wrapper = doc.querySelector('.invoice-wrapper');
const dataUrl = await window.htmlToImage.toPng(wrapper, {
quality: 0.95,
pixelRatio: 1.5,
backgroundColor: '#ffffff',
skipFonts: true,
cacheBust: false
});
resolve({ dataUrl, iframe });
} catch(e) {
reject(e);
}
};
if(total === 0) {
setTimeout(onReady, 300);
return;
}
const checkComplete = () => {
loaded++;
if(loaded >= total) setTimeout(onReady, 200);
};
images.forEach(img => {
if(img.complete && img.naturalHeight > 0) {
checkComplete();
} else {
img.onload = checkComplete;
img.onerror = checkComplete;
}
});
setTimeout(onReady, 5000);
});
}
async function directGeneratePDF(clickedBtn) {
const startTime = performance.now();
await ensureLibs();
if (!window.jspdf || !window.htmlToImage) {
alert('Библиотеки не загрузились.');
return;
}
clickedBtn.classList.add('is-generating');
const btnTextEl = clickedBtn.querySelector('.button-text');
const originalText = btnTextEl.textContent.trim();
btnTextEl.textContent = 'Создание PDF...';
let iframe = null;
try {
const t1 = performance.now();
const meta = collectPdfMetaData();
const products = collectCartProducts();
const currency = (typeof tcart !== 'undefined' && tcart.currency_txt) ? tcart.currency_txt : '₽';
const totalsHTML = collectCartTotals();
const orderData = { meta, products, currency, totalsHTML };
const projectFont = getProjectFont();
console.log('[PDF] Data collection:', Math.round(performance.now() - t1), 'ms');
const t2 = performance.now();
const fullHTML = buildInvoiceHTML(orderData, projectFont);
console.log('[PDF] HTML build:', Math.round(performance.now() - t2), 'ms');
const t3 = performance.now();
const { dataUrl, iframe: ifr } = await renderAndCapture(fullHTML);
iframe = ifr;
console.log('[PDF] Render + capture:', Math.round(performance.now() - t3), 'ms');
const t4 = performance.now();
const { jsPDF } = window.jspdf;
const pdf = new jsPDF('p', 'mm', 'a4');
const pageW = pdf.internal.pageSize.getWidth();
const pageH = pdf.internal.pageSize.getHeight();
const img = new Image();
img.src = dataUrl;
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
setTimeout(resolve, 3000);
});
const imgW_mm = pageW;
const imgH_mm = (img.height * imgW_mm) / img.width;
let heightLeft = imgH_mm;
let position_mm = 0;
pdf.addImage(dataUrl, 'PNG', 0, position_mm, imgW_mm, imgH_mm);
heightLeft -= pageH;
while (heightLeft > 0.5) {
position_mm = heightLeft - imgH_mm;
pdf.addPage();
pdf.addImage(dataUrl, 'PNG', 0, position_mm, imgW_mm, imgH_mm);
heightLeft -= pageH;
}
pdf.save(`КП-${getCurrentDateFormatted().replace(/\./g, '-')}.pdf`);
console.log('[PDF] jsPDF generation:', Math.round(performance.now() - t4), 'ms');
console.log('[PDF] TOTAL:', Math.round(performance.now() - startTime), 'ms');
} catch (e) {
console.error('PDF generation error:', e);
alert('Ошибка генерации: ' + e.message);
} finally {
if (iframe) iframe.remove();
clickedBtn.classList.remove('is-generating');
btnTextEl.textContent = originalText;
}
}
function createPrintBtn() {
const div = document.createElement('div');
div.className = 'print-price-button t-text';
div.innerHTML = `<div class="button-wrapper"><div class="button-icon"></div><div class="button-text">Скачать PDF</div></div>`;
return div;
}
function addBtnToCart() {
const btn = createPrintBtn();
const cp = document.querySelector('.t706__cartpage');
const simple = document.querySelector('.t706__cartwin-content');
if(cp) {
let wrap = cp.querySelector('.t706__cartpage-info-wrapper');
if(wrap) wrap.insertAdjacentElement('afterend', btn.cloneNode(true));
} else if(simple) {
const wrap = simple.querySelector('.t706__cartwin-bottom');
if(wrap) wrap.insertAdjacentElement('afterend', btn.cloneNode(true));
}
}
addBtnToCart();
document.addEventListener('click', function(e) {
const btn = e.target.closest('.print-price-button');
if(btn) {
e.preventDefault();
directGeneratePDF(btn);
}
});
});
}, 200);
});
});
</script>