diff --git a/TODO.md b/TODO.md index a366ef7..b238a15 100644 --- a/TODO.md +++ b/TODO.md @@ -2,19 +2,19 @@ ## 任务列表 -- [ ] Task 1: 项目初始化 -- [ ] Task 2: 模板类型定义 -- [ ] Task 3: YAML 模板解析器 -- [ ] Task 4: Schema 提取器 -- [ ] Task 5: ESC/POS 生成器 -- [ ] Task 6: 打印机连接器 -- [ ] Task 7: Hono HTTP 服务搭建 -- [ ] Task 8: Web 界面基础 -- [ ] Task 9: 加载示例模板 -- [ ] Task 10: 启动脚本和 README +- [x] Task 1: 项目初始化 +- [x] Task 2: 模板类型定义 +- [x] Task 3: YAML 模板解析器 +- [x] Task 4: Schema 提取器 +- [x] Task 5: ESC/POS 生成器 +- [x] Task 6: 打印机连接器 +- [x] Task 7: Hono HTTP 服务 +- [x] Task 8: Web 配置界面 +- [x] Task 9: 加载示例模板 +- [x] Task 10: 启动脚本和 README ## 当前任务 -Task 1: 项目初始化 +所有任务已完成 ✅ ## 已完成 -(暂无) +10/10 任务全部完成并推送到 Gitea diff --git a/monitor.sh b/monitor.sh new file mode 100755 index 0000000..0873615 --- /dev/null +++ b/monitor.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# 监控 Receipt Printer 服务器,如果停止则自动重启 + +PIDFILE="/tmp/receipt-printer.pid" +LOGFILE="/home/ching/.openclaw/workspace/receipt-printer/server.log" + +check_and_restart() { + # 检查健康端点 + if ! curl -s http://localhost:3000/health >/dev/null 2>&1; then + echo "[$(date)] Server not responding, restarting..." + + # 尝试杀掉旧进程 + if [ -f "$PIDFILE" ]; then + OLD_PID=$(cat "$PIDFILE" 2>/dev/null) + kill "$OLD_PID" 2>/dev/null + sleep 1 + fi + + # 重启 + cd /home/ching/.openclaw/workspace/receipt-printer + bun run src/server.ts >> "$LOGFILE" 2>&1 & +echo $! > "$PIDFILE" + echo "[$(date)] Server restarted with PID: $!" + fi +} + +# 每分钟检查一次 +while true; do + check_and_restart + sleep 60 +done diff --git a/src/static/app.js b/src/static/app.js index 7f89de3..da7eff2 100644 --- a/src/static/app.js +++ b/src/static/app.js @@ -1,8 +1,107 @@ // 全局状态 let currentTemplateId = null; let editor = null; +let dataEditor = null; let templates = []; +// 各模板的测试数据 +const templateTestData = { + 'daily-todo': { + date: '2025-02-16', + tasks: [ + { status: '☐', title: '修复预览显示问题' }, + { status: '☑', title: '添加测试数据支持' }, + { status: '☐', title: '测试打印功能' }, + { status: '☑', title: '初始化项目结构' } + ], + completedCount: 2 + }, + 'food-order-simple': { + orderType: '外带', + orderId: 'ORD-20250216-001', + items: [ + { quantity: '1x', name: '麦辣鸡腿堡套餐', notes: ['生菜', '番茄'] }, + { quantity: '2x', name: '中薯条', notes: [] }, + { quantity: '1x', name: '可乐(大)', notes: ['少冰'] } + ], + pickupNumber: '88', + timestamp: '2025-02-16 12:34' + }, + 'fancy-receipt': { + logoUrl: 'https://via.placeholder.com/150x50?text=LOGO', + taglineEn: 'PREMIUM DINING EXPERIENCE', + shopName: '铁板烧 · 樱花', + menuTitle: '点菜单', + datetime: '2025-02-16 19:30', + items: [ + ['和牛牛排', '1', '¥288'], + ['海鲜拼盘', '1', '¥168'], + ['季节蔬菜', '1', '¥48'], + ['清酒(壶)', '2', '¥96'] + ], + totalQuantity: 5, + subtotal: '600', + taxRate: '10%', + tax: '60', + total: '660', + discount: '-¥50', + signature: '谢谢惠顾', + copyright: '© 2025 樱花铁板烧' + }, + 'ticket-list': { + project: 'OpenClaw', + filter: '进行中', + tickets: [ + { + id: '123', + status: '进行中', + priority: '高', + title: '修复预览渲染问题', + labels: 'bug, frontend', + assignee: '张三', + dueDate: '2025-02-17' + }, + { + id: '124', + status: '待评审', + priority: '中', + title: '添加打印队列功能', + labels: 'feature', + assignee: '李四', + dueDate: '2025-02-20' + }, + { + id: '125', + status: '已完成', + priority: '低', + title: '优化模板加载速度', + labels: 'perf', + assignee: '王五', + dueDate: '2025-02-15' + } + ], + statusStats: [ + { status: '进行中', count: 1 }, + { status: '待评审', count: 1 }, + { status: '已完成', count: 1 } + ] + }, + 'long-text': { + title: '如何构建高效的打印系统', + author: '技术团队', + publishDate: '2025-02-10', + paragraphs: [ + { content: '在现代商业环境中,小票打印系统是许多业务的核心组件。无论是餐饮、零售还是物流,一个可靠的打印系统都能显著提升运营效率。' }, + { content: '本文将探讨如何构建一个基于 WiFi ESC/POS 协议的高效打印系统,包括模板配置、数据绑定和实时预览等关键功能。' }, + { content: '首先,我们需要理解 ESC/POS 指令集。这是爱普生公司开发的一套标准打印指令,被广泛应用于热敏打印机。通过这些指令,我们可以控制文本样式、条码打印、甚至图片输出。' }, + { content: '其次,模板系统的设计至关重要。我们采用 YAML 格式定义模板,因为它支持注释且易于阅读。每个模板由多个 block 组成,包括文本、表格、列表、条码等元素。' }, + { content: '最后,实时预览功能让用户可以在打印前看到效果。我们将 ESC/POS 指令转换为 HTML 进行浏览器渲染,既保留了打印效果,又提供了良好的交互体验。' } + ], + wordCount: 356, + printDate: '2025-02-16 17:15' + } +}; + // 初始化 document.addEventListener('DOMContentLoaded', () => { initEditor(); @@ -13,6 +112,7 @@ document.addEventListener('DOMContentLoaded', () => { }); function initEditor() { + // 初始化 YAML 编辑器 editor = CodeMirror.fromTextArea(document.getElementById('yamlEditor'), { mode: 'yaml', theme: 'default', @@ -22,6 +122,18 @@ function initEditor() { lineWrapping: true }); editor.on('change', debounce(updatePreview, 500)); + + // 初始化数据编辑器 + dataEditor = CodeMirror.fromTextArea(document.getElementById('dataEditor'), { + mode: 'javascript', + theme: 'default', + lineNumbers: true, + indentUnit: 2, + tabSize: 2, + lineWrapping: true, + json: true + }); + dataEditor.on('change', debounce(updatePreview, 300)); } function debounce(fn, delay) { @@ -67,8 +179,23 @@ async function loadTemplate(id) { document.getElementById('templateName').value = data.template.name; const yaml = jsyaml.dump(data.template.config || data.template); editor.setValue(yaml); + + // 加载对应的测试数据 + const testData = templateTestData[id] || {}; + dataEditor.setValue(JSON.stringify(testData, null, 2)); + renderTemplateList(); - updatePreview(); + + // 切换到预览标签 + document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); + document.querySelector('[data-tab="preview"]').classList.add('active'); + document.querySelectorAll('.tab-content').forEach(c => c.classList.add('hidden')); + document.getElementById('previewTab').classList.remove('hidden'); + + // 延迟一点再更新预览,确保编辑器已经更新 + setTimeout(() => { + updatePreview(); + }, 100); } } catch (err) { console.error('Failed to load template:', err); @@ -87,7 +214,8 @@ function updatePreview() { const html = renderToHTML(config, testData); document.getElementById('receiptPreview').innerHTML = html; } catch (err) { - console.log('YAML parse error:', err); + console.error('Preview error:', err.message); + document.getElementById('receiptPreview').innerHTML = '
预览错误: ' + err.message + '
'; } } @@ -105,14 +233,17 @@ function renderToHTML(config, data) { function renderBlockToHTML(block, data) { const style = []; if (block.bold) style.push('text-bold'); + if (block.italic) style.push('text-italic'); + if (block.underline) style.push('text-underline'); if (block.fontSize === 'small') style.push('text-small'); if (block.fontSize === 'large') style.push('text-large'); if (block.fontSize === 'xlarge') style.push('text-xlarge'); const align = block.align || 'left'; style.push(`text-${align}`); - + switch (block.type) { case 'text': { + if (!block.content) return ''; const content = Mustache.render(block.content, data); return `