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 代码,其中还包括 utility-first 样式方案的 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 natively
  • Customizing PrimeVue theme through design tokens and definePreset()
在以下场景中可以参考本指南:
  • 编写新的 Vue 组件或 composables
  • 基于 Composition API 实现功能
  • 评审代码是否符合 Vue.js 规范
  • 重构现有 Vue.js 代码
  • 搭建组件架构
  • 开发 Nuxt.js 应用
  • 使用 Tailwind CSS 工具类为 Vue 组件设置样式
  • 基于 Tailwind 和 Vue 创建设计系统
  • 原生使用 PrimeVue 组件库
  • 通过设计令牌和 definePreset() 自定义 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 & Events组件通信模式
props-
模板模式模板语法最佳实践
template-
代码组织项目与代码结构规范
organization-
TypeScriptVue.js 类型安全模式
typescript-
错误处理错误边界与处理方案
error-
Tailwind CSSutility-first 样式模式
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
    - 将可复用逻辑抽离为 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
    声明 refs
  • reactive-unwrap-template
    - 让 Vue 在模板中自动解包 refs(无需写
    .value
  • reactive-shallow-large-data
    - 大型非响应式数据使用
    shallowRef()
    /
    shallowReactive()
  • reactive-readonly-props
    - 使用
    readonly()
    防止状态被意外修改
  • reactive-toRefs-destructure
    - 解构响应式对象时使用
    toRefs()
  • reactive-avoid-mutation
    - 复杂状态优先使用不可变更新

4. Props & Events

4. Props & Events

  • 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>()
    定义 prop 类型
  • props-required-explicit
    - 明确区分必填和可选 props
  • props-default-values
    - 使用
    withDefaults()
    提供合理的默认值
  • props-immutable
    - 永远不要直接修改 props
  • props-validation
    - 复杂 prop 校验使用校验函数
  • events-define-emits
    - 始终使用
    defineEmits<T>()
    定义 emits
  • events-naming
    - 模板中事件名使用 kebab-case
  • events-payload-objects
    - 多参数事件传递对象作为 payload

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 统一放在专门的
    composables/
    目录下
  • organization-barrel-exports
    - 使用 index 文件简化导入路径
  • 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
    - prop 定义使用 TypeScript 接口
  • typescript-emit-types
    - 显式为 emit 的 payload 定义类型
  • typescript-ref-typing
    - 当类型无法自动推断时,为 refs 指定类型
  • 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-use-natively
    - Use PrimeVue components as-is with their documented props and API
  • primevue-design-tokens
    - Customize appearance exclusively through design tokens and
    definePreset()
  • primevue-no-pt-overrides
    - NEVER use PassThrough (pt) API to restyle components
  • primevue-no-unstyled-mode
    - NEVER use unstyled mode to strip and rebuild component styles
  • primevue-no-wrapper-components
    - NEVER wrap PrimeVue components just to override their styling
  • primevue-props-api
    - Use built-in props (severity, size, outlined, rounded, raised, text) for variants
  • primevue-css-layers
    - Configure CSS layer ordering for clean Tailwind coexistence
  • 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-use-natively
    - 直接使用 PrimeVue 组件及其官方文档提供的 props 和 API
  • primevue-design-tokens
    - 仅通过设计令牌和
    definePreset()
    自定义外观
  • primevue-no-pt-overrides
    - 永远不要使用 PassThrough (pt) API 重写组件样式
  • primevue-no-unstyled-mode
    - 永远不要使用无样式模式剥离并重建组件样式
  • primevue-no-wrapper-components
    - 永远不要只为了覆盖样式而封装 PrimeVue 组件
  • primevue-props-api
    - 使用内置 props(severity、size、outlined、rounded、raised、text)实现不同变体
  • primevue-css-layers
    - 配置 CSS 层级顺序,保证和 Tailwind 共存无冲突
  • 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 类型推断更好,性能更优
  • 按逻辑关注点组织代码:将相关的状态、计算属性、函数分组放在一起
  • 可复用逻辑抽离为 composables:遵循
    use
    前缀命名规范(如
    useAuth
    useFetch
  • 保持 setup 代码可读性:代码顺序:props/emits → 响应式状态 → 计算属性 → watchers → 方法 → 生命周期钩子

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 向下,Events 向上:遵循单向数据流
  • 优先组合而非继承:使用 composables 和插槽实现代码复用
  • 保持组件精简:如果组件代码超过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:永远不要在 refs 中存储派生状态,改用
    computed()
  • watch 仅用于副作用
    watch()
    只用于处理 API 调用、localStorage 操作等副作用
  • 注意响应式丢失:没有使用
    toRefs()
    时不要解构响应式对象

Props & Events Patterns

Props & Events 模式

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 定义类型:结合
    defineProps<T>()
    使用 TypeScript 接口
  • 校验复杂 props:使用校验函数实现业务逻辑层面的校验
  • 定义带类型的 emits:使用
    defineEmits<T>()
    实现类型安全的事件处理
  • 双向绑定用 v-model:实现
    modelValue
    prop 和
    update:modelValue
    emit

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. 导入
import { ref, computed, watch, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import type { User } from '@/types'

// 2. Props 和 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. 响应式状态
const formData = ref({ name: '', email: '' })
const isEditing = ref(false)

// 5. 计算属性
const isValid = computed(() => {
  return formData.value.name.length > 0 && formData.value.email.includes('@')
})

// 6. Watchers(仅用于处理副作用)
watch(() => props.userId, (newId) => {
  fetchUserData(newId)
})

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

// 8. 生命周期钩子
onMounted(() => {
  if (props.initialData) {
    formData.value = { ...props.initialData }
  }
})
</script>

Composable Pattern

Composable 模式

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))
  }
}
正确示例:结构清晰的 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) {
  // 状态
  const user = ref<User | null>(null)
  const loading = ref(false)
  const error = ref<Error | null>(null)

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

  // 方法
  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
    }
  }

  // userId 变化时自动获取(如果是响应式变量)
  if (isRef(userId)) {
    watch(userId, (newId) => fetchUser(newId), { immediate: true })
  } else {
    fetchUser(userId)
  }

  // 返回值
  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>
正确示例:带 payload 的带类型 emits
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>
正确示例:Vue 3.4+ 使用 defineModel 实现自定义 v-model
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>
正确示例:Vue 3.3 及更早版本自定义 v-model
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>
正确示例:带类型的模板 refs
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  // 10s 后超时
})

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 方式天然适配,遵循以下模式实现可维护、统一的样式。

Utility-First Approach

Utility-First 方式

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

Class Variance Authority (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. Use it natively. Do not fight the framework.
PrimeVue 是一个功能全面的 Vue UI 组件库,包含90+组件。请原生使用,不要对抗框架。

Core Principle

核心原则

PrimeVue v4 has a complete theming system based on design tokens and presets. This is the ONLY supported customization path. Do not bypass it.
PrimeVue v4 拥有基于设计令牌和预设的完整主题系统,这是唯一支持的自定义路径,不要绕过它。

Installation & Setup

安装与配置

Correct: PrimeVue v4 setup with theme preset
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: {
      prefix: 'p',
      darkModeSelector: '.dark-mode',
      // Ensure clean coexistence with Tailwind
      cssLayer: {
        name: 'primevue',
        order: 'tailwind-base, primevue, tailwind-utilities'
      }
    }
  }
})

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)
正确示例:带主题预设的 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: {
      prefix: 'p',
      darkModeSelector: '.dark-mode',
      // 保证和 Tailwind 共存无冲突
      cssLayer: {
        name: 'primevue',
        order: 'tailwind-base, primevue, tailwind-utilities'
      }
    }
  }
})

app.mount('#app')
正确示例:支持 tree-shaking 的组件注册
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)

Theme Customization with Design Tokens

基于设计令牌的主题自定义

Customize PrimeVue appearance through the design token system, NOT by overriding component internals:
Correct: Custom preset using definePreset()
typescript
// theme/my-preset.ts
import { definePreset } from '@primevue/themes'
import Aura from '@primevue/themes/aura'

const MyPreset = definePreset(Aura, {
  semantic: {
    primary: {
      50: '{indigo.50}',
      100: '{indigo.100}',
      200: '{indigo.200}',
      300: '{indigo.300}',
      400: '{indigo.400}',
      500: '{indigo.500}',
      600: '{indigo.600}',
      700: '{indigo.700}',
      800: '{indigo.800}',
      900: '{indigo.900}',
      950: '{indigo.950}'
    },
    colorScheme: {
      light: {
        surface: {
          0: '#ffffff',
          50: '{slate.50}',
          100: '{slate.100}',
          // ...
        }
      },
      dark: {
        surface: {
          0: '#0a0a0a',
          50: '{slate.950}',
          // ...
        }
      }
    }
  }
})

export default MyPreset
Usage in main.ts:
typescript
import MyPreset from './theme/my-preset'

app.use(PrimeVue, {
  theme: {
    preset: MyPreset
  }
})
通过设计令牌系统自定义 PrimeVue 外观,不要重写组件内部样式:
正确示例:使用 definePreset() 实现自定义预设
typescript
// theme/my-preset.ts
import { definePreset } from '@primevue/themes'
import Aura from '@primevue/themes/aura'

const MyPreset = definePreset(Aura, {
  semantic: {
    primary: {
      50: '{indigo.50}',
      100: '{indigo.100}',
      200: '{indigo.200}',
      300: '{indigo.300}',
      400: '{indigo.400}',
      500: '{indigo.500}',
      600: '{indigo.600}',
      700: '{indigo.700}',
      800: '{indigo.800}',
      900: '{indigo.900}',
      950: '{indigo.950}'
    },
    colorScheme: {
      light: {
        surface: {
          0: '#ffffff',
          50: '{slate.50}',
          100: '{slate.100}',
          // ...
        }
      },
      dark: {
        surface: {
          0: '#0a0a0a',
          50: '{slate.950}',
          // ...
        }
      }
    }
  }
})

export default MyPreset
main.ts 中使用:
typescript
import MyPreset from './theme/my-preset'

app.use(PrimeVue, {
  theme: {
    preset: MyPreset
  }
})

Using Components Natively

原生使用组件

Use PrimeVue components with their built-in props — do not restyle them:
Correct: Use built-in severity and variant props
vue
<template>
  <!-- Use severity prop for color variants -->
  <Button label="Save" severity="success" />
  <Button label="Delete" severity="danger" />
  <Button label="Info" severity="info" outlined />

  <!-- Use size prop -->
  <Button label="Small" size="small" />
  <Button label="Large" size="large" />

  <!-- Use style variant props -->
  <Button label="Text" text />
  <Button label="Outlined" outlined />
  <Button label="Rounded" rounded />
  <Button label="Raised" raised />

  <!-- Use icon prop -->
  <Button label="Search" icon="pi pi-search" />
  <Button icon="pi pi-check" rounded aria-label="Confirm" />
</template>
Correct: Use slot API for content customization
vue
<template>
  <DataTable :value="users" stripedRows paginator :rows="10">
    <Column field="name" header="Name" sortable />
    <Column field="status" header="Status">
      <template #body="{ data }">
        <Tag :value="data.status" :severity="getStatusSeverity(data.status)" />
      </template>
    </Column>
  </DataTable>
</template>
使用 PrimeVue 组件自带的 props,不要重写样式:
正确示例:使用内置的 severity 和 variant props
vue
<template>
  <!-- 使用 severity prop 实现颜色变体 -->
  <Button label="保存" severity="success" />
  <Button label="删除" severity="danger" />
  <Button label="信息" severity="info" outlined />

  <!-- 使用 size prop -->
  <Button label="小尺寸" size="small" />
  <Button label="大尺寸" size="large" />

  <!-- 使用样式变体 props -->
  <Button label="文字按钮" text />
  <Button label="描边按钮" outlined />
  <Button label="圆角按钮" rounded />
  <Button label="凸起按钮" raised />

  <!-- 使用 icon prop -->
  <Button label="搜索" icon="pi pi-search" />
  <Button icon="pi pi-check" rounded aria-label="确认" />
</template>
正确示例:使用 slot API 自定义内容
vue
<template>
  <DataTable :value="users" stripedRows paginator :rows="10">
    <Column field="name" header="姓名" sortable />
    <Column field="status" header="状态">
      <template #body="{ data }">
        <Tag :value="data.status" :severity="getStatusSeverity(data.status)" />
      </template>
    </Column>
  </DataTable>
</template>

What NEVER to Do with PrimeVue

PrimeVue 绝对禁止的操作

NEVER use PassThrough (pt) to restyle components:
vue
<!-- INCORRECT: Fighting the framework -->
<Button
  label="Submit"
  :pt="{
    root: { class: 'bg-blue-600 rounded-lg px-4 py-2' },
    label: { class: 'font-medium' }
  }"
/>

<!-- CORRECT: Use the component natively -->
<Button label="Submit" />
NEVER use unstyled mode to rebuild components:
typescript
// INCORRECT: Stripping the framework and rebuilding it
app.use(PrimeVue, { unstyled: true })

// CORRECT: Use a theme preset
app.use(PrimeVue, { theme: { preset: Aura } })
NEVER create wrapper components to override styling:
vue
<!-- INCORRECT: Unnecessary abstraction -->
<!-- components/AppButton.vue -->
<Button v-bind="$attrs" :pt="{ root: { class: customClasses } }">
  <slot />
</Button>

<!-- CORRECT: Use Button directly everywhere -->
<Button label="Submit" severity="primary" />
NEVER target PrimeVue internal CSS classes:
css
/* INCORRECT: Fragile, breaks on updates */
.p-button-label { font-weight: 700; }
.p-datatable-header { background: #f0f0f0; }

/* CORRECT: Customize through design tokens in definePreset() */
永远不要使用 PassThrough (pt) 重写组件样式:
vue
<!-- 错误示例:对抗框架 -->
<Button
  label="提交"
  :pt="{
    root: { class: 'bg-blue-600 rounded-lg px-4 py-2' },
    label: { class: 'font-medium' }
  }"
/>

<!-- 正确示例:原生使用组件 -->
<Button label="提交" />
永远不要使用无样式模式重建组件:
typescript
// 错误示例:剥离框架样式自行重建
app.use(PrimeVue, { unstyled: true })

// 正确示例:使用主题预设
app.use(PrimeVue, { theme: { preset: Aura } })
永远不要创建仅用于覆盖样式的包装组件:
vue
<!-- 错误示例:不必要的抽象 -->
<!-- components/AppButton.vue -->
<Button v-bind="$attrs" :pt="{ root: { class: customClasses } }">
  <slot />
</Button>

<!-- 正确示例:所有地方直接使用 Button -->
<Button label="提交" severity="primary" />
永远不要直接修改 PrimeVue 内部 CSS 类:
css
/* 错误示例:脆弱,版本更新会失效 */
.p-button-label { font-weight: 700; }
.p-datatable-header { background: #f0f0f0; }

/* 正确示例:在 definePreset() 中通过设计令牌自定义 */

Tailwind and PrimeVue Coexistence

Tailwind 和 PrimeVue 共存

Tailwind and PrimeVue serve different roles — do not mix their responsibilities:
  • PrimeVue owns: component appearance, internal spacing, interactive states, component variants
  • Tailwind owns: page layout, spacing between components, custom non-library elements, typography on non-library elements
Correct: Tailwind for layout, PrimeVue components used natively
vue
<template>
  <!-- Tailwind handles the page layout -->
  <div class="mx-auto max-w-4xl space-y-6 p-6">
    <h1 class="text-2xl font-bold text-gray-900 dark:text-white">Users</h1>

    <!-- PrimeVue components used as-is -->
    <DataTable :value="users" stripedRows paginator :rows="10">
      <Column field="name" header="Name" sortable />
      <Column field="email" header="Email" sortable />
      <Column field="role" header="Role" />
    </DataTable>

    <!-- Tailwind for spacing between PrimeVue components -->
    <div class="flex gap-3">
      <Button label="Add User" icon="pi pi-plus" />
      <Button label="Export" icon="pi pi-download" severity="secondary" outlined />
    </div>
  </div>
</template>
INCORRECT: Using Tailwind to restyle PrimeVue components
vue
<template>
  <!-- WRONG: Tailwind classes overriding PrimeVue button appearance -->
  <Button label="Submit" class="rounded-full bg-indigo-600 px-8 py-3 shadow-xl" />
</template>
Tailwind 和 PrimeVue 职责不同,不要混淆两者的功能边界:
  • PrimeVue 负责:组件外观、内部间距、交互状态、组件变体
  • Tailwind 负责:页面布局、组件间间距、自定义非库元素、非库元素的排版
正确示例:Tailwind 处理布局,原生使用 PrimeVue 组件
vue
<template>
  <!-- Tailwind 处理页面布局 -->
  <div class="mx-auto max-w-4xl space-y-6 p-6">
    <h1 class="text-2xl font-bold text-gray-900 dark:text-white">用户管理</h1>

    <!-- 原生使用 PrimeVue 组件 -->
    <DataTable :value="users" stripedRows paginator :rows="10">
      <Column field="name" header="姓名" sortable />
      <Column field="email" header="邮箱" sortable />
      <Column field="role" header="角色" />
    </DataTable>

    <!-- Tailwind 处理 PrimeVue 组件之间的间距 -->
    <div class="flex gap-3">
      <Button label="添加用户" icon="pi pi-plus" />
      <Button label="导出" icon="pi pi-download" severity="secondary" outlined />
    </div>
  </div>
</template>
错误示例:使用 Tailwind 重写 PrimeVue 组件样式
vue
<template>
  <!-- 错误:Tailwind 类覆盖 PrimeVue 按钮外观 -->
  <Button label="提交" class="rounded-full bg-indigo-600 px-8 py-3 shadow-xl" />
</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>
正确示例:基于 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: '确认删除',
      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

Toast 通知

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 }
}
正确示例:基于 composable 的 Toast 服务
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
违背了 utility-first 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 - 支持 tree-shaking 的导入
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 属性和 label -->
  <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 和 composables
  • useFetch/useAsyncData:使用 Nuxt 数据获取 composables 实现 SSR 兼容的数据加载
  • definePageMeta:用于页面级元数据和中间件配置
  • 服务端路由:使用
    server/api/
    目录编写 API 接口
  • 运行时配置:使用
    useRuntimeConfig()
    获取环境变量

References

参考资料

Vue.js

Vue.js

Tailwind CSS

Tailwind CSS

PrimeVue

PrimeVue