WIDGET DEVELOPMENT

开发文档 — API 接口与集成指南

控件开发文档:基于 InvisibleWidget 基类与 types 类型声明,开发对接打印机等外设的自定义控件。

控件系统概览

控件系统是一套可扩展的插件机制,用于对接打印机、显示屏等外设。每个控件由两部分组成:

01

types 类型声明

声明控件的标识、版本、可配置属性、方法、事件。是控件与打印方案系统、后台配置之间的契约。

02

Widget 类

继承 InvisibleWidget,实现 types 中声明的方法。运行时由 PluginManager 实例化并注入服务。

当前使用场景:对接热敏打印机(如 58mm 小票打印机)。打印方案系统会监听控件的 OnPrint / OnError 事件来反馈执行结果。

快速开始

开发一个打印机控件只需 3 步:

1

定义 types

声明控件 typemethodsevents。事件必须包含 OnPrintOnError

2

实现 Widget 类

继承 InvisibleWidget,实现 methods 中声明的所有方法。方法名必须与 methods[].key 完全一致。

3

注册或上传

内置控件在 plugins/init.tsBUILTIN_MAP 中注册;自定义控件通过「高级设置 → 自定义控件」上传 .js 文件。

types 类型声明

types 是控件的元数据,由 PluginParser 严格校验。任一字段不合规都会导致注册失败。

字段说明

字段类型必填说明
typestring控件类型标识,全局唯一,如 RECEIPT_PRINTER_WIDGET
titlestring控件显示名称
versionstring版本号,如 1.0.0
iconstring控件图标 URL
autherstring作者(注意拼写为 auther)
docs{ url: string }外部文档链接
propertiesWidgetProperty[]可配置属性列表
methodsWidgetMethod[]方法列表(至少 1 个)
eventsWidgetEventDef[]事件列表(含 OnPrint / OnError)

valueType 取值

属性、方法参数、事件参数均使用 valueType 声明数据类型。

string字符串"SN-001"
number数字42
boolean布尔true
object对象(非数组){ key: "v" }
array数组[1, 2, 3]

types 声明示例

types.ts
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打印成功
触发时机
打印指令被后端成功接收并下发到打印机后触发
参数约定
自定义。一般回传本次打印的关键数据(如 dishes、time、tableNo)
// 打印成功后触发
this.emit('OnPrint', dishes, time, tableNo, remark, operatorId, orderNo)
OnError打印失败
触发时机
参数校验失败、网络异常、后端返回非 0 code 时触发
参数约定
第一个参数必须是 string 类型,描述失败原因
// 失败时触发,传入原因字符串
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.typesexports.widget,缺一不可。 widget 必须是继承 InvisibleWidget 的类。
my-printer.js
// 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}}" }

方案配置示例

scheme.json
// 打印方案配置示例:将方法参数映射到数据上下文
{
  "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,包含时间、餐品列表与完整订单对象。 以下字段可在 fieldtemplate 配方中通过 dot-path 引用。

系统time当前格式化时间 yyyy-MM-dd HH:mm:ss
系统dishes过滤后的餐品列表(已排除 cancelled)
订单order.id订单 ID
订单order.order_no订单号
订单order.table_no桌号
订单order.remark订单备注
订单order.operator_id操作员 ID
订单order.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 声明

types.ts
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 类实现

MyPrinterWidget.ts
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>')
  }
}
关键点:
  • 方法名 Sendtypes.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。