@sv-print/plugin-view-data-source
v0.1.0
Published
sv-print plugin: view-data-source
Maintainers
Readme
@sv-print/plugin-view-data-source
给 sv-print 设计器提供「数据源」能力的插件。在设计器中显示一个可拖拽的数据源窗口(DragBox),支持多数据源、树形嵌套字段(对象/数组)、搜索过滤,可将字段拖拽到元素属性面板的「字段名」参数中,也可点击字段名弹窗选择。所有 UI 均可通过 render 回调自定义(Vue / React / jQuery / 原生 JS 均可)。
安装
npm install @sv-print/plugin-view-data-sourceimport "@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
开源使用须知
请自觉遵守 LGPL 协议,其他用途可联系作者;
允许用于个人学习、毕业设计、教学案例、公益事业、商业使用;
如果商用必须保留版权信息,请自觉遵守;
禁止将本开源的代码和资源进行任何形式任何名义的出售,否则产生的一切任何后果责任由侵权者自负;
商用请仔细审查代码和漏洞,不得用于任一国家许可范围之外的商业应用,产生的一切任何后果责任自负;
