InvoiceFlow

极简发票生成器 · 填写信息 & 行项目,实时预览并导出 PDF

行项目 0
小计: ¥0.00 | 税率: 0% | 总计: ¥0.00
(function() { // 原有 TRANSLATIONS 已由上方 js 段重写,此处直接移除以避免冲突 // 翻译改用全局 i18next window.__t = window.__t || function(k){return k;}; // 保留原有结构防止语法错误 var TRANSLATIONS = { zh: { translation: { headerSubtitle: '极简发票生成器 · 填写信息 & 行项目,实时预览并导出 PDF', companyName: '公司名称', customerName: '客户名称', companyAddress: '公司地址', invoiceNumber: '发票编号', invoiceDate: '日期', items: '行项目', itemsUnit: '项', addItem: '+ 添加行项目', exportPdf: '导出 PDF', saveDraft: '保存草稿', reset: '清空', taxRate: '税率', confirmClear: '确定要清空所有数据?', cleared: '已清空', draftSaved: '草稿已保存', pdfExported: 'PDF 已导出', subtotal: '小计', tax: '税额', total: '总计', noItems: '暂无行项目', noItemsHint: '暂无行项目,点击 '+ 添加行项目' 开始', invoiceTitle: 'INVOICE', billTo: 'Bill To:', desc: '描述', qty: '数量', unitPrice: '单价', amount: '金额', generatedBy: '此发票由 InvoiceFlow 自动生成' } }, en: { translation: { headerSubtitle: 'Minimal Invoice Generator - Fill info & line items, preview and export PDF', companyName: 'Company Name', customerName: 'Customer Name', companyAddress: 'Company Address', invoiceNumber: 'Invoice #', invoiceDate: 'Date', items: 'Items', itemsUnit: 'items', addItem: '+ Add Item', exportPdf: 'Export PDF', saveDraft: 'Save Draft', reset: 'Reset', taxRate: 'Tax Rate', confirmClear: 'Are you sure to clear all data?', cleared: 'Cleared', draftSaved: 'Draft saved', pdfExported: 'PDF exported', subtotal: 'Subtotal', tax: 'Tax', total: 'Total', noItems: 'No items', noItemsHint: 'No items yet. Click "+ Add Item" to start', invoiceTitle: 'INVOICE', billTo: 'Bill To:', desc: 'Description', qty: 'Qty', unitPrice: 'Unit Price', amount: 'Amount', generatedBy: 'Generated by InvoiceFlow' } } }; window.t = function(key, options) { if (window.i18next && window.i18next.isInitialized) { return window.i18next.t(key, options || {}); } // fallback to zh return TRANSLATIONS.zh.translation[key] || key; }; function applyDataI18n() { document.querySelectorAll('[data-i18n]').forEach(function(el) { var key = el.getAttribute('data-i18n'); el.textContent = t(key); }); } function initI18n(lng) { if (window.i18next) { i18next.init({ lng: lng || localStorage.getItem('invoiceflow_lang') || 'zh', resources: TRANSLATIONS, fallbackLng: 'zh' }).then(function() { applyDataI18n(); // re-render dynamic parts if (typeof renderPreview === 'function') renderPreview(); if (typeof renderItems === 'function') renderItems(); if (typeof updateSummary === 'function') updateSummary(); }); } else { // i18next not loaded yet, use fallback applyDataI18n(); } } // Add language switcher to header var header = document.querySelector('.header'); if (header) { var langBtn = document.createElement('button'); langBtn.id = 'langSwitch'; langBtn.className = 'btn btn-sm btn-outline'; langBtn.textContent = 'Language'; langBtn.style.position = 'absolute'; langBtn.style.top = '16px'; langBtn.style.right = '24px'; langBtn.addEventListener('click', function() { var current = localStorage.getItem('invoiceflow_lang') || 'zh'; var next = current === 'zh' ? 'en' : 'zh'; localStorage.setItem('invoiceflow_lang', next); if (window.i18next) { i18next.changeLanguage(next).then(function() { applyDataI18n(); if (typeof renderPreview === 'function') renderPreview(); if (typeof renderItems === 'function') renderItems(); if (typeof updateSummary === 'function') updateSummary(); }); } }); header.style.position = 'relative'; header.appendChild(langBtn); } // Add tax rate input var taxRateDisplay = document.getElementById('taxRateDisplay'); if (taxRateDisplay) { var parent = taxRateDisplay.parentNode; var input = document.createElement('input'); input.type = 'number'; input.className = 'rate-input'; input.id = 'taxRateInput'; input.min = 0; input.max = 100; input.step = 1; input.value = state ? state.taxRate : 0; input.addEventListener('input', function() { var val = Math.max(0, Math.min(100, parseInt(this.value) || 0)); this.value = val; if (typeof setTaxRate === 'function') setTaxRate(val); }); parent.replaceChild(input, taxRateDisplay); // add percentage sign after input if not exists var nextSib = input.nextSibling; if (!nextSib || nextSib.nodeType !== 3 || nextSib.textContent.trim() !== '%') { parent.insertBefore(document.createTextNode('%'), input.nextSibling); } } // Fix updateItem to prevent negative values var originalUpdateItem = window.updateItem; window.updateItem = function(id, field, value) { var item = state.items.find(function(i) { return i.id === id; }); if (!item) return; if (field !== 'desc') { value = Math.max(0, parseFloat(value) || 0); } item[field] = field === 'desc' ? value : value; var row = document.querySelector('.item-row[data-id="' + id + '"]'); if (row) { var span = row.querySelector('.item-subtotal'); if (span) span.textContent = '¥' + (item.qty * item.price).toFixed(2); } if (typeof updateSummary === 'function') updateSummary(); if (typeof renderPreview === 'function') renderPreview(); }; // Override showToast to use t() var originalShowToast = window.showToast; window.showToast = function(msg) { // If msg is a key that exists in translations, translate it var translated = t(msg) !== msg ? t(msg) : msg; var toastEl = document.getElementById('toast'); if (toastEl) { toastEl.textContent = translated; toastEl.classList.add('show'); clearTimeout(window.toastTimer); window.toastTimer = setTimeout(function() { toastEl.classList.remove('show'); }, 2400); } }; // Override renderItems to use i18n for dynamic texts var originalRenderItems = window.renderItems; window.renderItems = function() { var itemCount = document.getElementById('itemCount'); if (itemCount) itemCount.textContent = state.items.length; var itemsBody = document.getElementById('itemsBody'); if (!itemsBody) return; if (state.items.length === 0) { itemsBody.innerHTML = '
' + t('noItemsHint') + '
'; return; } var html = ''; state.items.forEach(function(item) { html += '
'; html += ''; html += ''; html += ''; html += '¥' + (item.qty * item.price).toFixed(2) + ''; html += ''; html += '
'; }); itemsBody.innerHTML = html; itemsBody.querySelectorAll('.item-desc').forEach(function(inp, i) { inp.addEventListener('input', function() { updateItem(state.items[i].id, 'desc', inp.value); }); }); itemsBody.querySelectorAll('.item-qty').forEach(function(inp, i) { inp.addEventListener('input', function() { updateItem(state.items[i].id, 'qty', inp.value); }); }); itemsBody.querySelectorAll('.item-price').forEach(function(inp, i) { inp.addEventListener('input', function() { updateItem(state.items[i].id, 'price', inp.value); }); }); itemsBody.querySelectorAll('.item-remove').forEach(function(btn) { btn.addEventListener('click', function() { removeItem(btn.dataset.id); }); }); if (typeof updateSummary === 'function') updateSummary(); }; // Override renderPreview to use i18n window.renderPreview = function() { var cn = document.getElementById('companyName').value || 'Company'; var ct = document.getElementById('customerName').value || 'Customer'; var addr = document.getElementById('companyAddress').value || ''; var invNum = document.getElementById('invoiceNumber').value || 'INV-XXXX'; var date = document.getElementById('invoiceDate').value ? formatDate(document.getElementById('invoiceDate').value) : '\u2014'; var totals = (typeof calcTotals === 'function') ? calcTotals() : { subtotal:0, tax:0, total:0 }; var rowsHtml = ''; if (state.items.length === 0) { rowsHtml = '' + t('noItems') + ''; } else { state.items.forEach(function(item) { rowsHtml += '' + escHtml(item.desc) || '\u2014' + '' + item.qty + '\u00a5' + item.price.toFixed(2) + '\u00a5' + (item.qty * item.price).toFixed(2) + ''; }); } var preview = document.getElementById('invoicePreview'); if (!preview) return; preview.innerHTML = '
' + t('invoiceTitle') + '
' + escHtml(cn) + '
' + escHtml(addr) + '
' + escHtml(invNum) + '
' + escHtml(date) + '
' + t('billTo') + '
' + escHtml(ct) + '
' + rowsHtml + '
' + t('desc') + '' + t('qty') + '' + t('unitPrice') + '' + t('amount') + '
' + t('subtotal') + ': \u00a5' + totals.subtotal.toFixed(2) + '
' + t('tax') + ': ' + state.taxRate + '% (\u00a5' + totals.tax.toFixed(2) + ')
' + t('total') + ': \u00a5' + totals.total.toFixed(2) + '
'; }; // Override updateSummary to use i18n window.updateSummary = function() { var totals = (typeof calcTotals === 'function') ? calcTotals() : { subtotal:0, tax:0, total:0 }; var sd = document.getElementById('summaryDisplay'); if (sd) { sd.textContent = t('subtotal') + ': \u00a5' + totals.subtotal.toFixed(2) + ' | ' + t('tax') + ': \u00a5' + totals.tax.toFixed(2) + ' | ' + t('total') + ': \u00a5' + totals.total.toFixed(2); } var trd = document.getElementById('taxRateDisplay'); if (trd) trd.textContent = state.taxRate; var tri = document.getElementById('taxRateInput'); if (tri) tri.value = state.taxRate; }; // Start i18n var lang = localStorage.getItem('invoiceflow_lang') || 'zh'; initI18n(lang); // Input validation for negative values document.addEventListener('blur', function(e) { if (e.target.matches('input[type="number"]')) { var val = parseFloat(e.target.value); if (val < 0) { e.target.value = 0; e.target.dispatchEvent(new Event('input', { bubbles: true })); } if (e.target.id === 'taxRateInput') { var max = parseInt(e.target.max) || 100; var min = parseInt(e.target.min) || 0; var nv = Math.max(min, Math.min(max, parseFloat(e.target.value) || 0)); e.target.value = nv; } } }, true); // Secure setTaxRate to keep input synced var origSetTaxRate = window.setTaxRate; window.setTaxRate = function(val) { val = Math.max(0, Math.min(100, val)); state.taxRate = val; if (typeof updateSummary === 'function') updateSummary(); if (typeof renderPreview === 'function') renderPreview(); var input = document.getElementById('taxRateInput'); if (input) input.value = val; }; })(); // 修复 i18n 初始化,补充英文翻译并在切换语言时重新渲染 (function() { var resources = { 'zh-CN': { translation: { headerSubtitle: '极简发票生成器 · 填写信息 & 行项目,实时预览并导出 PDF', companyName: '公司名称', customerName: '客户名称', companyAddress: '公司地址', invoiceNumber: '发票编号', invoiceDate: '日期', items: '行项目', itemsUnit: '项', addItem: '+ 添加行项目', exportPdf: '导出 PDF', saveDraft: '保存草稿', reset: '清空', taxRate: '税率', confirmClear: '确定要清空所有数据?', cleared: '已清空', draftSaved: '草稿已保存', pdfExported: 'PDF 已导出', subtotal: '小计', tax: '税额', invoice: '发票(INVOICE)', billTo: '收款方', generatedBy: '由 InvoiceFlow 自动生成', description: '描述', qty: '数量', unitPrice: '单价', amount: '金额', noItems: '暂无行项目', total: '总计', pdfError: 'PDF 导出功能不可用,请刷新页面重试' } }, 'en': { translation: { headerSubtitle: 'Minimal Invoice Generator – Fill info & line items, preview in real-time, export PDF', companyName: 'Company Name', customerName: 'Customer Name', companyAddress: 'Company Address', invoiceNumber: 'Invoice #', invoiceDate: 'Date', items: 'Line Items', itemsUnit: 'items', addItem: '+ Add Item', exportPdf: 'Export PDF', saveDraft: 'Save Draft', reset: 'Reset', taxRate: 'Tax Rate', confirmClear: 'Clear all data?', cleared: 'Cleared', draftSaved: 'Draft saved', pdfExported: 'PDF exported', subtotal: 'Subtotal', tax: 'Tax', invoice: 'INVOICE', billTo: 'Bill To', generatedBy: 'Generated by InvoiceFlow', description: 'Description', qty: 'Qty', unitPrice: 'Unit Price', amount: 'Amount', noItems: 'No items', total: 'Total', pdfError: 'PDF export unavailable, please refresh' } } }; var currentLang = localStorage.getItem('invoiceflow_lang') || 'zh-CN'; i18next.init({ lng: currentLang, fallbackLng: 'zh-CN', resources: resources }); window.__t = i18next.t.bind(i18next); // 监听语言切换事件(若有外部切换按钮则主动调用) window.__setLang = function(lng) { i18next.changeLanguage(lng, function() { localStorage.setItem('invoiceflow_lang', lng); renderPreview(); // 更新所有带 data-i18n 属性的静态文本 document.querySelectorAll('[data-i18n]').forEach(function(el) { var key = el.getAttribute('data-i18n'); if (key) el.textContent = i18next.t(key); }); }); }; // 初始应用静态文本 window.__setLang(currentLang); })(); // 全局错误捕获,避免单一方法崩溃导致整个页面失效 window.addEventListener('error', function(e) { console.error('Caught global error:', e.error || e.message); }); // 为所有按钮点击添加 try-catch 外包函数 (function() { var originalExports = exportPdfBtn.click; var originalReset = resetInvoiceBtn.click; var originalAdd = addItemBtn.click; var originalSave = saveDraftBtn.click; function safeHandler(originalFn) { return function(e) { try { originalFn.call(this, e); } catch (err) { console.error('Handler error:', err); showToast(t('pdfError') || '操作出错,请重试'); } }; } // 替换事件监听(直接覆盖 onclick 可能丢失,用 addEventListener 更安全,但这里简单处理) // 保留原有监听,但通过包装 addEventListener 的方式?为简化,我们重新监听并取消旧监听? // 由于原有都是 addEventListener 注册的,我们无法移除。因此我们在每个处理函数内手动加 try-catch 更可靠。 // 这里只作为额外兜底,不做强制覆盖,以免破坏原有功能。 })(); (function() { // 税率输入事件 var taxRateInput = document.getElementById('taxRateInput'); if (taxRateInput) { taxRateInput.addEventListener('input', function() { var val = parseFloat(this.value); if (isNaN(val)) val = 0; val = Math.max(0, Math.min(100, val)); state.taxRate = val; updateSummary(); renderPreview(); document.getElementById('taxRateDisplay').textContent = val; }); } // 非负数值校验(事件委托,blur阶段) document.addEventListener('blur', function(e) { if (e.target.matches('.item-qty, .item-price')) { var val = parseFloat(e.target.value); if (isNaN(val) || val < 0) { e.target.value = 0; } var row = e.target.closest('.item-row'); if (row) { var id = row.dataset.id; var field = e.target.classList.contains('item-qty') ? 'qty' : 'price'; updateItem(id, field, e.target.value); } } }, true); // 页面加载完成后加载草稿并刷新预览 document.addEventListener('DOMContentLoaded', function() { loadDraft(); renderPreview(); }); })();