AI 驱动的 Vue3 应用开发平台 深入探究(十六):扩展与定制之自定义组件与设计器面板

自定义组件与设计器面板

本文档提供了通过自定义组件和设计器面板扩展 VTJ Designer 的全面指南。组件系统作为将自定义功能集成到设计器 UI 的架构基础,使开发者能够创建针对特定工作流定制的专用工具、面板和界面。

组件系统架构

VTJ Designer 采用基于区域的组件架构,其中组件被组织到设计器布局中的指定区域内。这种设计促进了模块化,允许灵活的 UI 组合,并支持运行时动态组件注册。

flowchart TD subgraph WidgetSystem[Widget System Architecture] subgraph Skeleton[Skeleton Component] Brand[Brand Region] Toolbar[Toolbar Region] Actions[Actions Region] Apps[Apps Region] Workspace[Workspace Region] Settings[Settings Region] Status[Status Region] AppWidgets[AppWidgets Panel-type] TabWidgets[TabWidgets Workspace tabs] StdWidgets[Standard Widgets] end WM[WidgetManager Central Registry] BuiltIn[Built-in Widgets] Custom[Custom Widgets] WidgetDefs[Widget Definitions] RW[RegionWrapper] WW[WidgetWrapper] WM --> BuiltIn WM --> Custom WM --> WidgetDefs WidgetDefs --> RW WidgetDefs --> WW RW --> Brand RW --> Toolbar RW --> Actions RW --> Apps RW --> Workspace RW --> Settings RW --> Status WW --> AppWidgets WW --> TabWidgets WW --> StdWidgets end

WidgetManager (widget.ts#L8-L103) 作为中央注册表,管理所有组件注册并提供动态组件操作的方法。每个组件都与 RegionType 枚举 (types.ts#L40-L54) 中定义的特定区域相关联,从而确定其出现在设计器界面的哪个位置。

组件类型和配置

该框架支持三种主要的组件类型,每种类型服务于设计器环境中的不同 UI 集成模式。

基础组件

Widget 接口 (types.ts#L60-L96) 定义了所有组件的核心配置结构:

属性 类型 必填 描述
name string 组件的唯一标识符
region RegionType 组件放置的目标区域
component VueComponent Vue 组件实现
props Record<string, any> 传递给组件的默认属性
invisible boolean 控制组件的可见性
group string 用于过滤的逻辑分组
order number 区域内的显示顺序
remote boolean 指示远程资源依赖

AppWidget

AppWidget 类型 (types.ts#L102-L131) 扩展了基础组件,用于 Apps 区域中的应用程序样式面板:

附加属性 类型 描述
type 'app' 标识为应用程序组件
icon VueComponent 组件的显示图标
label string 人类可读的标签
openType `'panel' 'link'
url string 'link' 类型组件的 URL
cache boolean 启用 KeepAlive 缓存

内置示例包括 Pages、Blocks、Components 和 Apis 组件 (widgets.ts#L39-L98)。

TabWidget

TabWidget 类型 (types.ts#L137-L153) 在 Workspace 区域中创建选项卡界面:

附加属性 类型 描述
type 'tab' 标识为选项卡组件
label string 选项卡显示名称
icon VueComponent 可选的选项卡图标
closable boolean 选项卡是否可关闭
actions any[] 附加操作按钮

示例包括 Designer、Properties、Events、CSS 和 Style 组件 (widgets.ts#L181-L247)。

区域系统概述

设计器界面被组织成七个预定义区域,每个区域在用户体验中都有特定用途。

flowchart TD subgraph Header[Header Section] B[Brand
Logo & Switcher] T[Toolbar
Action Buttons] A[Actions
Global Actions] end subgraph MainBody[Main Body] L[Apps Region
Left Panel
Panel-type Widgets] W[Workspace Region
Center
Tab-type Widgets] R[Settings Region
Right Panel
Panel-type Widgets] end subgraph Footer[Footer Section] S[Status Region
Status Bar] end

区域描述

区域 用途 组件类型 示例组件
Brand Logo 和品牌标识 Standard Logo, Switcher
Toolbar 主要操作 Standard Toolbar
Actions 全局操作 Standard Actions
Apps 左侧面板导航 AppWidget Pages, Blocks, Components, Outline, History, Apis
Workspace 中央工作区 TabWidget Designer, Properties, Events, CSS, Style, Directives
Settings 右侧面板内容 AppWidget Deps, Globals, I18n, Env
Status 状态信息 Standard Status

每个区域都由专用的 Vue 组件 (regions/index.ts) 渲染,并由处理动态组件解析的 RegionWrapper 组件 (region.ts) 包装。

创建自定义组件

第 1 步:创建组件

将组件开发为 Vue 组件,通常放置在 packages/designer/src/components/widgets/ 中。组件可以通过可组合 Hooks 访问设计器上下文。

html 复制代码
<template>
  <div class="v-custom-widget">
    <h3>{{ title }}</h3>
    <p>{{ description }}</p>
    <!-- 组件内容 -->
  </div>
</template>

<script lang="ts" setup>
import { ref } from "vue";
import { useEngine, useCurrent } from "../hooks";

const engine = useEngine();
const { current, context } = useCurrent();

const title = ref("My Custom Widget");
const description = ref("This is a custom designer widget");

defineOptions({
  name: "CustomWidget",
});
</script>

可用的设计器 Hooks (hooks/index.ts) 包括:

  • useEngine - 访问设计器引擎实例
  • useCurrent - 当前项目和页面上下文
  • useSelected - 当前选中的节点信息
  • useNodeProps - 节点属性管理
  • useDesigner - 设计器交互工具
  • useRegion - 特定区域的组件管理
  • useWorkspace - 工作区选项卡管理
  • useHistory - 历史/撤销重做操作
  • 以及 18 个以上的其他专用 Hooks

第 2 步:定义组件配置

按照相应的接口类型创建组件定义。

对于 Apps 区域面板:

typescript 复制代码
import { type AppWidget } from "../framework";
import CustomWidget from "./widgets/custom-widget/index.vue";

export const customAppWidget: AppWidget = {
  name: "CustomTool",
  region: "Apps",
  component: CustomWidget,
  type: "app",
  openType: "panel",
  icon: VtjIconTool, // Import from @vtj/icons
  label: "Custom Tool",
  cache: true, // 可选:保持组件存活
  order: 100, // 显示顺序
};

对于 Workspace 区域选项卡:

typescript 复制代码
import { type TabWidget } from "../framework";
import CustomTab from "./widgets/custom-tab/index.vue";

export const customTabWidget: TabWidget = {
  name: "CustomTab",
  region: "Workspace",
  component: CustomTab,
  type: "tab",
  label: "Custom Tab",
  icon: VtjIconTab,
  closable: true,
  order: 50,
};

第 3 步:注册组件

向 WidgetManager 注册你的组件:

typescript 复制代码
import { widgetManager } from "../managers";
import { customAppWidget, customTabWidget } from "./custom-widgets";

widgetManager.register(customAppWidget);
widgetManager.register(customTabWidget);

WidgetManager (widget.ts#L27-L36) 提供了多种注册和管理方法:

方法 参数 描述
register(widget) Widget 注册新组件
get(name) string 按名称检索组件
set(name, widget) string, Partial<Widget> 更新现有组件
unregister(name) string 从注册表中删除组件
getWidgets(region, group) RegionType?, string? 获取过滤后的组件列表
getRemoteWidgets() - 获取带有 remote 标记的组件
removeRemoteWidgets() - 删除所有远程组件

组件集成模式

Apps 区域集成

Apps 区域 (apps.vue) 实现了一个双面板界面,具有基于图标的导航和可折叠的内容面板。

关键集成点:

  • 面板组件 (openType: 'panel') - 在可折叠面板中渲染
  • 链接组件 (openType: 'link') - 打开外部 URL
  • 对话框组件 (openType: 'dialog') - 作为模态对话框打开

💡 Apps 区域支持通过 cache: true 组件属性进行 KeepAlive 缓存。这在工具之间切换时保留组件状态,对于具有复杂状态(如表单输入或选择)的组件至关重要。缓存使用 Vue 的 KeepAlive 组件实现 (apps.vue#L57-L63)。

Apps 区域根据 openType 属性自动将组件分离为"面板组件"(顶部图标)和"其他组件"(底部图标)(apps.vue#L67-L82)。

Workspace 区域集成

Workspace 区域 (workspace.vue) 为作用于当前选中节点或页面的设计器工具提供了选项卡界面。

Workspace 功能:

  • 多选项卡界面 - 可以同时打开多个工具
  • 选项卡管理 - 关闭单个选项卡,关闭全部,关闭其他
  • 上下文感知 - 组件通过 Hooks 接收当前上下文
  • 操作菜单 - 选项卡级别的操作和菜单
  • 可检查选项卡 - 基于切换的选项卡激活

Workspace 使用 useWorkspace hook (hooks/useWorkspace.ts) 来管理选项卡状态,该状态与设计器的文件系统和选择模型集成。

Settings 区域集成

Settings 区域托管配置和管理面板。虽然它遵循与 Apps 区域相同的组件注册模式,但它位于设计器的右侧,通常包含修改项目级别设置或依赖项的组件。

Settings 组件示例:

  • Deps - 依赖项管理
  • Globals - 应用程序设置
  • I18n - 国际化资源
  • Env - 环境变量

组件通信和上下文

设计器引擎访问

所有组件都可以通过 useEngine hook 访问设计器引擎:

typescript 复制代码
import { useEngine } from "../hooks";

const engine = useEngine();

// 访问引擎状态
const isPreview = engine.state.previewMode;
const streaming = engine.state.streaming;

// 访问当前项目
const project = engine.project;

// 访问页面上下文
const page = engine.page;

引擎 (engine.ts) 为所有设计器操作提供了中央协调点。

当前上下文 Hook

useCurrent hook 提供对当前项目和页面上下文的访问:

typescript 复制代码
import { useCurrent } from "../hooks";

const { current, context } = useCurrent();

// 当前项目和页面
current.value.project; // ProjectModel
current.value.page; // PageModel

// 渲染上下文(用于表达式)
context.value; // Context object

选择管理

useSelected hook 管理节点选择状态:

typescript 复制代码
import { useSelected } from "../hooks";

const { selected } = useSelected();

// 当前选中的节点
const node = selected.value; // NodeModel | null

// 响应选择变化
watch(selected, (newNode) => {
  console.log("Selected node:", newNode);
});

属性管理

useNodeProps hook 为选中节点提供属性管理:

typescript 复制代码
import { useNodeProps } from "../hooks";

const {
  node,
  commonProps,
  componentProps,
  customProps,
  change,
  addCustom,
  removeCustom,
} = useNodeProps(selected);

// 修改属性
const handleChange = (key: string, value: any) => {
  change(key, value);
};

// 添加自定义属性
const addNewProp = (name: string) => {
  addCustom(name);
};

这个 hook 被 Properties 组件 (properties/index.vue) 广泛使用。

高级组件模式

条件可见性

通过 invisible 属性控制组件可见性:

typescript 复制代码
export const conditionalWidget: AppWidget = {
  name: "ConditionalTool",
  region: "Apps",
  component: ConditionalTool,
  type: "app",
  openType: "panel",
  icon: VtjIconTool,
  label: "Conditional Tool",
  invisible: true, // 初始隐藏
};

// 稍后,根据条件使其可见
widgetManager.set("ConditionalTool", { invisible: false });

动态组件注册

根据项目配置或运行时条件动态注册组件:

typescript 复制代码
// 仅在特定环境中注册
if (engine.env === "development") {
  widgetManager.register(devToolsWidget);
}

// 根据项目类型注册
if (project?.type === "uniapp") {
  widgetManager.register(uniConfigWidget);
}

组件分组

使用 group 属性组织组件以便进行过滤:

typescript 复制代码
export const groupWidget: Widget = {
  name: "GroupedTool",
  region: "Apps",
  component: GroupedTool,
  group: "advanced", // 自定义组名
  // ...
};

// 仅检索特定组
const advancedWidgets = widgetManager.getWidgets("Apps", "advanced");

远程组件支持

将需要服务器端资源的组件标记为 remote: true

typescript 复制代码
export const remoteWidget: AppWidget = {
  name: "RemoteTool",
  region: "Apps",
  component: RemoteTool,
  type: "app",
  openType: "panel",
  remote: true, // 需要远程服务
  icon: VtjIconCloud,
  label: "Remote Tool",
};

// 离线时删除远程组件
widgetManager.removeRemoteWidgets();

组件样式和主题

SCSS 结构

组件样式遵循 packages/designer/src/style/widgets/ 中的结构化模式:

scss 复制代码
// packages/designer/src/style/widgets/custom-tool.scss
.v-custom-tool {
  &__header {
    // Header styles
  }

  &__content {
    // Content styles
  }

  // 使用 BEM 方法论
  &--active {
    // Active state styles
  }
}

在主组件样式索引中导入组件样式:

scss 复制代码
// packages/designer/src/style/widgets/index.scss
@import "./custom-tool.scss";

主题集成

访问主题变量以保持样式一致:

scss 复制代码
.v-custom-tool {
  color: var(--vtj-text-color);
  background: var(--vtj-bg-color);
  border: 1px solid var(--vtj-border-color);

  &:hover {
    background: var(--vtj-bg-color-hover);
  }
}

主题变量定义在 packages/designer/src/style/core/_vars.scss 中。

组件包装组件

WidgetWrapper (widget.ts) 为所有组件提供渲染层,将默认 props 与运行时 props 和 attrs 合并:

typescript 复制代码
// WidgetWrapper 渲染实现
render() {
  const { $props = {}, $attrs = {} } = this as any;
  return h(this.widget.component, {
    ...this.widget.props,    // 组件默认 props
    ...$props,               // 运行时 props
    ...$attrs,               // HTML 属性
    ref: 'widgetRef'         // 组件引用
  });
}

此包装器确保组件既接收其配置的默认 props,也接收从父组件或区域传递的任何附加 props。

💡 在设计组件时,请记住 WidgetWrapper 会自动将组件 props 与运行时 props 合并。如果你的组件组件定义了自己的 props,它们将按以下顺序合并:widget.props → <math xmlns="http://www.w3.org/1998/Math/MathML"> p r o p s → props → </math>props→attrs。这允许灵活的 prop 注入,同时保持默认值。

最佳实践和指南

组件命名约定

  • 组件名称使用 PascalCase:CustomTool.vue
  • 组件名称使用 PascalCase:CustomTool
  • 添加描述性前缀以便组织:MyCompanyCustomTool

组件组织

packages/designer/src/components/widgets/ 目录中按功能组织组件:

vbnet 复制代码
widgets/
├── custom-tools/
│   ├── index.vue
│   ├── sub-component.vue
│   └── types.ts
├── custom-tabs/
│   ├── index.vue
│   └── helpers.ts

性能考虑

  • 谨慎使用 cache: true,仅用于初始化成本高的组件
  • 实现 defineExpose 以仅向父组件公开必要的方法
  • 对派生状态使用计算属性以避免不必要的重新计算
  • 利用 Vue 的 Composition API 以获得更好的代码组织和 tree-shaking

无障碍指南

  • 确保所有交互元素都可以通过键盘访问
  • 使用语义 HTML 元素
  • 为自定义组件提供适当的 ARIA 标签
  • 保持足够的颜色对比度
  • 支持屏幕阅读器公告以显示重要的状态变化

错误处理

实现适当的错误边界和用户反馈:

vue 复制代码
<script lang="ts" setup>
import { notify } from "../utils";

const handleAction = async () => {
  try {
    await performAction();
  } catch (error) {
    notify("Operation failed: " + error.message, "error");
    // 记录错误以进行调试
    console.error("Widget action error:", error);
  }
};
</script>

完整示例:自定义分析组件

此示例演示如何创建一个用于分析页面结构的综合自定义组件。

组件实现

html 复制代码
<template>
  <XContainer direction="column" fit class="v-analysis-widget">
    <div class="v-analysis-widget__header">
      <h3>Page Analysis</h3>
      <XButton type="primary" size="small" @click="analyze"> Analyze </XButton>
    </div>

    <div class="v-analysis-widget__content">
      <div v-if="loading" class="loading">
        <ElIcon class="is-loading"><Loading /></ElIcon>
        Analyzing...
      </div>

      <div v-else-if="results" class="results">
        <div class="result-item">
          <span class="label">Total Components:</span>
          <span class="value">{{ results.total }}</span>
        </div>
        <div class="result-item">
          <span class="label">Nesting Depth:</span>
          <span class="value">{{ results.depth }}</span>
        </div>
        <div class="result-item">
          <span class="label">Complexity Score:</span>
          <span class="value" :class="getScoreClass(results.score)">
            {{ results.score }}
          </span>
        </div>
      </div>

      <div v-else class="empty">Click "Analyze" to begin</div>
    </div>
  </XContainer>
</template>

<script lang="ts" setup>
import { ref, computed } from "vue";
import { XContainer, XButton } from "@vtj/ui";
import { ElIcon, Loading } from "element-plus";
import { useCurrent, useSelected } from "../../hooks";

const { current } = useCurrent();
const { selected } = useSelected();

const loading = ref(false);
const results = ref<any>(null);

const analyze = async () => {
  if (!current.value?.page) {
    notify("No page selected", "warning");
    return;
  }

  loading.value = true;
  try {
    // 执行分析逻辑
    const page = current.value.page;
    const analysis = analyzePage(page);
    results.value = analysis;
  } catch (error) {
    notify("Analysis failed: " + error.message, "error");
  } finally {
    loading.value = false;
  }
};

const analyzePage = (page: any) => {
  // 页面分析实现
  const total = countComponents(page);
  const depth = calculateDepth(page);
  const score = calculateScore(total, depth);

  return { total, depth, score };
};

const getScoreClass = (score: number) => {
  if (score < 50) return "good";
  if (score < 80) return "warning";
  return "danger";
};

defineOptions({
  name: "AnalysisWidget",
});
</script>

<style lang="scss" scoped>
@import "../../../style/widgets/analysis.scss";
</style>

组件注册

typescript 复制代码
// packages/designer/src/managers/built-in/custom-widgets.ts
import { type AppWidget } from "../../framework";
import { VtjIconChart } from "@vtj/icons";
import AnalysisWidget from "../../components/widgets/analysis/index.vue";

export const analysisWidget: AppWidget = {
  name: "Analysis",
  region: "Apps",
  component: AnalysisWidget,
  type: "app",
  openType: "panel",
  icon: VtjIconChart,
  label: "Page Analysis",
  order: 60,
};

在组件管理器中注册:

typescript 复制代码
import { analysisWidget } from "./custom-widgets";
import { builtInWidgets } from "./widgets";

// 添加到内置组件
builtInWidgets.push(analysisWidget);

常见问题故障排除

组件未出现

问题:已注册的组件未显示在设计器中。

解决方案

  1. 验证 region 属性是否与有效的 RegionType 匹配
  2. 检查 invisible 是否未设置为 true
  3. 确保组件已向 widgetManager 正确注册
  4. 确认组件已从 widgets/index.ts 导出
  5. 检查浏览器控制台是否有 Vue 警告或错误

组件 Props 未合并

问题:组件 props 未正确传递给组件。

解决方案

  1. 验证 props 是否在组件配置中定义
  2. 检查是否正在使用 WidgetWrapper 进行渲染
  3. 确保 props 不与保留的 prop 名称冲突
  4. 查看 Vue 组件定义中的 prop 类型

切换时组件状态丢失

问题:在 Apps 区域面板之间切换时,组件状态丢失。

解决方案

  1. cache: true 添加到组件配置中
  2. 确保组件实现了适当的状态管理
  3. 检查组件是否未意外卸载
  4. 验证 KeepAlive 是否正常工作

性能问题

问题:自定义组件导致设计器变慢。

解决方案

  1. 在 onUnmounted 中实现适当的清理
  2. 尽可能使用计算属性而不是侦听器
  3. 对昂贵的操作进行防抖
  4. 通过正确定义响应式依赖来避免过度重新渲染
  5. 使用 Vue DevTools 分析组件性能

相关文档

要更深入地了解相关系统,请探索以下文档部分:

  • 创建自定义物料组件 → - 了解如何创建与设计器集成的可重用 UI 组件
  • 自定义 Setters 和属性编辑器 → - 了解如何创建用于 Properties 组件的自定义属性编辑器
  • 物料模式配置 → - 了解如何为自定义组件定义物料模式
  • 插件系统开发 → - 发现如何将自定义组件打包为插件进行分发
  • 引擎 API 参考 → - 设计器引擎及其 API 的详细文档

参考资料

相关推荐
爱敲代码的菜菜2 小时前
【项目】基于正倒排索引的Java文档搜索引擎
java·开发语言·前端·javascript·搜索引擎·servlet
李剑一2 小时前
告别冗余代码!Cesium点位图标模糊、重叠?自适应参数调优攻略,一次封装终身复用!
前端·vue.js·cesium
sz_denny2 小时前
chrome os 如何进入开发者模式
前端·chrome
踩着两条虫2 小时前
🔥 实测对比:VTJ.PRO凭啥让头部企业放弃自研低代码?
前端·vue.js·ai编程
han_2 小时前
JavaScript 检测网络连接状态,以及网络测速方案
前端·javascript
小李子呢02112 小时前
JavaScript 中 Map 的完整解析
前端·javascript·vue.js
木梓辛铭2 小时前
关于Chrome无法上网的问题2
前端·chrome
Shirley~~2 小时前
ElementUI Carousel 取消hover暂停轮播的默认行为
前端·javascript·vue.js