控件系统概览
控件系统是一套可扩展的插件机制,用于对接打印机、显示屏等外设。每个控件由两部分组成:
types 类型声明
声明控件的标识、版本、可配置属性、方法、事件。是控件与打印方案系统、后台配置之间的契约。
Widget 类
继承 InvisibleWidget,实现 types 中声明的方法。运行时由 PluginManager 实例化并注入服务。
OnPrint / OnError 事件来反馈执行结果。 快速开始
开发一个打印机控件只需 3 步:
定义 types
声明控件 type、methods、events。事件必须包含 OnPrint 与 OnError。
实现 Widget 类
继承 InvisibleWidget,实现 methods 中声明的所有方法。方法名必须与 methods[].key 完全一致。
注册或上传
内置控件在 plugins/init.ts 的 BUILTIN_MAP 中注册;自定义控件通过「高级设置 → 自定义控件」上传 .js 文件。
types 类型声明
types 是控件的元数据,由 PluginParser 严格校验。任一字段不合规都会导致注册失败。
字段说明
valueType 取值
属性、方法参数、事件参数均使用 valueType 声明数据类型。
string字符串"SN-001"number数字42boolean布尔trueobject对象(非数组){ key: "v" }array数组[1, 2, 3]types 声明示例
export const myPrinterTypes = {
type: 'MY_PRINTER_WIDGET', // 全局唯一标识
title: '我的打印机',
version: '1.0.0',
auther: 'YourName',
icon: 'https://example.com/icon.png',
docs: { url: 'https://example.com/doc' },
properties: [], // 可配置属性,由后台注入
methods: [
{
key: 'Send', // 必须与 Widget 类方法名一致
label: '发送打印指令',
params: [
{ key: 'sn', valueType: 'string', label: '打印机SN', required: true },
{ key: 'dishes', valueType: 'array', label: '餐品列表', required: true },
{ key: 'time', valueType: 'string', label: '打印时间', required: true },
{ key: 'tableNo', valueType: 'string', label: '桌号', required: true },
],
},
],
events: [
{
key: 'OnPrint',
label: '打印成功',
params: [
{ key: 'dishes', valueType: 'array' },
{ key: 'time', valueType: 'string' },
{ key: 'tableNo', valueType: 'string' },
],
},
{
key: 'OnError',
label: '打印失败',
params: [{ key: 'reason', valueType: 'string' }],
},
],
}InvisibleWidget 基类
所有控件必须继承 InvisibleWidget。基类提供属性系统、事件系统、内置服务注入与日志能力, PluginManager 在注册时会自动完成服务注入与属性初始化。
this.emit(eventType, ...data)事件触发事件。打印成功后调用 this.emit('OnPrint', ...);失败时调用 this.emit('OnError', reason)
this.on(eventType, handler)事件监听事件,返回取消监听函数。一般控件内部不需要调用
this.off(eventType, handler)事件取消监听事件
this.getProperty(key)属性读取属性值(含后端 config 注入)
this.setProperty(key, value)属性修改属性值,只读属性不可修改
this.getAllProperties()属性获取所有属性当前值
this.require(name)服务获取内置服务实例。目前仅支持 'service',返回封装好的 axios 实例,可用于调用后端 API
this.widgetLog(msg)日志记录信息级日志(前缀 [Widget])
this.widgetWarn(msg)日志记录警告级日志
this.widgetError(err)日志记录错误级日志
this.require('service') 仅在控件方法被调用时可用(PluginManager 已注入解析器)。 不要在构造函数中调用。 必填事件 OnPrint / OnError 必填
打印机控件必须声明并触发这两个事件。打印方案系统(executeScheme)会监听它们来反馈执行结果, 缺少任一事件都会导致方案无法收到成功/失败回调。
OnPrint打印成功// 打印成功后触发
this.emit('OnPrint', dishes, time, tableNo, remark, operatorId, orderNo)OnError打印失败// 失败时触发,传入原因字符串
this.emit('OnError', '请先提供打印机SN')
this.emit('OnError', '网络异常,请检查网络')
this.emit('OnError', result.msg)- 方法执行成功路径必须触发
OnPrint,且仅触发一次 - 方法执行失败路径必须触发
OnError,第一个参数必须是描述原因的字符串 - 触发事件后应
return终止方法,避免重复触发 - 事件 key 大小写敏感,必须写作
OnPrint/OnError(首字母大写,驼峰)
自定义控件文件格式
自定义控件以 .js 文件形式上传,存于 localStorage。运行时在沙箱中执行, 通过 module.exports 收集导出。沙箱会注入:
InvisibleWidget—— 基类,Widget 必须继承它module/exports—— CommonJS 导出对象this.require('service')—— mock axios 实例(仅顶层沙箱可用,真实运行时由 PluginManager 注入)
exports.types 与 exports.widget,缺一不可。 widget 必须是继承 InvisibleWidget 的类。 // my-printer.js —— 用户上传的自定义控件文件
// 沙箱环境会注入:InvisibleWidget(基类)、module、exports
// 通过 this.require('service') 可获取 mock axios(仅顶层可用)
const types = {
type: 'MY_PRINTER_WIDGET',
title: '我的打印机',
version: '1.0.0',
methods: [
{
key: 'Send',
label: '发送打印指令',
params: [
{ key: 'sn', valueType: 'string', required: true, label: 'SN' },
],
},
],
events: [
{ key: 'OnPrint', label: '打印成功', params: [] },
{ key: 'OnError', label: '打印失败', params: [{ key: 'reason', valueType: 'string' }] },
],
}
class MyWidget extends InvisibleWidget {
constructor(props) { super(props) }
async Send(sn) {
if (!sn) { this.emit('OnError', '缺少 SN'); return }
// ... 调用接口 ...
this.emit('OnPrint')
}
}
// 必须导出 types 与 widget
exports.types = types
exports.widget = MyWidget参数配方系统
打印方案(PrintScheme)通过"配方"将控件方法参数映射到运行时数据上下文。 配方设计参考 Blockly 块式组合,支持三种类型:
literal字面值直接使用静态值。适合固定配置,如打印机 SN、门店编号
{ recipeType: "literal", value: "SN-001" }field字段取值从数据上下文中按 dot-path 取值。支持嵌套字段与数组投影(items[].prop)
{ recipeType: "field", value: "order.table_no" }template模板插值含 {{path}} 占位的字符串模板,多个字段拼接为字符串
{ recipeType: "template", value: "桌号{{order.table_no}} 时间{{time}}" }方案配置示例
// 打印方案配置示例:将方法参数映射到数据上下文
{
"pluginType": "RECEIPT_PRINTER_WIDGET",
"methodKey": "Send",
"paramMappings": [
// 固定 SN
{ "paramKey": "sn", "recipe": { "recipeType": "literal", "value": "SN-001" } },
// 从订单取桌号
{ "paramKey": "tableNo", "recipe": { "recipeType": "field", "value": "order.table_no" } },
// 系统计算的餐品列表
{ "paramKey": "dishes", "recipe": { "recipeType": "field", "value": "dishes" } },
// 时间字段
{ "paramKey": "time", "recipe": { "recipeType": "field", "value": "time" } }
],
"enabled": true
}数据上下文参考
方案执行时,系统会构造一个 SchemeDataContext,包含时间、餐品列表与完整订单对象。 以下字段可在 field 与 template 配方中通过 dot-path 引用。
time当前格式化时间 yyyy-MM-dd HH:mm:ssdishes过滤后的餐品列表(已排除 cancelled)order.id订单 IDorder.order_no订单号order.table_no桌号order.remark订单备注order.operator_id操作员 IDorder.total_amount订单总额order.actual_amount实付金额order.member.member_name会员姓名order.member.phone会员手机号order.member.member_card_no会员卡号order.member.points会员积分order.member.level.level_name会员等级名称dishes[].name餐品名称(遍历)dishes[].quantity餐品数量(遍历)dishes[].remark餐品备注(遍历)dishes[].name 表示对 dishes 数组每项取 name 字段,返回字符串数组。 完整示例:58mm 小票打印机
以下是内置控件 RECEIPT_PRINTER_WIDGET 的简化实现,演示了 types 声明、 方法实现、参数校验、API 调用与事件触发的完整流程。
1. types 声明
export const myPrinterTypes = {
type: 'MY_PRINTER_WIDGET', // 全局唯一标识
title: '我的打印机',
version: '1.0.0',
auther: 'YourName',
icon: 'https://example.com/icon.png',
docs: { url: 'https://example.com/doc' },
properties: [], // 可配置属性,由后台注入
methods: [
{
key: 'Send', // 必须与 Widget 类方法名一致
label: '发送打印指令',
params: [
{ key: 'sn', valueType: 'string', label: '打印机SN', required: true },
{ key: 'dishes', valueType: 'array', label: '餐品列表', required: true },
{ key: 'time', valueType: 'string', label: '打印时间', required: true },
{ key: 'tableNo', valueType: 'string', label: '桌号', required: true },
],
},
],
events: [
{
key: 'OnPrint',
label: '打印成功',
params: [
{ key: 'dishes', valueType: 'array' },
{ key: 'time', valueType: 'string' },
{ key: 'tableNo', valueType: 'string' },
],
},
{
key: 'OnError',
label: '打印失败',
params: [{ key: 'reason', valueType: 'string' }],
},
],
}2. Widget 类实现
import { InvisibleWidget } from '../base'
import { myPrinterTypes } from './types'
export class MyPrinterWidget extends InvisibleWidget {
constructor(props) {
super(props)
this.widgetLog('我的打印机控件初始化')
}
// 方法名必须与 types.methods[].key 完全一致
async Send(sn, dishes, time, tableNo) {
// 1. 参数校验 —— 失败必须触发 OnError
if (!sn) {
this.emit('OnError', '请先提供打印机SN')
return
}
if (!dishes || dishes.length === 0) {
this.emit('OnError', '餐品列表不能为空')
return
}
try {
// 2. 调用后端 API —— 通过 this.require('service') 获取 axios 实例
const service = this.require('service')
const content = this.buildContent(dishes, time, tableNo)
const res = await service.post('/printer/receipt', { sn, content })
// 3. 判断后端返回 —— 非 0 code 视为失败
if (res.data.code !== 0) {
this.emit('OnError', res.data.msg || '打印请求失败')
return
}
// 4. 成功 —— 触发 OnPrint,回传关键数据
this.emit('OnPrint', dishes, time, tableNo)
} catch (e) {
this.emit('OnError', '网络异常,请检查网络')
this.widgetError(e)
}
}
buildContent(dishes, time, tableNo) {
// 构建打印机指令内容(标签语言/ESC/POS 等)
return [tableNo, time, ...dishes.map(d => d.name)].join('<BR>')
}
}- 方法名
Send与types.methods[0].key完全一致 - 方法参数顺序与
params数组顺序一致 - 所有失败路径都通过
this.emit('OnError', ...)反馈 - 成功路径触发
OnPrint后立即return - 使用
this.require('service')获取 axios 实例调用后端
调试与日志
基类提供三个日志方法,所有日志都带 [Widget] 前缀,可在浏览器控制台筛选查看。
this.widgetLog(msg)INFO普通信息日志,用于记录初始化、关键流程节点。
this.widgetWarn(msg)WARN警告日志,如属性未定义、参数缺失等非致命问题。
this.widgetError(err)ERROR错误日志,记录异常对象或致命错误。
[Widget] 查看控件日志, 再确认是否触发了 OnError 事件。若事件未触发,通常是方法实现中遗漏了 emit。