npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sv-print/plugin-view-data-source

v0.1.0

Published

sv-print plugin: view-data-source

Readme

@sv-print/plugin-view-data-source

给 sv-print 设计器提供「数据源」能力的插件。在设计器中显示一个可拖拽的数据源窗口(DragBox),支持多数据源、树形嵌套字段(对象/数组)、搜索过滤,可将字段拖拽到元素属性面板的「字段名」参数中,也可点击字段名弹窗选择。所有 UI 均可通过 render 回调自定义(Vue / React / jQuery / 原生 JS 均可)。

安装

npm install @sv-print/plugin-view-data-source
import "@sv-print/plugin-view-data-source/dist/style.css";
import dataSourcePlugin from "@sv-print/plugin-view-data-source";

const plugins = [dataSourcePlugin({})];

功能特性

  • 可拖拽数据源窗口:基于 sv-print 的 DragBox 组件,可拖拽移动、缩放、最小化、隐藏
  • 多数据源:通过 sources 数组配置,以可折叠树列表形式展示,每个数据源可独立展开/折叠(defaultExpand 控制默认展开),按需懒加载
  • 内置添加数据源弹窗:面板标题栏「+」按钮打开,顶部 Tab 切换三种添加方式——JSON 数据(自动识别数据样本 / 字段列表)、Schema、接口请求(含高级参数与「测试请求」字段预览)
  • 树形嵌套字段:支持对象展开子字段(user.address.city)、数组展开子字段(items[].name),无限层级
  • 自动推断字段树:从数据样本(sampleData)或 JSON Schema(schema)自动生成字段树
  • 搜索过滤:字段列表实时搜索,支持跨数据源全局搜索
  • 拖拽赋值:拖拽数据源字段到右侧属性面板的「字段名」参数,自动填充字段路径
  • 拖拽到元素:拖拽数据源字段直接到设计器中的元素 DOM 上,自动设置元素的 field 属性
  • 弹窗选择:点击字段名参数弹出数据源选择弹窗,选中后回填
  • 字段名 Tag 盒子:将字段名参数替换为可多选、可拖拽排序、可删除的 Tag 标签盒子(默认开启),实际存储逗号分隔的 field 值
  • 点击设置:通过 enableClickToSetField 开关,点击字段项直接设置选中元素的 field(默认关闭)
  • UI 完全自定义:panelRender / fieldItemRender / pickerRender 回调,支持 Vue / React / jQuery
  • 模板 API 扩展:bindDataSource / printFromDataSource / print2FromDataSource / getHtmlFromDataSource
  • 运行时 API:designerUtils.dataSource 提供完整的数据源管理能力

配置项

interface PluginConfig {
  /** 数据源配置 */
  sources?: DataSourceItem[];
  /** 全局自定义请求适配器 */
  requestAdapter?: (config: DataSourceRequestConfig) => Promise<any>;

  // ---- 字段名 Tag 盒子 ----
  /** 是否将字段名参数替换为 Tag 盒子 (多选/拖拽排序/删除), 默认 true */
  enableFieldTagBox?: boolean;
  /** 自定义字段名 Tag 盒子渲染 */
  fieldTagRender?: FieldTagRender;
  /** 是否启用模板数据源扩展(bindDataSource/printFromDataSource 等) */
  enableTemplateExtend?: boolean;

  /** 是否启用字段名参数弹窗选择 */
  enableFieldPicker?: boolean;
  /** 点击数据源字段项时是否设置选中元素的 field, 默认 false */
  enableClickToSetField?: boolean;
  /** 弹窗是否多选模式 (选择后默认不自动关闭), 默认 false */
  pickerMultiple?: boolean;
  /**
   * 选择后是否关闭弹窗。
   * - undefined (默认): 单选模式关闭, 多选模式不关闭
   * - true:  选择后始终关闭 (多选模式也会关闭)
   * - false: 选择后始终不关闭 (单选模式也不关闭)
   */
  pickerCloseOnSelect?: boolean;
  /**
   * 字段被应用到元素 / 字段名 (tagbox) 时的回调。
   * 在用户「拖拽拖入」或「点击选中」字段时触发。
   *
   * 返回 false 表示禁止 / 拦截本次设置 (允许字段为元素/名 tagbox 添加跳过);
   * 返回 true / undefined 表示放行。
   *
   * @param field 被选中的字段项
   * @param element 始终是对应的 printElement:
   *   - kind = "field":        元素自身的 printElement
   *   - kind = "columnField": 父表的 printElement (tableElement)
   * @param kind 当前选项类型: "field" (元素字段名) 或 "columnField" (表格列字段名)
   */
  onFieldSet?: (
    field: DataSourceField,
    element: any,
    kind?: "field" | "columnField"
  ) => boolean | void;

  /** DragBox 样式 */
  dragBoxStyle?: string;
  /** 是否默认显示数据源窗口 */
  defaultShow?: boolean;
  /** 数据源图标 class(toolbar 中显示) */
  icon?: string;
  /**
   * DragBox 图标在 toolbar 中的插入位置 (索引), 默认 1 (第 2 个位置)。
   * 超出列表长度时追加到末尾, 负数从末尾倒数 (-1 = 末尾)。
   */
  dragBoxOrder?: number;
  /**
   * 自定义 DragBox 图标的插入位置回调。
   * 接收当前 dragBoxList 和待插入的 item, 返回插入后的完整列表。
   * 优先级高于 dragBoxOrder; 返回 falsy 时回退到 dragBoxOrder 默认逻辑。
   */
  dragBoxInsert?: (list: any[], item: any) => any[] | void;

  /** 数据源面板自定义渲染 */
  panelRender?: PanelRender;
  /** 字段项自定义渲染 */
  fieldItemRender?: FieldItemRender;
  /** 选择弹窗自定义渲染 */
  pickerRender?: PickerRender;
}

DataSourceItem

interface DataSourceItem {
  /** 数据源名称(唯一标识) */
  name: string;
  /** 显示名称,默认同 name */
  title?: string;
  /** 静态字段数据 */
  data?: DataSourceField[];
  /** 原始数据 (配合 data 使用, 提供打印数据; 不传时 rawData 回退为 data 自身) */
  rawData?: any;
  /** 原始数据样本(自动推断字段树) */
  sampleData?: any;
  /** JSON Schema(自动解析为字段树) */
  schema?: object;
  /** 请求配置 */
  request?: DataSourceRequestConfig;
  /** 自定义数据获取函数(优先级最高) */
  fetcher?: () => Promise<DataSourceField[] | any>;
  /** 是否将 fetcher 返回的数据直接作为打印数据 */
  asPrintData?: boolean;
  /** 数据源图标 */
  icon?: string;
  /** 多数据源树列表模式下, 该数据源是否默认展开, 默认 true */
  defaultExpand?: boolean;
}

DataSourceField

interface DataSourceField {
  /**
   * 字段标识,绑定到元素 field 参数。
   * - 普通字段 / 嵌套字段: 自身路径 (如 "name", "customer.name")
   * - 数组子项: 仅子段名 (如 "sku"), 配合 parent 字段还原完整路径
   */
  field: string;
  /** 显示名称,默认同 field */
  label?: string;
  /** 字段类型:string/number/boolean/date/object/array */
  type?: string;
  /** 字段描述 */
  description?: string;
  /** 所属数据源名称 */
  source?: string;
  /** 子字段(object/array 类型展开后的子字段) */
  children?: DataSourceField[];
  /** 是否为数组元素字段(如 items 子项 sku) */
  isArrayItem?: boolean;
  /** 数组父级路径 (仅数组子项设置, 如 "items"; 配合 field 还原为 "items[].sku") */
  parent?: string;
  /** 原始数据,用户自定义渲染时可用 */
  raw?: any;
}

DataSourceRequestConfig

interface DataSourceRequestConfig {
  url: string;
  method?: "GET" | "POST";
  params?: Record<string, any>;
  data?: Record<string, any>;
  headers?: Record<string, string>;
  /** 响应数据路径,如 "data.list" */
  dataPath?: string;
  /** 响应数据映射函数 */
  fieldMap?: (item: any) => DataSourceField;
  /** 是否自动从响应数据推断字段结构 */
  autoParse?: boolean;
}

FieldTagItem & FieldTagCtx & FieldTagRender

/** 字段名 Tag 项 */
interface FieldTagItem {
  /** 字段路径 / 子项名
   *  - 普通字段:   "customer.name" 或 "name" 等
   *  - 数组子项:   "sku" (parent 给出数组路径 "items", 等价 DS 路径 "items[].sku") */
  field: string;
  /** 显示名称 (可选, 不传时会从数据源匹配; 兜底为 field 自身) */
  label?: string;
  /** 数据源名称 (多源场景下用于区分同名字段, 新版本) */
  source?: string;
  /** 字段类型 (可选, 由 picker / drop 携带过来) */
  type?: string;
  /** 数组父级路径 (仅当 field 是数组子项时设置; 配合 field 还原为完整 DS 路径) */
  parent?: string;
}

/** 字段名 Tag 盒子渲染上下文 */
interface FieldTagCtx {
  /** 挂载目标 HTMLElement */
  container: HTMLElement;
  /** 当前已选字段列表 (有序) */
  tags: FieldTagItem[];
  /** 添加字段 (去重) */
  add: (field: string, label: string) => void;
  /** 移除字段 */
  remove: (field: string) => void;
  /** 移动字段 (拖拽排序) */
  move: (fromIndex: number, toIndex: number) => void;
  /** 清空所有字段 */
  clear: () => void;
  /** 获取当前 field 值 (逗号分隔) */
  getValue: () => string;
  /** 打开数据源选择弹窗 */
  openPicker: () => void;
}

/** 字段名 Tag 盒子自定义渲染 */
type FieldTagRender = (ctx: FieldTagCtx) => void;

运行时 API

插件挂载后可通过 designerUtils.dataSource 访问全部 API:

interface DataSourceAPI {
  // ---- 数据管理 ----
  refresh(name?: string): Promise<DataSourceField[]>;
  getFields(name?: string): DataSourceField[];
  getFieldTree(name?: string): DataSourceField[];
  setFields(fields: DataSourceField[], name?: string): void;
  getSources(): DataSourceItem[];
  getSource(name: string): DataSourceItem | undefined;
  addSource(config: DataSourceItem): void;
  removeSource(name: string): void;

  // ---- 弹窗 ----
  showPicker(
    callback?: (field: DataSourceField) => void,
    options?: { onlyFields?: string[] }
  ): void;
  closePicker(): void;

  // ---- DragBox 显隐 ----
  show(): void;
  hide(): void;
  toggle(): void;

  // ---- 搜索 ----
  search(keyword: string): DataSourceField[];
}

运行时调用示例

// 刷新全部数据源(不传 name)
await designerUtils.dataSource.refresh();

// 只刷新指定数据源
await designerUtils.dataSource.refresh("user");

// 获取字段列表
// 注意: request 等懒加载源未加载时返回空数组,
// 面板展开分组 / refresh / showPicker / getDataSourceData 均会触发加载
const fields = designerUtils.dataSource.getFields();
const tree = designerUtils.dataSource.getFieldTree();

// 手动设置字段(面板立即更新;后续 refresh 不会冲掉手动设置的字段)
// 如需恢复从原始配置拉取,先 removeSource 再 addSource
designerUtils.dataSource.setFields([{ field: "name", label: "姓名" }], "user");

// 弹窗选择字段
designerUtils.dataSource.showPicker((field) => {
  console.log("选中:", field);
});

// 弹窗只展示指定字段 (onlyFields 为字段完整路径, 如 "user.name")
designerUtils.dataSource.showPicker((field) => { /* ... */ }, { onlyFields: ["user.name", "user.age"] });

// 跨数据源搜索(仅搜索已加载的字段)
const results = designerUtils.dataSource.search("name");

// 动态添加数据源(面板立即显示新数据源分组, defaultExpand 控制是否默认展开)
// 同名覆盖时会清除该源的手动字段覆盖与缓存, 按新配置重新加载
designerUtils.dataSource.addSource({
  name: "product",
  title: "商品",
  defaultExpand: true,
  data: [{ field: "sku", label: "SKU" }],
});

// 显隐数据源窗口(与 toolbar 图标高亮态保持同步)
designerUtils.dataSource.show();
designerUtils.dataSource.hide();
designerUtils.dataSource.toggle();

模板扩展

插件通过 newPrintTemplate hook 扩展 PrintTemplate 实例,支持调用数据源后直接打印:

interface PrintTemplateDataSourceExtension {
  /** 绑定数据源: 设置 template.fields */
  bindDataSource(name?: string): DataSourceField[];
  /** 从数据源获取数据并打印 */
  printFromDataSource(name?: string, options?: PrintOptions): Promise<void>;
  /** 从数据源获取数据并静默打印 */
  print2FromDataSource(name?: string, options?: Print2Options): Promise<void>;
  /** 从数据源获取数据并生成 HTML */
  getHtmlFromDataSource(name?: string): Promise<any>;
  /** 获取数据源的打印数据 (未注册/加载失败时抛出错误) */
  getDataSourceData(name?: string): Promise<any>;
  /** 恢复被数据源插件覆盖的原生方法 */
  restoreDataSource(): void;
}

模板扩展示例

// 绑定数据源 (同时设置 template.fields, 字段名参数自动变为 select 下拉)
printTemplate.bindDataSource("user");

// 获取数据源数据并浏览器打印
await printTemplate.printFromDataSource("user", { leftOffset: -1 });

// 静默打印
await printTemplate.print2FromDataSource("user", { printer: "HP-LaserJet" });

// 生成 HTML
const html = await printTemplate.getHtmlFromDataSource("user");

// 恢复被覆盖的原生方法 (清理时调用, 例如销毁模板)
printTemplate.restoreDataSource();

说明:

  • getDataSourceData 返回数据源的原始数据(含数组,可用于列表打印);仅 schema 等无原始数据的类型返回按字段生成的测试数据
  • 传入了不存在的 name 或数据源加载失败(网络错误、接口未开启 CORS 跨域、非 JSON 响应等)时会抛出错误,不再静默返回空数据
  • request 类型数据源在首次调用时才真正发起请求;URL 粘贴时携带的引号/反引号会自动去除
  • 所有方法不传 name 时使用全部数据源:单数据源直接返回其数据,多数据源时对象浅合并(非对象数据挂到数据源名下)
  • bindDataSource 对未加载的 request 类型数据源会自动触发加载,完成后重新应用字段

asPrintData 数据源

当数据源配置 asPrintData: true 时,fetcher 返回的数据直接作为打印数据:

dataSourcePlugin({
  sources: [
    {
      name: "printData",
      title: "打印数据",
      asPrintData: true,
      fetcher: async () => {
        const res = await fetch("/api/order/data");
        return res.json(); // 直接返回打印数据对象
      },
    },
    {
      name: "fields",
      title: "字段定义",
      data: [{ field: "orderNo", label: "订单号" }],
    },
  ],
});

// bindDataSource 仍设置 template.fields (供设计时选择字段)
printTemplate.bindDataSource("fields");
// printFromDataSource 使用 asPrintData 数据源的数据
await printTemplate.printFromDataSource("printData");

工具函数

插件导出以下工具函数,可单独使用:

import {
  parseData, // 从数据样本推断字段树
  parseSchema, // 从 JSON Schema 解析字段树
  flattenFields, // 树形字段扁平化
  getByPath, // 按路径获取对象值 (支持 items[].name)
  buildPath, // 生成嵌套字段路径
  getFullPath, // 还原数组子项的完整路径 (parent + []. + field)
  findNodeInTree, // 在字段树中查找指定 path 的节点
  resolveColumnFieldPaths, // 解析表格列字段的可用字段路径列表
} from "@sv-print/plugin-view-data-source";

parseData

import { parseData } from "@sv-print/plugin-view-data-source";

const fields = parseData({
  orderNo: "A001",
  amount: 99.5,
  customer: { name: "张三", phone: "13800000000" },
  items: [{ sku: "S1", qty: 2 }],
});
// 返回:
// [
//   { field: "orderNo", label: "orderNo", type: "string" },
//   { field: "amount", label: "amount", type: "number" },
//   { field: "customer", label: "customer", type: "object", children: [
//     { field: "customer.name", label: "name", type: "string" },
//     { field: "customer.phone", label: "phone", type: "string" },
//   ]},
//   { field: "items", label: "items", type: "array", children: [
//     { field: "sku", label: "sku", type: "string", isArrayItem: true, parent: "items" },
//     { field: "qty", label: "qty", type: "number", isArrayItem: true, parent: "items" },
//   ]},
// ]

使用示例

1. 静态数据

import "@sv-print/plugin-view-data-source/dist/style.css";
import dataSourcePlugin from "@sv-print/plugin-view-data-source";

const plugins = [
  dataSourcePlugin({
    sources: [
      {
        name: "default",
        data: [
          { field: "name", label: "姓名", type: "string" },
          { field: "age", label: "年龄", type: "number" },
        ],
      },
    ],
  }),
];

2. 请求接口

dataSourcePlugin({
  sources: [
    {
      name: "default",
      request: {
        url: "/api/user/fields",
        method: "GET",
        /** 响应数据路径,如 "data.list" */
        dataPath: "data.list",
        /** 响应数据映射函数 */
        fieldMap: (item) => ({
          field: item.code,
          label: item.name,
          type: item.dataType,
        }),
      },
    },
  ],
});

3. 自定义 fetcher

dataSourcePlugin({
  sources: [
    {
      name: "default",
      fetcher: async () => {
        const res = await fetch("/api/fields");
        const json = await res.json();
        return json.data.map((item) => ({
          field: item.code,
          label: item.name,
        }));
      },
    },
  ],
});

4. 多数据源

多数据源以可折叠树列表形式展示,每个数据源是一个可展开/折叠的分组。通过 defaultExpand 控制是否默认展开(默认 true),首次展开时懒加载字段。

dataSourcePlugin({
  sources: [
    {
      name: "user",
      title: "用户数据",
      defaultExpand: true, // 默认展开 (默认 true, 可省略)
      request: { url: "/api/user/fields", dataPath: "data.list" },
    },
    {
      name: "order",
      title: "订单数据",
      defaultExpand: false, // 默认折叠, 点击后懒加载
      fetcher: async () => {
        const res = await fetch("/api/order/schema");
        return (await res.json()).fields;
      },
    },
  ],
});

搜索时自动展开有匹配字段的数据源,无需手动展开逐个查找。

5. 嵌套字段(对象 + 数组)

dataSourcePlugin({
  sources: [
    {
      name: "default",
      data: [
        {
          field: "user",
          label: "用户",
          type: "object",
          children: [
            { field: "user.name", label: "姓名", type: "string" },
            { field: "user.age", label: "年龄", type: "number" },
            {
              field: "user.address",
              label: "地址",
              type: "object",
              children: [
                { field: "user.address.city", label: "城市" },
                { field: "user.address.street", label: "街道" },
              ],
            },
          ],
        },
        {
          field: "items",
          label: "商品列表",
          type: "array",
          children: [
            { field: "items[].name", label: "商品名", isArrayItem: true },
            { field: "items[].price", label: "单价", isArrayItem: true },
          ],
        },
      ],
    },
  ],
});

6. 从数据样本自动推断字段树

dataSourcePlugin({
  sources: [
    {
      name: "order",
      sampleData: {
        orderNo: "A001",
        amount: 99.5,
        customer: { name: "张三", phone: "13800000000" },
        items: [{ sku: "S1", qty: 2 }],
      },
      // 自动推断为:
      //   orderNo (string)
      //   amount (number)
      //   customer.name (string)
      //   customer.phone (string)
      //   items[].sku (string)
      //   items[].qty (number)
    },
  ],
});

7. 使用 axios 作为请求适配器

import axios from "axios";

dataSourcePlugin({
  requestAdapter: async (config) => {
    const res = await axios({
      url: config.url,
      method: config.method || "GET",
      params: config.params,
      data: config.data,
      headers: config.headers,
    });
    return res.data;
  },
  sources: [{ name: "api", request: { url: "/api/fields" } }],
});

8. 拖拽字段到元素 DOM 上设置字段名

拖拽数据源字段到设计器中的元素 DOM 上时,自动设置该元素的 field 属性。此功能始终启用,无需配置。

默认行为是「首次设置、再拖追加」:

  • 首次拖拽字段到元素 → 设置为该字段
  • 再次拖拽其他字段到同一元素 → 追加为多字段(按 source + field 联合去重)
  • 多选元素时点击字段项 → 同样追加到所有选中元素
  • 自动同步更新 options.dsFieldTags(结构化 JSON)与 options.field(兼容字符串)

插件通过事件委托监听 #printTemplate 容器内的 .hiprint-printElement 元素的 drop 事件,通过 printTemplate.editingPanel.printElements 列表匹配 DOM 节点(designTarget[0])查找对应的 printElement 实例,统一调用 appendFieldToElement 助手完成追加。

9. 点击字段项设置选中元素的字段名

// 默认关闭。开启后, 选中元素 -> 点击数据源字段项 -> 自动设置元素 field
dataSourcePlugin({
  enableClickToSetField: true,
});

开启后,在数据源面板或选择弹窗中点击字段项时,会自动将字段路径设置到当前选中的元素(通过 .hiprint-printElement.selected 查找选中元素,再匹配 editingPanel.printElements 获取实例)。同拖拽场景一致,这里也是「追加」语义:多次点击会累加字段而不是覆盖。

10. 字段名 Tag 盒子(多字段选择)

默认开启。将元素属性面板中的「字段名」参数从单选 select/input 替换为 Tag 标签盒子:

  • 支持选择多个字段,每个字段显示为带 label 的标签
  • 标签左侧有拖拽句柄,可拖拽排序
  • 标签右侧有删除按钮,可移除单个字段
  • 末尾有 + 按钮,点击弹出数据源选择弹窗
  • 支持直接拖拽数据源字段到 Tag 盒子中添加
  • 实际存储:
    • options.field —— 逗号分隔的 path 字符串(如 name,age,user.address.city),用于兼容老模板 / hiprint 内部取值
    • options.dsFieldTags —— 结构化 JSON 数组,新事实来源,带数据源身份:[{ "source": "users", "field": "name", "label": "姓名", "type": "string" }, { "source": "default", "field": "sku", "parent": "items", "label": "SKU", "type": "string" }, ...]
  • 回显处理:
    • 优先读 options.dsFieldTags,用其中的 source 标识多源同名
    • 老模板(仅有 options.field 字符串)也能正常显示 —— 自动从 path 字符串 fallback 到数据源匹配 label
  • 多源同名消歧:当多数据源都有同名 name 字段时,每个 tag 的 title 显示为 [users] name 和 [products] name,用户能清楚区分
// 默认开启, 无需配置
dataSourcePlugin({ sources: [{ name: "default", data: [...] }] });

// 关闭 Tag 盒子, 使用原始 select/input
dataSourcePlugin({ enableFieldTagBox: false });

11. 列字段名 columnField 也支持 Tag 盒子

columnField(表格列字段名)与 field 的行为一致 —— 同一套 tagbox UI、拖拽、删除、+ 弹窗、多源结构化存储。唯一区别:columnField 会被限制到父表继承下来的可用字段。

读取路径:hiprint 在 createTarget 时把 printElement.options.columnFields || printElement.options.fields 作为可用字段列表传给 cell(这通常是父表 bindDataSource 设置的 template.fields)。本插件同样会在数据源选择弹窗里把不在列表中的字段忽略(控制台 console.warn),实现"只能选 sku/qty"的效果。

// 父表 bindDataSource 后, cell 可选的列字段自动来自 items[].*
printTemplate.bindDataSource("items");

// 用户在 cell 上点开 columnField tagbox 的 `+` 选择:
//   [✓] items[].sku
//   [✓] items[].qty
//   [✗] name         <- 不在 items 子字段里, 选不了
//   [✗] age

存储:

  • options.columnField —— 逗号分隔 path 字符串(兼容老用法)
  • options.dsColumnFieldTags —— 结构化 JSON 数组(新事实来源,多源同名消歧)

options.field / options.dsFieldTags 与 options.columnField / options.dsColumnFieldTags 是两套独立的存储,互不干扰。

UI 自定义 render 回调

插件提供三种 render 回调,用户可完全替换默认 Svelte UI:

panelRender

type PanelRender = (ctx: DataSourcePanelCtx) => void;

| ctx 属性 | 类型 | 说明 | | --------------------- | --------------------------- | -------------------------------- | | container | HTMLElement | 挂载目标,用户将 UI 渲染到此容器 | | fields | DataSourceField[] | 扁平化字段列表 | | fieldTree | DataSourceField[] | 字段树(含 children) | | sources | DataSourceItem[] | 数据源列表 | | currentSource | string | 当前选中的数据源 name | | switchSource(name) | (name: string) => void | 切换数据源 | | refresh() | () => Promise<void> | 刷新全部数据源 | | search(keyword) | (keyword: string) => void | 搜索字段 | | getFilteredFields() | () => DataSourceField[] | 获取过滤后的字段 | | close() | () => void | 关闭面板 | | onFieldsChange(cb) | (cb) => () => void | 订阅字段变化,返回取消订阅函数 |

pickerRender

type PickerRender = (ctx: DataSourcePickerCtx) => void;

| ctx 属性 | 类型 | 说明 | | --------------------- | ---------------------------------- | ------------------ | | container | HTMLElement | 挂载目标 | | fields | DataSourceField[] | 扁平化字段列表 | | fieldTree | DataSourceField[] | 字段树 | | search(keyword) | (keyword: string) => void | 搜索字段 | | getFilteredFields() | () => DataSourceField[] | 获取过滤后的字段 | | select(field) | (field: DataSourceField) => void | 选中字段并关闭弹窗 | | close() | () => void | 关闭弹窗 | | onFieldsChange(cb) | (cb) => () => void | 订阅字段变化 |

拖拽协议

字段拖拽使用 application/x-dataSourceField MIME 类型传输数据。支持两种 drop 目标:

1. 拖拽到字段名参数(属性面板):通过 getOptionItem hook 注入 drop 事件到 .hiprint-option-item-field,调用 tagbox 内的 addTag(...) 追加字段,并同步写回 options.field 与 options.dsFieldTags。

2. 拖拽到元素 DOM(设计器画布):通过事件委托监听 #printTemplate 容器内 .hiprint-printElement 的 drop 事件,匹配 editingPanel.printElements 中的 designTarget 查找元素实例,统一调用 appendFieldToElement(el, payload) 完成「首次设置 / 再次追加」。

// dragstart 时设置 (由 field-tree-item.svelte 自动完成)
e.dataTransfer.setData(
  "application/x-dataSourceField",
  JSON.stringify({
    source: "users", // 真实的数据源名 (多源同名消歧用), 兜底 "default"
    field: "items[].sku", // 完整路径
    label: "SKU",
    type: "string",
    parent: "items", // 数组父级路径 (仅数组子项有值)
  })
);

// drop 时读取 -- 同时拿到 source / field / label / type / parent
const data = e.dataTransfer.getData("application/x-dataSourceField");
const payload = JSON.parse(data);
// { source, field, label, type, parent }

拖拽到元素 DOM 时会有视觉反馈(ds-drag-over 高亮样式)。

示例:自定义面板 UI(Vue)

import { createApp, ref, h } from "vue";

dataSourcePlugin({
  sources: [{ name: "default", data: [...] }],
  panelRender: (ctx) => {
    const app = createApp({
      setup() {
        const keyword = ref("");
        // 订阅数据变化
        ctx.onFieldsChange((fields, tree) => {
          // 数据刷新 / 切换数据源时重新渲染
        });
        return () =>
          h("div", [
            h("input", {
              value: keyword.value,
              onInput: (e) => {
                keyword.value = e.target.value;
                ctx.search(keyword.value);
              },
              placeholder: "搜索字段...",
            }),
            h(
              "div",
              ctx.getFilteredFields().map((f) =>
                h("div", { draggable: true }, f.label)
              )
            ),
          ]);
      },
    });
    app.mount(ctx.container);
  },
});

示例:自定义弹窗 UI(jQuery)

dataSourcePlugin({
  sources: [{ name: "default", data: [...] }],
  pickerRender: (ctx) => {
    const $box = $('<div class="my-picker"></div>');
    $box.append('<input placeholder="搜索..." />');
    const $list = $('<div class="field-list"></div>');
    function renderList(fields) {
      $list.empty();
      fields.forEach((f) => {
        $list.append(
          $('<div class="field-item"></div>')
            .text(f.label)
            .on("click", () => ctx.select(f))
        );
      });
    }
    renderList(ctx.fields);
    $box.find("input").on("input", (e) => {
      ctx.search(e.target.value);
      renderList(ctx.getFilteredFields());
    });
    $(ctx.container).append($box);
  },
});

示例:自定义字段项 UI(fieldItemRender)

fieldItemRender 与 panelRender / pickerRender 不同 —— 它是逐项渲染回调,每个字段项调用一次,返回一个 HTMLElement 或字符串,替换默认的 svelte 字段项组件。上下文 FieldItemCtx 提供了字段数据、容器引用以及拖拽 / 选中 / 展开的内置方法。

/** 字段项上下文 */
interface FieldItemCtx {
  /** 当前字段的元数据 */
  field: DataSourceField;
  /** 默认挂载容器;你可以直接 append 到这里 */
  container: HTMLElement;
  /** 启动拖拽(已封装 application/x-dataSourceField MIME 的 setData) */
  startDrag: (e: DragEvent) => void;
  /** 选中字段(用于点击设置 / 弹窗回调) */
  select: () => void;
  /** 切换展开/折叠(仅树形字段有效) */
  toggleExpand: () => void;
  /** 当前是否已展开 */
  expanded: boolean;
  /** 树形深度(用于缩进) */
  depth: number;
}

/** 字段项渲染回调 */
type FieldItemRender = (ctx: FieldItemCtx) => HTMLElement | string;

示例 1:纯 jQuery 替换(保留拖拽 / 选中 / 展开)

dataSourcePlugin({
  sources: [{ name: "default", data: [...] }],
  fieldItemRender: (ctx) => {
    const $row = $(`
      <div class="my-field-row" style="padding-left:${ctx.depth * 16 + 4}px">
        <span class="my-field-label">${ctx.field.label || ctx.field.field}</span>
        <span class="my-field-type">${ctx.field.type || ""}</span>
      </div>
    `);

    // 必须自行挂到容器, 否则 ctx.fieldTree 的 children 不会显示
    $(ctx.container).append($row);

    // 启动拖拽: 用 ctx.startDrag 即可 (无需自己 setData)
    $row.on("dragstart", (e) => ctx.startDrag(e.originalEvent));
    // 选中: 直接调用 ctx.select()
    $row.on("click", () => {
      // 例如配合 enableClickToSetField 自动设置选中元素 field
      ctx.select();
    });
    // 折叠/展开: 仅树形字段 (有 children) 时才需要
    if (ctx.field.children && ctx.field.children.length) {
      $row.prepend(
        $(`<i class="svicon sv-nav-${ctx.expanded ? "down" : "right"}"></i>`)
          .on("click", (e) => {
            e.stopPropagation();
            ctx.toggleExpand();
          })
      );
    }

    // 注意: 返回 HTMLElement 或字符串都行, plugin 内部会挂到容器
    return $row[0];
  },
});

示例 2:原生 DOM 替换(最轻)

dataSourcePlugin({
  sources: [{ name: "default", data: [...] }],
  fieldItemRender: (ctx) => {
    // 返回字符串: plugin 会作为 innerHTML 写入
    return `
      <div class="ds-row" style="padding-left:${ctx.depth * 16 + 4}px">
        ${ctx.field.label || ctx.field.field}
        ${ctx.field.type ? `<em>${ctx.field.type}</em>` : ""}
      </div>
    `;
  },
});

注意:当提供 fieldItemRender 时,整个字段树由用户的回调完全接管——你需要自己处理展开/折叠交互、enableClickToSetField 集成、以及树形子项的展示。panelRender 与 fieldItemRender 不能同时提供;如需深度自定义 UI,优先用 panelRender 在 ctx.container 内自渲染。

示例:自定义字段名 Tag 盒子 UI

dataSourcePlugin({
  sources: [{ name: "default", data: [...] }],
  enableFieldTagBox: true,
  fieldTagRender: (ctx) => {
    // ctx.container: HTMLElement 挂载目标
    // ctx.tags: FieldTagItem[] 当前已选字段
    // ctx.add(field, label): 添加字段
    // ctx.remove(field): 移除字段
    // ctx.move(from, to): 拖拽排序
    // ctx.clear(): 清空
    // ctx.getValue(): 获取逗号分隔值
    // ctx.openPicker(): 打开数据源弹窗

    const $box = $('<div class="my-tagbox"></div>');
    function render() {
      $box.empty();
      ctx.tags.forEach((tag, i) => {
        $box.append(
          $('<span class="my-tag"></span>')
            .text(tag.label)
            .attr("draggable", true)
            .on("dragstart", (e) => e.originalEvent.dataTransfer.setData("text/plain", String(i)))
            .on("dragover", (e) => e.preventDefault())
            .on("drop", (e) => {
              e.preventDefault();
              const from = parseInt(e.originalEvent.dataTransfer.getData("text/plain"));
              ctx.move(from, i);
              render();
            })
            .append(
              $('<span class="my-tag-x">x</span>').on("click", () => {
                ctx.remove(tag.field);
                render();
              })
            )
        );
      });
      $box.append(
        $('<button>+</button>').on("click", () => ctx.openPicker())
      );
    }
    render();
    // 注意: 数据变化后需重新渲染, 可在 add/remove/move 调用后手动 render
    $(ctx.container).append($box);
  },
});

License

LGPL

开源使用须知

  1. 请自觉遵守 LGPL 协议,其他用途可联系作者;

  2. 允许用于个人学习、毕业设计、教学案例、公益事业、商业使用;

  3. 如果商用必须保留版权信息,请自觉遵守;

  4. 禁止将本开源的代码和资源进行任何形式任何名义的出售,否则产生的一切任何后果责任由侵权者自负;

  5. 商用请仔细审查代码和漏洞,不得用于任一国家许可范围之外的商业应用,产生的一切任何后果责任自负;