<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.onload = resolve; s.onerror = reject;
document.head.appendChild(s);
});
}
async function ensureLibs() {
if(typeof window.jspdf === 'undefined') await loadScript('https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js');
if(typeof window.html2canvas === 'undefined') await loadScript('https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js');
}
function getCurrentDateFormatted() {
const now = new Date();
return [String(now.getDate()).padStart(2, '0'), String(now.getMonth() + 1).padStart(2, '0'), now.getFullYear()].join('.');
}
function cloneAndReplaceDate(element) {
if(!element) return null;
const clone = element.cloneNode(true);
function replaceInTextNodes(node) {
if(node.nodeType === Node.TEXT_NODE && node.textContent.includes('{date}')) {
node.textContent = node.textContent.replace(/{date}/g, getCurrentDateFormatted());
} else { node.childNodes.forEach(replaceInTextNodes); }
}
replaceInTextNodes(clone); return clone;
}
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 pdfTitleEl = null, pdfDescrEl = null;
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');
pdfTitleEl = titleEl ? cloneAndReplaceDate(titleEl) : null;
pdfDescrEl = descrEl ? cloneAndReplaceDate(descrEl) : null;
}
return { headerImage, footerImage, pdfTitleEl, pdfDescrEl };
}
function collectCartProducts() {
const products = [];
if(typeof tcart === 'undefined' || !tcart.products) return products;
const productsContainers = document.querySelectorAll('[class*="-products"]');
let productsWrapper = null;
for(let c of productsContainers) { if(c.querySelector('.t706__product')) { productsWrapper = c; break; } }
if(!productsWrapper) return products;
productsWrapper.querySelectorAll('.t706__product').forEach(prodEl => {
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 titleElement = cloneAndReplaceDate(titleEl);
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, image: productImage, titleElement, pricePerOne, totalPrice, quantity, pricePerOneFormatted: formatPrice(pricePerOne), totalPriceFormatted: formatPrice(totalPrice) });
});
return products;
}
function collectCartTotals() {
const el = document.querySelector('.t706__cartpage-totals') || document.querySelector('.t706__cartwin-totalamount-wrap');
return el ? cloneAndReplaceDate(el) : null;
}
function buildInvoiceHTML(orderData, fontFam) {
const { meta, products, currency, totalsElement } = 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.pdfTitleEl ? `<div class="header-title">${meta.pdfTitleEl.outerHTML}</div>` : '<div class="header-title"><h1>КОММЕРЧЕСКОЕ ПРЕДЛОЖЕНИЕ</h1></div>';
let rows = '';
products.forEach((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 title = p.titleElement ? p.titleElement.outerHTML : '<span>Без названия</span>';
rows += `<tr>
<td class="num-cell">${p.index !== null ? Number(p.index) + 1 : i + 1}</td>
<td class="photo-cell">${photo}</td>
<td class="title-cell">${title}</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>`;
});
const totalRow = totalsElement ? `<tr class="total-row"><td colspan="6" class="totals-cell">${totalsElement.outerHTML}</td></tr>` : '';
const footerDesc = meta.pdfDescrEl ? `<div class="footer-desc">${meta.pdfDescrEl.outerHTML}</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 `<style>.invoice-wrapper, .invoice-wrapper * { font-family: ${fontFam}; -webkit-font-smoothing: antialiased; }</style>
<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>`;
}
async function preloadImages(element) {
const imgs = element.querySelectorAll('img');
const promises = Array.from(imgs).map(img => new Promise(resolve => {
if(img.complete && img.naturalHeight !== 0) return resolve();
const cleanup = () => { img.removeEventListener('load', ok); img.removeEventListener('error', err); resolve(); };
const ok = () => cleanup(); const err = () => cleanup();
img.addEventListener('load', ok, { once: true }); img.addEventListener('error', err, { once: true });
if(img.src.startsWith('http') && !img.crossOrigin) img.crossOrigin = 'anonymous';
}));
await Promise.all(promises); await new Promise(r => setTimeout(r, 200));
}
async function directGeneratePDF(clickedBtn) {
await ensureLibs();
if (!window.jspdf || !window.html2canvas) {
alert('Библиотеки не загрузились. Проверьте интернет.');
return;
}
clickedBtn.classList.add('is-generating');
const btnTextEl = clickedBtn.querySelector('.button-text');
const originalText = btnTextEl.textContent.trim();
btnTextEl.textContent = 'Создание PDF';
try {
const meta = collectPdfMetaData();
const products = collectCartProducts();
const currency = (typeof tcart !== 'undefined' && tcart.currency_txt) ? tcart.currency_txt : '₽';
const totalsElement = collectCartTotals();
const orderData = { meta, products, currency, totalsElement };
const projectFont = getProjectFont();
const tempContainer = document.createElement('div');
tempContainer.style.cssText = 'position:absolute; left:-10000px; top:0; width:800px; visibility:visible; z-index:-9999; overflow:hidden;';
tempContainer.innerHTML = buildInvoiceHTML(orderData, projectFont);
document.body.appendChild(tempContainer);
await document.fonts.ready;
void tempContainer.offsetHeight;
await new Promise(r => requestAnimationFrame(r));
await preloadImages(tempContainer);
const wrapper = tempContainer.querySelector('.invoice-wrapper');
const canvas = await html2canvas(wrapper, {
scale: 2.5,
useCORS: true,
allowTaint: true,
logging: false,
backgroundColor: '#ffffff',
windowWidth: 800,
onclone: (clonedDoc) => {
const f = projectFont;
clonedDoc.body.style.fontFamily = f;
clonedDoc.querySelectorAll('.invoice-wrapper *').forEach(el => {
el.style.fontFamily = f;
});
}
});
if (canvas.width === 0 || canvas.height === 0) throw new Error('Canvas пустой');
const imgData = canvas.toDataURL('image/jpeg', 0.92);
const { jsPDF } = window.jspdf;
const pdf = new jsPDF('p', 'mm', 'a4');
const pageW = pdf.internal.pageSize.getWidth();
const pageH = pdf.internal.pageSize.getHeight();
const imgW_mm = pageW;
const imgH_mm = (canvas.height * imgW_mm) / canvas.width;
let heightLeft = imgH_mm;
let position_mm = 0;
pdf.addImage(imgData, 'JPEG', 0, position_mm, imgW_mm, imgH_mm);
heightLeft -= pageH;
while (heightLeft > 0.5) {
position_mm = heightLeft - imgH_mm;
pdf.addPage();
pdf.addImage(imgData, 'JPEG', 0, position_mm, imgW_mm, imgH_mm);
heightLeft -= pageH;
}
pdf.save(`КП-${getCurrentDateFormatted().replace(/\./g, '-')}.pdf`);
} catch (e) {
console.error('PDF generation error:', e);
alert('⚠️ Ошибка генерации. Попробуйте обновить страницу или Ctrl+P → "Сохранить как PDF"');
} finally {
const temp = document.querySelector('[style*="-10000px"]');
if (temp) temp.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">Распечатать заказ</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>