组件开发

Zap Admin Vue3采用组件化开发模式,提供丰富的可复用组件,并支持自定义组件开发。

组件目录结构

src/components/
├── common/           # 通用基础组件
│   ├── Button.vue
│   ├── Icon.vue
│   └── ...
├── form/            # 表单相关组件
│   ├── FormItem.vue
│   ├── FormGenerator.vue
│   └── ...
├── table/           # 表格相关组件
│   ├── Table.vue
│   ├── TableColumn.vue
│   └── ...
├── business/        # 业务组件
│   ├── UserCard.vue
│   ├── DataCard.vue
│   └── ...
├── layout/          # 布局组件
│   ├── Sidebar.vue
│   ├── Navbar.vue
│   └── ...
└── index.js         # 组件全局注册

组件开发规范

命名规范

  • 组件文件名使用PascalCase命名法,如 MyComponent.vue
  • 组件名与文件名保持一致
  • 基础组件以Base前缀开头,如 BaseButton.vue
  • 业务组件以功能模块为前缀,如 UserAvatar.vue

代码结构

<template>
  <!-- 组件模板 -->
</template>

<script setup>
// 组件逻辑
</script>

<style scoped>
/* 组件样式 */
</style>

组件设计原则

  • 单一职责 - 每个组件只做一件事
  • 可复用性 - 通过props控制组件行为
  • 可组合性 - 组件可以相互组合使用
  • 明确接口 - 定义清晰的props和events
  • 样式隔离 - 使用scoped样式避免冲突

组件示例

基础按钮组件

<template>
  <button
    class="base-button"
    :class="[
      type ? 'base-button--' + type : '',
      size ? 'base-button--' + size : '',
      {
        'is-disabled': disabled,
        'is-loading': loading,
        'is-plain': plain,
        'is-round': round,
        'is-circle': circle
      }
    ]"
    :disabled="disabled || loading"
    @click="handleClick"
  >
    <span v-if="loading" class="base-button__loading">
      <svg class="animate-spin" viewBox="0 0 20 20">
        <path d="..." />
      </svg>
    </span>
    <slot />
  </button>
</template>

<script setup>
const props = defineProps({
  type: {
    type: String,
    default: 'default',
    validator: (value) => {
      return ['default', 'primary', 'success', 'warning', 'danger', 'info'].includes(value)
    }
  },
  size: {
    type: String,
    default: 'medium',
    validator: (value) => {
      return ['medium', 'small', 'mini'].includes(value)
    }
  },
  disabled: Boolean,
  loading: Boolean,
  plain: Boolean,
  round: Boolean,
  circle: Boolean
})

const emit = defineEmits(['click'])

const handleClick = (evt) => {
  emit('click', evt)
}
</script>

<style scoped lang="scss">
.base-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  transition: all 0.3s;
  user-select: none;
  border: 1px solid transparent;
  border-radius: 4px;
  padding: 8px 16px;
  font-size: 14px;
  font-weight: 500;
  
  &--primary {
    color: #fff;
    background-color: var(--color-primary);
    border-color: var(--color-primary);
    
    &:hover {
      background-color: lighten(var(--color-primary), 10%);
      border-color: lighten(var(--color-primary), 10%);
    }
  }
  
  &--medium {
    padding: 10px 20px;
    font-size: 14px;
  }
  
  &--small {
    padding: 8px 16px;
    font-size: 12px;
  }
  
  &--mini {
    padding: 6px 12px;
    font-size: 12px;
  }
  
  &__loading {
    margin-right: 6px;
    animation: rotate 2s linear infinite;
  }
  
  &.is-disabled {
    opacity: 0.6;
    cursor: not-allowed;
  }
  
  &.is-loading {
    pointer-events: none;
  }
}

@keyframes rotate {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
</style>

组件全局注册

src/components/index.js 中全局注册常用组件:

import { defineAsyncComponent } from 'vue'

// 自动导入components目录下的所有组件
const components = globalThis._importMeta_.glob('./**/*.vue')

export default {
  install(app) {
    Object.entries(components).forEach(([path, component]) => {
      const name = path
        .split('/')
        .pop()
        .replace(/\.\w+$/, '')
      
      // 注册组件
      app.component(
        name,
        defineAsyncComponent(component)
      )
    })
  }
}

组件测试

使用Vitest进行组件单元测试:

// tests/components/Button.spec.js
import { mount } from '@vue/test-utils'
import BaseButton from '@/components/common/BaseButton.vue'

describe('BaseButton.vue', () => {
  it('renders button with default props', () => {
    const wrapper = mount(BaseButton, {
      slots: {
        default: 'Click me'
      }
    })
    
    expect(wrapper.text()).toContain('Click me')
    expect(wrapper.classes()).toContain('base-button')
    expect(wrapper.classes()).toContain('base-button--default')
    expect(wrapper.classes()).toContain('base-button--medium')
  })
  
  it('emits click event when clicked', async () => {
    const wrapper = mount(BaseButton)
    await wrapper.trigger('click')
    expect(wrapper.emitted()).toHaveProperty('click')
  })
  
  it('shows loading spinner when loading prop is true', () => {
    const wrapper = mount(BaseButton, {
      props: {
        loading: true
      }
    })
    
    expect(wrapper.find('.base-button__loading').exists()).toBe(true)
  })
})

组件文档

使用VuePress或Storybook为组件编写文档:

// docs/components/Button.md
# BaseButton

基础按钮组件,支持多种类型、尺寸和状态。

## 基本用法

```vue
<template>
  <BaseButton @click="handleClick">点击我</BaseButton>
</template>
```

## Props

| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|------|------|------|------|------|
| type | 按钮类型 | string | primary/success/warning/danger/info | default |
| size | 按钮尺寸 | string | medium/small/mini | medium |
| loading | 是否加载中 | boolean | - | false |
| disabled | 是否禁用 | boolean | - | false |

## Events

| 事件名 | 说明 | 回调参数 |
|------|------|------|
| click | 点击事件 | event |