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

0 阅读11分钟

自定义组件与设计器面板

本文档提供了通过自定义组件和设计器面板扩展 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) 定义了所有组件的核心配置结构:

属性类型必填描述
namestring组件的唯一标识符
regionRegionType组件放置的目标区域
componentVueComponentVue 组件实现
propsRecord<string, any>传递给组件的默认属性
invisibleboolean控制组件的可见性
groupstring用于过滤的逻辑分组
ordernumber区域内的显示顺序
remoteboolean指示远程资源依赖

AppWidget

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

附加属性类型描述
type'app'标识为应用程序组件
iconVueComponent组件的显示图标
labelstring人类可读的标签
openType'panel'|'link'|'dialog'决定打开行为
urlstring'link' 类型组件的 URL
cacheboolean启用 KeepAlive 缓存

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

TabWidget

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

附加属性类型描述
type'tab'标识为选项卡组件
labelstring选项卡显示名称
iconVueComponent可选的选项卡图标
closableboolean选项卡是否可关闭
actionsany[]附加操作按钮

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

区域系统概述

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

flowchart TD
    subgraph Header[Header Section]
        B[Brand<br>Logo & Switcher]
        T[Toolbar<br>Action Buttons]
        A[Actions<br>Global Actions]
    end

    subgraph MainBody[Main Body]
        L[Apps Region<br>Left Panel<br>Panel-type Widgets]
        W[Workspace Region<br>Center<br>Tab-type Widgets]
        R[Settings Region<br>Right Panel<br>Panel-type Widgets]
    end

    subgraph Footer[Footer Section]
        S[Status Region<br>Status Bar]
    end

区域描述

区域用途组件类型示例组件
BrandLogo 和品牌标识StandardLogo, Switcher
Toolbar主要操作StandardToolbar
Actions全局操作StandardActions
Apps左侧面板导航AppWidgetPages, Blocks, Components, Outline, History, Apis
Workspace中央工作区TabWidgetDesigner, Properties, Events, CSS, Style, Directives
Settings右侧面板内容AppWidgetDeps, Globals, I18n, Env
Status状态信息StandardStatus

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

创建自定义组件

第 1 步:创建组件

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

<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 区域面板:

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 区域选项卡:

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 注册你的组件:

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 访问设计器引擎:

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 提供对当前项目和页面上下文的访问:

import { useCurrent } from "../hooks";

const { current, context } = useCurrent();

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

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

选择管理

useSelected hook 管理节点选择状态:

import { useSelected } from "../hooks";

const { selected } = useSelected();

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

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

属性管理

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

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 属性控制组件可见性:

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 });

动态组件注册

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

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

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

组件分组

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

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

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

远程组件支持

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

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/ 中的结构化模式:

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

  &__content {
    // Content styles
  }

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

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

// packages/designer/src/style/widgets/index.scss
@import "./custom-tool.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 合并:

// 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 → propsprops → attrs。这允许灵活的 prop 注入,同时保持默认值。

最佳实践和指南

组件命名约定

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

组件组织

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

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 标签
  • 保持足够的颜色对比度
  • 支持屏幕阅读器公告以显示重要的状态变化

错误处理

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

<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>

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

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

组件实现

<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>

组件注册

// 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,
};

在组件管理器中注册:

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 的详细文档

参考资料