vue-best-practices

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Vue.js Best Practices

Vue.js 最佳实践

Comprehensive best practices guide for Vue.js 3 applications. Contains guidelines across multiple categories to ensure idiomatic, maintainable, and scalable Vue.js code, including Tailwind CSS integration patterns for utility-first styling and PrimeVue component library best practices.
这是一份针对Vue.js 3应用的全面最佳实践指南,包含多类规范以确保代码符合Vue.js惯用写法、易于维护且可扩展,其中涵盖了Tailwind CSS优先工具类样式的集成模式以及PrimeVue组件库的最佳实践。

When to Apply

适用场景

Reference these guidelines when:
  • Writing new Vue components or composables
  • Implementing features with Composition API
  • Reviewing code for Vue.js patterns compliance
  • Refactoring existing Vue.js code
  • Setting up component architecture
  • Working with Nuxt.js applications
  • Styling Vue components with Tailwind CSS utility classes
  • Creating design systems with Tailwind and Vue
  • Using PrimeVue component library
  • Customizing PrimeVue components with PassThrough API
在以下场景中可参考本指南:
  • 编写新的Vue组件或组合式函数(composable)
  • 使用Composition API实现功能
  • 评审代码以确保符合Vue.js模式规范
  • 重构现有Vue.js代码
  • 搭建组件架构
  • 开发Nuxt.js应用
  • 使用Tailwind CSS工具类为Vue组件设置样式
  • 基于Tailwind和Vue创建设计系统
  • 使用PrimeVue组件库
  • 通过PassThrough API自定义PrimeVue组件

Rule Categories

规范分类

CategoryFocusPrefix
Composition APIProper use of Composition API patterns
composition-
Component DesignComponent structure and organization
component-
ReactivityReactive state management patterns
reactive-
Props & EventsComponent communication patterns
props-
Template PatternsTemplate syntax best practices
template-
Code OrganizationProject and code structure
organization-
TypeScriptType-safe Vue.js patterns
typescript-
Error HandlingError boundaries and handling
error-
Tailwind CSSUtility-first styling patterns
tailwind-
PrimeVueComponent library integration patterns
primevue-
分类核心关注点前缀
Composition APIComposition API的正确使用模式
composition-
组件设计组件结构与组织方式
component-
响应式响应式状态管理模式
reactive-
Props与事件组件通信模式
props-
模板模式模板语法最佳实践
template-
代码组织项目与代码结构
organization-
TypeScript类型安全的Vue.js模式
typescript-
错误处理错误边界与处理方式
error-
Tailwind CSS优先工具类样式模式
tailwind-
PrimeVue组件库集成模式
primevue-

Quick Reference

快速参考

1. Composition API Best Practices

1. Composition API最佳实践

  • composition-script-setup
    - Always use
    <script setup>
    for single-file components
  • composition-ref-vs-reactive
    - Use
    ref()
    for primitives,
    reactive()
    for objects
  • composition-computed-derived
    - Use
    computed()
    for all derived state
  • composition-watch-side-effects
    - Use
    watch()
    /
    watchEffect()
    only for side effects
  • composition-composables
    - Extract reusable logic into composables
  • composition-lifecycle-order
    - Place lifecycle hooks after reactive state declarations
  • composition-avoid-this
    - Never use
    this
    in Composition API
  • composition-script-setup
    - 单文件组件始终使用
    <script setup>
  • composition-ref-vs-reactive
    - 基础类型使用
    ref()
    ,对象使用
    reactive()
  • composition-computed-derived
    - 所有派生状态均使用
    computed()
  • composition-watch-side-effects
    - 仅在处理副作用时使用
    watch()
    /
    watchEffect()
  • composition-composables
    - 将可复用逻辑提取到组合式函数中
  • composition-lifecycle-order
    - 生命周期钩子放在响应式状态声明之后
  • composition-avoid-this
    - 在Composition API中绝不使用
    this

2. Component Design

2. 组件设计

  • component-single-responsibility
    - One component, one purpose
  • component-naming-convention
    - Use PascalCase for components, kebab-case in templates
  • component-small-focused
    - Keep components under 200 lines
  • component-presentational-container
    - Separate logic from presentation when beneficial
  • component-slots-flexibility
    - Use slots for flexible component composition
  • component-expose-minimal
    - Only expose what's necessary via
    defineExpose()
  • component-single-responsibility
    - 一个组件只负责一项功能
  • component-naming-convention
    - 组件使用PascalCase命名,模板中使用kebab-case
  • component-small-focused
    - 组件代码行数控制在200行以内
  • component-presentational-container
    - 必要时分离逻辑层与展示层
  • component-slots-flexibility
    - 使用插槽实现灵活的组件组合
  • component-expose-minimal
    - 仅通过
    defineExpose()
    暴露必要内容

3. Reactivity Patterns

3. 响应式模式

  • reactive-const-refs
    - Always declare refs with
    const
  • reactive-unwrap-template
    - Let Vue unwrap refs in templates (no
    .value
    )
  • reactive-shallow-large-data
    - Use
    shallowRef()
    /
    shallowReactive()
    for large non-reactive data
  • reactive-readonly-props
    - Use
    readonly()
    to prevent mutations
  • reactive-toRefs-destructure
    - Use
    toRefs()
    when destructuring reactive objects
  • reactive-avoid-mutation
    - Prefer immutable updates for complex state
  • reactive-const-refs
    - 始终使用
    const
    声明ref
  • reactive-unwrap-template
    - 让Vue在模板中自动解包ref(无需
    .value
  • reactive-shallow-large-data
    - 大型非响应式数据使用
    shallowRef()
    /
    shallowReactive()
  • reactive-readonly-props
    - 使用
    readonly()
    防止props被修改
  • reactive-toRefs-destructure
    - 解构响应式对象时使用
    toRefs()
  • reactive-avoid-mutation
    - 复杂状态优先使用不可变更新

4. Props & Events

4. Props与事件

  • props-define-types
    - Always define prop types with
    defineProps<T>()
  • props-required-explicit
    - Be explicit about required vs optional props
  • props-default-values
    - Provide sensible defaults with
    withDefaults()
  • props-immutable
    - Never mutate props directly
  • props-validation
    - Use validator functions for complex prop validation
  • events-define-emits
    - Always define emits with
    defineEmits<T>()
  • events-naming
    - Use kebab-case for event names in templates
  • events-payload-objects
    - Pass objects for events with multiple values
  • props-define-types
    - 始终使用
    defineProps<T>()
    定义props类型
  • props-required-explicit
    - 明确标记必填与可选props
  • props-default-values
    - 使用
    withDefaults()
    提供合理的默认值
  • props-immutable
    - 绝不直接修改props
  • props-validation
    - 复杂props使用验证函数
  • events-define-emits
    - 始终使用
    defineEmits<T>()
    定义事件
  • events-naming
    - 模板中事件名称使用kebab-case
  • events-payload-objects
    - 多值事件传递对象类型的载荷

5. Template Patterns

5. 模板模式

  • template-v-if-v-show
    - Use
    v-if
    for conditional rendering,
    v-show
    for toggling
  • template-v-for-key
    - Always use unique, stable
    :key
    with
    v-for
  • template-v-if-v-for
    - Never use
    v-if
    and
    v-for
    on the same element
  • template-computed-expressions
    - Move complex expressions to computed properties
  • template-event-modifiers
    - Use event modifiers (
    .prevent
    ,
    .stop
    ) appropriately
  • template-v-bind-shorthand
    - Use shorthand syntax (
    :
    for
    v-bind
    ,
    @
    for
    v-on
    )
  • template-v-model-modifiers
    - Use v-model modifiers (
    .trim
    ,
    .number
    ,
    .lazy
    )
  • template-v-if-v-show
    - 条件渲染使用
    v-if
    ,切换显示使用
    v-show
  • template-v-for-key
    -
    v-for
    始终搭配唯一且稳定的
    :key
  • template-v-if-v-for
    - 绝不在同一元素上同时使用
    v-if
    v-for
  • template-computed-expressions
    - 复杂表达式移至计算属性中
  • template-event-modifiers
    - 合理使用事件修饰符(
    .prevent
    ,
    .stop
    等)
  • template-v-bind-shorthand
    - 使用简写语法(
    :
    代替
    v-bind
    @
    代替
    v-on
  • template-v-model-modifiers
    - 使用v-model修饰符(
    .trim
    ,
    .number
    ,
    .lazy

6. Code Organization

6. 代码组织

  • organization-feature-folders
    - Organize by feature, not by type
  • organization-composables-folder
    - Keep composables in dedicated
    composables/
    folder
  • organization-barrel-exports
    - Use index files for clean imports
  • organization-consistent-naming
    - Follow consistent naming conventions
  • organization-colocation
    - Colocate related files (component, tests, styles)
  • organization-feature-folders
    - 按功能而非类型组织文件
  • organization-composables-folder
    - 组合式函数放在专用的
    composables/
    目录下
  • organization-barrel-exports
    - 使用索引文件实现清晰的导入
  • organization-consistent-naming
    - 遵循统一的命名规范
  • organization-colocation
    - 相关文件(组件、测试、样式)放在一起

7. TypeScript Integration

7. TypeScript集成

  • typescript-generic-components
    - Use generics for reusable typed components
  • typescript-prop-types
    - Use TypeScript interfaces for prop definitions
  • typescript-emit-types
    - Type emit payloads explicitly
  • typescript-ref-typing
    - Specify types for refs when not inferred
  • typescript-template-refs
    - Type template refs with
    ref<InstanceType<typeof Component> | null>(null)
  • typescript-generic-components
    - 可复用的类型化组件使用泛型
  • typescript-prop-types
    - 使用TypeScript接口定义props
  • typescript-emit-types
    - 显式定义事件载荷的类型
  • typescript-ref-typing
    - 无法自动推断时为ref指定类型
  • typescript-template-refs
    - 模板ref使用
    ref<InstanceType<typeof Component> | null>(null)
    进行类型声明

8. Error Handling

8. 错误处理

  • error-boundaries
    - Use
    onErrorCaptured()
    for component error boundaries
  • error-async-handling
    - Handle errors in async operations explicitly
  • error-provide-fallbacks
    - Provide fallback UI for error states
  • error-logging
    - Log errors appropriately for debugging
  • error-boundaries
    - 使用
    onErrorCaptured()
    实现组件错误边界
  • error-async-handling
    - 显式处理异步操作中的错误
  • error-provide-fallbacks
    - 为错误状态提供备用UI
  • error-logging
    - 合理记录错误以便调试

9. Tailwind CSS

9. Tailwind CSS

  • tailwind-utility-first
    - Apply utility classes directly in templates, avoid custom CSS
  • tailwind-class-order
    - Use consistent class ordering (layout → spacing → typography → visual)
  • tailwind-responsive-mobile-first
    - Use mobile-first responsive design (
    sm:
    ,
    md:
    ,
    lg:
    )
  • tailwind-component-extraction
    - Extract repeated utility patterns into Vue components
  • tailwind-dynamic-classes
    - Use computed properties or helper functions for dynamic classes
  • tailwind-complete-class-strings
    - Always use complete class strings, never concatenate
  • tailwind-state-variants
    - Use state variants (
    hover:
    ,
    focus:
    ,
    active:
    ) for interactions
  • tailwind-dark-mode
    - Use
    dark:
    prefix for dark mode support
  • tailwind-design-tokens
    - Configure design tokens in Tailwind config for consistency
  • tailwind-avoid-apply-overuse
    - Limit
    @apply
    usage; prefer Vue components for abstraction
  • tailwind-utility-first
    - 直接在模板中使用工具类,避免自定义CSS
  • tailwind-class-order
    - 遵循统一的类排序规则(布局 → 间距 → 排版 → 视觉)
  • tailwind-responsive-mobile-first
    - 使用移动端优先的响应式设计(
    sm:
    ,
    md:
    ,
    lg:
  • tailwind-component-extraction
    - 将重复的工具类模式提取为Vue组件
  • tailwind-dynamic-classes
    - 使用计算属性或辅助函数处理动态类
  • tailwind-complete-class-strings
    - 始终使用完整的类字符串,绝不拼接
  • tailwind-state-variants
    - 使用状态变体(
    hover:
    ,
    focus:
    ,
    active:
    )处理交互
  • tailwind-dark-mode
    - 使用
    dark:
    前缀实现暗色模式支持
  • tailwind-design-tokens
    - 在Tailwind配置中定义设计令牌以确保一致性
  • tailwind-avoid-apply-overuse
    - 限制
    @apply
    的使用;优先使用Vue组件进行抽象

10. PrimeVue

10. PrimeVue

  • primevue-design-tokens
    - Use design tokens over CSS overrides for theming
  • primevue-passthrough-api
    - Use PassThrough (pt) API for component customization
  • primevue-wrapper-components
    - Wrap PrimeVue components for consistent styling across apps
  • primevue-unstyled-mode
    - Use unstyled mode with Tailwind for full styling control
  • primevue-global-pt-config
    - Define shared PassThrough properties at app level
  • primevue-merge-strategies
    - Choose appropriate merge strategies for PT customization
  • primevue-use-passthrough-utility
    - Use
    usePassThrough
    for extending presets
  • primevue-typed-components
    - Leverage PrimeVue's TypeScript support for type safety
  • primevue-accessibility
    - Maintain WCAG compliance with proper aria attributes
  • primevue-lazy-loading
    - Use async components for large PrimeVue imports
  • primevue-design-tokens
    - 主题定制使用设计令牌而非CSS覆盖
  • primevue-passthrough-api
    - 使用PassThrough(pt)API自定义组件
  • primevue-wrapper-components
    - 对PrimeVue组件进行封装以确保跨应用样式一致
  • primevue-unstyled-mode
    - 使用无样式模式搭配Tailwind实现完全的样式控制
  • primevue-global-pt-config
    - 在应用层面定义共享的PassThrough属性
  • primevue-merge-strategies
    - 为PT自定义选择合适的合并策略
  • primevue-use-passthrough-utility
    - 使用
    usePassThrough
    扩展预设
  • primevue-typed-components
    - 利用PrimeVue的TypeScript支持实现类型安全
  • primevue-accessibility
    - 通过正确的aria属性保持WCAG合规性
  • primevue-lazy-loading
    - 大型PrimeVue导入使用异步组件

Key Principles

核心原则

Composition API Best Practices

Composition API最佳实践

The Composition API is the recommended approach for Vue.js 3. Follow these patterns:
  • Always use
    <script setup>
    : More concise, better TypeScript inference, and improved performance
  • Organize code by logical concern: Group related state, computed properties, and functions together
  • Extract reusable logic to composables: Follow the
    use
    prefix convention (e.g.,
    useAuth
    ,
    useFetch
    )
  • Keep setup code readable: Order: props/emits, reactive state, computed, watchers, methods, lifecycle hooks
Composition API是Vue.js 3推荐的开发方式,遵循以下模式:
  • 始终使用
    <script setup>
    :更简洁、TypeScript推断更友好、性能更优
  • 按逻辑关注点组织代码:将相关的状态、计算属性和函数分组
  • 将可复用逻辑提取到组合式函数:遵循
    use
    前缀命名规范(如
    useAuth
    ,
    useFetch
  • 保持setup代码可读性:顺序为:props/emits → 响应式状态 → 计算属性 → 监听器 → 方法 → 生命周期钩子

Component Design Principles

组件设计原则

Well-designed components are the foundation of maintainable Vue applications:
  • Single Responsibility: Each component should do one thing well
  • Props Down, Events Up: Follow unidirectional data flow
  • Prefer Composition over Inheritance: Use composables and slots for code reuse
  • Keep Components Small: If a component exceeds 200 lines, consider splitting it
设计良好的组件是可维护Vue应用的基础:
  • 单一职责:每个组件只做好一件事
  • Props向下,事件向上:遵循单向数据流
  • 优先组合而非继承:使用组合式函数和插槽实现代码复用
  • 保持组件小巧:组件代码超过200行时考虑拆分

Reactivity Guidelines

响应式指南

Understanding Vue's reactivity system is crucial:
  • ref vs reactive: Use
    ref()
    for primitives and values you'll reassign; use
    reactive()
    for objects you'll mutate
  • Computed for derived state: Never store derived state in refs; use
    computed()
    instead
  • Watch for side effects: Only use
    watch()
    for side effects like API calls or localStorage
  • Be mindful of reactivity loss: Don't destructure reactive objects without
    toRefs()
理解Vue的响应式系统至关重要:
  • ref vs reactive:基础类型和需要重新赋值的值使用
    ref()
    ;需要修改的对象使用
    reactive()
  • 派生状态使用computed:绝不将派生状态存储在ref中;始终使用
    computed()
  • 副作用使用watch:仅在处理副作用(如API调用或localStorage操作)时使用
    watch()
  • 注意响应式丢失:解构响应式对象时必须使用
    toRefs()

Props & Events Patterns

Props与事件模式

Proper component communication ensures maintainable code:
  • Type your props: Use TypeScript interfaces with
    defineProps<T>()
  • Validate complex props: Use validator functions for business logic validation
  • Emit typed events: Use
    defineEmits<T>()
    for type-safe event handling
  • Use v-model for two-way binding: Implement
    modelValue
    prop and
    update:modelValue
    emit
合理的组件通信确保代码可维护:
  • 为props添加类型:使用TypeScript接口搭配
    defineProps<T>()
  • 复杂props添加验证:使用验证函数处理业务逻辑相关的props验证
  • 事件添加类型:使用
    defineEmits<T>()
    实现类型安全的事件处理
  • 双向绑定使用v-model:实现
    modelValue
    props和
    update:modelValue
    事件

Common Patterns

常见模式

Script Setup Structure

Script Setup结构

Recommended structure for
<script setup>
:
vue
<script setup lang="ts">
// 1. Imports
import { ref, computed, watch, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import type { User } from '@/types'

// 2. Props and Emits
const props = defineProps<{
  userId: string
  initialData?: User
}>()

const emit = defineEmits<{
  submit: [user: User]
  cancel: []
}>()

// 3. Composables
const router = useRouter()
const { user, loading, error } = useUser(props.userId)

// 4. Reactive State
const formData = ref({ name: '', email: '' })
const isEditing = ref(false)

// 5. Computed Properties
const isValid = computed(() => {
  return formData.value.name.length > 0 && formData.value.email.includes('@')
})

// 6. Watchers (for side effects only)
watch(() => props.userId, (newId) => {
  fetchUserData(newId)
})

// 7. Methods
function handleSubmit() {
  if (isValid.value) {
    emit('submit', formData.value)
  }
}

// 8. Lifecycle Hooks
onMounted(() => {
  if (props.initialData) {
    formData.value = { ...props.initialData }
  }
})
</script>
推荐的
<script setup>
结构:
vue
<script setup lang="ts">
// 1. Imports
import { ref, computed, watch, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import type { User } from '@/types'

// 2. Props and Emits
const props = defineProps<{
  userId: string
  initialData?: User
}>()

const emit = defineEmits<{
  submit: [user: User]
  cancel: []
}>()

// 3. Composables
const router = useRouter()
const { user, loading, error } = useUser(props.userId)

// 4. Reactive State
const formData = ref({ name: '', email: '' })
const isEditing = ref(false)

// 5. Computed Properties
const isValid = computed(() => {
  return formData.value.name.length > 0 && formData.value.email.includes('@')
})

// 6. Watchers (for side effects only)
watch(() => props.userId, (newId) => {
  fetchUserData(newId)
})

// 7. Methods
function handleSubmit() {
  if (isValid.value) {
    emit('submit', formData.value)
  }
}

// 8. Lifecycle Hooks
onMounted(() => {
  if (props.initialData) {
    formData.value = { ...props.initialData }
  }
})
</script>

Composable Pattern

组合式函数模式

Correct: Well-structured composable
typescript
// composables/useUser.ts
import { ref, computed, watch } from 'vue'
import type { Ref } from 'vue'
import type { User } from '@/types'

export function useUser(userId: Ref<string> | string) {
  // State
  const user = ref<User | null>(null)
  const loading = ref(false)
  const error = ref<Error | null>(null)

  // Computed
  const fullName = computed(() => {
    if (!user.value) return ''
    return `${user.value.firstName} ${user.value.lastName}`
  })

  // Methods
  async function fetchUser(id: string) {
    loading.value = true
    error.value = null
    try {
      const response = await api.getUser(id)
      user.value = response.data
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }

  // Auto-fetch when userId changes (if reactive)
  if (isRef(userId)) {
    watch(userId, (newId) => fetchUser(newId), { immediate: true })
  } else {
    fetchUser(userId)
  }

  // Return
  return {
    user: readonly(user),
    fullName,
    loading: readonly(loading),
    error: readonly(error),
    refresh: () => fetchUser(unref(userId))
  }
}
正确示例:结构清晰的组合式函数
typescript
// composables/useUser.ts
import { ref, computed, watch } from 'vue'
import type { Ref } from 'vue'
import type { User } from '@/types'

export function useUser(userId: Ref<string> | string) {
  // State
  const user = ref<User | null>(null)
  const loading = ref(false)
  const error = ref<Error | null>(null)

  // Computed
  const fullName = computed(() => {
    if (!user.value) return ''
    return `${user.value.firstName} ${user.value.lastName}`
  })

  // Methods
  async function fetchUser(id: string) {
    loading.value = true
    error.value = null
    try {
      const response = await api.getUser(id)
      user.value = response.data
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }

  // Auto-fetch when userId changes (if reactive)
  if (isRef(userId)) {
    watch(userId, (newId) => fetchUser(newId), { immediate: true })
  } else {
    fetchUser(userId)
  }

  // Return
  return {
    user: readonly(user),
    fullName,
    loading: readonly(loading),
    error: readonly(error),
    refresh: () => fetchUser(unref(userId))
  }
}

Props with Defaults

带默认值的Props

Correct: Typed props with defaults
vue
<script setup lang="ts">
interface Props {
  title: string
  size?: 'sm' | 'md' | 'lg'
  disabled?: boolean
  items?: string[]
}

const props = withDefaults(defineProps<Props>(), {
  size: 'md',
  disabled: false,
  items: () => []  // Use factory function for arrays/objects
})
</script>
正确示例:带类型的Props与默认值
vue
<script setup lang="ts">
interface Props {
  title: string
  size?: 'sm' | 'md' | 'lg'
  disabled?: boolean
  items?: string[]
}

const props = withDefaults(defineProps<Props>(), {
  size: 'md',
  disabled: false,
  items: () => []  // 数组/对象使用工厂函数
})
</script>

Event Handling

事件处理

Correct: Typed emits with payloads
vue
<script setup lang="ts">
interface FormData {
  name: string
  email: string
}

const emit = defineEmits<{
  submit: [data: FormData]
  cancel: []
  'update:modelValue': [value: string]
}>()

function handleSubmit(data: FormData) {
  emit('submit', data)
}
</script>
正确示例:带类型的事件与载荷
vue
<script setup lang="ts">
interface FormData {
  name: string
  email: string
}

const emit = defineEmits<{
  submit: [data: FormData]
  cancel: []
  'update:modelValue': [value: string]
}>()

function handleSubmit(data: FormData) {
  emit('submit', data)
}
</script>

v-model Implementation

v-model实现

Correct: Custom v-model with defineModel (Vue 3.4+)
vue
<script setup lang="ts">
const model = defineModel<string>({ required: true })

// Or with default
const modelWithDefault = defineModel<string>({ default: '' })
</script>

<template>
  <input :value="model" @input="model = $event.target.value" />
</template>
Correct: Custom v-model (Vue 3.3 and earlier)
vue
<script setup lang="ts">
const props = defineProps<{
  modelValue: string
}>()

const emit = defineEmits<{
  'update:modelValue': [value: string]
}>()

const value = computed({
  get: () => props.modelValue,
  set: (val) => emit('update:modelValue', val)
})
</script>

<template>
  <input v-model="value" />
</template>
正确示例:使用defineModel的自定义v-model(Vue 3.4+)
vue
<script setup lang="ts">
const model = defineModel<string>({ required: true })

// 或带默认值
const modelWithDefault = defineModel<string>({ default: '' })
</script>

<template>
  <input :value="model" @input="model = $event.target.value" />
</template>
正确示例:自定义v-model(Vue 3.3及更早版本)
vue
<script setup lang="ts">
const props = defineProps<{
  modelValue: string
}>()

const emit = defineEmits<{
  'update:modelValue': [value: string]
}>()

const value = computed({
  get: () => props.modelValue,
  set: (val) => emit('update:modelValue', val)
})
</script>

<template>
  <input v-model="value" />
</template>

Template Ref Typing

模板Ref类型声明

Correct: Typed template refs
vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import MyComponent from './MyComponent.vue'

// DOM element ref
const inputRef = ref<HTMLInputElement | null>(null)

// Component ref
const componentRef = ref<InstanceType<typeof MyComponent> | null>(null)

onMounted(() => {
  inputRef.value?.focus()
  componentRef.value?.someExposedMethod()
})
</script>

<template>
  <input ref="inputRef" />
  <MyComponent ref="componentRef" />
</template>
正确示例:带类型的模板Ref
vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import MyComponent from './MyComponent.vue'

// DOM元素ref
const inputRef = ref<HTMLInputElement | null>(null)

// 组件ref
const componentRef = ref<InstanceType<typeof MyComponent> | null>(null)

onMounted(() => {
  inputRef.value?.focus()
  componentRef.value?.someExposedMethod()
})
</script>

<template>
  <input ref="inputRef" />
  <MyComponent ref="componentRef" />
</template>

Provide/Inject with Types

带类型的Provide/Inject

Correct: Type-safe provide/inject
typescript
// types/injection-keys.ts
import type { InjectionKey, Ref } from 'vue'
import type { User } from './user'

export const UserKey: InjectionKey<Ref<User>> = Symbol('user')

// Parent component
import { provide, ref } from 'vue'
import { UserKey } from '@/types/injection-keys'

const user = ref<User>({ id: '1', name: 'John' })
provide(UserKey, user)

// Child component
import { inject } from 'vue'
import { UserKey } from '@/types/injection-keys'

const user = inject(UserKey)
if (!user) {
  throw new Error('User not provided')
}
正确示例:类型安全的Provide/Inject
typescript
// types/injection-keys.ts
import type { InjectionKey, Ref } from 'vue'
import type { User } from './user'

export const UserKey: InjectionKey<Ref<User>> = Symbol('user')

// 父组件
import { provide, ref } from 'vue'
import { UserKey } from '@/types/injection-keys'

const user = ref<User>({ id: '1', name: 'John' })
provide(UserKey, user)

// 子组件
import { inject } from 'vue'
import { UserKey } from '@/types/injection-keys'

const user = inject(UserKey)
if (!user) {
  throw new Error('User not provided')
}

Error Boundary Component

错误边界组件

Correct: Error boundary with onErrorCaptured
vue
<script setup lang="ts">
import { ref, onErrorCaptured } from 'vue'

const error = ref<Error | null>(null)

onErrorCaptured((err) => {
  error.value = err
  // Return false to stop error propagation
  return false
})

function reset() {
  error.value = null
}
</script>

<template>
  <div v-if="error" class="error-boundary">
    <p>Something went wrong: {{ error.message }}</p>
    <button @click="reset">Try again</button>
  </div>
  <slot v-else />
</template>
正确示例:使用onErrorCaptured的错误边界
vue
<script setup lang="ts">
import { ref, onErrorCaptured } from 'vue'

const error = ref<Error | null>(null)

onErrorCaptured((err) => {
  error.value = err
  // 返回false阻止错误传播
  return false
})

function reset() {
  error.value = null
}
</script>

<template>
  <div v-if="error" class="error-boundary">
    <p>出现错误:{{ error.message }}</p>
    <button @click="reset">重试</button>
  </div>
  <slot v-else />
</template>

Async Component Loading

异步组件加载

Correct: Async components with loading/error states
typescript
import { defineAsyncComponent } from 'vue'

const AsyncDashboard = defineAsyncComponent({
  loader: () => import('./Dashboard.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,  // Show loading after 200ms
  timeout: 10000  // Timeout after 10s
})
正确示例:带加载/错误状态的异步组件
typescript
import { defineAsyncComponent } from 'vue'

const AsyncDashboard = defineAsyncComponent({
  loader: () => import('./Dashboard.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,  // 200ms后显示加载状态
  timeout: 10000  // 10秒后超时
})

Tailwind CSS Best Practices

Tailwind CSS最佳实践

Vue's component-based architecture pairs naturally with Tailwind's utility-first approach. Follow these patterns for maintainable, consistent styling.
Vue的组件化架构与Tailwind的优先工具类方法天然契合。遵循以下模式以确保样式可维护、一致:

Utility-First Approach

优先工具类方法

Apply Tailwind utility classes directly in Vue templates for rapid, consistent styling:
Correct: Utility classes in template
vue
<template>
  <div class="mx-auto max-w-md rounded-xl bg-white p-6 shadow-lg">
    <h2 class="text-xl font-semibold text-gray-900">{{ title }}</h2>
    <p class="mt-2 text-gray-600">{{ description }}</p>
    <button class="mt-4 rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700">
      {{ buttonText }}
    </button>
  </div>
</template>
直接在Vue模板中应用Tailwind工具类以实现快速、一致的样式:
正确示例:模板中使用工具类
vue
<template>
  <div class="mx-auto max-w-md rounded-xl bg-white p-6 shadow-lg">
    <h2 class="text-xl font-semibold text-gray-900">{{ title }}</h2>
    <p class="mt-2 text-gray-600">{{ description }}</p>
    <button class="mt-4 rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700">
      {{ buttonText }}
    </button>
  </div>
</template>

Class Ordering Convention

类排序规范

Maintain consistent class ordering for readability. Recommended order:
  1. Layout -
    flex
    ,
    grid
    ,
    block
    ,
    hidden
  2. Positioning -
    relative
    ,
    absolute
    ,
    fixed
  3. Box Model -
    w-
    ,
    h-
    ,
    m-
    ,
    p-
  4. Typography -
    text-
    ,
    font-
    ,
    leading-
  5. Visual -
    bg-
    ,
    border-
    ,
    rounded-
    ,
    shadow-
  6. Interactive -
    hover:
    ,
    focus:
    ,
    active:
Use the official Prettier plugin (
prettier-plugin-tailwindcss
) to automatically sort classes.
保持统一的类排序以提升可读性。推荐顺序:
  1. 布局 -
    flex
    ,
    grid
    ,
    block
    ,
    hidden
  2. 定位 -
    relative
    ,
    absolute
    ,
    fixed
  3. 盒模型 -
    w-
    ,
    h-
    ,
    m-
    ,
    p-
  4. 排版 -
    text-
    ,
    font-
    ,
    leading-
  5. 视觉 -
    bg-
    ,
    border-
    ,
    rounded-
    ,
    shadow-
  6. 交互 -
    hover:
    ,
    focus:
    ,
    active:
使用官方Prettier插件(
prettier-plugin-tailwindcss
)自动排序类。

Responsive Design (Mobile-First)

响应式设计(移动端优先)

Use Tailwind's responsive prefixes for mobile-first responsive design:
Correct: Mobile-first responsive layout
vue
<template>
  <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
    <article
      v-for="item in items"
      :key="item.id"
      class="p-4 text-sm sm:p-6 sm:text-base lg:text-lg"
    >
      <h3 class="font-medium">{{ item.title }}</h3>
    </article>
  </div>
</template>
Breakpoint Reference:
  • sm:
    - 640px and up
  • md:
    - 768px and up
  • lg:
    - 1024px and up
  • xl:
    - 1280px and up
  • 2xl:
    - 1536px and up
使用Tailwind的响应式前缀实现移动端优先的响应式设计:
正确示例:移动端优先的响应式布局
vue
<template>
  <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
    <article
      v-for="item in items"
      :key="item.id"
      class="p-4 text-sm sm:p-6 sm:text-base lg:text-lg"
    >
      <h3 class="font-medium">{{ item.title }}</h3>
    </article>
  </div>
</template>
断点参考:
  • sm:
    - 640px及以上
  • md:
    - 768px及以上
  • lg:
    - 1024px及以上
  • xl:
    - 1280px及以上
  • 2xl:
    - 1536px及以上

State Variants

状态变体

Use state variants for interactive elements:
Correct: State variants for buttons
vue
<template>
  <button
    class="rounded-lg bg-indigo-600 px-4 py-2 font-medium text-white
           transition-colors duration-150
           hover:bg-indigo-700
           focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2
           active:bg-indigo-800
           disabled:cursor-not-allowed disabled:opacity-50"
    :disabled="isLoading"
  >
    {{ isLoading ? 'Loading...' : 'Submit' }}
  </button>
</template>
使用状态变体处理交互元素:
正确示例:按钮的状态变体
vue
<template>
  <button
    class="rounded-lg bg-indigo-600 px-4 py-2 font-medium text-white
           transition-colors duration-150
           hover:bg-indigo-700
           focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2
           active:bg-indigo-800
           disabled:cursor-not-allowed disabled:opacity-50"
    :disabled="isLoading"
  >
    {{ isLoading ? '加载中...' : '提交' }}
  </button>
</template>

Dark Mode Support

暗色模式支持

Use the
dark:
prefix for dark mode styles:
Correct: Dark mode support
vue
<template>
  <div class="bg-white dark:bg-gray-900">
    <h1 class="text-gray-900 dark:text-white">{{ title }}</h1>
    <p class="text-gray-600 dark:text-gray-400">{{ content }}</p>
    <div class="border-gray-200 dark:border-gray-700 rounded-lg border p-4">
      <slot />
    </div>
  </div>
</template>
使用
dark:
前缀实现暗色模式样式:
正确示例:暗色模式支持
vue
<template>
  <div class="bg-white dark:bg-gray-900">
    <h1 class="text-gray-900 dark:text-white">{{ title }}</h1>
    <p class="text-gray-600 dark:text-gray-400">{{ content }}</p>
    <div class="border-gray-200 dark:border-gray-700 rounded-lg border p-4">
      <slot />
    </div>
  </div>
</template>

Dynamic Classes with Computed Properties

使用计算属性处理动态类

Use computed properties for conditional class binding:
Correct: Computed classes for variants
vue
<script setup lang="ts">
import { computed } from 'vue'

type ButtonVariant = 'primary' | 'secondary' | 'danger'
type ButtonSize = 'sm' | 'md' | 'lg'

const props = withDefaults(defineProps<{
  variant?: ButtonVariant
  size?: ButtonSize
}>(), {
  variant: 'primary',
  size: 'md'
})

const variantClasses = computed(() => {
  const variants: Record<ButtonVariant, string> = {
    primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
    secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500',
    danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500'
  }
  return variants[props.variant]
})

const sizeClasses = computed(() => {
  const sizes: Record<ButtonSize, string> = {
    sm: 'px-3 py-1.5 text-sm',
    md: 'px-4 py-2 text-base',
    lg: 'px-6 py-3 text-lg'
  }
  return sizes[props.size]
})

const buttonClasses = computed(() => [
  'inline-flex items-center justify-center rounded-md font-medium',
  'transition-colors duration-150',
  'focus:outline-none focus:ring-2 focus:ring-offset-2',
  variantClasses.value,
  sizeClasses.value
])
</script>

<template>
  <button :class="buttonClasses">
    <slot />
  </button>
</template>
使用计算属性处理条件类绑定:
正确示例:使用计算属性处理变体类
vue
<script setup lang="ts">
import { computed } from 'vue'

type ButtonVariant = 'primary' | 'secondary' | 'danger'
type ButtonSize = 'sm' | 'md' | 'lg'

const props = withDefaults(defineProps<{
  variant?: ButtonVariant
  size?: ButtonSize
}>(), {
  variant: 'primary',
  size: 'md'
})

const variantClasses = computed(() => {
  const variants: Record<ButtonVariant, string> = {
    primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
    secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500',
    danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500'
  }
  return variants[props.variant]
})

const sizeClasses = computed(() => {
  const sizes: Record<ButtonSize, string> = {
    sm: 'px-3 py-1.5 text-sm',
    md: 'px-4 py-2 text-base',
    lg: 'px-6 py-3 text-lg'
  }
  return sizes[props.size]
})

const buttonClasses = computed(() => [
  'inline-flex items-center justify-center rounded-md font-medium',
  'transition-colors duration-150',
  'focus:outline-none focus:ring-2 focus:ring-offset-2',
  variantClasses.value,
  sizeClasses.value
])
</script>

<template>
  <button :class="buttonClasses">
    <slot />
  </button>
</template>

Class Variance Authority (CVA) Pattern

类变体权限(CVA)模式

For complex component variants, use the CVA pattern with a helper library:
Correct: CVA-style variant management
vue
<script setup lang="ts">
import { computed } from 'vue'
import { cva, type VariantProps } from 'class-variance-authority'

const button = cva(
  'inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2',
  {
    variants: {
      intent: {
        primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
        secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500',
        danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500'
      },
      size: {
        sm: 'px-3 py-1.5 text-sm',
        md: 'px-4 py-2 text-base',
        lg: 'px-6 py-3 text-lg'
      }
    },
    defaultVariants: {
      intent: 'primary',
      size: 'md'
    }
  }
)

type ButtonProps = VariantProps<typeof button>

const props = defineProps<{
  intent?: ButtonProps['intent']
  size?: ButtonProps['size']
}>()

const classes = computed(() => button({ intent: props.intent, size: props.size }))
</script>

<template>
  <button :class="classes">
    <slot />
  </button>
</template>
对于复杂的组件变体,使用辅助库实现CVA模式:
正确示例:CVA风格的变体管理
vue
<script setup lang="ts">
import { computed } from 'vue'
import { cva, type VariantProps } from 'class-variance-authority'

const button = cva(
  'inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2',
  {
    variants: {
      intent: {
        primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
        secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500',
        danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500'
      },
      size: {
        sm: 'px-3 py-1.5 text-sm',
        md: 'px-4 py-2 text-base',
        lg: 'px-6 py-3 text-lg'
      }
    },
    defaultVariants: {
      intent: 'primary',
      size: 'md'
    }
  }
)

type ButtonProps = VariantProps<typeof button>

const props = defineProps<{
  intent?: ButtonProps['intent']
  size?: ButtonProps['size']
}>()

const classes = computed(() => button({ intent: props.intent, size: props.size }))
</script>

<template>
  <button :class="classes">
    <slot />
  </button>
</template>

Component Extraction for Reusable Patterns

提取可复用模式为组件

Extract repeated utility patterns into Vue components:
Correct: Reusable card component
vue
<!-- components/BaseCard.vue -->
<script setup lang="ts">
withDefaults(defineProps<{
  padding?: 'none' | 'sm' | 'md' | 'lg'
  shadow?: 'none' | 'sm' | 'md' | 'lg'
}>(), {
  padding: 'md',
  shadow: 'md'
})
</script>

<template>
  <div
    class="rounded-xl bg-white dark:bg-gray-800"
    :class="[
      {
        'p-0': padding === 'none',
        'p-4': padding === 'sm',
        'p-6': padding === 'md',
        'p-8': padding === 'lg'
      },
      {
        'shadow-none': shadow === 'none',
        'shadow-sm': shadow === 'sm',
        'shadow-md': shadow === 'md',
        'shadow-lg': shadow === 'lg'
      }
    ]"
  >
    <slot />
  </div>
</template>
将重复的工具类模式提取为Vue组件:
正确示例:可复用卡片组件
vue
<!-- components/BaseCard.vue -->
<script setup lang="ts">
withDefaults(defineProps<{
  padding?: 'none' | 'sm' | 'md' | 'lg'
  shadow?: 'none' | 'sm' | 'md' | 'lg'
}>(), {
  padding: 'md',
  shadow: 'md'
})
</script>

<template>
  <div
    class="rounded-xl bg-white dark:bg-gray-800"
    :class="[
      {
        'p-0': padding === 'none',
        'p-4': padding === 'sm',
        'p-6': padding === 'md',
        'p-8': padding === 'lg'
      },
      {
        'shadow-none': shadow === 'none',
        'shadow-sm': shadow === 'sm',
        'shadow-md': shadow === 'md',
        'shadow-lg': shadow === 'lg'
      }
    ]"
  >
    <slot />
  </div>
</template>

Tailwind Configuration with Design Tokens

使用设计令牌配置Tailwind

Define design tokens in your Tailwind config for consistency:
Correct: tailwind.config.js with design tokens
javascript
/** @type {import('tailwindcss').Config} */
export default {
  content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
  theme: {
    extend: {
      colors: {
        // Semantic color tokens
        primary: {
          50: '#eff6ff',
          100: '#dbeafe',
          500: '#3b82f6',
          600: '#2563eb',
          700: '#1d4ed8'
        },
        surface: {
          light: '#ffffff',
          dark: '#1f2937'
        }
      },
      spacing: {
        // Custom spacing tokens
        '4.5': '1.125rem',
        '18': '4.5rem'
      },
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif']
      },
      borderRadius: {
        '4xl': '2rem'
      }
    }
  },
  plugins: []
}
在Tailwind配置中定义设计令牌以确保一致性:
正确示例:带设计令牌的tailwind.config.js
javascript
/** @type {import('tailwindcss').Config} */
export default {
  content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
  theme: {
    extend: {
      colors: {
        // 语义化颜色令牌
        primary: {
          50: '#eff6ff',
          100: '#dbeafe',
          500: '#3b82f6',
          600: '#2563eb',
          700: '#1d4ed8'
        },
        surface: {
          light: '#ffffff',
          dark: '#1f2937'
        }
      },
      spacing: {
        // 自定义间距令牌
        '4.5': '1.125rem',
        '18': '4.5rem'
      },
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif']
      },
      borderRadius: {
        '4xl': '2rem'
      }
    }
  },
  plugins: []
}

Tailwind CSS v4 Configuration

Tailwind CSS v4配置

For Tailwind CSS v4, use the CSS-first configuration approach:
Correct: Tailwind v4 CSS configuration
css
/* main.css */
@import "tailwindcss";

@theme {
  /* Custom colors */
  --color-primary-500: #3b82f6;
  --color-primary-600: #2563eb;
  --color-primary-700: #1d4ed8;

  /* Custom spacing */
  --spacing-4-5: 1.125rem;
  --spacing-18: 4.5rem;

  /* Custom fonts */
  --font-family-sans: 'Inter', system-ui, sans-serif;
}
对于Tailwind CSS v4,使用CSS优先的配置方式:
正确示例:Tailwind v4 CSS配置
css
/* main.css */
@import "tailwindcss";

@theme {
  /* 自定义颜色 */
  --color-primary-500: #3b82f6;
  --color-primary-600: #2563eb;
  --color-primary-700: #1d4ed8;

  /* 自定义间距 */
  --spacing-4-5: 1.125rem;
  --spacing-18: 4.5rem;

  /* 自定义字体 */
  --font-family-sans: 'Inter', system-ui, sans-serif;
}

Using
cn()
Helper for Conditional Classes

使用
cn()
辅助函数处理条件类

Use a class merging utility for conditional classes:
Correct: cn() helper with clsx and tailwind-merge
typescript
// utils/cn.ts
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}
Usage in component:
vue
<script setup lang="ts">
import { cn } from '@/utils/cn'

const props = defineProps<{
  class?: string
  isActive?: boolean
}>()
</script>

<template>
  <div
    :class="cn(
      'rounded-lg border p-4 transition-colors',
      isActive ? 'border-blue-500 bg-blue-50' : 'border-gray-200 bg-white',
      props.class
    )"
  >
    <slot />
  </div>
</template>
使用类合并工具处理条件类:
正确示例:使用clsx和tailwind-merge实现cn()辅助函数
typescript
// utils/cn.ts
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}
组件中使用:
vue
<script setup lang="ts">
import { cn } from '@/utils/cn'

const props = defineProps<{
  class?: string
  isActive?: boolean
}>()
</script>

<template>
  <div
    :class="cn(
      'rounded-lg border p-4 transition-colors',
      isActive ? 'border-blue-500 bg-blue-50' : 'border-gray-200 bg-white',
      props.class
    )"
  >
    <slot />
  </div>
</template>

PrimeVue Best Practices

PrimeVue最佳实践

PrimeVue is a comprehensive Vue UI component library with 90+ components. Follow these patterns for effective integration and customization.
PrimeVue是一个全面的Vue UI组件库,包含90+组件。遵循以下模式以实现有效的集成和定制:

Installation & Setup

安装与设置

Correct: PrimeVue v4 setup with Vue 3
typescript
// main.ts
import { createApp } from 'vue'
import PrimeVue from 'primevue/config'
import Aura from '@primevue/themes/aura'
import App from './App.vue'

const app = createApp(App)

app.use(PrimeVue, {
  theme: {
    preset: Aura,
    options: {
      darkModeSelector: '.dark-mode'
    }
  }
})

app.mount('#app')
Correct: Component registration (tree-shakeable)
typescript
// main.ts - Register only components you use
import Button from 'primevue/button'
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'

app.component('Button', Button)
app.component('DataTable', DataTable)
app.component('Column', Column)
正确示例:Vue 3搭配PrimeVue v4设置
typescript
// main.ts
import { createApp } from 'vue'
import PrimeVue from 'primevue/config'
import Aura from '@primevue/themes/aura'
import App from './App.vue'

const app = createApp(App)

app.use(PrimeVue, {
  theme: {
    preset: Aura,
    options: {
      darkModeSelector: '.dark-mode'
    }
  }
})

app.mount('#app')
正确示例:组件注册(支持摇树优化)
typescript
// main.ts - 仅注册需要使用的组件
import Button from 'primevue/button'
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'

app.component('Button', Button)
app.component('DataTable', DataTable)
app.component('Column', Column)

PassThrough (PT) API

PassThrough(PT)API

The PassThrough API allows customization of internal DOM elements without modifying component source:
Correct: Component-level PassThrough
vue
<script setup lang="ts">
import Panel from 'primevue/panel'
</script>

<template>
  <Panel
    header="User Profile"
    toggleable
    :pt="{
      header: {
        class: 'bg-primary-100 dark:bg-primary-900'
      },
      content: {
        class: 'p-6'
      },
      title: {
        class: 'text-xl font-semibold'
      },
      toggler: {
        class: 'hover:bg-primary-200 dark:hover:bg-primary-800 rounded-full'
      }
    }"
  >
    <p>Panel content here</p>
  </Panel>
</template>
Correct: Dynamic PassThrough with state
vue
<script setup lang="ts">
import Panel from 'primevue/panel'
</script>

<template>
  <Panel
    header="Collapsible Panel"
    toggleable
    :pt="{
      header: (options) => ({
        class: [
          'transition-colors duration-200',
          {
            'bg-primary-500 text-white': options.state.d_collapsed,
            'bg-surface-100 dark:bg-surface-800': !options.state.d_collapsed
          }
        ]
      })
    }"
  >
    <p>Content changes header style when collapsed</p>
  </Panel>
</template>
PassThrough API允许在不修改组件源码的情况下自定义内部DOM元素:
正确示例:组件级PassThrough
vue
<script setup lang="ts">
import Panel from 'primevue/panel'
</script>

<template>
  <Panel
    header="用户资料"
    toggleable
    :pt="{
      header: {
        class: 'bg-primary-100 dark:bg-primary-900'
      },
      content: {
        class: 'p-6'
      },
      title: {
        class: 'text-xl font-semibold'
      },
      toggler: {
        class: 'hover:bg-primary-200 dark:hover:bg-primary-800 rounded-full'
      }
    }"
  >
    <p>面板内容</p>
  </Panel>
</template>
正确示例:结合状态的动态PassThrough
vue
<script setup lang="ts">
import Panel from 'primevue/panel'
</script>

<template>
  <Panel
    header="可折叠面板"
    toggleable
    :pt="{
      header: (options) => ({
        class: [
          'transition-colors duration-200',
          {
            'bg-primary-500 text-white': options.state.d_collapsed,
            'bg-surface-100 dark:bg-surface-800': !options.state.d_collapsed
          }
        ]
      })
    }"
  >
    <p>折叠时标题样式会变化</p>
  </Panel>
</template>

Global PassThrough Configuration

全局PassThrough配置

Define shared styles at the application level:
Correct: Global PT configuration
typescript
// main.ts
import PrimeVue from 'primevue/config'
import Aura from '@primevue/themes/aura'

app.use(PrimeVue, {
  theme: {
    preset: Aura
  },
  pt: {
    // All buttons get consistent styling
    button: {
      root: {
        class: 'rounded-lg font-medium transition-all duration-200'
      }
    },
    // All inputs get consistent styling
    inputtext: {
      root: {
        class: 'rounded-lg border-2 focus:ring-2 focus:ring-primary-500'
      }
    },
    // All panels share styling
    panel: {
      header: {
        class: 'bg-surface-50 dark:bg-surface-900'
      }
    },
    // Global CSS injection
    global: {
      css: `
        .p-component {
          font-family: 'Inter', sans-serif;
        }
      `
    }
  }
})
在应用层面定义共享样式:
正确示例:全局PT配置
typescript
// main.ts
import PrimeVue from 'primevue/config'
import Aura from '@primevue/themes/aura'

app.use(PrimeVue, {
  theme: {
    preset: Aura
  },
  pt: {
    // 所有按钮使用统一样式
    button: {
      root: {
        class: 'rounded-lg font-medium transition-all duration-200'
      }
    },
    // 所有输入框使用统一样式
    inputtext: {
      root: {
        class: 'rounded-lg border-2 focus:ring-2 focus:ring-primary-500'
      }
    },
    // 所有面板使用统一样式
    panel: {
      header: {
        class: 'bg-surface-50 dark:bg-surface-900'
      }
    },
    // 全局CSS注入
    global: {
      css: `
        .p-component {
          font-family: 'Inter', sans-serif;
        }
      `
    }
  }
})

usePassThrough Utility

usePassThrough工具

Extend existing presets with custom modifications:
Correct: Extending Tailwind preset
typescript
// presets/custom-tailwind.ts
import { usePassThrough } from 'primevue/passthrough'
import Tailwind from 'primevue/passthrough/tailwind'

export const CustomTailwind = usePassThrough(
  Tailwind,
  {
    panel: {
      header: {
        class: ['bg-gradient-to-r from-primary-500 to-primary-600']
      },
      title: {
        class: ['text-white font-bold']
      }
    },
    button: {
      root: {
        class: ['shadow-lg hover:shadow-xl transition-shadow']
      }
    }
  },
  {
    mergeSections: true,  // Keep original sections
    mergeProps: false     // Replace props (don't merge arrays)
  }
)
Merge Strategy Reference:
mergeSectionsmergePropsBehavior
true
false
Custom value replaces original (default)
true
true
Custom values merge with original
false
true
Only custom sections included
false
false
Minimal - only custom sections, no merging
扩展现有预设并添加自定义修改:
正确示例:扩展Tailwind预设
typescript
// presets/custom-tailwind.ts
import { usePassThrough } from 'primevue/passthrough'
import Tailwind from 'primevue/passthrough/tailwind'

export const CustomTailwind = usePassThrough(
  Tailwind,
  {
    panel: {
      header: {
        class: ['bg-gradient-to-r from-primary-500 to-primary-600']
      },
      title: {
        class: ['text-white font-bold']
      }
    },
    button: {
      root: {
        class: ['shadow-lg hover:shadow-xl transition-shadow']
      }
    }
  },
  {
    mergeSections: true,  // 保留原始部分
    mergeProps: false     // 替换props(不合并数组)
  }
)
合并策略参考:
mergeSectionsmergeProps行为
true
false
自定义值替换原始值(默认)
true
true
自定义值与原始值合并
false
true
仅包含自定义部分
false
false
最小化 - 仅包含自定义部分,不合并

Unstyled Mode with Tailwind

无样式模式搭配Tailwind

Use unstyled PrimeVue components with full Tailwind control:
Correct: Unstyled mode configuration
typescript
// main.ts
import PrimeVue from 'primevue/config'

app.use(PrimeVue, {
  unstyled: true  // Remove all default styles
})
Correct: Custom styled button with unstyled mode
vue
<script setup lang="ts">
import Button from 'primevue/button'
</script>

<template>
  <Button
    label="Submit"
    :pt="{
      root: {
        class: [
          'inline-flex items-center justify-center',
          'px-4 py-2 rounded-lg font-medium',
          'bg-primary-600 text-white',
          'hover:bg-primary-700 active:bg-primary-800',
          'focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
          'transition-colors duration-150',
          'disabled:opacity-50 disabled:cursor-not-allowed'
        ]
      },
      label: {
        class: 'font-medium'
      },
      icon: {
        class: 'mr-2'
      }
    }"
    :ptOptions="{ mergeSections: false, mergeProps: false }"
  />
</template>
使用无样式PrimeVue组件并完全通过Tailwind控制样式:
正确示例:无样式模式配置
typescript
// main.ts
import PrimeVue from 'primevue/config'

app.use(PrimeVue, {
  unstyled: true  // 移除所有默认样式
})
正确示例:无样式模式下的自定义样式按钮
vue
<script setup lang="ts">
import Button from 'primevue/button'
</script>

<template>
  <Button
    label="提交"
    :pt="{
      root: {
        class: [
          'inline-flex items-center justify-center',
          'px-4 py-2 rounded-lg font-medium',
          'bg-primary-600 text-white',
          'hover:bg-primary-700 active:bg-primary-800',
          'focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
          'transition-colors duration-150',
          'disabled:opacity-50 disabled:cursor-not-allowed'
        ]
      },
      label: {
        class: 'font-medium'
      },
      icon: {
        class: 'mr-2'
      }
    }"
    :ptOptions="{ mergeSections: false, mergeProps: false }"
  />
</template>

Wrapper Components Pattern

包装器组件模式

Create reusable wrapper components for consistent styling:
Correct: Button wrapper component
vue
<!-- components/ui/AppButton.vue -->
<script setup lang="ts">
import Button from 'primevue/button'

type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost'
type ButtonSize = 'sm' | 'md' | 'lg'

const props = withDefaults(defineProps<{
  variant?: ButtonVariant
  size?: ButtonSize
  loading?: boolean
}>(), {
  variant: 'primary',
  size: 'md',
  loading: false
})

const variantClasses: Record<ButtonVariant, string> = {
  primary: 'bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500',
  secondary: 'bg-surface-200 text-surface-900 hover:bg-surface-300 focus:ring-surface-500',
  danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
  ghost: 'bg-transparent text-primary-600 hover:bg-primary-50 focus:ring-primary-500'
}

const sizeClasses: Record<ButtonSize, string> = {
  sm: 'px-3 py-1.5 text-sm',
  md: 'px-4 py-2 text-base',
  lg: 'px-6 py-3 text-lg'
}
</script>

<template>
  <Button
    v-bind="$attrs"
    :loading="loading"
    :pt="{
      root: {
        class: [
          'inline-flex items-center justify-center rounded-lg font-medium',
          'transition-all duration-200',
          'focus:outline-none focus:ring-2 focus:ring-offset-2',
          'disabled:opacity-50 disabled:cursor-not-allowed',
          variantClasses[variant],
          sizeClasses[size]
        ]
      }
    }"
    :ptOptions="{ mergeSections: false, mergeProps: false }"
  >
    <slot />
  </Button>
</template>

<script lang="ts">
export default {
  inheritAttrs: false
}
</script>
Usage:
vue
<template>
  <AppButton variant="primary" size="lg" @click="handleSubmit">
    Submit Form
  </AppButton>
  <AppButton variant="ghost" size="sm">
    Cancel
  </AppButton>
</template>
创建可复用的包装器组件以确保样式一致:
正确示例:按钮包装器组件
vue
<!-- components/ui/AppButton.vue -->
<script setup lang="ts">
import Button from 'primevue/button'

type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost'
type ButtonSize = 'sm' | 'md' | 'lg'

const props = withDefaults(defineProps<{
  variant?: ButtonVariant
  size?: ButtonSize
  loading?: boolean
}>(), {
  variant: 'primary',
  size: 'md',
  loading: false
})

const variantClasses: Record<ButtonVariant, string> = {
  primary: 'bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500',
  secondary: 'bg-surface-200 text-surface-900 hover:bg-surface-300 focus:ring-surface-500',
  danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
  ghost: 'bg-transparent text-primary-600 hover:bg-primary-50 focus:ring-primary-500'
}

const sizeClasses: Record<ButtonSize, string> = {
  sm: 'px-3 py-1.5 text-sm',
  md: 'px-4 py-2 text-base',
  lg: 'px-6 py-3 text-lg'
}
</script>

<template>
  <Button
    v-bind="$attrs"
    :loading="loading"
    :pt="{
      root: {
        class: [
          'inline-flex items-center justify-center rounded-lg font-medium',
          'transition-all duration-200',
          'focus:outline-none focus:ring-2 focus:ring-offset-2',
          'disabled:opacity-50 disabled:cursor-not-allowed',
          variantClasses[variant],
          sizeClasses[size]
        ]
      }
    }"
    :ptOptions="{ mergeSections: false, mergeProps: false }"
  >
    <slot />
  </Button>
</template>

<script lang="ts">
export default {
  inheritAttrs: false
}
</script>
使用方式:
vue
<template>
  <AppButton variant="primary" size="lg" @click="handleSubmit">
    提交表单
  </AppButton>
  <AppButton variant="ghost" size="sm">
    取消
  </AppButton>
</template>

DataTable Best Practices

DataTable最佳实践

Correct: Typed DataTable with Composition API
vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'

interface User {
  id: number
  name: string
  email: string
  role: string
  status: 'active' | 'inactive'
}

const users = ref<User[]>([])
const loading = ref(true)
const selectedUsers = ref<User[]>([])

// Pagination
const first = ref(0)
const rows = ref(10)

// Sorting
const sortField = ref<string>('name')
const sortOrder = ref<1 | -1>(1)

onMounted(async () => {
  loading.value = true
  try {
    users.value = await fetchUsers()
  } finally {
    loading.value = false
  }
})
</script>

<template>
  <DataTable
    v-model:selection="selectedUsers"
    :value="users"
    :loading="loading"
    :paginator="true"
    :rows="rows"
    :first="first"
    :sortField="sortField"
    :sortOrder="sortOrder"
    dataKey="id"
    stripedRows
    removableSort
    @page="(e) => first = e.first"
    @sort="(e) => { sortField = e.sortField; sortOrder = e.sortOrder }"
  >
    <Column selectionMode="multiple" headerStyle="width: 3rem" />
    <Column field="name" header="Name" sortable />
    <Column field="email" header="Email" sortable />
    <Column field="role" header="Role" sortable />
    <Column field="status" header="Status">
      <template #body="{ data }">
        <span
          :class="[
            'px-2 py-1 rounded-full text-xs font-medium',
            data.status === 'active'
              ? 'bg-green-100 text-green-800'
              : 'bg-red-100 text-red-800'
          ]"
        >
          {{ data.status }}
        </span>
      </template>
    </Column>
  </DataTable>
</template>
正确示例:使用Composition API的类型化DataTable
vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'

interface User {
  id: number
  name: string
  email: string
  role: string
  status: 'active' | 'inactive'
}

const users = ref<User[]>([])
const loading = ref(true)
const selectedUsers = ref<User[]>([])

// 分页
const first = ref(0)
const rows = ref(10)

// 排序
const sortField = ref<string>('name')
const sortOrder = ref<1 | -1>(1)

onMounted(async () => {
  loading.value = true
  try {
    users.value = await fetchUsers()
  } finally {
    loading.value = false
  }
})
</script>

<template>
  <DataTable
    v-model:selection="selectedUsers"
    :value="users"
    :loading="loading"
    :paginator="true"
    :rows="rows"
    :first="first"
    :sortField="sortField"
    :sortOrder="sortOrder"
    dataKey="id"
    stripedRows
    removableSort
    @page="(e) => first = e.first"
    @sort="(e) => { sortField = e.sortField; sortOrder = e.sortOrder }"
  >
    <Column selectionMode="multiple" headerStyle="width: 3rem" />
    <Column field="name" header="姓名" sortable />
    <Column field="email" header="邮箱" sortable />
    <Column field="role" header="角色" sortable />
    <Column field="status" header="状态">
      <template #body="{ data }">
        <span
          :class="[
            'px-2 py-1 rounded-full text-xs font-medium',
            data.status === 'active'
              ? 'bg-green-100 text-green-800'
              : 'bg-red-100 text-red-800'
          ]"
        >
          {{ data.status }}
        </span>
      </template>
    </Column>
  </DataTable>
</template>

Form Components Pattern

表单组件模式

Correct: Form with validation using PrimeVue
vue
<script setup lang="ts">
import { ref, computed } from 'vue'
import InputText from 'primevue/inputtext'
import Password from 'primevue/password'
import Dropdown from 'primevue/dropdown'
import Button from 'primevue/button'
import Message from 'primevue/message'

interface FormData {
  email: string
  password: string
  role: string | null
}

const formData = ref<FormData>({
  email: '',
  password: '',
  role: null
})

const errors = ref<Partial<Record<keyof FormData, string>>>({})
const submitted = ref(false)

const roles = [
  { label: 'Admin', value: 'admin' },
  { label: 'User', value: 'user' },
  { label: 'Guest', value: 'guest' }
]

const isValid = computed(() => {
  return Object.keys(errors.value).length === 0
})

function validate(): boolean {
  errors.value = {}

  if (!formData.value.email) {
    errors.value.email = 'Email is required'
  } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.value.email)) {
    errors.value.email = 'Invalid email format'
  }

  if (!formData.value.password) {
    errors.value.password = 'Password is required'
  } else if (formData.value.password.length < 8) {
    errors.value.password = 'Password must be at least 8 characters'
  }

  if (!formData.value.role) {
    errors.value.role = 'Role is required'
  }

  return Object.keys(errors.value).length === 0
}

function handleSubmit() {
  submitted.value = true
  if (validate()) {
    // Submit form
    console.log('Form submitted:', formData.value)
  }
}
</script>

<template>
  <form @submit.prevent="handleSubmit" class="space-y-4">
    <div class="flex flex-col gap-2">
      <label for="email" class="font-medium">Email</label>
      <InputText
        id="email"
        v-model="formData.email"
        :class="{ 'p-invalid': errors.email }"
        aria-describedby="email-error"
      />
      <Message v-if="errors.email" severity="error" :closable="false">
        {{ errors.email }}
      </Message>
    </div>

    <div class="flex flex-col gap-2">
      <label for="password" class="font-medium">Password</label>
      <Password
        id="password"
        v-model="formData.password"
        :class="{ 'p-invalid': errors.password }"
        toggleMask
        :feedback="false"
        aria-describedby="password-error"
      />
      <Message v-if="errors.password" severity="error" :closable="false">
        {{ errors.password }}
      </Message>
    </div>

    <div class="flex flex-col gap-2">
      <label for="role" class="font-medium">Role</label>
      <Dropdown
        id="role"
        v-model="formData.role"
        :options="roles"
        optionLabel="label"
        optionValue="value"
        placeholder="Select a role"
        :class="{ 'p-invalid': errors.role }"
        aria-describedby="role-error"
      />
      <Message v-if="errors.role" severity="error" :closable="false">
        {{ errors.role }}
      </Message>
    </div>

    <Button type="submit" label="Submit" class="w-full" />
  </form>
</template>
正确示例:使用PrimeVue的带验证表单
vue
<script setup lang="ts">
import { ref, computed } from 'vue'
import InputText from 'primevue/inputtext'
import Password from 'primevue/password'
import Dropdown from 'primevue/dropdown'
import Button from 'primevue/button'
import Message from 'primevue/message'

interface FormData {
  email: string
  password: string
  role: string | null
}

const formData = ref<FormData>({
  email: '',
  password: '',
  role: null
})

const errors = ref<Partial<Record<keyof FormData, string>>>({})
const submitted = ref(false)

const roles = [
  { label: '管理员', value: 'admin' },
  { label: '普通用户', value: 'user' },
  { label: '访客', value: 'guest' }
]

const isValid = computed(() => {
  return Object.keys(errors.value).length === 0
})

function validate(): boolean {
  errors.value = {}

  if (!formData.value.email) {
    errors.value.email = '邮箱为必填项'
  } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.value.email)) {
    errors.value.email = '邮箱格式无效'
  }

  if (!formData.value.password) {
    errors.value.password = '密码为必填项'
  } else if (formData.value.password.length < 8) {
    errors.value.password = '密码长度至少为8位'
  }

  if (!formData.value.role) {
    errors.value.role = '请选择角色'
  }

  return Object.keys(errors.value).length === 0
}

function handleSubmit() {
  submitted.value = true
  if (validate()) {
    // 提交表单
    console.log('表单提交:', formData.value)
  }
}
</script>

<template>
  <form @submit.prevent="handleSubmit" class="space-y-4">
    <div class="flex flex-col gap-2">
      <label for="email" class="font-medium">邮箱</label>
      <InputText
        id="email"
        v-model="formData.email"
        :class="{ 'p-invalid': errors.email }"
        aria-describedby="email-error"
      />
      <Message v-if="errors.email" severity="error" :closable="false">
        {{ errors.email }}
      </Message>
    </div>

    <div class="flex flex-col gap-2">
      <label for="password" class="font-medium">密码</label>
      <Password
        id="password"
        v-model="formData.password"
        :class="{ 'p-invalid': errors.password }"
        toggleMask
        :feedback="false"
        aria-describedby="password-error"
      />
      <Message v-if="errors.password" severity="error" :closable="false">
        {{ errors.password }}
      </Message>
    </div>

    <div class="flex flex-col gap-2">
      <label for="role" class="font-medium">角色</label>
      <Dropdown
        id="role"
        v-model="formData.role"
        :options="roles"
        optionLabel="label"
        optionValue="value"
        placeholder="选择角色"
        :class="{ 'p-invalid': errors.role }"
        aria-describedby="role-error"
      />
      <Message v-if="errors.role" severity="error" :closable="false">
        {{ errors.role }}
      </Message>
    </div>

    <Button type="submit" label="提交" class="w-full" />
  </form>
</template>

Dialog & Overlay Patterns

对话框与覆盖层模式

Correct: Confirmation dialog with composable
typescript
// composables/useConfirmDialog.ts
import { useConfirm } from 'primevue/useconfirm'

export function useConfirmDialog() {
  const confirm = useConfirm()

  function confirmDelete(
    message: string,
    onAccept: () => void,
    onReject?: () => void
  ) {
    confirm.require({
      message,
      header: 'Confirm Delete',
      icon: 'pi pi-exclamation-triangle',
      rejectClass: 'p-button-secondary p-button-outlined',
      acceptClass: 'p-button-danger',
      rejectLabel: 'Cancel',
      acceptLabel: 'Delete',
      accept: onAccept,
      reject: onReject
    })
  }

  function confirmAction(options: {
    message: string
    header: string
    onAccept: () => void
    onReject?: () => void
  }) {
    confirm.require({
      message: options.message,
      header: options.header,
      icon: 'pi pi-info-circle',
      rejectClass: 'p-button-secondary p-button-outlined',
      acceptClass: 'p-button-primary',
      accept: options.onAccept,
      reject: options.onReject
    })
  }

  return {
    confirmDelete,
    confirmAction
  }
}
Usage:
vue
<script setup lang="ts">
import { useConfirmDialog } from '@/composables/useConfirmDialog'
import ConfirmDialog from 'primevue/confirmdialog'

const { confirmDelete } = useConfirmDialog()

function handleDelete(item: Item) {
  confirmDelete(
    `Are you sure you want to delete "${item.name}"?`,
    () => deleteItem(item.id)
  )
}
</script>

<template>
  <ConfirmDialog />
  <Button label="Delete" severity="danger" @click="handleDelete(item)" />
</template>
正确示例:使用组合式函数的确认对话框
typescript
// composables/useConfirmDialog.ts
import { useConfirm } from 'primevue/useconfirm'

export function useConfirmDialog() {
  const confirm = useConfirm()

  function confirmDelete(
    message: string,
    onAccept: () => void,
    onReject?: () => void
  ) {
    confirm.require({
      message,
      header: '确认删除',
      icon: 'pi pi-exclamation-triangle',
      rejectClass: 'p-button-secondary p-button-outlined',
      acceptClass: 'p-button-danger',
      rejectLabel: '取消',
      acceptLabel: '删除',
      accept: onAccept,
      reject: onReject
    })
  }

  function confirmAction(options: {
    message: string
    header: string
    onAccept: () => void
    onReject?: () => void
  }) {
    confirm.require({
      message: options.message,
      header: options.header,
      icon: 'pi pi-info-circle',
      rejectClass: 'p-button-secondary p-button-outlined',
      acceptClass: 'p-button-primary',
      accept: options.onAccept,
      reject: options.onReject
    })
  }

  return {
    confirmDelete,
    confirmAction
  }
}
使用方式:
vue
<script setup lang="ts">
import { useConfirmDialog } from '@/composables/useConfirmDialog'
import ConfirmDialog from 'primevue/confirmdialog'

const { confirmDelete } = useConfirmDialog()

function handleDelete(item: Item) {
  confirmDelete(
    `确定要删除"${item.name}"吗?`,
    () => deleteItem(item.id)
  )
}
</script>

<template>
  <ConfirmDialog />
  <Button label="删除" severity="danger" @click="handleDelete(item)" />
</template>

Toast Notifications

提示通知

Correct: Toast service with composable
typescript
// composables/useNotifications.ts
import { useToast } from 'primevue/usetoast'

export function useNotifications() {
  const toast = useToast()

  function success(summary: string, detail?: string) {
    toast.add({
      severity: 'success',
      summary,
      detail,
      life: 3000
    })
  }

  function error(summary: string, detail?: string) {
    toast.add({
      severity: 'error',
      summary,
      detail,
      life: 5000
    })
  }

  function warn(summary: string, detail?: string) {
    toast.add({
      severity: 'warn',
      summary,
      detail,
      life: 4000
    })
  }

  function info(summary: string, detail?: string) {
    toast.add({
      severity: 'info',
      summary,
      detail,
      life: 3000
    })
  }

  return { success, error, warn, info }
}
正确示例:使用组合式函数的提示服务
typescript
// composables/useNotifications.ts
import { useToast } from 'primevue/usetoast'

export function useNotifications() {
  const toast = useToast()

  function success(summary: string, detail?: string) {
    toast.add({
      severity: 'success',
      summary,
      detail,
      life: 3000
    })
  }

  function error(summary: string, detail?: string) {
    toast.add({
      severity: 'error',
      summary,
      detail,
      life: 5000
    })
  }

  function warn(summary: string, detail?: string) {
    toast.add({
      severity: 'warn',
      summary,
      detail,
      life: 4000
    })
  }

  function info(summary: string, detail?: string) {
    toast.add({
      severity: 'info',
      summary,
      detail,
      life: 3000
    })
  }

  return { success, error, warn, info }
}

Accessibility Best Practices

无障碍最佳实践

PrimeVue components are WCAG 2.0 compliant. Ensure proper usage:
Correct: Accessible form fields
vue
<template>
  <div class="flex flex-col gap-2">
    <label :for="id" class="font-medium">
      {{ label }}
      <span v-if="required" class="text-red-500" aria-hidden="true">*</span>
    </label>
    <InputText
      :id="id"
      v-model="modelValue"
      :aria-required="required"
      :aria-invalid="!!error"
      :aria-describedby="error ? `${id}-error` : undefined"
    />
    <small
      v-if="error"
      :id="`${id}-error`"
      class="text-red-500"
      role="alert"
    >
      {{ error }}
    </small>
  </div>
</template>
PrimeVue组件符合WCAG 2.0标准。确保正确使用:
正确示例:无障碍表单字段
vue
<template>
  <div class="flex flex-col gap-2">
    <label :for="id" class="font-medium">
      {{ label }}
      <span v-if="required" class="text-red-500" aria-hidden="true">*</span>
    </label>
    <InputText
      :id="id"
      v-model="modelValue"
      :aria-required="required"
      :aria-invalid="!!error"
      :aria-describedby="error ? `${id}-error` : undefined"
    />
    <small
      v-if="error"
      :id="`${id}-error`"
      class="text-red-500"
      role="alert"
    >
      {{ error }}
    </small>
  </div>
</template>

Lazy Loading Components

懒加载组件

Correct: Async component loading for large PrimeVue components
typescript
// components/lazy/index.ts
import { defineAsyncComponent } from 'vue'

export const LazyDataTable = defineAsyncComponent({
  loader: () => import('primevue/datatable'),
  loadingComponent: () => import('@/components/ui/TableSkeleton.vue'),
  delay: 200
})

export const LazyEditor = defineAsyncComponent({
  loader: () => import('primevue/editor'),
  loadingComponent: () => import('@/components/ui/EditorSkeleton.vue'),
  delay: 200
})

export const LazyChart = defineAsyncComponent({
  loader: () => import('primevue/chart'),
  loadingComponent: () => import('@/components/ui/ChartSkeleton.vue'),
  delay: 200
})
正确示例:大型PrimeVue组件的异步加载
typescript
// components/lazy/index.ts
import { defineAsyncComponent } from 'vue'

export const LazyDataTable = defineAsyncComponent({
  loader: () => import('primevue/datatable'),
  loadingComponent: () => import('@/components/ui/TableSkeleton.vue'),
  delay: 200
})

export const LazyEditor = defineAsyncComponent({
  loader: () => import('primevue/editor'),
  loadingComponent: () => import('@/components/ui/EditorSkeleton.vue'),
  delay: 200
})

export const LazyChart = defineAsyncComponent({
  loader: () => import('primevue/chart'),
  loadingComponent: () => import('@/components/ui/ChartSkeleton.vue'),
  delay: 200
})

Anti-Patterns to Avoid

需要避免的反模式

Don't Mutate Props

不要修改Props

Incorrect:
vue
<script setup>
const props = defineProps(['items'])

function addItem(item) {
  props.items.push(item)  // Never mutate props!
}
</script>
Correct:
vue
<script setup>
const props = defineProps(['items'])
const emit = defineEmits(['update:items'])

function addItem(item) {
  emit('update:items', [...props.items, item])
}
</script>
错误示例:
vue
<script setup>
const props = defineProps(['items'])

function addItem(item) {
  props.items.push(item)  // 绝不要修改props!
}
</script>
正确示例:
vue
<script setup>
const props = defineProps(['items'])
const emit = defineEmits(['update:items'])

function addItem(item) {
  emit('update:items', [...props.items, item])
}
</script>

Don't Use v-if with v-for

不要在同一元素上同时使用v-if和v-for

Incorrect:
vue
<template>
  <div v-for="item in items" v-if="item.isActive" :key="item.id">
    {{ item.name }}
  </div>
</template>
Correct:
vue
<script setup>
const activeItems = computed(() => items.value.filter(item => item.isActive))
</script>

<template>
  <div v-for="item in activeItems" :key="item.id">
    {{ item.name }}
  </div>
</template>
错误示例:
vue
<template>
  <div v-for="item in items" v-if="item.isActive" :key="item.id">
    {{ item.name }}
  </div>
</template>
正确示例:
vue
<script setup>
const activeItems = computed(() => items.value.filter(item => item.isActive))
</script>

<template>
  <div v-for="item in activeItems" :key="item.id">
    {{ item.name }}
  </div>
</template>

Don't Store Derived State

不要存储派生状态

Incorrect:
vue
<script setup>
const items = ref([])
const itemCount = ref(0)  // Derived state stored separately

watch(items, () => {
  itemCount.value = items.value.length  // Manually syncing
})
</script>
Correct:
vue
<script setup>
const items = ref([])
const itemCount = computed(() => items.value.length)  // Computed property
</script>
错误示例:
vue
<script setup>
const items = ref([])
const itemCount = ref(0)  // 派生状态单独存储

watch(items, () => {
  itemCount.value = items.value.length  // 手动同步
})
</script>
正确示例:
vue
<script setup>
const items = ref([])
const itemCount = computed(() => items.value.length)  // 使用计算属性
</script>

Don't Destructure Reactive Objects

不要解构响应式对象

Incorrect:
vue
<script setup>
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = state  // Loses reactivity!
</script>
Correct:
vue
<script setup>
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = toRefs(state)  // Preserves reactivity
</script>
错误示例:
vue
<script setup>
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = state  // 会丢失响应式!
</script>
正确示例:
vue
<script setup>
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = toRefs(state)  // 保留响应式
</script>

Don't Concatenate Tailwind Class Names

不要拼接Tailwind类名

Dynamic class concatenation breaks Tailwind's compiler and classes get purged in production:
Incorrect:
vue
<script setup>
const color = ref('blue')
</script>

<template>
  <!-- Classes will be purged in production! -->
  <div :class="`bg-${color}-500 text-${color}-900`">
    Content
  </div>
</template>
Correct:
vue
<script setup>
const color = ref<'blue' | 'green' | 'red'>('blue')

const colorClasses = computed(() => {
  const colors = {
    blue: 'bg-blue-500 text-blue-900',
    green: 'bg-green-500 text-green-900',
    red: 'bg-red-500 text-red-900'
  }
  return colors[color.value]
})
</script>

<template>
  <div :class="colorClasses">
    Content
  </div>
</template>
动态拼接类名会破坏Tailwind的编译器,生产环境中类会被清除:
错误示例:
vue
<script setup>
const color = ref('blue')
</script>

<template>
  <!-- 生产环境中这些类会被清除! -->
  <div :class="`bg-${color}-500 text-${color}-900`">
    内容
  </div>
</template>
正确示例:
vue
<script setup>
const color = ref<'blue' | 'green' | 'red'>('blue')

const colorClasses = computed(() => {
  const colors = {
    blue: 'bg-blue-500 text-blue-900',
    green: 'bg-green-500 text-green-900',
    red: 'bg-red-500 text-red-900'
  }
  return colors[color.value]
})
</script>

<template>
  <div :class="colorClasses">
    内容
  </div>
</template>

Don't Overuse @apply

不要过度使用@apply

Excessive
@apply
usage defeats the purpose of utility-first CSS:
Incorrect:
css
/* styles.css */
.card {
  @apply mx-auto max-w-md rounded-xl bg-white p-6 shadow-lg;
}

.card-title {
  @apply text-xl font-semibold text-gray-900;
}

.card-description {
  @apply mt-2 text-gray-600;
}

.card-button {
  @apply mt-4 rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700;
}
Correct: Use Vue components instead
vue
<!-- components/Card.vue -->
<template>
  <div class="mx-auto max-w-md rounded-xl bg-white p-6 shadow-lg">
    <h2 class="text-xl font-semibold text-gray-900">
      <slot name="title" />
    </h2>
    <p class="mt-2 text-gray-600">
      <slot name="description" />
    </p>
    <div class="mt-4">
      <slot name="actions" />
    </div>
  </div>
</template>
过度使用
@apply
违背了优先工具类CSS的初衷:
错误示例:
css
/* styles.css */
.card {
  @apply mx-auto max-w-md rounded-xl bg-white p-6 shadow-lg;
}

.card-title {
  @apply text-xl font-semibold text-gray-900;
}

.card-description {
  @apply mt-2 text-gray-600;
}

.card-button {
  @apply mt-4 rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700;
}
正确示例:使用Vue组件替代
vue
<!-- components/Card.vue -->
<template>
  <div class="mx-auto max-w-md rounded-xl bg-white p-6 shadow-lg">
    <h2 class="text-xl font-semibold text-gray-900">
      <slot name="title" />
    </h2>
    <p class="mt-2 text-gray-600">
      <slot name="description" />
    </p>
    <div class="mt-4">
      <slot name="actions" />
    </div>
  </div>
</template>

Don't Use Conflicting Utilities

不要使用冲突的工具类

Applying multiple utilities that target the same CSS property causes unpredictable results:
Incorrect:
vue
<template>
  <!-- Both flex and grid target display property -->
  <div class="flex grid">Content</div>

  <!-- Multiple margin utilities conflict -->
  <div class="m-4 mx-6">Content</div>
</template>
Correct:
vue
<template>
  <div :class="isGrid ? 'grid' : 'flex'">Content</div>

  <!-- Use specific margin utilities -->
  <div class="mx-6 my-4">Content</div>
</template>
同时应用多个针对同一CSS属性的工具类会导致不可预测的结果:
错误示例:
vue
<template>
  <!-- flex和grid都针对display属性 -->
  <div class="flex grid">内容</div>

  <!-- 多个margin工具类冲突 -->
  <div class="m-4 mx-6">内容</div>
</template>
正确示例:
vue
<template>
  <div :class="isGrid ? 'grid' : 'flex'">内容</div>

  <!-- 使用具体的margin工具类 -->
  <div class="mx-6 my-4">内容</div>
</template>

Don't Ignore Accessibility

不要忽略无障碍

Always include proper accessibility attributes alongside visual styling:
Incorrect:
vue
<template>
  <button class="rounded bg-blue-600 p-2 text-white">
    <IconX />
  </button>
</template>
Correct:
vue
<template>
  <button
    class="rounded bg-blue-600 p-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
    aria-label="Close dialog"
  >
    <IconX aria-hidden="true" />
  </button>
</template>
添加视觉样式时始终包含正确的无障碍属性:
错误示例:
vue
<template>
  <button class="rounded bg-blue-600 p-2 text-white">
    <IconX />
  </button>
</template>
正确示例:
vue
<template>
  <button
    class="rounded bg-blue-600 p-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
    aria-label="关闭对话框"
  >
    <IconX aria-hidden="true" />
  </button>
</template>

Don't Create Overly Long Class Strings

不要创建过长的类字符串

Break down complex class combinations into logical groups or components:
Incorrect:
vue
<template>
  <div class="mx-auto mt-8 flex max-w-4xl flex-col items-center justify-between gap-4 rounded-xl border border-gray-200 bg-white p-6 shadow-lg transition-all duration-300 hover:border-blue-500 hover:shadow-xl dark:border-gray-700 dark:bg-gray-800 sm:flex-row sm:gap-6 md:p-8 lg:gap-8">
    <!-- 15+ utilities on one element -->
  </div>
</template>
Correct: Extract to component or use computed
vue
<script setup>
const containerClasses = [
  // Layout
  'mx-auto max-w-4xl flex flex-col sm:flex-row',
  'items-center justify-between',
  'gap-4 sm:gap-6 lg:gap-8',
  // Spacing
  'mt-8 p-6 md:p-8',
  // Visual
  'rounded-xl border bg-white shadow-lg',
  'border-gray-200 dark:border-gray-700 dark:bg-gray-800',
  // Interactive
  'transition-all duration-300',
  'hover:border-blue-500 hover:shadow-xl'
]
</script>

<template>
  <div :class="containerClasses">
    <slot />
  </div>
</template>
将复杂的类组合拆分为逻辑组或组件:
错误示例:
vue
<template>
  <div class="mx-auto mt-8 flex max-w-4xl flex-col items-center justify-between gap-4 rounded-xl border border-gray-200 bg-white p-6 shadow-lg transition-all duration-300 hover:border-blue-500 hover:shadow-xl dark:border-gray-700 dark:bg-gray-800 sm:flex-row sm:gap-6 md:p-8 lg:gap-8">
    <!-- 一个元素上有15+个工具类 -->
  </div>
</template>
正确示例:提取为组件或使用计算属性
vue
<script setup>
const containerClasses = [
  // 布局
  'mx-auto max-w-4xl flex flex-col sm:flex-row',
  'items-center justify-between',
  'gap-4 sm:gap-6 lg:gap-8',
  // 间距
  'mt-8 p-6 md:p-8',
  // 视觉
  'rounded-xl border bg-white shadow-lg',
  'border-gray-200 dark:border-gray-700 dark:bg-gray-800',
  // 交互
  'transition-all duration-300',
  'hover:border-blue-500 hover:shadow-xl'
]
</script>

<template>
  <div :class="containerClasses">
    <slot />
  </div>
</template>

Don't Override PrimeVue Styles with CSS

不要使用CSS覆盖PrimeVue样式

Using CSS overrides bypasses the design system and causes maintenance issues:
Incorrect:
css
/* styles.css - Avoid this approach */
.p-button {
  background-color: #3b82f6 !important;
  border-radius: 8px !important;
}

.p-datatable .p-datatable-thead > tr > th {
  background: #f3f4f6 !important;
}
Correct: Use design tokens or PassThrough
typescript
// main.ts - Use design tokens
app.use(PrimeVue, {
  theme: {
    preset: Aura,
    options: {
      cssLayer: {
        name: 'primevue',
        order: 'tailwind-base, primevue, tailwind-utilities'
      }
    }
  },
  pt: {
    button: {
      root: { class: 'rounded-lg' }
    }
  }
})
使用CSS覆盖会绕过设计系统,导致维护问题:
错误示例:
css
/* styles.css - 避免这种方式 */
.p-button {
  background-color: #3b82f6 !important;
  border-radius: 8px !important;
}

.p-datatable .p-datatable-thead > tr > th {
  background: #f3f4f6 !important;
}
正确示例:使用设计令牌或PassThrough
typescript
// main.ts - 使用设计令牌
app.use(PrimeVue, {
  theme: {
    preset: Aura,
    options: {
      cssLayer: {
        name: 'primevue',
        order: 'tailwind-base, primevue, tailwind-utilities'
      }
    }
  },
  pt: {
    button: {
      root: { class: 'rounded-lg' }
    }
  }
})

Don't Import Entire PrimeVue Library

不要导入整个PrimeVue库

Importing everything bloats bundle size:
Incorrect:
typescript
// main.ts - Don't do this
import PrimeVue from 'primevue/config'
import * as PrimeVueComponents from 'primevue'  // Imports everything!

Object.entries(PrimeVueComponents).forEach(([name, component]) => {
  app.component(name, component)
})
Correct: Import only what you need
typescript
// main.ts - Tree-shakeable imports
import Button from 'primevue/button'
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'

app.component('Button', Button)
app.component('DataTable', DataTable)
app.component('Column', Column)
导入所有组件会增加包体积:
错误示例:
typescript
// main.ts - 不要这样做
import PrimeVue from 'primevue/config'
import * as PrimeVueComponents from 'primevue'  // 导入了所有组件!

Object.entries(PrimeVueComponents).forEach(([name, component]) => {
  app.component(name, component)
})
正确示例:仅导入需要的组件
typescript
// main.ts - 支持摇树优化的导入
import Button from 'primevue/button'
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'

app.component('Button', Button)
app.component('DataTable', DataTable)
app.component('Column', Column)

Don't Mix Styled and Unstyled Inconsistently

不要混合使用有样式和无样式模式

Mixing modes creates visual inconsistency:
Incorrect:
typescript
// main.ts
app.use(PrimeVue, {
  unstyled: true  // Global unstyled
})

// SomeComponent.vue - Using styled component anyway
<Button label="Click" />  // No styles applied, looks broken
Correct: Choose one approach consistently
typescript
// Option 1: Styled mode with PT customization
app.use(PrimeVue, {
  theme: { preset: Aura },
  pt: { /* global customizations */ }
})

// Option 2: Unstyled mode with complete PT styling
app.use(PrimeVue, {
  unstyled: true,
  pt: {
    button: {
      root: { class: 'px-4 py-2 bg-primary-600 text-white rounded-lg' }
    }
    // ... complete styling for all components
  }
})
混合模式会导致视觉不一致:
错误示例:
typescript
// main.ts
app.use(PrimeVue, {
  unstyled: true  // 全局无样式
})

// SomeComponent.vue - 仍然使用有样式组件
<Button label="点击" />  // 没有样式,显示异常
正确示例:始终选择一种方式
typescript
// 选项1:有样式模式搭配PT定制
app.use(PrimeVue, {
  theme: { preset: Aura },
  pt: { /* 全局定制 */ }
})

// 选项2:无样式模式搭配完整的PT样式
app.use(PrimeVue, {
  unstyled: true,
  pt: {
    button: {
      root: { class: 'px-4 py-2 bg-primary-600 text-white rounded-lg' }
    }
    // ... 所有组件的完整样式
  }
})

Don't Ignore Accessibility Attributes

不要忽略无障碍属性

PrimeVue provides accessibility out of the box, don't disable or ignore it:
Incorrect:
vue
<template>
  <!-- Missing aria attributes and label -->
  <Button icon="pi pi-trash" @click="deleteItem" />

  <!-- No error message association -->
  <InputText v-model="email" :class="{ 'p-invalid': hasError }" />
  <span class="error">Invalid email</span>
</template>
Correct: Maintain accessibility
vue
<template>
  <Button
    icon="pi pi-trash"
    aria-label="Delete item"
    @click="deleteItem"
  />

  <div class="flex flex-col gap-2">
    <label for="email">Email</label>
    <InputText
      id="email"
      v-model="email"
      :class="{ 'p-invalid': hasError }"
      :aria-invalid="hasError"
      aria-describedby="email-error"
    />
    <small id="email-error" v-if="hasError" class="text-red-500" role="alert">
      Invalid email
    </small>
  </div>
</template>
PrimeVue原生支持无障碍,不要禁用或忽略:
错误示例:
vue
<template>
  <!-- 缺少aria属性和标签 -->
  <Button icon="pi pi-trash" @click="deleteItem" />

  <!-- 错误消息未关联 -->
  <InputText v-model="email" :class="{ 'p-invalid': hasError }" />
  <span class="error">无效邮箱</span>
</template>
正确示例:保持无障碍支持
vue
<template>
  <Button
    icon="pi pi-trash"
    aria-label="删除项"
    @click="deleteItem"
  />

  <div class="flex flex-col gap-2">
    <label for="email">邮箱</label>
    <InputText
      id="email"
      v-model="email"
      :class="{ 'p-invalid': hasError }"
      :aria-invalid="hasError"
      aria-describedby="email-error"
    />
    <small id="email-error" v-if="hasError" class="text-red-500" role="alert">
      无效邮箱
    </small>
  </div>
</template>

Don't Hardcode PassThrough in Every Component

不要在每个组件中硬编码PassThrough

Repeating PT configuration across components creates duplication:
Incorrect:
vue
<!-- ComponentA.vue -->
<Button :pt="{ root: { class: 'rounded-lg shadow-md' } }" />

<!-- ComponentB.vue -->
<Button :pt="{ root: { class: 'rounded-lg shadow-md' } }" />

<!-- ComponentC.vue -->
<Button :pt="{ root: { class: 'rounded-lg shadow-md' } }" />
Correct: Use global PT or wrapper components
typescript
// main.ts - Global configuration
app.use(PrimeVue, {
  pt: {
    button: {
      root: { class: 'rounded-lg shadow-md' }
    }
  }
})

// Or use wrapper components (see Wrapper Components Pattern above)
在多个组件中重复PT配置会导致代码重复:
错误示例:
vue
<!-- ComponentA.vue -->
<Button :pt="{ root: { class: 'rounded-lg shadow-md' } }" />

<!-- ComponentB.vue -->
<Button :pt="{ root: { class: 'rounded-lg shadow-md' } }" />

<!-- ComponentC.vue -->
<Button :pt="{ root: { class: 'rounded-lg shadow-md' } }" />
正确示例:使用全局PT或包装器组件
typescript
// main.ts - 全局配置
app.use(PrimeVue, {
  pt: {
    button: {
      root: { class: 'rounded-lg shadow-md' }
    }
  }
})

// 或使用包装器组件(参见上面的包装器组件模式)

Nuxt.js Specific Guidelines

Nuxt.js特定指南

When using Nuxt.js, follow these additional patterns:
  • Auto-imports: Leverage Nuxt's auto-imports for Vue APIs and composables
  • useFetch/useAsyncData: Use Nuxt's data fetching composables for SSR-compatible data loading
  • definePageMeta: Use for page-level metadata and middleware
  • Server routes: Use
    server/api/
    for API endpoints
  • Runtime config: Use
    useRuntimeConfig()
    for environment variables
使用Nuxt.js时,遵循以下额外模式:
  • 自动导入:利用Nuxt的自动导入功能导入Vue API和组合式函数
  • useFetch/useAsyncData:使用Nuxt的数据获取组合式函数实现SSR兼容的数据加载
  • definePageMeta:用于页面级元数据和中间件
  • 服务端路由:使用
    server/api/
    创建API端点
  • 运行时配置:使用
    useRuntimeConfig()
    获取环境变量

References

参考资料

Vue.js

Vue.js

Tailwind CSS

Tailwind CSS

PrimeVue

PrimeVue