前言

在 Vue 3 项目开发中,工具栏(Toolbar)是一个常见的 UI 组件。本文将通过一个实际的工具栏组件代码示例,详细讲解 Vue 3 的各类核心概念和最佳实践。这个组件使用了 TDesign Vue Next UI 库,但讲解的重点将放在 Vue 3 的实现原理上。

组件概览

首先让我们看一下完整的组件代码(简化版):

<template>
  <div class="top-toolbar">
    <div class="toolbar-group" v-for="(group, index) in toolbarGroups" :key="index">
      <t-tooltip v-for="btn in group" :key="btn.command" :content="btn.tooltip">
        <t-button
          shape="square"
          variant="text"
          :class="['tool-btn', { 'active': activeCommand === btn.command }]"
          @click="$emit('command', btn.command)"
        >
          <component :is="btn.icon" />
        </t-button>
      </t-tooltip>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
// 导入各种图标组件...
</script>

1. 模板结构解析

1.1 基础布局

组件最外层是一个 div,类名为 top-toolbar,这是工具栏的容器。内部通过 v-for 循环渲染多个工具按钮组。

1.2 分组渲染

toolbarGroups 是一个数组,每个元素代表一组工具按钮。通过 v-for 循环渲染:

<div class="toolbar-group" v-for="(group, index) in toolbarGroups" :key="index">

这里有几个关键点:

  1. v-for 指令用于循环渲染

  2. (group, index) 解构出当前项和索引

  3. :key="index" 提供唯一标识(后面会讨论其重要性)

1.3 按钮渲染

每组内部再通过 v-for 渲染各个按钮:

<t-tooltip v-for="btn in group" :key="btn.command">

注意这里的 :key="btn.command" 使用了按钮的命令作为唯一标识,比外层使用的 index 更可靠。

2. 组件通信

2.1 事件发射

当点击按钮时,通过 @click 发射事件:

@click="$emit('command', btn.command)"

这是子组件向父组件通信的标准方式。父组件可以这样监听:

<top-toolbar @command="handleCommand" />

2.2 props 和 emits

虽然这个例子中没有显式定义 props,但在正式项目中,建议使用 TypeScript 定义组件接口:

defineProps<{
  // props 定义
}>()

defineEmits<{
  (e: 'command', value: string): void
}>()

3. 动态组件

按钮图标通过动态组件渲染:

<component :is="btn.icon" />

3.1 动态组件原理

<component> 是 Vue 内置组件,is 属性决定渲染哪个组件。btn.icon 可以是一个已注册的组件名或组件选项对象。

3.2 图标组件导入

在 script 部分需要先导入所有图标组件:

import IconNewFile from './icons/IconNewFile.vue'
// 其他图标...

然后在 toolbarGroups 中引用:

const toolbarGroups = [
  [
    { command: 'new-file', icon: IconNewFile, tooltip: '创建空白文件' }
    // 其他按钮...
  ]
]

4. 样式和交互

4.1 按钮样式控制

按钮通过三个属性控制样式:

  1. shape="square" - 方形按钮

  2. variant="text" - 文本样式(无背景)

  3. :class 动态绑定 - 控制激活状态

4.2 激活状态管理

通过 activeCommand 响应式变量跟踪当前激活的按钮:

const activeCommand = ref('')

在模板中绑定鼠标事件:

@mouseenter="activeCommand = btn.command"
@mouseleave="activeCommand = ''"

4.3 CSS 类名绑定

:class 的两种用法:

  1. 固定类名:'tool-btn'

  2. 条件类名:{ 'active': activeCommand === btn.command }

对应的样式:

.tool-btn {
  /* 基础样式 */
}
.tool-btn.active {
  /* 激活状态样式 */
}

5. 最佳实践讨论

5.1 key 的使用

  • 外层循环使用 :key="index" 是可以的,因为分组是静态的

  • 内层循环使用 :key="btn.command" 更可靠,因为 command 是唯一标识

5.2 组件设计原则

  1. 单一职责:工具栏只负责渲染和事件传递

  2. 可配置性:通过 toolbarGroups 配置按钮,便于维护

  3. 响应式设计:合理使用响应式变量管理状态

5.3 性能考虑

  1. 图标组件按需导入

  2. 避免不必要的响应式数据

  3. 合理使用事件机制,避免过度渲染

6. 完整代码实现

以下是完整的组件实现,包含详细注释:

<template>
  <div class="top-toolbar">
    <div class="toolbar-group" v-for="(group, index) in toolbarGroups" :key="index">
      <t-tooltip
        v-for="btn in group"
        :key="btn.command"
        :content="btn.tooltip"
        placement="bottom"
        :delay="2000"
      >
        <t-button
          shape="square"
          variant="text"
          :class="['tool-btn', { 'active': activeCommand === btn.command }]"
          @click="$emit('command', btn.command)"
          @mouseenter="activeCommand = btn.command"
          @mouseleave="activeCommand = ''"
        >
          <component :is="btn.icon" />
        </t-button>
      </t-tooltip>
      <div class="group-divider" v-if="index < toolbarGroups.length - 1"></div>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'
import IconNewFile from './icons/IconNewFile.vue'
import IconNewLight from './icons/IconNewLight.vue'
import IconNewLine from './icons/IconNewLine.vue'
import IconNewNode from './icons/IconNewNode.vue'
import IconSelectOnly from './icons/IconSelectOnly.vue'
import IconMove  from './icons/IconMove.vue'
import IconRotate from './icons/IconRotate.vue'
import IconScale  from './icons/IconScale.vue'
import IconCoordsLocal  from './icons/IconCoordsLocal.vue'
import IconFocus from './icons/IconFocus.vue'
import IconCopy from './icons/IconCopy.vue'
import IconDelete from './icons/IconDelete.vue'
import IconUndo from './icons/IconUndo.vue'
import IconRedo from './icons/IconRedo.vue'
import IconPlay from './icons/IconPlay.vue'

export default defineComponent({  
  name: 'TopToolbar',
  emits: ['command'],
  setup() {
    const activeCommand = ref('')
    
    const toolbarGroups = [
      // 第一组:创建空白文件
      [
        { command: 'new-file', icon: IconNewFile, tooltip: '创建空白文件' }
      ],
      // 第二组:新建对象
      [
        { command: 'new-light', icon: IconNewLight, tooltip: '新建灯光' },
        { command: 'new-line', icon: IconNewLine, tooltip: '新建线条' },
        { command: 'new-node', icon: IconNewNode, tooltip: '新建节点' }
      ],
      // 第三组:选择和变换
      [
        { command: 'select', icon: IconSelectOnly, tooltip: '选择物体' },
        { command: 'move', icon: IconMove, tooltip: '移动物体' },
        { command: 'rotate', icon: IconRotate, tooltip: '旋转物体' },
        { command: 'scale', icon: IconScale, tooltip: '缩放物体' },
        { command: 'toggle-coords', icon: IconCoordsLocal, tooltip: '切换坐标系' },
        { command: 'focus-selection', icon: IconFocus, tooltip: '最佳化显示' }
      ],
      // 第四组:对象操作
      [
        { command: 'copy', icon: IconCopy, tooltip: '复制对象' },
        { command: 'delete', icon: IconDelete, tooltip: '删除对象' }
      ],
      // 第五组:撤销重做
      [
        { command: 'undo', icon: IconUndo, tooltip: '撤销' },
        { command: 'redo', icon: IconRedo, tooltip: '重做' }
      ],
      // 第六组:运行预览
      [
        { command: 'preview', icon: IconPlay, tooltip: '运行预览' }
      ]
    ]
    
    return { toolbarGroups, activeCommand }
  }
})
</script>

<style scoped>
.top-toolbar {
  height: 60px;
  display: flex;
  align-items: center;
  padding: 0 16px;
  background-color: var(--td-bg-color-container);
  border-bottom: 1px solid #444;
}

.toolbar-group {
  display: flex;
  align-items: center;
  gap: 8px;
}

.tool-btn {
  width: 36px;
  height: 36px;
  transition: all 0.2s;
}

.tool-btn:hover {
  background-color: var(--td-brand-color-11);
}

.tool-btn.active {
  background-color: var(--td-brand-color-11);
}

.group-divider {
  width: 1px;
  height: 32px;
  background-color: #444;
  margin: 0 12px;
}
</style>

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐