# builderx-extensions-samples **Repository Path**: isupermap/builderx-extensions-samples ## Basic Information - **Project Name**: builderx-extensions-samples - **Description**: builderx自定义插件项目 方便用户直接在这个项目里开发自己的builderx 插件 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-09-09 - **Last Updated**: 2025-11-26 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SuperMap Builderx 自定义扩展插件开发指南 ## 开始之前 插件是 `Builderx` 重要的组件部分。 `Builderx` 自身为用户提供了丰富的组件,数据来源。 为了满足多样需求, `Builderx` 也支持用户自定义组件、自定义数据源等。 ## 开发要求 自定义扩展插件的开发需要 `Vue 3` 开发经验。 > 在开始之前,建议先掌握 [Vue.js](https://cn.vuejs.org/) 基础知识。本指南假设你已熟悉 Vue 3 中的 [组合式 API](https://cn.vuejs.org/api/composition-api-setup.html) 和 [选项式 API](https://cn.vuejs.org/api/options-state.html),并能使用 [CSS Modules](https://github.com/css-modules/css-modules) 编写组件样式。 ## 开发步骤 ### 1. 克隆项目 ```bash git clone https://gitee.com/isupermap/builderx-extensions-samples.git ``` 下载后的目录结构如下: ``` builderx-extensions-samples/ ├── .gitignore # Git忽略规则文件 ├── README.md # 项目说明文档 ├── package.json # 项目依赖配置 ├── public/ # 静态资源目录 │ └── vite.svg # Vite图标 ├── src/ # 源代码目录 │ ├── plugins/ # 插件实现目录 │ │ ├── index.ts # 插件入口文件 │ │ ├── plugin-custom-components-sample/ # 注册自定义组件插件 │ │ ├── plugin-custom-setter-sample/ # 注册自定义设置器插件 │ │ ├── plugin-custom-datasource-sample/ # 添加自定义添加数据源选项卡插件 │ │ ├── plugin-datasource-inject-sample/ # 添加自定义内置数据插件 │ │ └── plugin-template-picker-sample/ # 添加自定义左侧侧边栏选项插件 │ └── plugins.ts # 插件导出文件 ├── tsconfig.json # TypeScript配置 ├── tsconfig.app.json # 应用TypeScript配置 ├── tsconfig.node.json # Node环境TypeScript配置 ├── typings/ # 类型定义目录 └── vite.config.*.ts # Vite构建配置 ``` ### 2. 安装依赖 ```bash pnpm install # or npm install ``` ### 3. 检验调试环境 调试需要 `SuperMap iPortal` 中的 `Builderx` 环境支持,请确保: - 已正确配置 `SuperMap iPortal` 服务地址(修改`vite.config.dev.ts` 文件中的 `IPORTAL_URL` 变量) ```js // 请指定 SuperMap iPortal 服务地址 const IPORTAL_URL = 'http://localhost:8190/iportal'; ``` - 本地开发服务器与iPortal服务运行在同一网络环境下 ### 4. 新建扩展插件 在 `src/plugins` 目录下创建新的扩展插件,并导出插件。插件的实现可以参考 `src/plugins` 目录下的示例插件 ### 5. 导出扩展插件 在 `src/plugins/index.ts` 中引入新插件。 ## 启动与访问 ### 1. 启动开发服务器 ```bash # serve with hot reload at localhost:5175 or other port pnpm dev # or npm run dev ``` ### 2. 访问 `Builderx` 主界面 - 开发服务器启动后会自动打开浏览器访问 - 或手动输入访问地址:http://localhost:5175/dev-builderx/index.html ### 3. 验证自定义扩展插件 | 插件类型 | 验证位置 | 操作说明 | |----------------|----------|--------------------------| | 自定义组件 | 左侧组件面板 | 直接查看新增组件 | | 自定义设置器 | 右侧属性面板 | 拖入组件后查看属性设置器 | | 自定义数据源选项卡 | 左侧数据面板 | 点击"添加数据"查看新增选项卡 | | 自定义内置数据 | 左侧数据面板 | 查看已添加的数据列表 | | 自定义侧边栏选项卡 | 左侧侧边栏 | 直接查看新增选项卡 | ## 编译自定义扩展插件 将开发的 自定义扩展插件 编译成 `Builderx` 可执行的 js 文件。 ```bash # build for production with minification pnpm build # or npm run build ``` ## 部署自定义扩展插件 编译完成后,将 `dist` 目录下的 `plugins.js` 拷贝至目录 `%IPORTAL_HOME%/webapps/iportal/resources/builderx/plugins`。 打开 `SuperMap iPortal ` 中的 `Builderx`,刷新页面,验证自定义扩展插件。 ## 自定义扩展插件配置 详细类型定义请参考项目的 `typings` 目录中的类型声明文件,这些文件包含了插件开发所需的所有核心接口和类型声明。 - `typings/builderx-context.d.ts` Builderx 扩展插件上下文类型定义 - `typings/data-selector-skeleton.d.ts` 数据源选择器骨架类型定义 - `typings/data-source.d.ts` 数据源相关类型定义 - `typings/interaction.d.ts` 交互相关类型定义 - `typings/hooks.d.ts` Vue 组件 钩子相关类型定义 ### 插件接口类型 ```ts export interface BuilderxExtensionsPlugin { active?: 'onBeforeProjectCreate' | 'onProjectCreated'; init: (ctx: BuilderxExtensionsContext) => void; } ``` `BuilderxExtensionsPlugin` 是自定义扩展插件的接口,包含以下属性: **active**: - **类型**: `string` - **枚举**: `onBeforeProjectCreate` | `onProjectCreated` - **默认值**: `onBeforeProjectCreate` - **描述**: 插件的激活事件,表示插件在应用初始化前或应用初始化后激活。 **init(ctx: BuilderxExtensionsContext)**: - **类型**: `Function` - **参数**: `ctx: BuilderxExtensionsContext` - **描述**: 插件的初始化函数。用于插件的初始化操作。 ### 插件上下文接口 ```ts export interface BuilderxExtensionsContext extends PluginContext { vue: Vue; dataSource: DataSourceContext; dataSelectorSkeleton: DataSeletorSkeleton; interaction: InteractionContext; hooks: HooksMap; } export interface PluginContext { editor: Editor; skeleton: Skeleton; components: Components; setters: Setters; project: Project; app: App; } ``` `BuilderxExtensionsContext` 是插件上下文对象,包含以下属性: **vue**: - **类型**: [Vue](https://vuejs.org/api/general.html#definecomponent) - **描述**: Vue 实例对象,用于创建 Vue 组件。如自定义组件,自定义设置器,自定义数据源选项卡内容等 UI 组件。 **dataSource**: - **类型**: [DataSource](./docs/api/datasource.md) - **描述**: 数据源上下文对象,用于管理数据源,如添加数据源,获取数据源的内容。 **dataSelectorSkeleton**: - **类型**: [DataSeletorSkeleton](./docs/api/data-selector-skeleton.md) - **描述**: 数据选择器骨架对象,用于创建添加数据中的弹窗内容中的数据源选项卡。 **interaction**: - **类型**: [Interaction](./docs/api/interaction.md) - **描述**: 交互上下文对象,用于管理交互操作。 **hooks**: - **类型**: [Hooks](./docs/api/hooks.md) - **描述**: 钩子上下文对象,用于管理钩子函数,如与地图交互的钩子函数。 **skeleton**: - **类型**: [Skeleton](./docs/api/skeleton.md) - **描述**: 骨架上下文对象,用于创建 `Builderx` 框架的骨架组成。 **components**: - **类型**: [Components](./docs/api/components.md) - **描述**: 组件上下文对象,用于注册自定义组件。 **setters**: - **类型**: [Setters](./docs/api/setters.md) - **描述**: 设置器上下文对象,用于注册自定义设置器。 **project**: - **类型**: [Project](./docs/api/project.md) - **描述**: 工程上下文对象,用于管理项目相关操作,如监听项目初始化操作等。 **editor**: - **类型**: [Editor](./docs/api/editor.md) - **描述**: 编辑器上下文对象,用于管理编辑器相关操作,如获取已注册的组件的相关信息等。 **app**: - **类型**: [App](https://vuejs.org/api/application.html#app-use) - **描述**: Vue 应用实例。 ### 组件描述协议接口类型 ```ts /** * 单个组件描述协议完整结构 */ export interface AssetsComponentItem { /** * 描述协议 */ description: LCFTypeComponentDescription; /** * 组件 */ component: VueComponent; /** * 组件是否可选中,组件池过滤 * @default true */ selectable?: boolean; } /** * 组件描述 */ export interface LCFTypeComponentDescription{ /** * 组件名 */ componentName: string; /** * 组件显示的别名,缺省值和组件名一致 */ title?: string; /** * 组件选中时显示的图标 */ icon?: string; /** * 组件描述, 目前用于组件树搜索 */ description?: string; /** * 组件快照 */ screenshot?: string; /** * 组件属性信息 */ props?: LCFTypePropConfig[]; /** * 编辑体验增强 */ configure?: LCFTypeConfigure; /** * 可用片段 */ snippets?: LCFTypeSnippet[]; /** * 一级分组 */ group?: string; /** * 二级分组 */ category?: string; /** 其他扩展协议 */ [key: string]: any; } /** * 组件属性信息 */ export interface LCFTypePropConfig { /** * 属性名称 */ name: string; /** * 属性默认值 */ defaultValue?: any; } /** * 编辑体验增强 */ export interface LCFTypeConfigure{ supports?: { // 支持事件列表 events?: ConfigureSupportEvent[]; // 交互的触发器和接收器配置 actions?: { triggers: Record, listeners: Record }; style?: boolean | SupportStyleConfigure; [key: string]: any; }; // 属性面板配置 props?: LCFFieldConfig[]; // 组件能力配置 component?: LCFTypeComponentConfigure; } /** * 可用片段 * * 内容为组件不同状态下的低代码 schema (可以有多个),用户从组件面板拖入组件到设计器时会向页面 schema 中插入 snippets 中定义的组件低代码 schema */ export interface LCFTypeSnippet { /** * 组件显示的别名,缺省值和组件名一致 */ title?: string; /** * snippet 截图 */ screenshot?: string; /** * 待插入的 工程描述中的组件 schema */ schema?: LCFTypeRootSchema; } /** * 支持事件列表 */ export interface ConfigureSupportEvent { // 事件类型 name: string; // 事件标题 description?: string; // 指定事件回调参数的为作用域变量 scope?: string; // 需要监听绑定的作用域变量的事件类型 scopeEvent?: string; } // style 样式面板自定义参数 export type SupportStyleConfigure = { // 自定义尺寸组件 sizeSetter?: RenderComponent; // 自定义背景组件 backgroundSetter?: RenderComponent; // 自定义边框组件 borderSetter?: RenderComponent; // 自定义阴影组件 shadowSetter?: RenderComponent; // 枚举值有 size、background、border、shadow showStyleList?: string[]; // 默认尺寸组件配置 sizeConfig?: { // 是否显示修改尺寸的 UI isShowSize?: boolean; // 是否显示修改内边距的 UI isShowPadding?: boolean; } } /** * 属性面板配置 */ export interface LCFTypeFieldConfig { /** * 面板配置隶属于单个 field 还是分组 */ type?: 'field' | 'group'; /** * 属性名 */ name?: string | number; /** * 属性标题 */ title?: string; /** * 单个控件的描述协议,type = 'field' 生效 */ setter?: string | LCFTypeSetterConfig; /** * 分类下的属性列表,type = 'group' 生效 */ items?: LCFTypeFieldConfig[]; /** * 配置当前 prop 是否展示 * 其他配置属性(不做流通要求) */ condition?: ((props: Record) => boolean) | boolean; /** * 其他配置属性(不做流通要求) */ display?: 'accordion' | 'inline' | 'block' | 'plain' | 'popup' | 'entry'; // setter 内容修改时调用 onChange?: (value: any) => void; // setter 渲染时被调用,setter 会根据该函数的返回值设置 setter 当前值 getValue?: (settingField: SettingField, value: any) => any; // setter 内容修改时调用,开发者可在该函数内部修改节点 schema 或者进行其他操作 setValue?: (settingField: SettingField, value: any) => void; // 作用1:若 display !== 'plain' 如果 forceInline有值,则显示等同于'plain'的效果;作用2:ArraySetter 里有个快捷预览,可以在不打开面板的情况下直接编辑, 若forceInline > 1则显示2个重要或必填属性,否则显示3个重要或必填属性,其余字段以弹窗的形式显示 forceInline?: number; // internal 是否必填参数 use array-setter 和 object-setter的配置 isRequired?: boolean; /** * important field */ important?: boolean; } /** * 组件能力配置 */ export interface LCFTypeComponentConfigure { /** * 是否容器组件 */ isContainer?: boolean; } export interface LCFTypeSetterConfig { /** * 配置设置器用哪一个 setter,内置的 setter 或者 提前已注册的 setter */ componentName: string; /** * 传递给 setter 的属性 */ props?: Record; /** * Setter 的初始值 */ defaultValue?: any; /** * 包裹 Setter 父元素 Setter */ extraSetter?: string | LCFTypeSetterConfig; } ``` 支持以下属性值类型 propType: - string - number - boolean - object - array - LCFTypeJSExpression - LCFTypeJSFunction - LCFTypeJSSlot 详细属性值类型定义如下: **LCFTypeJSExpression**: ```ts /** * 变量表达式 * * 表达式内通过 this 对象获取上下文 */ export interface LCFTypeJSExpression { type: 'JSExpression'; /** * 表达式字符串 */ value: string; /** * 模拟值 */ mock?: any; } ``` **示例** ```ts // 在组件描述协议中使用 JSExpression const propConfig: LCFTypeComponentDescription = { props: [ { name: 'iportalServiceProxyUrlPrefix', defaultValue: { type: 'JSExpression', value: `window.iportalServiceProxyUrl` } }, { name: 'geoJSON', defaultValue: { type: 'JSExpression', // dataSource是内置的数据源示例,dataId1是已添加的数据项的id,features 是该数据项的对应的geoJSON数据结果 value: 'dataSource.dataId1.features' } } ] }; ``` **LCFTypeJSFunction**: ```ts /** * 事件函数类型 * 保留与原组件属性 一致的输入参数,并给所有事件函数 binding 统一一致的上下文(当前组件所在容器结构的 this 对象) */ export interface LCFTypeJSFunction { type: 'JSFunction'; /** * 函数定义,或直接函数表达式 */ value: string; /** * 编译后可执行的源码 */ compiled?: string; /** * 模拟值 */ mock?: any; /** * 额外扩展属性,如 extType、events */ [key: string]: any; // isCustom 若为 true 则表示不自动解析函数 isCustom?: boolean; } ``` **示例** ```ts // 在组件描述协议中使用 JSFunction const componentDescription: LCFTypeComponentDescription = { configure: { props: [ { name: 'target', type: 'field', title: '目标', setter: { componentName: 'StringSetter', defaultValue: { type: 'JSFunction', value: `function (props) { return props.name; }` } } } ] } }; // 在自定义数据源中使用 JSFunction const dataSourceConfig: DataSourceRequestOptions = { type: 'custom', isInit: true, requestHandler: { type: 'JSFunction', value: `getFeaturesPromise` }, dataHandler: { type: 'JSFunction', value: `dataHandler` }, errorHandler: { type: 'JSFunction', value: `errorHandler` } }; // 其中 getFeaturesPromise,dataHandler,errorHandler 可挂在window上或者通过扩展插件上下文对象 ctx.project.registerMethod 注册 ctx.project.registerMethod('getFeaturesPromise', function () {}); ctx.project.registerMethod('dataHandler', function () {}); ctx.project.registerMethod('errorHandler', function () {}); ``` **LCFTypeJSSlot**: ```ts /** * slot 变量表达式 */ export interface LCFTypeJSSlot { /** * type */ type: 'JSSlot'; title?: string; id?: string; /** * 组件的某一个属性为 Function return ReactNode 时,函数的入参 * * 其子节点可以通过 this[参数名] 来获取对应的参数。 */ params?: string[]; /** * 具体的值。 */ value?: LCFTypeNodeData[]; name?: string; // 是否支持拖拽编辑 editable?: boolean; } export interface LCFTypeNodeSchema { id?: string; /** * 组件名称 必填、首字母大写 */ componentName: string; // 组件名称 title?: string; /** * 组件属性对象 */ props?: Recrd; /** * 子节点 */ children?: LCFTypeNodeData[]; /** * 当前应用的公共数据源 */ dataSource?: LCFTypeComponentDataSource; // 是否是纯展示的组件内容,即不生成对应的grid布局 isPureContent?: boolean; // 是否是容器 isContainer?: boolean; // 关联的id,如和 SmSlot 关联的子 node id relatedId?: string; /** * 容器初始数据 */ state?: { [key: string]: any; }; /** * 自定义方法设置 */ methods?: { [key: string]: LCFTypeJSFunction; }; } // 应用数据源配置 export interface LCFTypeComponentDataSource { list: LCFTypeDataSourceConfig[]; dataHandler?: LCFTypeJSFunction; } // 单条数据源配置 export interface LCFTypeDataSourceConfig { id: string; type: string; isInit?: boolean; requestHandler?: JSFunction; dataHandler?: JSFunction; errorHandler?: JSFunction; willFetch?: JSFunction; shouldFetch?: JSFunction; options?: DataSourceRequestOptions; [otherKey: string]: any; } export interface DataSourceRequestOptions { uri: string; params?: Record; method?: string; isCors?: boolean; timeout?: number; headers?: Record; [option: string]: any; } ``` **示例** ```ts // 在组件描述协议中使用 JSExpression const propConfig: LCFTypeComponentDescription = { props: [ { name: 'customRender', defaultValue: { type: 'JSSlot', id: 'title', // value 是 LCFTypeNodeSchema 类型,且必须是已注册的组件 value: [ { componentName: 'SmTextSlot', props: { linkTitle: { type: 'JSExpression', value: `slotScope.text` } } } ] } } ] }; ``` ### 代码示例 #### 自定义添加数据弹窗的数据源选项卡实现示例: ```ts const registry: BuilderxExtensionsPlugin = { active: 'onProjectCreated', init(ctx) { ctx.dataSource.addDataSource({ options: { uri: 'https://iclient.supermap.io/examples/data/fire.json', name: 'info2' } }); } }; export default registry; ``` #### 注册自定义组件实现示例: ##### 自定义组件的渲染组件 ```ts // 自定义组件的 Vue 渲染组件 import style from './style.module.css'; interface CustomProps { mode?: 'click' | 'mousemove'; fontSize?: number; textColor?: string; backgroundColor?: string; mapTarget?: string; } export default function createComponent(builderCtx: BuilderxExtensionsContext) { const { vue, hooks: { useMapGetter } } = builderCtx; const { defineComponent, computed, ref, watch } = vue; return defineComponent({ name: 'CoordinatesPicker', props: { mode: { type: String, default: 'mousemove' }, fontSize: { type: String, default: '18px' }, textColor: { type: String, default: '#fff' }, backgroundColor: { type: String, default: '#4a4a4a' }, mapTarget: { type: String } }, setup(props: CustomProps) { const optionsList = { click: '点击', mousemove: '鼠标移动' } as const; const { getMap } = useMapGetter({ loaded, removed }); const lngLat = ref<{ lng: number; lat: number } | null>(null); const fontStyle = computed(() => { const style: CSSProperties = {}; const { textColor, backgroundColor, fontSize } = props; if (textColor) { style.color = textColor; } if (backgroundColor) { style.backgroundColor = backgroundColor; } if (fontSize) { style.fontSize = fontSize; } return style; }); const modeState = computed(() => { return props.mode ?? 'mousemove'; }); const showCoors = (event: { lngLat: { lng: number; lat: number } }) => { lngLat.value = Object.assign({}, event.lngLat); }; function loaded(map: any) { map.on(modeState.value, showCoors); } function removed(map: any) { map.off(modeState.value, showCoors); lngLat.value = null; } const triggerMapEvent = () => { const map = getMap(); if (map) { if (modeState.value === 'click') { map.off('mousemove', showCoors); } else { map.off('click', showCoors); } map.on(modeState.value, showCoors); } }; watch( () => props.mode, () => { triggerMapEvent(); } ); return () => (

经纬度信息 demo:

{lngLat.value && (
{optionsList[modeState.value]}
{lngLat.value.lng.toFixed(6)}
{lngLat.value.lat.toFixed(6)}
)}
); } }); } ``` ##### 自定义组件的组件描述 ```ts // 自定义组件的组件描述 export default function createComponentDescription(builderCtx: BuilderxExtensionsContext) { const description: LCFTypeComponentDescription = { group: '自定义组件库', category: '地图子组件', title: '坐标获取', keywords: ['坐标获取', 'CoordinatesPicker'], description: '自定义地图子组件', screenshot, props: [ { name: 'mode', defaultValue: 'mousemove' }, { name: 'fontSize', defaultValue: '14px' }, { name: 'backgroundColor', defaultValue: '#333' }, { name: 'mapTarget' }, { name: 'extraStyle', defaultValue: { width: 280, height: 160 } } ], componentName: 'CoordinatesPicker', configure: { props: [ { name: 'mapTarget', type: 'field', title: '地图目标', setter: 'MapTargetSetter', }, { name: 'mode', type: 'field', title: '触发模式', setter: { componentName: 'SelectSetter', props: { options: [ { label: '鼠标移动', value: 'mousemove' }, { label: '点击', value: 'click' } ] } } }, { name: 'fontSize', type: 'field', title: '字体大小', setter: 'NumberSetter' }, { name: 'backgroundColor', type: 'field', title: '背景颜色', setter: 'ColorSetter' }, { type: 'group', title: '尺寸', items: [ { type: 'field', name: 'extraStyle.width', title: '宽度', setter: 'NumberSetter' }, { type: 'field', name: 'extraStyle.height', title: '高度', setter: 'NumberSetter' } ] } ] } }; const item: AssetsComponentItem = { description, component: createComponent(builderCtx) }; return item; } ``` ##### 注册自定义组件 ```ts // 注册自定义组件 const registry: BuilderxExtensionsPlugin = { init(ctx: BuilderxExtensionsContext) { const componentsAssets: LCFTypeAssetsJson = { components: ComponentDescriptions.map((registry) => registry(ctx)), version: '1.0.0' }; ctx.components.registerComponents(componentsAssets); } }; export default registry; ``` #### 注册自定义设置器实现示例: 设置器组件,固定的 props 有 `value` 和 `onChange`,`value` 是设置器的值,`onChange` 是设置器值改变时的回调函数。 ##### 自定义设置的渲染组件 ```ts // 自定义设置器的组件 import style from './style.module.css'; interface CustomImageSetterProps { value?: string; onChange?: (value: CustomImageSetterProps['value']) => void; } export default function createComponent(builderCtx: BuilderxExtensionsContext) { const { vue } = builderCtx; const { defineComponent, resolveComponent, ref, watch } = vue; return defineComponent({ name: 'CustomImageSetter', props: { value: { type: String }, onChange: { type: Function } }, setup(props: CustomImageSetterProps) { const AUpload = resolveComponent('a-upload'); const imageUrl = ref(''); const handleUploadChange = (file: Blob) => { getBase64(file, (base64Url: string) => { imageUrl.value = base64Url; }); return false; }; const handleRemove = (e: MouseEvent) => { e.stopPropagation(); imageUrl.value = ''; }; const handleChange = (src: string) => { props.onChange?.(src); }; watch(imageUrl, (url) => { handleChange(url); }); function getBase64(img: Blob, callback: (base64Url: string) => void) { const reader = new FileReader(); reader.addEventListener('load', () => callback(reader.result as string) ); reader.readAsDataURL(img); } return () => (
{() => imageUrl.value ? (
avatar
删除
) : (
Upload
) }
); } }); } ``` ##### 注册自定义设置器 ```ts // 注册自定义设置器 const registry: BuilderxExtensionsPlugin = { init(ctx) { ctx.setters.registerSetter({ CustomImageSetter: createComponent(ctx) }); } }; export default registry; ```