高效管理图标资源:GrapesJS 与 Vue3 搭配 Ant Design Icons 的应用实践

在构建可视化编辑器时,图标的选择和配置往往是提升用户体验的关键因素之一。作为前端开发者,我深知在设计界面时,如何高效、直观地管理图标资源是一个不小的挑战。今天,我将分享如何基于 GrapesJS、Vue3 以及 Ant Design Icons 实现一个功能强大的 Icon Trait 设置,帮助开发者轻松实现按钮和输入框图标的选择、设置,并支持搜索与清除功能。

背景故事:追寻高效图标管理的解决方案

回想起我刚开始接触 GrapesJS 时,遇到的第一个难题就是在组件中添加和管理图标。尽管 GrapesJS 提供了强大的定制能力,但在图标选择和配置方面却显得有些力不从心。尤其是在需要支持大量图标的情况下,如何实现高效的搜索和选择功能成为了亟待解决的问题。

于是,我决定结合 Vue3 的响应式特性和 Ant Design Icons 丰富的图标库,打造一个可视化、易用的 Icon Trait 解决方案。这个过程不仅提升了我的开发效率,也大大优化了最终用户的体验。

方案价值:解决开发与使用中的痛点

在传统的图标管理中,开发者往往需要手动编码或依赖较为简单的选择器,这不仅效率低下,还容易出错。而本方案通过以下几个方面解决了这些痛点:

  1. 可视化搜索与选择:用户可以直观地通过搜索框快速找到所需图标,无需记忆所有图标名称。
  2. 实时预览:选择过程中,用户可以实时查看所选图标在按钮或输入框中的展示效果,提升设计准确性。
  3. 高扩展性:基于 Vue3 和 Ant Design Icons 的组件化设计,使得该方案易于扩展和维护。

核心实现:TraitIcon 的开发

TraitIcon.ts

首先,我们需要在 GrapesJS 中添加一个新的 Trait 类型,用于管理图标的选择和设置。以下是核心代码:

export const initIconTrait = (editor: Editor) => {
  loadVueAndAntd().then(() => {
    editor.TraitManager.addType('icon', {
      noLabel: false,
      createInput({ trait }) {
        const el = document.createElement('div');
        el.style.marginRight = '1px';

        const icon = trait.attributes['value']

        const onUpdateIconDataVue = (newValue) => {
          trait.target.set(String(trait.id), newValue);

          if (!trait.target.attributes) {
            trait.target.attributes = {};
          }
          if (!trait.target.attributes.attributes) {
            trait.target.attributes.attributes = {};
          }

          trait.target.attributes.attributes[trait.id] = newValue;
          trait.target.set('timestamp', Date.now());
        }

        mountVueComponent({
          el,
          componentImporter: () => import('@/views/system/function/editor/traits/Icon.vue'),
          props: {
            icon, onUpdateIconData: onUpdateIconDataVue
          }
        })
          .then((app) => {
            el.addEventListener('destroy', () => {
              app.unmount();
            });
          })
          .catch((err) => {
            console.error(`Failed to mount icon trait component:`, err);
          });

        return el;
      },
    });
  });
};

TraitIcon.vue

接下来,我们编写 Vue 组件,用于图标的搜索、选择和清除功能:

<template>
  <div class="icon-selector">
    <!-- 搜索框、清除按钮和展开/缩起按钮 -->
    <a-row justify="space-between" :gutter="[0, 2]" :wrap="false">
      <a-col :span="3">
        <a-flex style="padding-left: 5px;">
          <a-tooltip :title="isCollapsed ? '显示图标列表' : '隐藏图标列表'" placement="top">
            <a-button type="default" size="small" :icon="h(Icons[isCollapsed ? 'EyeOutlined' : 'EyeInvisibleOutlined'])"
              @click="toggleCollapse" />
          </a-tooltip>
        </a-flex>
      </a-col>
      <a-col :span="17">
        <a-input class="input-search" v-model:value="searchKeyword" placeholder="输入图标" :bordered="true"
          @focus="showIconGrid" />
      </a-col>

      <a-col :span="3">
        <a-flex justify="end" style="padding-right: 5px;">
          <a-tooltip title="清除选择" placement="top">
            <a-button type="primary" danger size="small" :icon="h(Icons.DeleteOutlined)" @click="clearSelection" />
          </a-tooltip>
        </a-flex>
      </a-col>
    </a-row>

    <!-- 图标网格展示区域 -->
    <div class="icon-grid" v-show="!isCollapsed">
      <div v-for="icon in filteredIcons" :key="icon.name" class="icon-item"
        :class="{ selected: selectedIcon === icon.name }" @click="selectIcon(icon)">
        <component :is="icon.component" two-tone-color="#1890ff" style="font-size: 24px;" />
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, defineEmits, defineProps, h } from 'vue';
import * as Icons from '@ant-design/icons-vue';

const emit = defineEmits(['updateIconData']);

defineProps<{
  label: string;
}>();

// 收集所有图标数据
const iconData = Object.keys(Icons).map((name) => ({
  name,
  component: Icons[name]
}));

// 排除的不需要显示的图标
const EXCLUDED_ICONS = [
  'default',
  'createFromIconfontCN',
  'getTwoToneColor',
  'setTwoToneColor'
];

const searchKeyword = ref('');
const selectedIcon = ref<string | null>(null);
const selectedIconComponent = ref<any>(null);

// 过滤后的图标列表
const filteredIcons = computed(() =>
  iconData.filter((icon) => {
    const matchesSearch = icon.name.toLowerCase().includes(searchKeyword.value.toLowerCase());
    const isNotExcluded = !EXCLUDED_ICONS.includes(icon.name);
    return matchesSearch && isNotExcluded;
  })
);

// 是否折叠图标列表
const isCollapsed = ref(true);

function selectIcon(icon: { name: string; component: any }) {
  selectedIcon.value = icon.name;
  selectedIconComponent.value = icon.component;
  emit('updateIconData', icon.name);
}

function clearSelection() {
  selectedIcon.value = null;
  selectedIconComponent.value = null;
  emit('updateIconData', undefined);
}

function toggleCollapse() {
  isCollapsed.value = !isCollapsed.value;
}

// 当输入框聚焦时显示图标网格
function showIconGrid() {
  isCollapsed.value = false;
}
</script>

<style scoped>
.icon-selector {
  width: 100%;
  padding-top: 8px;
  padding-bottom: 8px;
  max-width: 100%;
  background-color: #fff;
  border: 1px solid #f0f0f0;
  border-radius: 4px;
  overflow-x: hidden;
}

/* 输入框样式 */
.input-search {
  height: 24px;
  border: 1px solid #d9d9d9;
  color: black;
  border-radius: 3px;
}

/* 图标网格布局 */
.icon-grid {
  width: 100%;
  display: grid;
  margin-top: 5px;
  padding-left: 5px;
  padding-right: 5px;
  max-height: 200px;
  overflow-y: auto;
  grid-template-columns: repeat(8, 1fr);
  gap: 4px;
}

/* 单个图标项 */
.icon-item {
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid #e8e8e8;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.3s ease;
  overflow: hidden;
  min-width: 0;
  color: black;
}

.icon-item:hover {
  border-color: #1890ff;
}

.icon-item.selected {
  border-color: #1890ff;
}
</style>

使用示例

在实际使用中,可以通过以下方式将图标 Trait 集成到按钮组件中:

<template>
  <a-button v-bind="filteredInputProps" @click="handleClick">{
   
   { filteredInputProps.text }}</a-button>
</template>

<script lang="ts" setup>
import { computed, h } from 'vue';
import * as Icons from '@ant-design/icons-vue';

const props = defineProps<{
  buttonProps: ButtonProps;
  model?: any;
}>();

// 过滤掉 undefined 的属性
const filteredButtonProps = computed(() => {
  const entries = Object.entries(props.buttonProps)
    .filter(([, val]) => val !== undefined)
    .map(([key, val]) => {
      if (key === 'iconName' && val) {
        return ['icon', h(Icons[val])];
      }
      return [key, val];
    });

  return Object.fromEntries(entries);
});
</script>

深入解析:核心代码详解

TraitIcon.ts 的作用

TraitIcon.ts 文件的主要职责是在 GrapesJS 中定义一个新的 Trait 类型,命名为 icon。该 Trait 类型负责加载 Vue 组件,并将其挂载到 GrapesJS 的 Trait 面板中。通过这种方式,开发者可以在 GrapesJS 的组件属性面板中,直观地进行图标的搜索与选择。

TraitIcon.vue 的功能

TraitIcon.vue 组件是整个图标选择功能的核心。它包含以下几个关键部分:

  1. 搜索与清除功能:通过 a-input 组件,用户可以输入关键词快速搜索所需图标。同时,清除按钮允许用户快速取消选择。
  2. 图标网格展示:使用 CSS Grid 布局,将所有可选图标整齐地排列在网格中,支持滚动查看和点击选择。
  3. 状态管理:通过 Vue3 的 refcomputed,实现图标列表的动态过滤和选中状态的管理。

使用示例的作用

使用示例代码展示了如何在实际的按钮组件中集成图标 Trait。通过 filteredInputProps,我们可以动态过滤掉未定义的属性,并将选中的图标渲染到按钮上,实现图标与按钮文本的无缝结合。

总结与展望

通过本文的介绍,我们探讨了如何基于 GrapesJS、Vue3 以及 Ant Design Icons 构建一个高效、直观的 Icon Trait 设置。这不仅提升了开发者的工作效率,也为最终用户提供了更友好的操作体验。在未来的开发中,可以进一步扩展该方案,支持更多图标库或自定义图标的导入,满足更广泛的需求。

希望这篇文章能够为需要在 GrapesJS 中集成图标选择功能的开发者提供有价值的参考。如果你有任何疑问或建议,欢迎在评论区交流讨论!