Compare commits
No commits in common. "fff24a08ed445dc4dfa04f82f05dd66642e087d2" and "b4e16d65154990124b1f406d4516c1bf580c5952" have entirely different histories.
fff24a08ed
...
b4e16d6515
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,5 +0,0 @@
|
|||||||
node_modules/
|
|
||||||
dist/
|
|
||||||
.env
|
|
||||||
*.log
|
|
||||||
.DS_Store
|
|
||||||
20
TODO.md
20
TODO.md
@ -1,20 +0,0 @@
|
|||||||
# Receipt Printer 实施进度
|
|
||||||
|
|
||||||
## 任务列表
|
|
||||||
|
|
||||||
- [ ] 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
|
|
||||||
|
|
||||||
## 当前任务
|
|
||||||
Task 1: 项目初始化
|
|
||||||
|
|
||||||
## 已完成
|
|
||||||
(暂无)
|
|
||||||
46
bun.lock
46
bun.lock
@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"lockfileVersion": 1,
|
|
||||||
"configVersion": 1,
|
|
||||||
"workspaces": {
|
|
||||||
"": {
|
|
||||||
"name": "receipt-printer",
|
|
||||||
"dependencies": {
|
|
||||||
"hono": "^4.11.9",
|
|
||||||
"js-yaml": "^4.1.1",
|
|
||||||
"mustache": "^4.2.0",
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/bun": "latest",
|
|
||||||
"@types/js-yaml": "^4.0.9",
|
|
||||||
"@types/mustache": "^4.2.6",
|
|
||||||
"bun-types": "^1.3.9",
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"typescript": "^5",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"packages": {
|
|
||||||
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
|
|
||||||
|
|
||||||
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
|
|
||||||
|
|
||||||
"@types/mustache": ["@types/mustache@4.2.6", "", {}, "sha512-t+8/QWTAhOFlrF1IVZqKnMRJi84EgkIK5Kh0p2JV4OLywUvCwJPFxbJAl7XAow7DVIHsF+xW9f1MVzg0L6Szjw=="],
|
|
||||||
|
|
||||||
"@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="],
|
|
||||||
|
|
||||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
|
|
||||||
|
|
||||||
"hono": ["hono@4.11.9", "", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="],
|
|
||||||
|
|
||||||
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
|
||||||
|
|
||||||
"mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="],
|
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
15
package.json
15
package.json
@ -10,15 +10,14 @@
|
|||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hono": "^4.11.9",
|
"hono": "^4.0.0",
|
||||||
"js-yaml": "^4.1.1",
|
"js-yaml": "^4.1.0",
|
||||||
"mustache": "^4.2.0"
|
"mustache": "^4.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/mustache": "^4.2.6",
|
"@types/mustache": "^4.2.5",
|
||||||
"bun-types": "^1.3.9"
|
"bun-types": "latest"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"receipt",
|
"receipt",
|
||||||
@ -27,9 +26,5 @@
|
|||||||
"thermal-printer",
|
"thermal-printer",
|
||||||
"80mm"
|
"80mm"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"private": true,
|
|
||||||
"peerDependencies": {
|
|
||||||
"typescript": "^5"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,115 +0,0 @@
|
|||||||
import { Hono } from 'hono';
|
|
||||||
import { parseTemplate } from '../engine/parser';
|
|
||||||
import { renderTemplate } from '../engine/render';
|
|
||||||
import { extractSchema } from '../engine/schema';
|
|
||||||
import { PrinterConnector } from '../printer/connector';
|
|
||||||
import type { Template } from '../types/template';
|
|
||||||
|
|
||||||
const templates = new Map<string, Template>();
|
|
||||||
let printerConnector: PrinterConnector | null = null;
|
|
||||||
|
|
||||||
function getPrinter(): PrinterConnector {
|
|
||||||
if (!printerConnector) {
|
|
||||||
const ip = process.env.PRINTER_IP || '192.168.1.100';
|
|
||||||
const port = parseInt(process.env.PRINTER_PORT || '9100');
|
|
||||||
printerConnector = new PrinterConnector({ ip, port });
|
|
||||||
}
|
|
||||||
return printerConnector;
|
|
||||||
}
|
|
||||||
|
|
||||||
const api = new Hono();
|
|
||||||
|
|
||||||
// 模板管理
|
|
||||||
api.get('/templates', (c) => {
|
|
||||||
const list = Array.from(templates.values()).map(t => ({
|
|
||||||
id: t.id,
|
|
||||||
name: t.name,
|
|
||||||
description: t.description,
|
|
||||||
}));
|
|
||||||
return c.json({ success: true, templates: list });
|
|
||||||
});
|
|
||||||
|
|
||||||
api.get('/templates/:id', (c) => {
|
|
||||||
const id = c.req.param('id');
|
|
||||||
const template = templates.get(id);
|
|
||||||
if (!template) {
|
|
||||||
return c.json({ success: false, error: { code: 'TEMPLATE_NOT_FOUND', message: `Template '${id}' not found` }}, 404);
|
|
||||||
}
|
|
||||||
return c.json({ success: true, template });
|
|
||||||
});
|
|
||||||
|
|
||||||
api.post('/templates', async (c) => {
|
|
||||||
const body = await c.req.json();
|
|
||||||
if (!body.id || !body.config) {
|
|
||||||
return c.json({ success: false, error: { code: 'INVALID_REQUEST', message: 'Missing id or config' }}, 400);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const config = typeof body.config === 'string' ? parseTemplate(body.config) : body.config;
|
|
||||||
const template: Template = { ...config, id: body.id };
|
|
||||||
templates.set(body.id, template);
|
|
||||||
return c.json({ success: true, template: { id: template.id, name: template.name }}, 201);
|
|
||||||
} catch (error) {
|
|
||||||
return c.json({ success: false, error: { code: 'INVALID_TEMPLATE', message: String(error) }}, 400);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
api.get('/templates/:id/schema', (c) => {
|
|
||||||
const id = c.req.param('id');
|
|
||||||
const template = templates.get(id);
|
|
||||||
if (!template) {
|
|
||||||
return c.json({ success: false, error: { code: 'TEMPLATE_NOT_FOUND', message: `Template '${id}' not found` }}, 404);
|
|
||||||
}
|
|
||||||
const { schema, example } = extractSchema(template);
|
|
||||||
return c.json({ success: true, templateId: id, schema, example });
|
|
||||||
});
|
|
||||||
|
|
||||||
// 打印
|
|
||||||
api.post('/print/:templateId', async (c) => {
|
|
||||||
const templateId = c.req.param('templateId');
|
|
||||||
const template = templates.get(templateId);
|
|
||||||
if (!template) {
|
|
||||||
return c.json({ success: false, error: { code: 'TEMPLATE_NOT_FOUND', message: `Template '${templateId}' not found` }}, 404);
|
|
||||||
}
|
|
||||||
const body = await c.req.json();
|
|
||||||
const { data = {} } = body;
|
|
||||||
try {
|
|
||||||
const escData = renderTemplate(template, data);
|
|
||||||
const printer = getPrinter();
|
|
||||||
const jobId = printer.queue(escData);
|
|
||||||
return c.json({ success: true, jobId, status: 'queued', estimatedTime: '5s' });
|
|
||||||
} catch (error) {
|
|
||||||
return c.json({ success: false, error: { code: 'RENDER_ERROR', message: String(error) }}, 500);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 打印机状态
|
|
||||||
api.get('/printer/status', async (c) => {
|
|
||||||
const printer = getPrinter();
|
|
||||||
const status = await printer.getStatus();
|
|
||||||
return c.json({
|
|
||||||
success: true,
|
|
||||||
printer: {
|
|
||||||
ip: process.env.PRINTER_IP || '192.168.1.100',
|
|
||||||
port: parseInt(process.env.PRINTER_PORT || '9100'),
|
|
||||||
...status,
|
|
||||||
queueLength: printer.getQueueLength()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
api.post('/printer/test', (c) => {
|
|
||||||
const printer = getPrinter();
|
|
||||||
const testData = new Uint8Array([
|
|
||||||
0x1B, 0x40,
|
|
||||||
0x1B, 0x61, 0x01,
|
|
||||||
...new TextEncoder().encode('Printer Test Page\n'),
|
|
||||||
...new TextEncoder().encode('================\n'),
|
|
||||||
0x1B, 0x61, 0x00,
|
|
||||||
0x0A, 0x0A,
|
|
||||||
0x1D, 0x56, 0x00,
|
|
||||||
]);
|
|
||||||
const jobId = printer.queue(testData);
|
|
||||||
return c.json({ success: true, jobId, message: 'Test page queued' });
|
|
||||||
});
|
|
||||||
|
|
||||||
export { api as apiRoutes };
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
import { parseTemplate } from '../engine/parser';
|
|
||||||
import type { Template } from '../types/template';
|
|
||||||
|
|
||||||
export async function loadExampleTemplates(): Promise<Map<string, Template>> {
|
|
||||||
const templates = new Map<string, Template>();
|
|
||||||
|
|
||||||
const examples = [
|
|
||||||
'daily-todo.yaml',
|
|
||||||
'food-order-simple.yaml',
|
|
||||||
'fancy-receipt.yaml',
|
|
||||||
'ticket-list.yaml',
|
|
||||||
'long-text.yaml'
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const filename of examples) {
|
|
||||||
try {
|
|
||||||
const file = Bun.file(`./templates/examples/${filename}`);
|
|
||||||
if (await file.exists()) {
|
|
||||||
const content = await file.text();
|
|
||||||
const template = parseTemplate(content);
|
|
||||||
templates.set(template.id, template);
|
|
||||||
console.log(`✅ Loaded example template: ${template.name}`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`❌ Failed to load ${filename}:`, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return templates;
|
|
||||||
}
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
// ESC/POS 指令常量
|
|
||||||
export const ESC = 0x1B;
|
|
||||||
export const GS = 0x1D;
|
|
||||||
export const LF = 0x0A;
|
|
||||||
export const CR = 0x0D;
|
|
||||||
|
|
||||||
// 初始化打印机
|
|
||||||
export function initialize(): Uint8Array {
|
|
||||||
return new Uint8Array([ESC, 0x40]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切纸
|
|
||||||
export function cut(): Uint8Array {
|
|
||||||
return new Uint8Array([GS, 0x56, 0x00]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 文本编码
|
|
||||||
export function text(str: string): Uint8Array {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
return encoder.encode(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 换行
|
|
||||||
export function newline(): Uint8Array {
|
|
||||||
return new Uint8Array([LF]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加粗
|
|
||||||
export function bold(on: boolean): Uint8Array {
|
|
||||||
return new Uint8Array([ESC, 0x45, on ? 1 : 0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下划线
|
|
||||||
export function underline(on: boolean): Uint8Array {
|
|
||||||
return new Uint8Array([ESC, 0x2D, on ? 1 : 0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对齐
|
|
||||||
export function align(align: 'left' | 'center' | 'right'): Uint8Array {
|
|
||||||
const n = align === 'left' ? 0 : align === 'center' ? 1 : 2;
|
|
||||||
return new Uint8Array([ESC, 0x61, n]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 字体大小
|
|
||||||
export function fontSize(size: 'small' | 'normal' | 'large' | 'xlarge'): Uint8Array {
|
|
||||||
let n = 0;
|
|
||||||
switch (size) {
|
|
||||||
case 'small': n = 0; break;
|
|
||||||
case 'normal': n = 0; break;
|
|
||||||
case 'large': n = 0x11; break;
|
|
||||||
case 'xlarge': n = 0x22; break;
|
|
||||||
}
|
|
||||||
return new Uint8Array([GS, 0x21, n]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 恢复默认字体大小
|
|
||||||
export function resetFontSize(): Uint8Array {
|
|
||||||
return new Uint8Array([GS, 0x21, 0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打印条码 (CODE128)
|
|
||||||
export function barcodeCode128(data: string, height: number = 64): Uint8Array {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const dataBytes = encoder.encode(data);
|
|
||||||
return new Uint8Array([
|
|
||||||
GS, 0x68, height,
|
|
||||||
GS, 0x6B, 0x49, dataBytes.length,
|
|
||||||
...dataBytes,
|
|
||||||
0
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打印 QR 码
|
|
||||||
export function qrCode(data: string, size: number = 3): Uint8Array {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const dataBytes = encoder.encode(data);
|
|
||||||
const len = dataBytes.length + 3;
|
|
||||||
return new Uint8Array([
|
|
||||||
GS, 0x28, 0x6B, 0x03, 0x00, 0x31, 0x43, size,
|
|
||||||
GS, 0x28, 0x6B, len & 0xFF, (len >> 8) & 0xFF, 0x31, 0x50, 0x30,
|
|
||||||
...dataBytes,
|
|
||||||
GS, 0x28, 0x6B, 0x03, 0x00, 0x31, 0x51, 0x30
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
import YAML from 'js-yaml';
|
|
||||||
import type { Template } from '../types/template';
|
|
||||||
|
|
||||||
export function parseTemplate(yamlContent: string): Template {
|
|
||||||
try {
|
|
||||||
const parsed = YAML.load(yamlContent) as any;
|
|
||||||
|
|
||||||
// 基础验证
|
|
||||||
if (!parsed.name || !parsed.id) {
|
|
||||||
throw new Error('Template must have name and id');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(parsed.blocks)) {
|
|
||||||
throw new Error('Template must have blocks array');
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsed as Template;
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Failed to parse template: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loadTemplate(filePath: string): Template {
|
|
||||||
const content = Bun.file(filePath).text();
|
|
||||||
return parseTemplate(content);
|
|
||||||
}
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
import Mustache from 'mustache';
|
|
||||||
import type { Template, Block } from '../types/template';
|
|
||||||
import * as escpos from './escpos';
|
|
||||||
|
|
||||||
export function renderTemplate(template: Template, data: any): Uint8Array {
|
|
||||||
const buffers: Uint8Array[] = [];
|
|
||||||
|
|
||||||
// 初始化打印机
|
|
||||||
buffers.push(escpos.initialize());
|
|
||||||
|
|
||||||
// 渲染每个 block
|
|
||||||
for (const block of template.blocks) {
|
|
||||||
buffers.push(renderBlock(block, data, template.defaults));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切纸
|
|
||||||
buffers.push(escpos.cut());
|
|
||||||
|
|
||||||
return mergeBuffers(buffers);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderBlock(block: Block, data: any, defaults: any = {}): Uint8Array {
|
|
||||||
// 根据 block 类型渲染
|
|
||||||
switch (block.type) {
|
|
||||||
case 'text':
|
|
||||||
return renderTextBlock(block, data);
|
|
||||||
case 'divider':
|
|
||||||
return renderDividerBlock(block);
|
|
||||||
case 'space':
|
|
||||||
return renderSpaceBlock(block);
|
|
||||||
default:
|
|
||||||
console.warn(`Unknown block type: ${(block as any).type}`);
|
|
||||||
return new Uint8Array(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTextBlock(block: any, data: any): Uint8Array {
|
|
||||||
const buffers: Uint8Array[] = [];
|
|
||||||
buffers.push(escpos.align(block.align || 'left'));
|
|
||||||
buffers.push(escpos.bold(block.bold || false));
|
|
||||||
const content = Mustache.render(block.content, data);
|
|
||||||
buffers.push(escpos.text(content));
|
|
||||||
buffers.push(escpos.newline());
|
|
||||||
buffers.push(escpos.bold(false));
|
|
||||||
buffers.push(escpos.align('left'));
|
|
||||||
return mergeBuffers(buffers);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDividerBlock(block: any): Uint8Array {
|
|
||||||
const char = block.char || '-';
|
|
||||||
const line = char.repeat(48);
|
|
||||||
return mergeBuffers([escpos.text(line), escpos.newline()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSpaceBlock(block: any): Uint8Array {
|
|
||||||
const lines = block.lines || 1;
|
|
||||||
const buffers: Uint8Array[] = [];
|
|
||||||
for (let i = 0; i < lines; i++) {
|
|
||||||
buffers.push(escpos.newline());
|
|
||||||
}
|
|
||||||
return mergeBuffers(buffers);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeBuffers(buffers: Uint8Array[]): Uint8Array {
|
|
||||||
const totalLength = buffers.reduce((sum, buf) => sum + buf.length, 0);
|
|
||||||
const result = new Uint8Array(totalLength);
|
|
||||||
let offset = 0;
|
|
||||||
for (const buf of buffers) {
|
|
||||||
result.set(buf, offset);
|
|
||||||
offset += buf.length;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
import type { Template, Block } from '../types/template';
|
|
||||||
|
|
||||||
interface JSONSchema {
|
|
||||||
type: string;
|
|
||||||
properties: Record<string, any>;
|
|
||||||
required?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extractSchema(template: Template): { schema: JSONSchema; example: any } {
|
|
||||||
const variables = new Set<string>();
|
|
||||||
const arrayVariables = new Set<string>();
|
|
||||||
|
|
||||||
function extractFromString(str: string) {
|
|
||||||
const matches = str.match(/\{\{([^}]+)\}\}/g);
|
|
||||||
if (matches) {
|
|
||||||
matches.forEach(match => {
|
|
||||||
const varName = match.replace(/\{\{|\}\}/g, '').trim().split('.')[0];
|
|
||||||
variables.add(varName);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractFromBlock(block: Block) {
|
|
||||||
switch (block.type) {
|
|
||||||
case 'text':
|
|
||||||
extractFromString(block.content);
|
|
||||||
break;
|
|
||||||
case 'row':
|
|
||||||
block.columns.forEach(col => {
|
|
||||||
if (col.content) extractFromString(col.content);
|
|
||||||
if (col.header) extractFromString(col.header);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'table':
|
|
||||||
extractFromString(block.data);
|
|
||||||
block.columns.forEach(col => {
|
|
||||||
if (col.header) extractFromString(col.header);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'list':
|
|
||||||
extractFromString(block.data);
|
|
||||||
block.itemTemplate.forEach(extractFromBlock);
|
|
||||||
break;
|
|
||||||
case 'divider':
|
|
||||||
break;
|
|
||||||
case 'image':
|
|
||||||
extractFromString(block.src);
|
|
||||||
break;
|
|
||||||
case 'barcode':
|
|
||||||
extractFromString(block.data);
|
|
||||||
break;
|
|
||||||
case 'space':
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
template.blocks.forEach(extractFromBlock);
|
|
||||||
|
|
||||||
// 构建 JSON Schema
|
|
||||||
const properties: Record<string, any> = {};
|
|
||||||
|
|
||||||
variables.forEach(varName => {
|
|
||||||
if (varName === 'items' || varName === 'tasks' || varName === 'tickets') {
|
|
||||||
properties[varName] = { type: 'array', description: `${varName} list` };
|
|
||||||
arrayVariables.add(varName);
|
|
||||||
} else if (varName.endsWith('Count') || varName === 'quantity' || varName === 'total') {
|
|
||||||
properties[varName] = { type: 'number', description: varName };
|
|
||||||
} else {
|
|
||||||
properties[varName] = { type: 'string', description: varName };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const schema: JSONSchema = {
|
|
||||||
type: 'object',
|
|
||||||
properties
|
|
||||||
};
|
|
||||||
|
|
||||||
// 生成示例数据
|
|
||||||
const example: any = {};
|
|
||||||
variables.forEach(varName => {
|
|
||||||
if (arrayVariables.has(varName)) {
|
|
||||||
example[varName] = [
|
|
||||||
{ name: 'Item 1', value: 'value1' },
|
|
||||||
{ name: 'Item 2', value: 'value2' }
|
|
||||||
];
|
|
||||||
} else if (properties[varName].type === 'number') {
|
|
||||||
example[varName] = 42;
|
|
||||||
} else if (varName.includes('date') || varName.includes('Date')) {
|
|
||||||
example[varName] = '2025-02-12';
|
|
||||||
} else if (varName.includes('time') || varName.includes('Time')) {
|
|
||||||
example[varName] = '14:30:00';
|
|
||||||
} else {
|
|
||||||
example[varName] = varName;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return { schema, example };
|
|
||||||
}
|
|
||||||
@ -1,134 +0,0 @@
|
|||||||
interface PrinterConfig {
|
|
||||||
ip: string;
|
|
||||||
port: number;
|
|
||||||
timeout?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PrintJob {
|
|
||||||
id: string;
|
|
||||||
data: Uint8Array;
|
|
||||||
status: 'pending' | 'printing' | 'completed' | 'failed';
|
|
||||||
createdAt: Date;
|
|
||||||
startedAt?: Date;
|
|
||||||
completedAt?: Date;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PrinterConnector {
|
|
||||||
private config: PrinterConfig;
|
|
||||||
private queueItems: PrintJob[] = [];
|
|
||||||
private currentJob: PrintJob | null = null;
|
|
||||||
private connected = false;
|
|
||||||
private processing = false;
|
|
||||||
|
|
||||||
constructor(config: PrinterConfig) {
|
|
||||||
this.config = {
|
|
||||||
timeout: 5000,
|
|
||||||
...config
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
isConnected(): boolean {
|
|
||||||
return this.connected;
|
|
||||||
}
|
|
||||||
|
|
||||||
getQueueLength(): number {
|
|
||||||
return this.queueItems.length + (this.currentJob ? 1 : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
queue(data: Uint8Array): string {
|
|
||||||
const job: PrintJob = {
|
|
||||||
id: `job_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
||||||
data,
|
|
||||||
status: 'pending',
|
|
||||||
createdAt: new Date()
|
|
||||||
};
|
|
||||||
|
|
||||||
this.queueItems.push(job);
|
|
||||||
this.processQueue();
|
|
||||||
|
|
||||||
return job.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
getJob(jobId: string): PrintJob | undefined {
|
|
||||||
if (this.currentJob?.id === jobId) return this.currentJob;
|
|
||||||
return this.queueItems.find(j => j.id === jobId);
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllJobs(): PrintJob[] {
|
|
||||||
return this.currentJob
|
|
||||||
? [this.currentJob, ...this.queueItems]
|
|
||||||
: [...this.queueItems];
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelJob(jobId: string): boolean {
|
|
||||||
const index = this.queueItems.findIndex(j => j.id === jobId);
|
|
||||||
if (index >= 0 && this.queueItems[index].status === 'pending') {
|
|
||||||
this.queueItems[index].status = 'cancelled' as any;
|
|
||||||
this.queueItems.splice(index, 1);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async processQueue(): Promise<void> {
|
|
||||||
if (this.processing || this.queueItems.length === 0) return;
|
|
||||||
|
|
||||||
this.processing = true;
|
|
||||||
|
|
||||||
while (this.queueItems.length > 0) {
|
|
||||||
const job = this.queueItems.shift()!;
|
|
||||||
this.currentJob = job;
|
|
||||||
job.status = 'printing';
|
|
||||||
job.startedAt = new Date();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.sendToPrinter(job.data);
|
|
||||||
job.status = 'completed';
|
|
||||||
job.completedAt = new Date();
|
|
||||||
} catch (error) {
|
|
||||||
job.status = 'failed';
|
|
||||||
job.error = String(error);
|
|
||||||
console.error(`Print job ${job.id} failed:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentJob = null;
|
|
||||||
this.processing = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async sendToPrinter(data: Uint8Array): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const socket = new (Bun as any).connect({
|
|
||||||
hostname: this.config.ip,
|
|
||||||
port: this.config.port,
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.then((conn: any) => {
|
|
||||||
this.connected = true;
|
|
||||||
conn.write(data);
|
|
||||||
conn.end();
|
|
||||||
setTimeout(() => {
|
|
||||||
this.connected = false;
|
|
||||||
resolve();
|
|
||||||
}, 1000);
|
|
||||||
}).catch((err: any) => {
|
|
||||||
this.connected = false;
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getStatus(): Promise<{ online: boolean; paperStatus: string }> {
|
|
||||||
try {
|
|
||||||
const socket = await (Bun as any).connect({
|
|
||||||
hostname: this.config.ip,
|
|
||||||
port: this.config.port,
|
|
||||||
});
|
|
||||||
socket.end();
|
|
||||||
return { online: true, paperStatus: 'ok' };
|
|
||||||
} catch {
|
|
||||||
return { online: false, paperStatus: 'unknown' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
import { Hono } from 'hono';
|
|
||||||
import { serveStatic } from 'hono/bun';
|
|
||||||
import { apiRoutes } from './api/routes';
|
|
||||||
import { loadExampleTemplates } from './config/loader';
|
|
||||||
|
|
||||||
// 加载示例模板
|
|
||||||
const templates = await loadExampleTemplates();
|
|
||||||
console.log(`📋 Loaded ${templates.size} example templates`);
|
|
||||||
|
|
||||||
const app = new Hono();
|
|
||||||
|
|
||||||
// 静态文件服务(Web 界面)
|
|
||||||
app.use('/*', serveStatic({ root: './src/static' }));
|
|
||||||
|
|
||||||
// API 路由
|
|
||||||
app.route('/api', apiRoutes);
|
|
||||||
|
|
||||||
// 健康检查
|
|
||||||
app.get('/health', (c) => c.json({ status: 'ok' }));
|
|
||||||
|
|
||||||
const port = process.env.PORT || 3000;
|
|
||||||
|
|
||||||
console.log(`🖨️ Receipt Printer Server starting on http://localhost:${port}`);
|
|
||||||
|
|
||||||
export default {
|
|
||||||
port,
|
|
||||||
fetch: app.fetch,
|
|
||||||
};
|
|
||||||
@ -1,227 +0,0 @@
|
|||||||
// 全局状态
|
|
||||||
let currentTemplateId = null;
|
|
||||||
let editor = null;
|
|
||||||
let templates = [];
|
|
||||||
|
|
||||||
// 初始化
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
initEditor();
|
|
||||||
loadTemplates();
|
|
||||||
checkPrinterStatus();
|
|
||||||
bindEvents();
|
|
||||||
setInterval(checkPrinterStatus, 5000);
|
|
||||||
});
|
|
||||||
|
|
||||||
function initEditor() {
|
|
||||||
editor = CodeMirror.fromTextArea(document.getElementById('yamlEditor'), {
|
|
||||||
mode: 'yaml',
|
|
||||||
theme: 'default',
|
|
||||||
lineNumbers: true,
|
|
||||||
indentUnit: 2,
|
|
||||||
tabSize: 2,
|
|
||||||
lineWrapping: true
|
|
||||||
});
|
|
||||||
editor.on('change', debounce(updatePreview, 500));
|
|
||||||
}
|
|
||||||
|
|
||||||
function debounce(fn, delay) {
|
|
||||||
let timeout;
|
|
||||||
return (...args) => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(() => fn(...args), delay);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadTemplates() {
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/templates');
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.success) {
|
|
||||||
templates = data.templates;
|
|
||||||
renderTemplateList();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load templates:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTemplateList() {
|
|
||||||
const list = document.getElementById('templateList');
|
|
||||||
list.innerHTML = '';
|
|
||||||
templates.forEach(t => {
|
|
||||||
const li = document.createElement('li');
|
|
||||||
li.textContent = t.name;
|
|
||||||
li.dataset.id = t.id;
|
|
||||||
if (t.id === currentTemplateId) li.classList.add('active');
|
|
||||||
li.addEventListener('click', () => loadTemplate(t.id));
|
|
||||||
list.appendChild(li);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadTemplate(id) {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`/api/templates/${id}`);
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.success) {
|
|
||||||
currentTemplateId = id;
|
|
||||||
document.getElementById('templateName').value = data.template.name;
|
|
||||||
const yaml = jsyaml.dump(data.template.config || data.template);
|
|
||||||
editor.setValue(yaml);
|
|
||||||
renderTemplateList();
|
|
||||||
updatePreview();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load template:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePreview() {
|
|
||||||
try {
|
|
||||||
const yaml = editor.getValue();
|
|
||||||
if (!yaml.trim()) {
|
|
||||||
document.getElementById('receiptPreview').innerHTML = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const config = jsyaml.load(yaml);
|
|
||||||
const testData = getTestData();
|
|
||||||
const html = renderToHTML(config, testData);
|
|
||||||
document.getElementById('receiptPreview').innerHTML = html;
|
|
||||||
} catch (err) {
|
|
||||||
console.log('YAML parse error:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderToHTML(config, data) {
|
|
||||||
if (!config.blocks || !Array.isArray(config.blocks)) {
|
|
||||||
return '<p style="color: #999;">配置无效</p>';
|
|
||||||
}
|
|
||||||
let html = '';
|
|
||||||
for (const block of config.blocks) {
|
|
||||||
html += renderBlockToHTML(block, data);
|
|
||||||
}
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderBlockToHTML(block, data) {
|
|
||||||
const style = [];
|
|
||||||
if (block.bold) style.push('text-bold');
|
|
||||||
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': {
|
|
||||||
const content = Mustache.render(block.content, data);
|
|
||||||
return `<div class="${style.join(' ')}">${escapeHtml(content)}</div>`;
|
|
||||||
}
|
|
||||||
case 'divider': {
|
|
||||||
const char = block.char || '-';
|
|
||||||
return `<div class="divider">${char.repeat(48)}</div>`;
|
|
||||||
}
|
|
||||||
case 'row': {
|
|
||||||
const cols = block.columns.map(col => {
|
|
||||||
const content = col.content ? Mustache.render(col.content, data) : '';
|
|
||||||
return `<span>${escapeHtml(content)}</span>`;
|
|
||||||
});
|
|
||||||
return `<div style="display: flex; justify-content: space-between;">${cols.join('')}</div>`;
|
|
||||||
}
|
|
||||||
case 'space': {
|
|
||||||
return '<br>'.repeat(block.lines || 1);
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(text) {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.textContent = text;
|
|
||||||
return div.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTestData() {
|
|
||||||
try {
|
|
||||||
const json = document.getElementById('dataEditor').value;
|
|
||||||
return json ? JSON.parse(json) : {};
|
|
||||||
} catch {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkPrinterStatus() {
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/printer/status');
|
|
||||||
const data = await res.json();
|
|
||||||
const statusEl = document.getElementById('printerStatus');
|
|
||||||
const dot = statusEl.querySelector('.status-dot');
|
|
||||||
const text = statusEl.querySelector('.status-text');
|
|
||||||
if (data.success && data.printer.online) {
|
|
||||||
dot.className = 'status-dot online';
|
|
||||||
text.textContent = '在线';
|
|
||||||
} else {
|
|
||||||
dot.className = 'status-dot offline';
|
|
||||||
text.textContent = '离线';
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
const dot = document.querySelector('.status-dot');
|
|
||||||
const text = document.querySelector('.status-text');
|
|
||||||
if (dot) dot.className = 'status-dot offline';
|
|
||||||
if (text) text.textContent = '离线';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindEvents() {
|
|
||||||
document.getElementById('newTemplate').addEventListener('click', () => {
|
|
||||||
currentTemplateId = null;
|
|
||||||
document.getElementById('templateName').value = '';
|
|
||||||
editor.setValue(`name: "新模板"
|
|
||||||
id: "new-template"
|
|
||||||
width: 80mm
|
|
||||||
blocks:
|
|
||||||
- type: text
|
|
||||||
content: "Hello World"
|
|
||||||
align: center
|
|
||||||
`);
|
|
||||||
document.getElementById('receiptPreview').innerHTML = '';
|
|
||||||
renderTemplateList();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('saveTemplate').addEventListener('click', async () => {
|
|
||||||
if (!currentTemplateId) {
|
|
||||||
const id = prompt('输入模板 ID(英文、数字、连字符):');
|
|
||||||
if (!id) return;
|
|
||||||
currentTemplateId = id;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const yaml = editor.getValue();
|
|
||||||
const config = jsyaml.load(yaml);
|
|
||||||
const name = document.getElementById('templateName').value || config.name || '未命名';
|
|
||||||
const res = await fetch('/api/templates', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ id: currentTemplateId, name, config })
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.success) {
|
|
||||||
alert('保存成功');
|
|
||||||
loadTemplates();
|
|
||||||
} else {
|
|
||||||
alert('保存失败: ' + data.error.message);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
alert('保存失败: ' + err.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', (e) => {
|
|
||||||
const tab = e.target.dataset.tab;
|
|
||||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
||||||
e.target.classList.add('active');
|
|
||||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.add('hidden'));
|
|
||||||
document.getElementById(tab + 'Tab').classList.remove('hidden');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,88 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Receipt Printer - 小票打印机配置</title>
|
|
||||||
<link rel="stylesheet" href="style.css">
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="app">
|
|
||||||
<header class="header">
|
|
||||||
<h1>🖨️ Receipt Printer</h1>
|
|
||||||
<div class="printer-status" id="printerStatus">
|
|
||||||
<span class="status-dot offline"></span>
|
|
||||||
<span class="status-text">离线</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="main">
|
|
||||||
<!-- 左侧:模板列表 -->
|
|
||||||
<aside class="sidebar">
|
|
||||||
<div class="sidebar-header">
|
|
||||||
<h2>模板</h2>
|
|
||||||
<button class="btn btn-primary" id="newTemplate">+ 新建</button>
|
|
||||||
</div>
|
|
||||||
<ul class="template-list" id="templateList">
|
|
||||||
<!-- 动态加载 -->
|
|
||||||
</ul>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<!-- 中间:编辑器 -->
|
|
||||||
<div class="editor">
|
|
||||||
<div class="editor-header">
|
|
||||||
<input type="text" id="templateName" placeholder="模板名称" class="template-name">
|
|
||||||
<div class="editor-actions">
|
|
||||||
<button class="btn" id="saveTemplate">保存</button>
|
|
||||||
<button class="btn btn-danger" id="deleteTemplate">删除</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="editor-body">
|
|
||||||
<textarea id="yamlEditor"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="editor-toolbar">
|
|
||||||
<span>插入:</span>
|
|
||||||
<button class="btn btn-sm" data-block="text">文本</button>
|
|
||||||
<button class="btn btn-sm" data-block="row">多列</button>
|
|
||||||
<button class="btn btn-sm" data-block="table">表格</button>
|
|
||||||
<button class="btn btn-sm" data-block="list">列表</button>
|
|
||||||
<button class="btn btn-sm" data-block="divider">分隔线</button>
|
|
||||||
<button class="btn btn-sm" data-block="barcode">条码</button>
|
|
||||||
<button class="btn btn-sm" data-block="space">空行</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 右侧:预览和数据 -->
|
|
||||||
<aside class="preview-panel">
|
|
||||||
<div class="preview-tabs">
|
|
||||||
<button class="tab-btn active" data-tab="preview">预览</button>
|
|
||||||
<button class="tab-btn" data-tab="data">测试数据</button>
|
|
||||||
<button class="tab-btn" data-tab="schema">Schema</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-content" id="previewTab">
|
|
||||||
<div class="receipt-preview" id="receiptPreview">
|
|
||||||
<!-- 实时预览 -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-content hidden" id="dataTab">
|
|
||||||
<textarea id="dataEditor" placeholder='{"key": "value"}'></textarea>
|
|
||||||
<button class="btn btn-primary" id="testPrint">打印测试</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-content hidden" id="schemaTab">
|
|
||||||
<pre id="schemaDisplay"></pre>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/yaml/yaml.min.js"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/4.2.0/mustache.min.js"></script>
|
|
||||||
<script src="app.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,301 +0,0 @@
|
|||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
background: #f5f5f5;
|
|
||||||
color: #333;
|
|
||||||
height: 100vh;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header */
|
|
||||||
.header {
|
|
||||||
background: #fff;
|
|
||||||
padding: 12px 24px;
|
|
||||||
border-bottom: 1px solid #e0e0e0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.printer-status {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot.online { background: #4caf50; }
|
|
||||||
.status-dot.offline { background: #f44336; }
|
|
||||||
|
|
||||||
/* Main Layout */
|
|
||||||
.main {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sidebar */
|
|
||||||
.sidebar {
|
|
||||||
width: 220px;
|
|
||||||
background: #fff;
|
|
||||||
border-right: 1px solid #e0e0e0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-header {
|
|
||||||
padding: 16px;
|
|
||||||
border-bottom: 1px solid #e0e0e0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-header h2 {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-list {
|
|
||||||
list-style: none;
|
|
||||||
overflow-y: auto;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-list li {
|
|
||||||
padding: 12px 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-list li:hover {
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-list li.active {
|
|
||||||
background: #e3f2fd;
|
|
||||||
color: #1976d2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Editor */
|
|
||||||
.editor {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-header {
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-bottom: 1px solid #e0e0e0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-name {
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-body {
|
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror {
|
|
||||||
height: 100%;
|
|
||||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-toolbar {
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-top: 1px solid #e0e0e0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-toolbar span {
|
|
||||||
color: #666;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Preview Panel */
|
|
||||||
.preview-panel {
|
|
||||||
width: 360px;
|
|
||||||
background: #fff;
|
|
||||||
border-left: 1px solid #e0e0e0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-tabs {
|
|
||||||
display: flex;
|
|
||||||
border-bottom: 1px solid #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-btn {
|
|
||||||
flex: 1;
|
|
||||||
padding: 12px;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #666;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-btn:hover {
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-btn.active {
|
|
||||||
color: #1976d2;
|
|
||||||
border-bottom: 2px solid #1976d2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-content {
|
|
||||||
flex: 1;
|
|
||||||
overflow: auto;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-content.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Receipt Preview */
|
|
||||||
.receipt-preview {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
padding: 20px;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.6;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.receipt-preview .text-left { text-align: left; }
|
|
||||||
.receipt-preview .text-center { text-align: center; }
|
|
||||||
.receipt-preview .text-right { text-align: right; }
|
|
||||||
.receipt-preview .text-bold { font-weight: bold; }
|
|
||||||
.receipt-preview .text-small { font-size: 10px; }
|
|
||||||
.receipt-preview .text-large { font-size: 14px; }
|
|
||||||
.receipt-preview .text-xlarge { font-size: 18px; }
|
|
||||||
.receipt-preview .divider {
|
|
||||||
border-top: 1px dashed #999;
|
|
||||||
margin: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Data Editor */
|
|
||||||
#dataEditor {
|
|
||||||
width: 100%;
|
|
||||||
height: 300px;
|
|
||||||
padding: 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
#schemaDisplay {
|
|
||||||
background: #f5f5f5;
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.btn {
|
|
||||||
padding: 8px 16px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 13px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:hover {
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: #1976d2;
|
|
||||||
color: #fff;
|
|
||||||
border-color: #1976d2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: #1565c0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
color: #f44336;
|
|
||||||
border-color: #f44336;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger:hover {
|
|
||||||
background: #ffebee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sm {
|
|
||||||
padding: 4px 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbar */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: #f1f1f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: #c1c1c1;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #a1a1a1;
|
|
||||||
}
|
|
||||||
@ -1,109 +0,0 @@
|
|||||||
// 对齐方式
|
|
||||||
export type Align = 'left' | 'center' | 'right';
|
|
||||||
|
|
||||||
// 字体大小
|
|
||||||
export type FontSize = 'small' | 'normal' | 'large' | 'xlarge';
|
|
||||||
|
|
||||||
// 条码格式
|
|
||||||
export type BarcodeFormat = 'CODE128' | 'QR' | 'EAN13';
|
|
||||||
|
|
||||||
// 基础样式
|
|
||||||
export interface BlockStyle {
|
|
||||||
align?: Align;
|
|
||||||
fontSize?: FontSize;
|
|
||||||
bold?: boolean;
|
|
||||||
italic?: boolean;
|
|
||||||
underline?: boolean;
|
|
||||||
lineHeight?: number;
|
|
||||||
marginTop?: number;
|
|
||||||
marginBottom?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 列定义
|
|
||||||
export interface Column {
|
|
||||||
content?: string;
|
|
||||||
align?: Align;
|
|
||||||
width?: string | number;
|
|
||||||
bold?: boolean;
|
|
||||||
header?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block 类型
|
|
||||||
export interface TextBlock extends BlockStyle {
|
|
||||||
type: 'text';
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RowBlock extends BlockStyle {
|
|
||||||
type: 'row';
|
|
||||||
columns: Column[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TableBlock extends BlockStyle {
|
|
||||||
type: 'table';
|
|
||||||
columns: Column[];
|
|
||||||
data: string; // mustache expression
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ListBlock extends BlockStyle {
|
|
||||||
type: 'list';
|
|
||||||
data: string; // mustache expression
|
|
||||||
itemTemplate: Block[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DividerBlock extends BlockStyle {
|
|
||||||
type: 'divider';
|
|
||||||
char?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ImageBlock extends BlockStyle {
|
|
||||||
type: 'image';
|
|
||||||
src: string; // mustache expression or URL
|
|
||||||
maxWidth?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BarcodeBlock extends BlockStyle {
|
|
||||||
type: 'barcode';
|
|
||||||
format: BarcodeFormat;
|
|
||||||
data: string; // mustache expression
|
|
||||||
height?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SpaceBlock extends BlockStyle {
|
|
||||||
type: 'space';
|
|
||||||
lines?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Block =
|
|
||||||
| TextBlock
|
|
||||||
| RowBlock
|
|
||||||
| TableBlock
|
|
||||||
| ListBlock
|
|
||||||
| DividerBlock
|
|
||||||
| ImageBlock
|
|
||||||
| BarcodeBlock
|
|
||||||
| SpaceBlock;
|
|
||||||
|
|
||||||
// 页面设置
|
|
||||||
export interface PageConfig {
|
|
||||||
marginTop?: number;
|
|
||||||
marginBottom?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认样式
|
|
||||||
export interface DefaultStyle {
|
|
||||||
fontSize?: FontSize;
|
|
||||||
lineHeight?: number;
|
|
||||||
marginBottom?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 完整模板
|
|
||||||
export interface Template {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
width: string; // e.g., "80mm"
|
|
||||||
page?: PageConfig;
|
|
||||||
defaults?: DefaultStyle;
|
|
||||||
blocks: Block[];
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import { describe, it, expect } from 'bun:test';
|
|
||||||
import { PrinterConnector } from '../src/printer/connector';
|
|
||||||
|
|
||||||
describe('Printer Connector', () => {
|
|
||||||
it('should create connector with config', () => {
|
|
||||||
const connector = new PrinterConnector({
|
|
||||||
ip: '192.168.1.100',
|
|
||||||
port: 9100
|
|
||||||
});
|
|
||||||
expect(connector).toBeDefined();
|
|
||||||
expect(connector.isConnected()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should queue print jobs', () => {
|
|
||||||
const connector = new PrinterConnector({
|
|
||||||
ip: '192.168.1.100',
|
|
||||||
port: 9100
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = new Uint8Array([0x1B, 0x40]);
|
|
||||||
const jobId = connector.queue(data);
|
|
||||||
expect(jobId).toMatch(/^job_/);
|
|
||||||
expect(connector.getQueueLength()).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
import { describe, it, expect } from 'bun:test';
|
|
||||||
import { parseTemplate } from '../src/engine/parser';
|
|
||||||
|
|
||||||
describe('Template Parser', () => {
|
|
||||||
it('should parse simple text template', () => {
|
|
||||||
const yaml = `
|
|
||||||
name: "Test Template"
|
|
||||||
id: "test"
|
|
||||||
width: 80mm
|
|
||||||
blocks:
|
|
||||||
- type: text
|
|
||||||
content: "Hello World"
|
|
||||||
align: center
|
|
||||||
`;
|
|
||||||
const template = parseTemplate(yaml);
|
|
||||||
expect(template.name).toBe('Test Template');
|
|
||||||
expect(template.blocks).toHaveLength(1);
|
|
||||||
expect(template.blocks[0].type).toBe('text');
|
|
||||||
expect((template.blocks[0] as any).content).toBe('Hello World');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse row with columns', () => {
|
|
||||||
const yaml = `
|
|
||||||
name: "Row Test"
|
|
||||||
id: "row-test"
|
|
||||||
width: 80mm
|
|
||||||
blocks:
|
|
||||||
- type: row
|
|
||||||
columns:
|
|
||||||
- content: "Left"
|
|
||||||
align: left
|
|
||||||
width: 50%
|
|
||||||
- content: "Right"
|
|
||||||
align: right
|
|
||||||
width: 50%
|
|
||||||
`;
|
|
||||||
const template = parseTemplate(yaml);
|
|
||||||
const row = template.blocks[0] as any;
|
|
||||||
expect(row.type).toBe('row');
|
|
||||||
expect(row.columns).toHaveLength(2);
|
|
||||||
expect(row.columns[0].align).toBe('left');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse list with item template', () => {
|
|
||||||
const yaml = `
|
|
||||||
name: "List Test"
|
|
||||||
id: "list-test"
|
|
||||||
width: 80mm
|
|
||||||
blocks:
|
|
||||||
- type: list
|
|
||||||
data: "{{items}}"
|
|
||||||
itemTemplate:
|
|
||||||
- type: text
|
|
||||||
content: "{{name}}"
|
|
||||||
`;
|
|
||||||
const template = parseTemplate(yaml);
|
|
||||||
const list = template.blocks[0] as any;
|
|
||||||
expect(list.type).toBe('list');
|
|
||||||
expect(list.data).toBe('{{items}}');
|
|
||||||
expect(list.itemTemplate).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw on invalid YAML', () => {
|
|
||||||
const yaml = 'invalid: [unclosed';
|
|
||||||
expect(() => parseTemplate(yaml)).toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
import { describe, it, expect } from 'bun:test';
|
|
||||||
import { renderTemplate } from '../src/engine/render';
|
|
||||||
import type { Template } from '../src/types/template';
|
|
||||||
|
|
||||||
describe('Template Renderer', () => {
|
|
||||||
it('should render text block', () => {
|
|
||||||
const template: Template = {
|
|
||||||
id: 'test',
|
|
||||||
name: 'Test',
|
|
||||||
width: '80mm',
|
|
||||||
blocks: [
|
|
||||||
{ type: 'text', content: 'Hello World', align: 'center' }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = {};
|
|
||||||
const result = renderTemplate(template, data);
|
|
||||||
expect(result).toBeInstanceOf(Uint8Array);
|
|
||||||
expect(result.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should substitute mustache variables', () => {
|
|
||||||
const template: Template = {
|
|
||||||
id: 'test',
|
|
||||||
name: 'Test',
|
|
||||||
width: '80mm',
|
|
||||||
blocks: [
|
|
||||||
{ type: 'text', content: 'Hello {{name}}' }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = { name: 'Alice' };
|
|
||||||
const result = renderTemplate(template, data);
|
|
||||||
const text = new TextDecoder().decode(result);
|
|
||||||
expect(text).toContain('Hello Alice');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render divider', () => {
|
|
||||||
const template: Template = {
|
|
||||||
id: 'test',
|
|
||||||
name: 'Test',
|
|
||||||
width: '80mm',
|
|
||||||
blocks: [
|
|
||||||
{ type: 'divider', char: '=' }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = renderTemplate(template, {});
|
|
||||||
const text = new TextDecoder().decode(result);
|
|
||||||
expect(text).toContain('========');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
import { describe, it, expect } from 'bun:test';
|
|
||||||
import { extractSchema } from '../src/engine/schema';
|
|
||||||
import type { Template } from '../src/types/template';
|
|
||||||
|
|
||||||
describe('Schema Extractor', () => {
|
|
||||||
it('should extract simple variables', () => {
|
|
||||||
const template: Template = {
|
|
||||||
id: 'test',
|
|
||||||
name: 'Test',
|
|
||||||
width: '80mm',
|
|
||||||
blocks: [
|
|
||||||
{ type: 'text', content: 'Hello {{name}}' },
|
|
||||||
{ type: 'text', content: 'Date: {{date}}' }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const { schema } = extractSchema(template);
|
|
||||||
expect(schema.properties.name).toEqual({ type: 'string', description: 'name' });
|
|
||||||
expect(schema.properties.date).toEqual({ type: 'string', description: 'date' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should extract list variables', () => {
|
|
||||||
const template: Template = {
|
|
||||||
id: 'test',
|
|
||||||
name: 'Test',
|
|
||||||
width: '80mm',
|
|
||||||
blocks: [
|
|
||||||
{
|
|
||||||
type: 'list',
|
|
||||||
data: '{{items}}',
|
|
||||||
itemTemplate: [
|
|
||||||
{ type: 'text', content: '{{title}}' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const { schema } = extractSchema(template);
|
|
||||||
expect(schema.properties.items).toEqual({ type: 'array', description: 'items list' });
|
|
||||||
expect(schema.properties.title).toEqual({ type: 'string', description: 'title' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should generate example data', () => {
|
|
||||||
const template: Template = {
|
|
||||||
id: 'test',
|
|
||||||
name: 'Test',
|
|
||||||
width: '80mm',
|
|
||||||
blocks: [
|
|
||||||
{ type: 'text', content: '{{name}} - {{count}}' }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const { example } = extractSchema(template);
|
|
||||||
expect(example).toHaveProperty('name');
|
|
||||||
expect(example).toHaveProperty('count');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"lib": ["ESNext"],
|
|
||||||
"module": "esnext",
|
|
||||||
"target": "esnext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"strict": true,
|
|
||||||
"declaration": true,
|
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./src",
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"allowSyntheticDefaultImports": true
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"],
|
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user