完整的零售小票打印系统,使用纯前端技术实现,无需安装任何插件,通过浏览器原生打印功能实现小票打印。
实现思路
使用CSS媒体查询和打印样式控制小票格式
通过JavaScript动态生成小票内容
利用浏览器内置的打印功能
使用58mm纸张规格的CSS设置
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>零售小票打印系统</title>
<link href="https://cdn.jsdelivr.net/npm/layui@2.9.6/dist/css/layui.min.css" rel="stylesheet">
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
.header {
text-align: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.form-section {
margin-bottom: 20px;
}
.preview-section {
margin-top: 30px;
border-top: 1px solid #eee;
padding-top: 20px;
}
.preview-box {
border: 1px dashed #ccc;
padding: 15px;
background: #f9f9f9;
min-height: 200px;
}
.receipt {
width: 58mm; /* POS58打印机标准宽度 */
margin: 0 auto;
font-family: "Courier New", monospace;
font-size: 12px;
line-height: 1.2;
padding: 10px;
background: white;
box-shadow: 0 0 5px rgba(0,0,0,0.1);
}
.receipt-header {
text-align: center;
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 1px dashed #000;
}
.receipt-title {
font-weight: bold;
font-size: 14px;
margin-bottom: 5px;
}
.receipt-info {
font-size: 10px;
margin-bottom: 5px;
}
.receipt-items {
width: 100%;
margin: 10px 0;
}
.receipt-item {
display: flex;
justify-content: space-between;
margin-bottom: 3px;
}
.item-name {
flex: 3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-qty, .item-price, .item-total {
flex: 1;
text-align: right;
}
.receipt-divider {
border-top: 1px dashed #000;
margin: 5px 0;
}
.receipt-totals {
margin-top: 10px;
}
.receipt-total-line {
display: flex;
justify-content: space-between;
margin-bottom: 3px;
}
.receipt-footer {
text-align: center;
margin-top: 15px;
font-size: 10px;
}
.barcode {
text-align: center;
margin: 10px 0;
font-family: "Libre Barcode 39", monospace;
font-size: 24px;
}
/* 打印样式 */
@media print {
body * {
visibility: hidden;
}
.receipt, .receipt * {
visibility: visible;
}
.receipt {
position: absolute;
left: 0;
top: 0;
width: 58mm;
box-shadow: none;
margin: 0;
padding: 5mm;
}
.no-print {
display: none !important;
}
}
/* 小票纸张模拟 */
.receipt-paper {
background: white;
min-height: 200px;
width: 58mm;
margin: 0 auto;
position: relative;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
.receipt-paper::before {
content: "";
position: absolute;
left: 0;
right: 0;
top: 0;
height: 100%;
background: repeating-linear-gradient(
transparent,
transparent 19px,
#f0f0f0 19px,
#f0f0f0 20px
);
pointer-events: none;
opacity: 0.3;
}
</style>
</head>
<body>
<div class="container no-print">
<div class="header">
<h1>零售小票打印系统</h1>
<p>基于浏览器打印功能,兼容POS58热敏打印机</p>
</div>
<div class="form-section">
<form class="layui-form">
<div class="layui-form-item">
<label class="layui-form-label">店铺名称</label>
<div class="layui-input-block">
<input type="text" id="storeName" class="layui-input" value="零售小店" placeholder="请输入店铺名称">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">联系电话</label>
<div class="layui-input-block">
<input type="text" id="storePhone" class="layui-input" value="138-XXXX-XXXX" placeholder="请输入联系电话">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">地址</label>
<div class="layui-input-block">
<input type="text" id="storeAddress" class="layui-input" value="XX市XX区XX路XX号" placeholder="请输入店铺地址">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">订单号</label>
<div class="layui-input-block">
<input type="text" id="orderNo" class="layui-input" value="DD20230520001" placeholder="请输入订单号">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">收银员</label>
<div class="layui-input-block">
<input type="text" id="cashier" class="layui-input" value="张三" placeholder="请输入收银员姓名">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">商品列表</label>
<div class="layui-input-block">
<table class="layui-table" id="itemTable">
<thead>
<tr>
<th>商品名称</th>
<th width="100">数量</th>
<th width="100">单价(元)</th>
<th width="100">操作</th>
</tr>
</thead>
<tbody id="itemList">
<tr>
<td><input type="text" class="layui-input item-name" value="苹果"></td>
<td><input type="number" class="layui-input item-qty" value="2" min="1"></td>
<td><input type="number" class="layui-input item-price" value="5.00" min="0" step="0.01"></td>
<td><button type="button" class="layui-btn layui-btn-sm layui-btn-danger remove-item">删除</button></td>
</tr>
<tr>
<td><input type="text" class="layui-input item-name" value="面包"></td>
<td><input type="number" class="layui-input item-qty" value="1" min="1"></td>
<td><input type="number" class="layui-input item-price" value="10.00" min="0" step="0.01"></td>
<td><button type="button" class="layui-btn layui-btn-sm layui-btn-danger remove-item">删除</button></td>
</tr>
</tbody>
</table>
<button type="button" class="layui-btn layui-btn-sm" id="addItem">
<i class="layui-icon"></i> 添加商品
</button>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button type="button" class="layui-btn layui-btn-normal" id="updatePreview">更新预览</button>
<button type="button" class="layui-btn" id="printReceipt">打印小票</button>
</div>
</div>
</form>
</div>
<div class="preview-section no-print">
<h3>小票预览</h3>
<div class="preview-box">
<div class="receipt-paper">
<div class="receipt" id="receiptPreview">
<!-- 小票内容将通过JavaScript动态生成 -->
</div>
</div>
</div>
</div>
</div>
<!-- 小票打印模板 -->
<div id="receiptTemplate" style="display: none;">
<div class="receipt">
<div class="receipt-header">
<div class="receipt-title" id="r-storeName">零售小店</div>
<div class="receipt-info" id="r-storePhone">电话: 138-XXXX-XXXX</div>
<div class="receipt-info" id="r-storeAddress">地址: XX市XX区XX路XX号</div>
</div>
<div class="receipt-info">
订单号: <span id="r-orderNo">DD20230520001</span>
</div>
<div class="receipt-info">
收银员: <span id="r-cashier">张三</span>
</div>
<div class="receipt-info">
日期: <span id="r-date">2023-05-20 14:30:25</span>
</div>
<div class="receipt-divider"></div>
<div class="receipt-items">
<div class="receipt-item">
<div class="item-name"><strong>商品</strong></div>
<div class="item-qty"><strong>数量</strong></div>
<div class="item-price"><strong>单价</strong></div>
<div class="item-total"><strong>金额</strong></div>
</div>
<div class="receipt-divider"></div>
<!-- 商品列表将通过JavaScript动态生成 -->
<div id="r-items">
<div class="receipt-item">
<div class="item-name">苹果</div>
<div class="item-qty">2</div>
<div class="item-price">5.00</div>
<div class="item-total">10.00</div>
</div>
<div class="receipt-item">
<div class="item-name">面包</div>
<div class="item-qty">1</div>
<div class="item-price">10.00</div>
<div class="item-total">10.00</div>
</div>
</div>
<div class="receipt-divider"></div>
</div>
<div class="receipt-totals">
<div class="receipt-total-line">
<div>合计:</div>
<div>¥<span id="r-subtotal">20.00</span></div>
</div>
<div class="receipt-total-line">
<div>优惠:</div>
<div>-¥<span id="r-discount">0.00</span></div>
</div>
<div class="receipt-total-line">
<div><strong>应收:</strong></div>
<div><strong>¥<span id="r-total">20.00</span></strong></div>
</div>
</div>
<div class="receipt-divider"></div>
<div class="barcode">
*<span id="r-barcode">DD20230520001</span>*
</div>
<div class="receipt-footer">
<div>谢谢惠顾,欢迎再次光临!</div>
<div>本小票可作为购物凭证</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/layui@2.9.6/dist/layui.min.js"></script>
<script>
layui.use(['form', 'layer'], function(){
var form = layui.form;
var layer = layui.layer;
// 初始化
document.addEventListener('DOMContentLoaded', function() {
updateReceiptPreview();
// 添加商品行
document.getElementById('addItem').addEventListener('click', function() {
var tbody = document.getElementById('itemList');
var newRow = document.createElement('tr');
newRow.innerHTML = `
<td><input type="text" class="layui-input item-name" value="新商品"></td>
<td><input type="number" class="layui-input item-qty" value="1" min="1"></td>
<td><input type="number" class="layui-input item-price" value="0.00" min="0" step="0.01"></td>
<td><button type="button" class="layui-btn layui-btn-sm layui-btn-danger remove-item">删除</button></td>
`;
tbody.appendChild(newRow);
// 绑定删除事件
newRow.querySelector('.remove-item').addEventListener('click', function() {
tbody.removeChild(newRow);
updateReceiptPreview();
});
// 绑定输入事件
var inputs = newRow.querySelectorAll('input');
inputs.forEach(function(input) {
input.addEventListener('input', updateReceiptPreview);
});
});
// 绑定现有删除按钮事件
var removeButtons = document.querySelectorAll('.remove-item');
removeButtons.forEach(function(button) {
button.addEventListener('click', function() {
var row = this.closest('tr');
row.parentNode.removeChild(row);
updateReceiptPreview();
});
});
// 绑定输入事件
var inputs = document.querySelectorAll('#storeName, #storePhone, #storeAddress, #orderNo, #cashier, .item-name, .item-qty, .item-price');
inputs.forEach(function(input) {
input.addEventListener('input', updateReceiptPreview);
});
// 更新预览按钮
document.getElementById('updatePreview').addEventListener('click', updateReceiptPreview);
// 打印按钮
document.getElementById('printReceipt').addEventListener('click', function() {
// 创建打印内容
var printContent = document.getElementById('receiptTemplate').innerHTML;
var originalContent = document.body.innerHTML;
// 替换body内容为小票
document.body.innerHTML = printContent;
// 调用打印
window.print();
// 恢复原始内容
document.body.innerHTML = originalContent;
// 重新绑定事件
window.location.reload();
});
});
// 更新小票预览
function updateReceiptPreview() {
// 获取表单数据
var storeName = document.getElementById('storeName').value;
var storePhone = document.getElementById('storePhone').value;
var storeAddress = document.getElementById('storeAddress').value;
var orderNo = document.getElementById('orderNo').value;
var cashier = document.getElementById('cashier').value;
// 获取当前时间
var now = new Date();
var dateStr = now.getFullYear() + '-' +
String(now.getMonth() + 1).padStart(2, '0') + '-' +
String(now.getDate()).padStart(2, '0') + ' ' +
String(now.getHours()).padStart(2, '0') + ':' +
String(now.getMinutes()).padStart(2, '0') + ':' +
String(now.getSeconds()).padStart(2, '0');
// 计算商品总额
var subtotal = 0;
var itemsHtml = '';
var itemRows = document.querySelectorAll('#itemList tr');
itemRows.forEach(function(row) {
var name = row.querySelector('.item-name').value;
var qty = parseFloat(row.querySelector('.item-qty').value) || 0;
var price = parseFloat(row.querySelector('.item-price').value) || 0;
var total = qty * price;
subtotal += total;
itemsHtml += `
<div class="receipt-item">
<div class="item-name">${name}</div>
<div class="item-qty">${qty}</div>
<div class="item-price">${price.toFixed(2)}</div>
<div class="item-total">${total.toFixed(2)}</div>
</div>
`;
});
// 更新预览
var preview = document.getElementById('receiptPreview');
preview.innerHTML = `
<div class="receipt-header">
<div class="receipt-title">${storeName}</div>
<div class="receipt-info">电话: ${storePhone}</div>
<div class="receipt-info">地址: ${storeAddress}</div>
</div>
<div class="receipt-info">
订单号: ${orderNo}
</div>
<div class="receipt-info">
收银员: ${cashier}
</div>
<div class="receipt-info">
日期: ${dateStr}
</div>
<div class="receipt-divider"></div>
<div class="receipt-items">
<div class="receipt-item">
<div class="item-name"><strong>商品</strong></div>
<div class="item-qty"><strong>数量</strong></div>
<div class="item-price"><strong>单价</strong></div>
<div class="item-total"><strong>金额</strong></div>
</div>
<div class="receipt-divider"></div>
${itemsHtml}
<div class="receipt-divider"></div>
</div>
<div class="receipt-totals">
<div class="receipt-total-line">
<div>合计:</div>
<div>¥${subtotal.toFixed(2)}</div>
</div>
<div class="receipt-total-line">
<div>优惠:</div>
<div>-¥0.00</div>
</div>
<div class="receipt-total-line">
<div><strong>应收:</strong></div>
<div><strong>¥${subtotal.toFixed(2)}</strong></div>
</div>
</div>
<div class="receipt-divider"></div>
<div class="barcode">
*${orderNo}*
</div>
<div class="receipt-footer">
<div>谢谢惠顾,欢迎再次光临!</div>
<div>本小票可作为购物凭证</div>
</div>
`;
// 同时更新打印模板
document.getElementById('r-storeName').textContent = storeName;
document.getElementById('r-storePhone').textContent = '电话: ' + storePhone;
document.getElementById('r-storeAddress').textContent = '地址: ' + storeAddress;
document.getElementById('r-orderNo').textContent = orderNo;
document.getElementById('r-cashier').textContent = cashier;
document.getElementById('r-date').textContent = dateStr;
document.getElementById('r-items').innerHTML = itemsHtml;
document.getElementById('r-subtotal').textContent = subtotal.toFixed(2);
document.getElementById('r-total').textContent = subtotal.toFixed(2);
document.getElementById('r-barcode').textContent = orderNo;
}
});
</script>
</body>
</html>系统特点
响应式设计:适配POS58打印机纸张宽度(58mm)
实时预览:输入内容时实时更新小票预览
动态商品管理:可添加、删除商品行
自动计算:自动计算商品总价
打印优化:使用专门的打印样式,隐藏不相关元素
模拟纸张:预览区域模拟热敏纸效果
使用说明
在表单中填写店铺信息、订单信息和商品信息
查看右侧的实时预览效果
点击"打印小票"按钮调用浏览器打印功能
在打印对话框中选择POS58打印机并设置纸张类型为58mm宽度
打印前建议先使用"打印到PDF"功能测试效果
注意事项
确保POS58打印机已正确安装驱动
在打印设置中选择正确的纸张大小(58mm宽度)
部分浏览器可能需要允许弹出窗口以进行打印
实际打印效果可能因打印机型号和驱动略有差异
这个方案完全基于浏览器原生功能,无需安装任何插件,适合零售行业快速部署使用。
评论区