Selaa lähdekoodia

feat: 添加可视化标签编辑器和配置驱动的打印系统
新增功能:
- 添加 Web 可视化编辑器 (label-editor.html)
* 拖拽调整标签元素位置
* 实时预览标签效果(支持二维码显示)
* 调整字体、大小、样式
* 缩放功能(50%-200%)
* 导出配置为 JSON

- 添加配置文件系统
* label-config.json:标签布局配置
* 支持动态数据源映射(sn, productModel, date, serialNo 等)
* 标签尺寸、元素位置、字体样式可配置

- 添加配置加载器 (LabelConfigLoader.java)
* 解析 JSON 配置文件
* 支持文本和二维码元素
* 动态数据源解析

- 重构 TscPrinter.java
* 从配置文件读取标签布局
* 支持动态数据映射
* 根据配置绘制标签元素

- 添加图形界面 (TscPrintUI.java)
* Swing UI 替代控制台输入
* 避免编码问题
* 自动递增序号
* 实时日志显示

修复问题:
- 修正标签高度参数为 41.1mm(标签38.1mm + 间隙3.0mm)
- 解决打印多张标签问题

wangxichen 1 päivä sitten
vanhempi
commit
839e56b28e

+ 96 - 0
config/label-config.json

@@ -0,0 +1,96 @@
+{
+  "labelSize": {
+    "width": 76.2,
+    "height": 38.1,
+    "gap": 3,
+    "totalHeight": 41.1,
+    "unit": "mm"
+  },
+  "elements": [
+    {
+      "id": "qrcode",
+      "type": "qrcode",
+      "label": "二维码",
+      "x": 80,
+      "y": 120,
+      "width": 100,
+      "height": 100,
+      "eccLevel": "L",
+      "cellWidth": "3",
+      "text": "+KB60IS3031T811001201",
+      "dataSource": "sn"
+    },
+    {
+      "id": "company-line1",
+      "type": "text",
+      "label": "公司名-第1行",
+      "x": 77,
+      "y": 81,
+      "fontSize": 26,
+      "fontFamily": "SimSun",
+      "fontStyle": 1,
+      "text": "广西巨石",
+      "dataSource": "static"
+    },
+    {
+      "id": "company-line2",
+      "type": "text",
+      "label": "公司名-第2行",
+      "x": 22,
+      "y": 115,
+      "fontSize": 26,
+      "fontFamily": "SimSun",
+      "fontStyle": 1,
+      "text": "新能源科技有限公司",
+      "dataSource": "static"
+    },
+    {
+      "id": "product-line1",
+      "type": "text",
+      "label": "产品名",
+      "x": 277,
+      "y": 81,
+      "fontSize": 28,
+      "fontFamily": "SimSun",
+      "fontStyle": 1,
+      "text": "高压电池下壳体总成",
+      "dataSource": "static"
+    },
+    {
+      "id": "product-line2",
+      "type": "text",
+      "label": "产品型号",
+      "x": 300,
+      "y": 115,
+      "fontSize": 24,
+      "fontFamily": "Arial",
+      "fontStyle": 0,
+      "text": "2510094CDE2200",
+      "dataSource": "productModel"
+    },
+    {
+      "id": "date-serial",
+      "type": "text",
+      "label": "日期+流水号",
+      "x": 276,
+      "y": 200,
+      "fontSize": 24,
+      "fontFamily": "Arial",
+      "fontStyle": 0,
+      "text": "2026/08/11 NO:0000001",
+      "dataSource": "dateSerial"
+    },
+    {
+      "id": "batch-number",
+      "type": "text",
+      "label": "批次号",
+      "x": 288,
+      "y": 255,
+      "fontSize": 20,
+      "fontFamily": "Arial",
+      "fontStyle": 0,
+      "text": "+KB60IS3031T811001201",
+      "dataSource": "sn"
+    }
+  ]
+}

+ 687 - 0
label-editor.html

@@ -0,0 +1,687 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>TSC 标签可视化编辑器</title>
+    <!-- QR Code 生成库 -->
+    <script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+
+        body {
+            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+            background: #f5f5f5;
+            padding: 20px;
+        }
+
+        .container {
+            max-width: 1400px;
+            margin: 0 auto;
+            background: white;
+            border-radius: 8px;
+            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+            overflow: hidden;
+        }
+
+        .header {
+            background: #2c3e50;
+            color: white;
+            padding: 20px 30px;
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+        }
+
+        .header h1 {
+            font-size: 24px;
+            font-weight: 500;
+        }
+
+        .header-actions {
+            display: flex;
+            gap: 10px;
+        }
+
+        .btn {
+            padding: 10px 20px;
+            border: none;
+            border-radius: 4px;
+            cursor: pointer;
+            font-size: 14px;
+            transition: all 0.3s;
+        }
+
+        .btn-primary {
+            background: #3498db;
+            color: white;
+        }
+
+        .btn-primary:hover {
+            background: #2980b9;
+        }
+
+        .btn-success {
+            background: #27ae60;
+            color: white;
+        }
+
+        .btn-success:hover {
+            background: #229954;
+        }
+
+        .main-content {
+            display: flex;
+            height: calc(100vh - 140px);
+        }
+
+        .sidebar {
+            width: 300px;
+            border-right: 1px solid #ddd;
+            overflow-y: auto;
+            padding: 20px;
+        }
+
+        .sidebar h3 {
+            margin-bottom: 15px;
+            color: #2c3e50;
+            font-size: 16px;
+        }
+
+        .element-list {
+            list-style: none;
+        }
+
+        .element-item {
+            padding: 12px;
+            margin-bottom: 8px;
+            background: #f8f9fa;
+            border-radius: 4px;
+            cursor: pointer;
+            transition: all 0.2s;
+            border-left: 3px solid #3498db;
+        }
+
+        .element-item:hover {
+            background: #e9ecef;
+        }
+
+        .element-item.selected {
+            background: #d4e6f1;
+            border-left-color: #2980b9;
+        }
+
+        .element-item .label {
+            font-weight: 500;
+            color: #2c3e50;
+            display: block;
+            margin-bottom: 4px;
+        }
+
+        .element-item .type {
+            font-size: 12px;
+            color: #7f8c8d;
+        }
+
+        .canvas-area {
+            flex: 1;
+            padding: 30px;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            background: #ecf0f1;
+        }
+
+        .canvas-wrapper {
+            background: white;
+            box-shadow: 0 4px 15px rgba(0,0,0,0.15);
+            border-radius: 4px;
+            padding: 20px;
+            position: relative;
+        }
+
+        .canvas-container {
+            position: relative;
+            border: 2px solid #bdc3c7;
+            background: white;
+            cursor: crosshair;
+        }
+
+        .canvas-element {
+            position: absolute;
+            cursor: move;
+            border: 2px solid transparent;
+            transition: border-color 0.2s;
+        }
+
+        .canvas-element:hover {
+            border-color: #3498db;
+        }
+
+        .canvas-element.selected {
+            border-color: #e74c3c;
+            box-shadow: 0 0 0 2px rgba(231, 76, 60, 0.3);
+        }
+
+        .canvas-element.text-element {
+            white-space: nowrap;
+            font-family: Arial, sans-serif;
+            padding: 2px 4px;
+        }
+
+        .canvas-element.qrcode-element {
+            background: #ecf0f1;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            font-size: 10px;
+            color: #7f8c8d;
+        }
+
+        .properties-panel {
+            width: 320px;
+            border-left: 1px solid #ddd;
+            overflow-y: auto;
+            padding: 20px;
+        }
+
+        .properties-panel h3 {
+            margin-bottom: 15px;
+            color: #2c3e50;
+            font-size: 16px;
+        }
+
+        .property-group {
+            margin-bottom: 20px;
+        }
+
+        .property-group label {
+            display: block;
+            margin-bottom: 5px;
+            color: #34495e;
+            font-size: 13px;
+            font-weight: 500;
+        }
+
+        .property-group input,
+        .property-group select {
+            width: 100%;
+            padding: 8px;
+            border: 1px solid #ddd;
+            border-radius: 4px;
+            font-size: 13px;
+        }
+
+        .property-group input:focus,
+        .property-group select:focus {
+            outline: none;
+            border-color: #3498db;
+        }
+
+        .info-bar {
+            padding: 10px 20px;
+            background: #34495e;
+            color: white;
+            font-size: 13px;
+            display: flex;
+            justify-content: space-between;
+        }
+
+        .zoom-controls {
+            display: flex;
+            align-items: center;
+            gap: 10px;
+            margin-bottom: 15px;
+        }
+
+        .zoom-btn {
+            padding: 5px 12px;
+            background: #7f8c8d;
+            color: white;
+            border: none;
+            border-radius: 3px;
+            cursor: pointer;
+            font-size: 12px;
+        }
+
+        .zoom-btn:hover {
+            background: #6c7a7d;
+        }
+
+        .zoom-level {
+            font-size: 13px;
+            color: #2c3e50;
+            min-width: 60px;
+            text-align: center;
+        }
+
+        .property-row {
+            display: grid;
+            grid-template-columns: 1fr 1fr;
+            gap: 10px;
+        }
+    </style>
+</head>
+<body>
+    <div class="container">
+        <div class="header">
+            <h1>TSC 标签可视化编辑器</h1>
+            <div class="header-actions">
+                <button class="btn btn-primary" onclick="loadConfig()">加载配置</button>
+                <button class="btn btn-success" onclick="saveConfig()">保存配置</button>
+            </div>
+        </div>
+
+        <div class="main-content">
+            <!-- 左侧元素列表 -->
+            <div class="sidebar">
+                <h3>📋 标签元素</h3>
+                <ul class="element-list" id="elementList"></ul>
+            </div>
+
+            <!-- 中间画布区域 -->
+            <div class="canvas-area">
+                <div class="zoom-controls">
+                    <button class="zoom-btn" onclick="zoomOut()">缩小 -</button>
+                    <span class="zoom-level" id="zoomLevel">100%</span>
+                    <button class="zoom-btn" onclick="zoomIn()">放大 +</button>
+                    <button class="zoom-btn" onclick="resetZoom()">重置</button>
+                </div>
+                <div class="canvas-wrapper">
+                    <div class="canvas-container" id="canvas"></div>
+                </div>
+            </div>
+
+            <!-- 右侧属性面板 -->
+            <div class="properties-panel">
+                <h3>⚙️ 属性设置</h3>
+                <div id="propertiesContent">
+                    <p style="color: #7f8c8d; font-size: 13px;">请选择一个元素</p>
+                </div>
+            </div>
+        </div>
+
+        <div class="info-bar">
+            <span>标签尺寸: <span id="labelSize">76.2mm × 38.1mm</span></span>
+            <span>选中元素: <span id="selectedElement">无</span></span>
+        </div>
+    </div>
+
+    <script>
+        // 全局变量
+        let config = null;
+        let selectedElementId = null;
+        let zoom = 1.0;
+        const DPI = 203 / 25.4; // 203 DPI,1mm = 8 dots
+        let isDragging = false;
+        let dragStartX = 0;
+        let dragStartY = 0;
+        let dragElement = null;
+
+        // 加载配置
+        async function loadConfig() {
+            try {
+                const response = await fetch('../config/label-config.json');
+                config = await response.json();
+                renderCanvas();
+                renderElementList();
+                showMessage('配置加载成功', 'success');
+            } catch (error) {
+                showMessage('配置加载失败: ' + error.message, 'error');
+            }
+        }
+
+        // 保存配置
+        function saveConfig() {
+            if (!config) {
+                showMessage('没有配置可保存', 'error');
+                return;
+            }
+
+            const json = JSON.stringify(config, null, 2);
+            const blob = new Blob([json], { type: 'application/json' });
+            const url = URL.createObjectURL(blob);
+            const a = document.createElement('a');
+            a.href = url;
+            a.download = 'label-config.json';
+            a.click();
+            URL.revokeObjectURL(url);
+            showMessage('配置已下载', 'success');
+        }
+
+        // 渲染画布
+        function renderCanvas() {
+            if (!config) return;
+
+            const canvas = document.getElementById('canvas');
+            const widthPx = config.labelSize.width * DPI * zoom;
+            const heightPx = config.labelSize.height * DPI * zoom;
+
+            canvas.style.width = widthPx + 'px';
+            canvas.style.height = heightPx + 'px';
+            canvas.innerHTML = '';
+
+            config.elements.forEach(element => {
+                const div = document.createElement('div');
+                div.className = 'canvas-element';
+                div.id = 'canvas-' + element.id;
+                div.dataset.id = element.id;
+
+                const x = element.x * zoom;
+                const y = element.y * zoom;
+
+                div.style.left = x + 'px';
+                div.style.top = y + 'px';
+
+                if (element.type === 'text') {
+                    div.className += ' text-element';
+                    div.textContent = element.text;
+                    div.style.fontSize = (element.fontSize * zoom) + 'px';
+                    div.style.fontFamily = element.fontFamily;
+                    div.style.fontWeight = element.fontStyle === 1 ? 'bold' : 'normal';
+                } else if (element.type === 'qrcode') {
+                    div.className += ' qrcode-element';
+                    div.style.width = (element.width * zoom) + 'px';
+                    div.style.height = (element.height * zoom) + 'px';
+                    div.style.background = 'white';
+                    div.style.padding = '0';
+
+                    // 生成真实二维码
+                    const qrData = element.text || '+KB60IS3031T811001201'; // 默认示例数据
+                    try {
+                        new QRCode(div, {
+                            text: qrData,
+                            width: element.width * zoom,
+                            height: element.height * zoom,
+                            colorDark: '#000000',
+                            colorLight: '#ffffff',
+                            correctLevel: QRCode.CorrectLevel.L
+                        });
+                    } catch (e) {
+                        div.textContent = 'QR';
+                        div.style.background = '#ecf0f1';
+                    }
+                }
+
+                if (element.id === selectedElementId) {
+                    div.className += ' selected';
+                }
+
+                div.addEventListener('click', (e) => {
+                    e.stopPropagation();
+                    selectElement(element.id);
+                });
+
+                div.addEventListener('mousedown', startDrag);
+
+                canvas.appendChild(div);
+            });
+
+            document.getElementById('labelSize').textContent =
+                `${config.labelSize.width}mm × ${config.labelSize.height}mm`;
+        }
+
+        // 拖拽开始
+        function startDrag(e) {
+            isDragging = true;
+            dragElement = e.target;
+            dragStartX = e.clientX;
+            dragStartY = e.clientY;
+
+            const elementId = dragElement.dataset.id;
+            selectElement(elementId);
+
+            document.addEventListener('mousemove', onDrag);
+            document.addEventListener('mouseup', stopDrag);
+
+            e.preventDefault();
+        }
+
+        // 拖拽中
+        function onDrag(e) {
+            if (!isDragging || !dragElement) return;
+
+            const dx = (e.clientX - dragStartX) / zoom;
+            const dy = (e.clientY - dragStartY) / zoom;
+
+            const elementId = dragElement.dataset.id;
+            const element = config.elements.find(el => el.id === elementId);
+
+            if (element) {
+                element.x = Math.max(0, element.x + dx);
+                element.y = Math.max(0, element.y + dy);
+                renderCanvas();
+                updateProperties();
+            }
+
+            dragStartX = e.clientX;
+            dragStartY = e.clientY;
+        }
+
+        // 拖拽结束
+        function stopDrag() {
+            isDragging = false;
+            dragElement = null;
+            document.removeEventListener('mousemove', onDrag);
+            document.removeEventListener('mouseup', stopDrag);
+        }
+
+        // 渲染元素列表
+        function renderElementList() {
+            const list = document.getElementById('elementList');
+            list.innerHTML = '';
+
+            config.elements.forEach(element => {
+                const li = document.createElement('li');
+                li.className = 'element-item';
+                li.dataset.id = element.id;
+                li.innerHTML = `
+                    <span class="label">${element.label}</span>
+                    <span class="type">${element.type === 'text' ? '文本' : '二维码'} - (${Math.round(element.x)}, ${Math.round(element.y)})</span>
+                `;
+                li.addEventListener('click', () => selectElement(element.id));
+                list.appendChild(li);
+            });
+        }
+
+        // 选择元素
+        function selectElement(elementId) {
+            selectedElementId = elementId;
+
+            // 更新列表选中状态
+            document.querySelectorAll('.element-item').forEach(item => {
+                item.classList.toggle('selected', item.dataset.id === elementId);
+            });
+
+            // 更新画布选中状态
+            document.querySelectorAll('.canvas-element').forEach(el => {
+                el.classList.toggle('selected', el.dataset.id === elementId);
+            });
+
+            const element = config.elements.find(el => el.id === elementId);
+            document.getElementById('selectedElement').textContent = element ? element.label : '无';
+
+            updateProperties();
+        }
+
+        // 更新属性面板
+        function updateProperties() {
+            if (!selectedElementId) {
+                document.getElementById('propertiesContent').innerHTML =
+                    '<p style="color: #7f8c8d; font-size: 13px;">请选择一个元素</p>';
+                return;
+            }
+
+            const element = config.elements.find(el => el.id === selectedElementId);
+            if (!element) return;
+
+            let html = '';
+
+            // 位置属性
+            html += `
+                <div class="property-group">
+                    <label>ID</label>
+                    <input type="text" value="${element.id}" disabled>
+                </div>
+                <div class="property-group">
+                    <div class="property-row">
+                        <div>
+                            <label>X 坐标 (dots)</label>
+                            <input type="number" value="${element.x}" onchange="updateElementProperty('x', parseFloat(this.value))">
+                        </div>
+                        <div>
+                            <label>Y 坐标 (dots)</label>
+                            <input type="number" value="${element.y}" onchange="updateElementProperty('y', parseFloat(this.value))">
+                        </div>
+                    </div>
+                </div>
+            `;
+
+            if (element.type === 'text') {
+                html += `
+                    <div class="property-group">
+                        <label>文本内容</label>
+                        <input type="text" value="${element.text}" onchange="updateElementProperty('text', this.value)">
+                    </div>
+                    <div class="property-group">
+                        <label>字体大小 (pt)</label>
+                        <input type="number" value="${element.fontSize}" onchange="updateElementProperty('fontSize', parseInt(this.value))">
+                    </div>
+                    <div class="property-group">
+                        <label>字体</label>
+                        <select value="${element.fontFamily}" onchange="updateElementProperty('fontFamily', this.value)">
+                            <option value="Arial" ${element.fontFamily === 'Arial' ? 'selected' : ''}>Arial</option>
+                            <option value="SimSun" ${element.fontFamily === 'SimSun' ? 'selected' : ''}>SimSun (宋体)</option>
+                            <option value="SimHei" ${element.fontFamily === 'SimHei' ? 'selected' : ''}>SimHei (黑体)</option>
+                        </select>
+                    </div>
+                    <div class="property-group">
+                        <label>字体样式</label>
+                        <select value="${element.fontStyle}" onchange="updateElementProperty('fontStyle', parseInt(this.value))">
+                            <option value="0" ${element.fontStyle === 0 ? 'selected' : ''}>正常</option>
+                            <option value="1" ${element.fontStyle === 1 ? 'selected' : ''}>加粗</option>
+                        </select>
+                    </div>
+                `;
+            } else if (element.type === 'qrcode') {
+                html += `
+                    <div class="property-group">
+                        <div class="property-row">
+                            <div>
+                                <label>宽度 (dots)</label>
+                                <input type="number" value="${element.width}" onchange="updateElementProperty('width', parseInt(this.value))">
+                            </div>
+                            <div>
+                                <label>高度 (dots)</label>
+                                <input type="number" value="${element.height}" onchange="updateElementProperty('height', parseInt(this.value))">
+                            </div>
+                        </div>
+                    </div>
+                    <div class="property-group">
+                        <label>纠错等级</label>
+                        <select value="${element.eccLevel}" onchange="updateElementProperty('eccLevel', this.value)">
+                            <option value="L" ${element.eccLevel === 'L' ? 'selected' : ''}>L (7%)</option>
+                            <option value="M" ${element.eccLevel === 'M' ? 'selected' : ''}>M (15%)</option>
+                            <option value="Q" ${element.eccLevel === 'Q' ? 'selected' : ''}>Q (25%)</option>
+                            <option value="H" ${element.eccLevel === 'H' ? 'selected' : ''}>H (30%)</option>
+                        </select>
+                    </div>
+                    <div class="property-group">
+                        <label>单元宽度</label>
+                        <input type="number" value="${element.cellWidth}" onchange="updateElementProperty('cellWidth', this.value)">
+                    </div>
+                `;
+            }
+
+            document.getElementById('propertiesContent').innerHTML = html;
+        }
+
+        // 更新元素属性
+        function updateElementProperty(property, value) {
+            if (!selectedElementId) return;
+
+            const element = config.elements.find(el => el.id === selectedElementId);
+            if (element) {
+                element[property] = value;
+                renderCanvas();
+                renderElementList();
+            }
+        }
+
+        // 缩放控制
+        function zoomIn() {
+            zoom = Math.min(zoom + 0.1, 2.0);
+            document.getElementById('zoomLevel').textContent = Math.round(zoom * 100) + '%';
+            renderCanvas();
+        }
+
+        function zoomOut() {
+            zoom = Math.max(zoom - 0.1, 0.5);
+            document.getElementById('zoomLevel').textContent = Math.round(zoom * 100) + '%';
+            renderCanvas();
+        }
+
+        function resetZoom() {
+            zoom = 1.0;
+            document.getElementById('zoomLevel').textContent = '100%';
+            renderCanvas();
+        }
+
+        // 显示消息
+        function showMessage(message, type) {
+            const color = type === 'success' ? '#27ae60' : '#e74c3c';
+            const div = document.createElement('div');
+            div.style.cssText = `
+                position: fixed;
+                top: 20px;
+                right: 20px;
+                background: ${color};
+                color: white;
+                padding: 15px 25px;
+                border-radius: 4px;
+                box-shadow: 0 4px 12px rgba(0,0,0,0.15);
+                z-index: 1000;
+                animation: slideIn 0.3s ease-out;
+            `;
+            div.textContent = message;
+            document.body.appendChild(div);
+
+            setTimeout(() => {
+                div.style.animation = 'slideOut 0.3s ease-out';
+                setTimeout(() => div.remove(), 300);
+            }, 2000);
+        }
+
+        // 初始化
+        window.addEventListener('load', loadConfig);
+    </script>
+
+    <style>
+        @keyframes slideIn {
+            from {
+                transform: translateX(400px);
+                opacity: 0;
+            }
+            to {
+                transform: translateX(0);
+                opacity: 1;
+            }
+        }
+
+        @keyframes slideOut {
+            from {
+                transform: translateX(0);
+                opacity: 1;
+            }
+            to {
+                transform: translateX(400px);
+                opacity: 0;
+            }
+        }
+    </style>
+</body>
+</html>

+ 109 - 0
src/com/mes/print/config/LabelConfigLoader.java

@@ -0,0 +1,109 @@
+package com.mes.print.config;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 标签配置加载器
+ * 从 JSON 文件加载标签布局配置
+ */
+public class LabelConfigLoader {
+
+    private JSONObject config;
+
+    public LabelConfigLoader(String configPath) throws IOException {
+        String content = new String(Files.readAllBytes(Paths.get(configPath)), "UTF-8");
+        this.config = JSON.parseObject(content);
+    }
+
+    /**
+     * 获取标签尺寸配置
+     */
+    public LabelSize getLabelSize() {
+        JSONObject sizeObj = config.getJSONObject("labelSize");
+        LabelSize size = new LabelSize();
+        size.width = sizeObj.getDoubleValue("width");
+        size.height = sizeObj.getDoubleValue("height");
+        size.gap = sizeObj.getDoubleValue("gap");
+        size.totalHeight = sizeObj.getDoubleValue("totalHeight");
+        return size;
+    }
+
+    /**
+     * 获取所有元素配置
+     */
+    public List<LabelElement> getElements() {
+        JSONArray elementsArray = config.getJSONArray("elements");
+        List<LabelElement> elements = new ArrayList<>();
+
+        for (int i = 0; i < elementsArray.size(); i++) {
+            JSONObject obj = elementsArray.getJSONObject(i);
+            LabelElement element = new LabelElement();
+
+            element.id = obj.getString("id");
+            element.type = obj.getString("type");
+            element.label = obj.getString("label");
+            element.x = obj.getIntValue("x");
+            element.y = obj.getIntValue("y");
+            element.dataSource = obj.getString("dataSource");
+
+            if ("text".equals(element.type)) {
+                element.fontSize = obj.getIntValue("fontSize");
+                element.fontFamily = obj.getString("fontFamily");
+                element.fontStyle = obj.getIntValue("fontStyle");
+                element.text = obj.getString("text");
+            } else if ("qrcode".equals(element.type)) {
+                element.width = obj.getIntValue("width");
+                element.height = obj.getIntValue("height");
+                element.eccLevel = obj.getString("eccLevel");
+                element.cellWidth = obj.getString("cellWidth");
+                element.text = obj.getString("text");  // 添加 text 字段加载
+            }
+
+            elements.add(element);
+        }
+
+        return elements;
+    }
+
+    /**
+     * 标签尺寸
+     */
+    public static class LabelSize {
+        public double width;
+        public double height;
+        public double gap;
+        public double totalHeight;
+    }
+
+    /**
+     * 标签元素
+     */
+    public static class LabelElement {
+        public String id;
+        public String type;
+        public String label;
+        public int x;
+        public int y;
+        public String dataSource;
+
+        // 文本元素属性
+        public int fontSize;
+        public String fontFamily;
+        public int fontStyle;
+        public String text;
+
+        // 二维码元素属性
+        public int width;
+        public int height;
+        public String eccLevel;
+        public String cellWidth;
+    }
+}

+ 8 - 1
src/com/mes/print/test/TestTscPrint.java

@@ -9,7 +9,14 @@ import java.util.Scanner;
 public class TestTscPrint {
 
     public static void main(String[] args) {
-        Scanner scanner = new Scanner(System.in);
+        // 设置控制台编码为 UTF-8,避免中文乱码
+        Scanner scanner = null;
+        try {
+            System.setOut(new java.io.PrintStream(System.out, true, "GBK"));
+            scanner = new Scanner(System.in, "GBK");
+        } catch (Exception e) {
+            scanner = new Scanner(System.in);
+        }
 
         try {
             System.out.println("===== TSC DLL 打印测试 =====");

+ 175 - 0
src/com/mes/print/test/TscPrintUI.java

@@ -0,0 +1,175 @@
+package com.mes.print.test;
+
+import com.mes.print.tsc.TscPrinter;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+
+/**
+ * TSC 打印机 UI 界面
+ */
+public class TscPrintUI extends JFrame {
+
+    private JTextField snField;
+    private JTextField indexField;
+    private JTextArea logArea;
+    private JButton printButton;
+
+    public TscPrintUI() {
+        setTitle("TSC 标签打印");
+        setSize(500, 400);
+        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+        setLocationRelativeTo(null);
+
+        initComponents();
+    }
+
+    private void initComponents() {
+        // 主面板
+        JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
+        mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+
+        // 输入面板
+        JPanel inputPanel = new JPanel(new GridLayout(3, 2, 10, 10));
+
+        inputPanel.add(new JLabel("批次号 (SN):"));
+        snField = new JTextField("+KB60IS3031T811001201");
+        inputPanel.add(snField);
+
+        inputPanel.add(new JLabel("打印序号:"));
+        indexField = new JTextField("1");
+        inputPanel.add(indexField);
+
+        inputPanel.add(new JLabel(""));
+        printButton = new JButton("打印");
+        printButton.addActionListener(this::onPrint);
+        inputPanel.add(printButton);
+
+        mainPanel.add(inputPanel, BorderLayout.NORTH);
+
+        // 日志区域
+        logArea = new JTextArea();
+        logArea.setEditable(false);
+        logArea.setFont(new Font("Monospaced", Font.PLAIN, 12));
+        JScrollPane scrollPane = new JScrollPane(logArea);
+        scrollPane.setBorder(BorderFactory.createTitledBorder("打印日志"));
+        mainPanel.add(scrollPane, BorderLayout.CENTER);
+
+        // 提示信息
+        JPanel infoPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+        JLabel infoLabel = new JLabel("支持: +KB60* (T09) / +KB87* (T68)");
+        infoLabel.setForeground(Color.GRAY);
+        infoPanel.add(infoLabel);
+        mainPanel.add(infoPanel, BorderLayout.SOUTH);
+
+        setContentPane(mainPanel);
+
+        log("TSC 标签打印系统已启动");
+        log("支持的 SN 前缀:");
+        log("  +KB60* → 产品型号 2510094CDE2200 (T09)");
+        log("  +KB87* → 产品型号 2510094ANB0900 (T68)");
+        log("----------------------------------------");
+    }
+
+    private void onPrint(ActionEvent e) {
+        String sn = snField.getText().trim();
+        String indexStr = indexField.getText().trim();
+
+        // 验证输入
+        if (sn.isEmpty()) {
+            JOptionPane.showMessageDialog(this,
+                "批次号不能为空!",
+                "输入错误",
+                JOptionPane.ERROR_MESSAGE);
+            return;
+        }
+
+        int printIndex;
+        try {
+            printIndex = Integer.parseInt(indexStr);
+            if (printIndex < 0) {
+                throw new NumberFormatException();
+            }
+        } catch (NumberFormatException ex) {
+            JOptionPane.showMessageDialog(this,
+                "打印序号必须是正整数!",
+                "输入错误",
+                JOptionPane.ERROR_MESSAGE);
+            return;
+        }
+
+        // 显示确认信息
+        String serialNo = String.format("%07d", printIndex);
+        String message = String.format(
+            "批次号: %s\n打印序号: %d\n流水号: %s\n\n确认打印?",
+            sn, printIndex, serialNo
+        );
+
+        int confirm = JOptionPane.showConfirmDialog(this,
+            message,
+            "确认打印",
+            JOptionPane.YES_NO_OPTION);
+
+        if (confirm != JOptionPane.YES_OPTION) {
+            log("用户取消打印");
+            return;
+        }
+
+        // 执行打印
+        printButton.setEnabled(false);
+        log("开始打印...");
+        log("批次号: " + sn);
+        log("打印序号: " + printIndex);
+        log("流水号: " + serialNo);
+
+        new Thread(() -> {
+            try {
+                TscPrinter printer = new TscPrinter();
+                printer.printLabel(sn, printIndex);
+
+                SwingUtilities.invokeLater(() -> {
+                    log("✓ 打印成功!");
+                    log("----------------------------------------");
+
+                    // 自动递增序号
+                    indexField.setText(String.valueOf(printIndex + 1));
+                    printButton.setEnabled(true);
+                });
+
+            } catch (Exception ex) {
+                SwingUtilities.invokeLater(() -> {
+                    log("✗ 打印失败: " + ex.getMessage());
+                    log("----------------------------------------");
+                    printButton.setEnabled(true);
+
+                    JOptionPane.showMessageDialog(TscPrintUI.this,
+                        "打印失败:\n" + ex.getMessage(),
+                        "打印错误",
+                        JOptionPane.ERROR_MESSAGE);
+                });
+            }
+        }).start();
+    }
+
+    private void log(String message) {
+        SwingUtilities.invokeLater(() -> {
+            logArea.append(message + "\n");
+            logArea.setCaretPosition(logArea.getDocument().getLength());
+        });
+    }
+
+    public static void main(String[] args) {
+        // 设置系统外观
+        try {
+            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+        } catch (Exception e) {
+            // 使用默认外观
+        }
+
+        SwingUtilities.invokeLater(() -> {
+            TscPrintUI ui = new TscPrintUI();
+            ui.setVisible(true);
+        });
+    }
+}

+ 112 - 45
src/com/mes/print/tsc/TscPrinter.java

@@ -1,16 +1,35 @@
 package com.mes.print.tsc;
 
+import com.mes.print.config.LabelConfigLoader;
 import com.mes.print.config.PrinterConfig;
+
+import java.io.File;
 import java.text.SimpleDateFormat;
 import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 /**
  * TSC 打印机 - 通过 DLL 直接控制
- * 不需要 BarTender,不使用 .btw 模板
+ * 支持从配置文件加载标签布局
  */
 public class TscPrinter {
 
     private TSCLIB tsc = TSCLIB.INSTANCE;
+    private LabelConfigLoader configLoader;
+
+    public TscPrinter() {
+        this("config/label-config.json");
+    }
+
+    public TscPrinter(String configPath) {
+        try {
+            this.configLoader = new LabelConfigLoader(configPath);
+        } catch (Exception e) {
+            throw new RuntimeException("加载标签配置失败: " + e.getMessage(), e);
+        }
+    }
 
     /**
      * 打印标签
@@ -26,17 +45,23 @@ public class TscPrinter {
                 throw new RuntimeException("打开打印机失败: " + printerName);
             }
 
-            // 2. 设置标签参数
-            // 纸张尺寸:76.2mm x 38.1mm
-            // 关键:标签高度 = 实际高度 + 间隙高度
-            // 38.1 + 3.0 = 41.1mm(告诉打印机一个完整周期的长度)
-            tsc.setup("76.2", "41.1", "4", "10", "0", "3", "0");
+            // 2. 设置标签参数(从配置文件读取)
+            LabelConfigLoader.LabelSize labelSize = configLoader.getLabelSize();
+            tsc.setup(
+                String.valueOf(labelSize.width),
+                String.valueOf(labelSize.totalHeight),
+                "4",  // 速度
+                "10", // 浓度
+                "0",  // 传感器类型
+                String.valueOf(labelSize.gap),
+                "0"   // 偏移
+            );
 
             // 3. 清除缓冲区
             tsc.clearbuffer();
 
-            // 4. 绘制标签内容
-            drawLabel(sn, printIndex);
+            // 4. 绘制标签内容(从配置文件读取)
+            drawLabelFromConfig(sn, printIndex);
 
             // 5. 打印
             tsc.printlabel("1", "1");  // 打印1份,每份1张
@@ -52,52 +77,94 @@ public class TscPrinter {
     }
 
     /**
-     * 绘制标签内容(根据精确布局坐标 + 边距调整)
-     * 标签尺寸:73.7mm x 38.2mm
-     * 边距:上 0mm,左 1.6mm
-     * 打印机分辨率:203 DPI (1mm ≈ 8 dots)
+     * 从配置文件绘制标签内容
      */
-    private void drawLabel(String sn, int printIndex) {
-        // 根据 SN 前缀判断产品型号
-        String productModel = getProductModel(sn);
-
-        // 生成日期和流水号
-        String date = new SimpleDateFormat("yyyy/MM/dd").format(new Date());
-        String serialNo = String.format("%07d", printIndex);
+    private void drawLabelFromConfig(String sn, int printIndex) {
+        // 准备数据源
+        Map<String, String> dataMap = prepareDataMap(sn, printIndex);
+
+        // 获取所有元素配置
+        List<LabelConfigLoader.LabelElement> elements = configLoader.getElements();
+
+        // 绘制每个元素
+        for (LabelConfigLoader.LabelElement element : elements) {
+            if ("text".equals(element.type)) {
+                // 获取文本内容
+                String text = resolveData(element.dataSource, dataMap, element.text);
+
+                // 绘制文本
+                tsc.windowsfont(
+                    element.x,
+                    element.y,
+                    element.fontSize,
+                    0,  // rotation
+                    element.fontStyle,
+                    0,  // underline
+                    element.fontFamily,
+                    text
+                );
+            } else if ("qrcode".equals(element.type)) {
+                // 获取二维码内容
+                String qrData = resolveData(element.dataSource, dataMap, "");
+
+                System.out.println("[DEBUG] 二维码元素: " + element.id);
+                System.out.println("[DEBUG] 二维码数据源: " + element.dataSource);
+                System.out.println("[DEBUG] 二维码内容: " + qrData);
+                System.out.println("[DEBUG] 二维码位置: (" + element.x + ", " + element.y + ")");
+                System.out.println("[DEBUG] 二维码尺寸: " + element.width + "x" + element.height);
+
+                // 绘制二维码
+                tsc.qrcode(
+                    String.valueOf(element.x),
+                    String.valueOf(element.y),
+                    element.eccLevel,
+                    element.cellWidth,
+                    "A",  // mode
+                    "0",  // rotation
+                    qrData
+                );
+            }
+        }
+    }
 
-        // 左边距:1.6mm ≈ 13 dots
-        final int LEFT_MARGIN = 13;
-        // 上边距调整:整体上移 20 dots,确保内容在一张标签内
-        final int TOP_ADJUST = -20;
+    /**
+     * 准备数据映射
+     */
+    private Map<String, String> prepareDataMap(String sn, int printIndex) {
+        Map<String, String> dataMap = new HashMap<>();
 
-        // 1. 二维码(左侧中间)
-        // 原始位置:(56, 115),加上左边距和上边距调整
-        tsc.qrcode(String.valueOf(56 + LEFT_MARGIN), String.valueOf(115 + TOP_ADJUST), "L", "4", "A", "0", sn);
+        // SN 批次号
+        dataMap.put("sn", sn);
 
-        // 2. 文本1 - 产品名称(右上角,2行)
-        // 原始位置:(267, 32),加上左边距和上边距调整
-        // 第1行:高压电池下壳体总成
-        tsc.windowsfont(267 + LEFT_MARGIN, 32 + TOP_ADJUST, 28, 0, 1, 0, "SimSun", "高压电池下壳体总成");
+        // 产品型号
+        String productModel = getProductModel(sn);
+        dataMap.put("productModel", productModel);
 
-        // 第2行:产品型号(距离第1行约 10mm = 80 dots)
-        tsc.windowsfont(267 + LEFT_MARGIN, 112 + TOP_ADJUST, 24, 0, 0, 0, "Arial", productModel);
+        // 日期
+        String date = new SimpleDateFormat("yyyy/MM/dd").format(new Date());
+        dataMap.put("date", date);
 
-        // 3. 文本2 - 公司名称(左上角,2行)
-        // 原始位置:(6, 32),加上左边距和上边距调整
-        // 第1行:广西巨石
-        tsc.windowsfont(6 + LEFT_MARGIN, 32 + TOP_ADJUST, 26, 0, 1, 0, "SimSun", "广西巨石");
+        // 流水号
+        String serialNo = String.format("%07d", printIndex);
+        dataMap.put("serialNo", serialNo);
 
-        // 第2行:新能源科技有限公司(距离第1行约 10mm = 80 dots)
-        tsc.windowsfont(6 + LEFT_MARGIN, 112 + TOP_ADJUST, 22, 0, 0, 0, "SimSun", "新能源科技有限公司");
+        // 日期+流水号
+        dataMap.put("dateSerial", date + " NO:" + serialNo);
 
-        // 4. 日期 + 流水号(右侧中间)
-        // 原始位置:(208, 160),加上左边距和上边距调整
-        String dateSerial = date + " NO:" + serialNo;
-        tsc.windowsfont(208 + LEFT_MARGIN, 160 + TOP_ADJUST, 24, 0, 0, 0, "Arial", dateSerial);
+        return dataMap;
+    }
 
-        // 5. 批次号(右下角)
-        // 原始位置:(208, 248),加上左边距和上边距调整
-        tsc.windowsfont(208 + LEFT_MARGIN, 248 + TOP_ADJUST, 20, 0, 0, 0, "Arial", sn);
+    /**
+     * 解析数据源
+     */
+    private String resolveData(String dataSource, Map<String, String> dataMap, String defaultValue) {
+        if ("static".equals(dataSource)) {
+            return defaultValue;
+        } else if (dataMap.containsKey(dataSource)) {
+            return dataMap.get(dataSource);
+        } else {
+            return defaultValue;
+        }
     }
 
     /**