# UI 交互规范

## 数据表格

```vue
<template>
  <v-data-table
    :headers="headers"
    :items="items"
    :loading="loading"
    fixed-header
    height="calc(100vh - 200px)"
  >
    <!-- 截断文本 + Tooltip -->
    <template #item.description="{ value }">
      <v-tooltip :text="value" location="top">
        <template #activator="{ props }">
          <span
            v-bind="props"
            class="text-truncate d-inline-block"
            style="max-width: 200px"
          >
            {{ value }}
          </span>
        </template>
      </v-tooltip>
    </template>

    <!-- 空状态 -->
    <template #no-data>
      <v-empty-state
        icon="mdi-database-off"
        title="暂无数据"
        text="请尝试调整筛选条件或新建记录"
      >
        <template #actions>
          <v-btn color="primary" @click="refresh">刷新</v-btn>
        </template>
      </v-empty-state>
    </template>
  </v-data-table>
</template>
```

## 虚拟滚动列表

```vue
<template>
  <v-virtual-scroll :items="largeList" height="400" item-height="64">
    <template #default="{ item }">
      <v-list-item :title="item.name" :subtitle="item.description">
        <template #append>
          <v-btn icon="mdi-chevron-right" variant="text" />
        </template>
      </v-list-item>
    </template>
  </v-virtual-scroll>
</template>
```

## 骨架屏加载

```vue
<template>
  <div>
    <!-- 表格骨架 -->
    <v-skeleton-loader v-if="loading" type="table-heading, table-row@5" />

    <!-- 卡片骨架 -->
    <v-skeleton-loader v-if="loading" type="card" />

    <!-- 列表骨架 -->
    <v-skeleton-loader v-if="loading" type="list-item-avatar-two-line@3" />

    <!-- 文章骨架 -->
    <v-skeleton-loader v-if="loading" type="article" />

    <!-- 自定义组合骨架 -->
    <v-skeleton-loader
      v-if="loading"
      type="heading, list-item-two-line@3, actions"
    />

    <!-- 实际内容 -->
    <template v-else>...</template>
  </div>
</template>
```

## 空状态组件

```vue
<template>
  <v-empty-state
    :icon="icon"
    :title="title"
    :text="description"
  >
    <template #actions>
      <v-btn v-if="showRefresh" variant="outlined" @click="$emit('refresh')">
        刷新
      </v-btn>
      <v-btn v-if="showCreate" color="primary" @click="$emit('create')">
        新建
      </v-btn>
    </template>
  </v-empty-state>
</template>

<script setup lang="ts">
interface Props {
  icon?: string
  title?: string
  description?: string
  showRefresh?: boolean
  showCreate?: boolean
}

withDefaults(defineProps<Props>(), {
  icon: 'mdi-folder-open-outline',
  title: '暂无数据',
  description: '当前没有可显示的内容',
  showRefresh: true,
  showCreate: false,
})

defineEmits<{
  (e: 'refresh'): void
  (e: 'create'): void
}>()
</script>
```

## 多行文本截断

```vue
<template>
  <div class="line-clamp-container">
    <p :class="{ 'line-clamp-2': !expanded }">{{ longText }}</p>
    <v-btn
      v-if="needsExpand"
      variant="text"
      size="small"
      @click="expanded = !expanded"
    >
      {{ expanded ? '收起' : '展开' }}
    </v-btn>
  </div>
</template>

<style scoped>
.line-clamp-2 {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
</style>
```

## 确认对话框

```vue
<template>
  <v-dialog v-model="dialog" max-width="400" persistent>
    <v-card>
      <v-card-title class="d-flex align-center">
        <v-icon :color="iconColor" class="mr-2">{{ icon }}</v-icon>
        {{ title }}
      </v-card-title>
      <v-card-text>{{ message }}</v-card-text>
      <v-card-actions>
        <v-spacer />
        <v-btn variant="text" @click="cancel">取消</v-btn>
        <v-btn :color="confirmColor" :loading="loading" @click="confirm">
          {{ confirmText }}
        </v-btn>
      </v-card-actions>
    </v-card>
  </v-dialog>
</template>

<script setup lang="ts">
interface Props {
  title?: string
  message: string
  icon?: string
  iconColor?: string
  confirmText?: string
  confirmColor?: string
}

withDefaults(defineProps<Props>(), {
  title: '确认操作',
  icon: 'mdi-alert-circle-outline',
  iconColor: 'warning',
  confirmText: '确认',
  confirmColor: 'primary',
})

const dialog = defineModel<boolean>({ default: false })
const loading = ref(false)

const emit = defineEmits<{
  (e: 'confirm'): void
  (e: 'cancel'): void
}>()

function confirm() {
  emit('confirm')
}

function cancel() {
  dialog.value = false
  emit('cancel')
}
</script>
```

## 表单验证

```vue
<template>
  <v-form ref="formRef" v-model="valid" @submit.prevent="submit">
    <v-text-field
      v-model="form.name"
      :rules="rules.name"
      label="姓名"
      required
    />
    <v-text-field
      v-model="form.email"
      :rules="rules.email"
      label="邮箱"
      type="email"
    />
    <v-btn type="submit" :disabled="!valid" :loading="loading">
      提交
    </v-btn>
  </v-form>
</template>

<script setup lang="ts">
import type { VForm } from 'vuetify/components'

const formRef = ref<VForm | null>(null)
const valid = ref(false)
const loading = ref(false)

const form = reactive({
  name: '',
  email: '',
})

const rules = {
  name: [
    (v: string) => !!v || '姓名不能为空',
    (v: string) => v.length <= 20 || '姓名不能超过20个字符',
  ],
  email: [
    (v: string) => !!v || '邮箱不能为空',
    (v: string) => /.+@.+\..+/.test(v) || '请输入有效的邮箱地址',
  ],
}

async function submit() {
  const { valid } = await formRef.value!.validate()
  if (!valid) return

  loading.value = true
  try {
    await api.submit(form)
  } finally {
    loading.value = false
  }
}
</script>
```

## Snackbar 全局通知

```typescript
// composables/useSnackbar.ts
import { ref } from 'vue'

interface SnackbarOptions {
  text: string
  color?: string
  timeout?: number
}

const snackbar = ref<SnackbarOptions & { show: boolean }>({
  show: false,
  text: '',
  color: 'success',
  timeout: 3000,
})

export function useSnackbar() {
  function show(options: SnackbarOptions) {
    snackbar.value = { ...snackbar.value, ...options, show: true }
  }

  function success(text: string) {
    show({ text, color: 'success' })
  }

  function error(text: string) {
    show({ text, color: 'error', timeout: 5000 })
  }

  function warning(text: string) {
    show({ text, color: 'warning' })
  }

  function info(text: string) {
    show({ text, color: 'info' })
  }

  return { snackbar, show, success, error, warning, info }
}
```

```vue
<!-- App.vue 中使用 -->
<template>
  <v-app>
    <router-view />

    <v-snackbar
      v-model="snackbar.show"
      :color="snackbar.color"
      :timeout="snackbar.timeout"
    >
      {{ snackbar.text }}
    </v-snackbar>
  </v-app>
</template>

<script setup lang="ts">
import { useSnackbar } from '@/composables/useSnackbar'

const { snackbar } = useSnackbar()
</script>
```
