AgentSkillsCN

vuetify

Vuetify 3种组件模式、主题化设计、数据表格、表单、网格系统。触发条件:在使用Vuetify组件——v-btn、v-card、v-data-table、v-form、v-dialog时。

SKILL.md
--- frontmatter
name: vuetify
description: >
  Vuetify 3 component patterns, theming, data tables, forms, grid system.
  Trigger: When using Vuetify components - v-btn, v-card, v-data-table, v-form, v-dialog.
license: Apache-2.0
metadata:
  author: gentleman-programming
  version: "1.0"

When to Use

  • Building UI with Vuetify components
  • Creating forms with validation
  • Configuring themes (dark mode, custom colors)
  • Setting up data tables with pagination/sorting
  • Using the grid system (v-container, v-row, v-col)
  • Navigation patterns (app-bar, drawer, bottom-nav)

Nuxt 4 Integration

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['vuetify-nuxt-module'],
  vuetify: {
    moduleOptions: {
      treeshaking: true,
      styles: { configFile: 'assets/settings.scss' },
    },
    vuetifyOptions: {
      theme: {
        defaultTheme: 'gentleman',
        themes: {
          gentleman: {
            dark: true,
            colors: {
              primary: '#7FB4CA',
              secondary: '#A3B5D6',
              accent: '#E0C15A',
              success: '#B7CC85',
              error: '#CB7C94',
              background: '#1a1b26',
              surface: '#24283b',
            },
          },
        },
      },
    },
  },
})

Critical Patterns

Theming

typescript
// plugins/vuetify.ts (standalone Vue) or nuxt.config.ts (Nuxt)
import { createVuetify } from 'vuetify'

export default createVuetify({
  theme: {
    defaultTheme: 'dark',
    themes: {
      dark: {
        dark: true,
        colors: {
          primary: '#7FB4CA',
          secondary: '#A3B5D6',
          accent: '#E0C15A',
          error: '#CB7C94',
          background: '#1a1b26',
          surface: '#24283b',
        },
      },
      light: {
        dark: false,
        colors: {
          primary: '#1976D2',
          secondary: '#424242',
        },
      },
    },
  },
})
vue
<!-- Toggle theme -->
<script setup>
import { useTheme } from 'vuetify'
const theme = useTheme()
const toggleTheme = () => {
  theme.global.name.value = theme.global.current.value.dark ? 'light' : 'dark'
}
</script>

Grid System

vue
<template>
  <v-container>
    <v-row>
      <v-col cols="12" md="6" lg="4">
        <!-- Full width mobile, half tablet, third desktop -->
      </v-col>
      <v-col cols="12" md="6" lg="4">
        <!-- Content -->
      </v-col>
    </v-row>
  </v-container>
</template>

<!-- Responsive with useDisplay -->
<script setup>
import { useDisplay } from 'vuetify'
const { mobile, mdAndUp, lgAndUp, name } = useDisplay()
</script>

Breakpoints: xs (<600), sm (600-959), md (960-1279), lg (1280-1919), xl (1920-2559), xxl (2560+)


Forms & Validation

vue
<template>
  <v-form ref="form" v-model="valid" @submit.prevent="onSubmit">
    <v-text-field
      v-model="email"
      label="Email"
      :rules="emailRules"
      type="email"
      required
    />
    <v-text-field
      v-model="password"
      label="Password"
      :rules="passwordRules"
      type="password"
      required
    />
    <v-select
      v-model="role"
      label="Role"
      :items="['Admin', 'User', 'Moderator']"
      :rules="[v => !!v || 'Role is required']"
    />
    <v-btn type="submit" :disabled="!valid" color="primary">Submit</v-btn>
  </v-form>
</template>

<script setup lang="ts">
const form = ref()
const valid = ref(false)
const email = ref('')
const password = ref('')
const role = ref('')

// Validation rules — array of functions returning true or error string
const emailRules = [
  (v: string) => !!v || 'Email is required',
  (v: string) => /.+@.+\..+/.test(v) || 'Email must be valid',
]

const passwordRules = [
  (v: string) => !!v || 'Password is required',
  (v: string) => v.length >= 8 || 'Min 8 characters',
]

async function onSubmit() {
  const { valid } = await form.value.validate()
  if (!valid) return
  // Submit logic
}

// Reset form
function resetForm() {
  form.value.reset()       // Clear values
  form.value.resetValidation() // Clear errors only
}
</script>

Reusable Validation Rules

typescript
// utils/validation.ts
export const rules = {
  required: (v: any) => !!v || 'Field is required',
  email: (v: string) => /.+@.+\..+/.test(v) || 'Invalid email',
  minLength: (min: number) => (v: string) => v.length >= min || `Min ${min} characters`,
  maxLength: (max: number) => (v: string) => v.length <= max || `Max ${max} characters`,
  numeric: (v: string) => /^\d+$/.test(v) || 'Must be numeric',
}

Data Tables

Client-Side

vue
<template>
  <v-data-table
    :headers="headers"
    :items="users"
    :search="search"
    :items-per-page="10"
    :sort-by="[{ key: 'name', order: 'asc' }]"
  >
    <template #top>
      <v-text-field v-model="search" label="Search" prepend-inner-icon="mdi-magnify" />
    </template>

    <template #item.actions="{ item }">
      <v-btn icon="mdi-pencil" size="small" @click="editUser(item)" />
      <v-btn icon="mdi-delete" size="small" color="error" @click="deleteUser(item)" />
    </template>
  </v-data-table>
</template>

<script setup lang="ts">
const search = ref('')
const headers = [
  { title: 'Name', key: 'name', sortable: true },
  { title: 'Email', key: 'email', sortable: true },
  { title: 'Role', key: 'role', sortable: true },
  { title: 'Actions', key: 'actions', sortable: false },
]
</script>

Server-Side Pagination

vue
<template>
  <v-data-table-server
    :headers="headers"
    :items="items"
    :items-length="totalItems"
    :loading="loading"
    :items-per-page="itemsPerPage"
    @update:options="loadItems"
  >
  </v-data-table-server>
</template>

<script setup lang="ts">
const items = ref([])
const totalItems = ref(0)
const loading = ref(false)
const itemsPerPage = ref(10)

async function loadItems({ page, itemsPerPage, sortBy, search }: any) {
  loading.value = true
  const { data, total } = await $fetch('/api/users', {
    query: {
      page,
      limit: itemsPerPage,
      sortBy: sortBy[0]?.key,
      sortOrder: sortBy[0]?.order,
      search,
    },
  })
  items.value = data
  totalItems.value = total
  loading.value = false
}
</script>

Navigation Layout

vue
<!-- layouts/default.vue -->
<template>
  <v-app>
    <v-app-bar color="primary" density="comfortable">
      <v-app-bar-nav-icon @click="drawer = !drawer" />
      <v-app-bar-title>My App</v-app-bar-title>
      <v-spacer />
      <v-btn icon="mdi-theme-light-dark" @click="toggleTheme" />
    </v-app-bar>

    <v-navigation-drawer v-model="drawer" :rail="rail" permanent @click="rail = false">
      <v-list nav>
        <v-list-item
          v-for="item in navItems"
          :key="item.to"
          :prepend-icon="item.icon"
          :title="item.title"
          :to="item.to"
        />
      </v-list>
    </v-navigation-drawer>

    <v-main>
      <v-container>
        <slot />
      </v-container>
    </v-main>
  </v-app>
</template>

<script setup>
const drawer = ref(true)
const rail = ref(false)
const navItems = [
  { title: 'Dashboard', icon: 'mdi-view-dashboard', to: '/' },
  { title: 'Users', icon: 'mdi-account-group', to: '/users' },
  { title: 'Settings', icon: 'mdi-cog', to: '/settings' },
]
</script>

Dialogs & Overlays

vue
<!-- Confirmation Dialog Pattern -->
<template>
  <v-dialog v-model="dialog" max-width="500" persistent>
    <v-card>
      <v-card-title>{{ title }}</v-card-title>
      <v-card-text>{{ message }}</v-card-text>
      <v-card-actions>
        <v-spacer />
        <v-btn variant="text" @click="dialog = false">Cancel</v-btn>
        <v-btn color="error" variant="flat" :loading="loading" @click="confirm">
          Confirm
        </v-btn>
      </v-card-actions>
    </v-card>
  </v-dialog>
</template>

<!-- Menu -->
<v-menu>
  <template #activator="{ props }">
    <v-btn v-bind="props" icon="mdi-dots-vertical" />
  </template>
  <v-list>
    <v-list-item v-for="action in actions" :key="action.title" @click="action.handler">
      <v-list-item-title>{{ action.title }}</v-list-item-title>
    </v-list-item>
  </v-list>
</v-menu>

<!-- Tooltip -->
<v-tooltip text="Delete this item" location="top">
  <template #activator="{ props }">
    <v-btn v-bind="props" icon="mdi-delete" color="error" />
  </template>
</v-tooltip>

Composables

typescript
import { useDisplay, useTheme, useLayout } from 'vuetify'

// Responsive breakpoints
const { mobile, mdAndUp, lgAndUp, name, width, height } = useDisplay()

// Theme control
const theme = useTheme()
theme.global.name.value = 'dark'
const isDark = computed(() => theme.global.current.value.dark)

// Layout measurements
const { mainRect, mainStyles } = useLayout()

Decision Tree

code
Need a data grid?               → v-data-table (client) or v-data-table-server (API)
Need form validation?            → v-form with :rules array
Need responsive layout?          → v-container + v-row + v-col
Need sidebar navigation?         → v-navigation-drawer (rail mode for collapsed)
Need confirmation action?        → v-dialog with persistent prop
Need dropdown actions?           → v-menu with activator slot
Need feedback/loading?           → v-snackbar, v-progress-linear, v-skeleton-loader
Need theme toggle?               → useTheme() composable
Need responsive logic in JS?     → useDisplay() composable

Component Prop Conventions

vue
<!-- Button variants -->
<v-btn variant="flat">Primary</v-btn>      <!-- Filled -->
<v-btn variant="outlined">Outlined</v-btn>
<v-btn variant="text">Text</v-btn>
<v-btn variant="tonal">Tonal</v-btn>

<!-- Density -->
<v-text-field density="compact" />          <!-- compact, comfortable, default -->

<!-- Loading states -->
<v-btn :loading="saving" @click="save">Save</v-btn>
<v-skeleton-loader type="card" :loading="loading" />

<!-- Snackbar (feedback) -->
<v-snackbar v-model="snackbar" :color="snackColor" :timeout="3000">
  {{ snackMessage }}
</v-snackbar>