Files
gt-agent-company/.claude/COMPONENT-PATTERNS.md
sexygoat 3c62cc1cd1 first
2026-03-31 18:41:52 +08:00

13 KiB

Common Component Patterns

Quick copy-paste patterns for frequent UI components.

🔘 Buttons

Primary CTA Button

<button
  class="
    bg-primary text-white
    px-6 py-3 rounded-8px
    min-h-44px
    font-600 text-16px
    transition-colors duration-200
    hover:bg-[#e65a00]
    active:bg-[#e65a00]
    cursor-pointer
    disabled:opacity-60 disabled:cursor-not-allowed
  "
  :disabled="loading"
  @click="handleSubmit"
>
  <i v-if="loading" class="i-mdi-loading animate-spin w-20px h-20px mr-2"></i>
  {{ loading ? '提交中...' : '确认提交' }}
</button>

Secondary/Outline Button

<button
  class="
    bg-transparent border-2 border-primary text-primary
    px-6 py-3 rounded-8px
    min-h-44px
    font-600 text-16px
    transition-all duration-200
    hover:bg-primary hover:text-white
    cursor-pointer
  "
  @click="handleCancel"
>
  取消
</button>

Icon Button (Accessible)

<button
  aria-label="关闭"
  class="
    min-h-44px min-w-44px
    center
    rounded-full
    transition-colors duration-200
    hover:bg-gray-100
    cursor-pointer
  "
  @click="handleClose"
>
  <i class="i-mdi-close w-24px h-24px text-gray-600"></i>
</button>

📋 Cards

Glass Card (Primary Style)

<view
  class="
    bg-white/80 dark:bg-white/10
    backdrop-blur-md
    border border-gray-200 dark:border-white/20
    rounded-16px
    p-6
    shadow-lg
  "
>
  <text class="text-20px font-600 text-main block mb-2">
    Card Title
  </text>
  <text class="text-14px text-content block">
    Card description content goes here.
  </text>
</view>

Flat Card (Secondary Style)

<view
  class="
    bg-white dark:bg-[#2d2d2d]
    border border-gray-200 dark:border-gray-700
    rounded-8px
    p-4
    shadow-sm
  "
>
  <view class="flex items-center gap-3">
    <i class="i-mdi-information w-24px h-24px text-primary"></i>
    <text class="text-16px text-main">Information</text>
  </view>
</view>

Interactive Card (Clickable)

<view
  class="
    bg-white/80 dark:bg-white/10
    backdrop-blur-md
    border border-gray-200 dark:border-white/20
    rounded-16px
    p-6
    shadow-lg
    transition-colors duration-200
    hover:bg-white/90 dark:hover:bg-white/15
    cursor-pointer
  "
  @click="handleCardClick"
>
  <view class="flex items-center justify-between">
    <text class="text-18px font-600 text-main">Clickable Card</text>
    <i class="i-mdi-chevron-right w-24px h-24px text-gray-400"></i>
  </view>
</view>

📝 Forms

Form Input (Accessible)

<view class="mb-4">
  <label
    for="email-input"
    class="block text-14px font-500 text-main mb-2"
  >
    邮箱地址 <span class="text-error">*</span>
  </label>
  <input
    id="email-input"
    v-model="email"
    type="email"
    inputmode="email"
    class="
      w-full px-4 py-3 min-h-44px
      bg-white dark:bg-[#2d2d2d]
      border-2 border-gray-200 dark:border-gray-700
      rounded-8px
      text-16px text-main
      focus:border-primary focus:outline-none
      transition-colors duration-200
      placeholder:text-gray-400
    "
    placeholder="输入您的邮箱"
    :aria-invalid="!!errors.email"
    :aria-describedby="errors.email ? 'email-error' : undefined"
  />
  <text
    v-if="errors.email"
    id="email-error"
    role="alert"
    class="text-12px text-error mt-1 block"
  >
    {{ errors.email }}
  </text>
</view>

Select/Dropdown

<view class="mb-4">
  <label
    for="category-select"
    class="block text-14px font-500 text-main mb-2"
  >
    选择分类
  </label>
  <select
    id="category-select"
    v-model="selectedCategory"
    class="
      w-full px-4 py-3 min-h-44px
      bg-white dark:bg-[#2d2d2d]
      border-2 border-gray-200 dark:border-gray-700
      rounded-8px
      text-16px text-main
      cursor-pointer
      focus:border-primary focus:outline-none
    "
  >
    <option value="">请选择...</option>
    <option value="1">分类一</option>
    <option value="2">分类二</option>
  </select>
</view>

Checkbox (Accessible)

<view class="flex items-center gap-2 min-h-44px">
  <input
    id="agree-terms"
    v-model="agreedToTerms"
    type="checkbox"
    class="
      w-20px h-20px
      accent-primary
      cursor-pointer
    "
  />
  <label
    for="agree-terms"
    class="text-14px text-content cursor-pointer"
  >
    我同意 <text class="text-primary">服务条款</text>  <text class="text-primary">隐私政策</text>
  </label>
</view>

🧭 Navigation

Floating Top Navbar

<view
  class="
    fixed top-4 left-4 right-4
    bg-white/90 dark:bg-[#1a1a1a]/90
    backdrop-blur-md
    border border-gray-200 dark:border-white/20
    rounded-full
    px-6 py-3
    shadow-lg
    z-50
  "
>
  <view class="flex justify-between items-center">
    <view class="flex items-center gap-2">
      <i class="i-mdi-home w-24px h-24px text-primary"></i>
      <text class="font-600 text-18px text-main">GT Agent</text>
    </view>

    <button
      aria-label="打开菜单"
      class="min-h-44px min-w-44px center cursor-pointer"
      @click="toggleMenu"
    >
      <i class="i-mdi-menu w-24px h-24px text-main"></i>
    </button>
  </view>
</view>

<!-- Account for navbar height -->
<view class="pt-72px">
  <!-- Main content -->
</view>

Bottom Navigation (Safe Area)

<view
  class="
    fixed bottom-0 left-0 right-0
    bg-white/90 dark:bg-[#1a1a1a]/90
    backdrop-blur-md
    border-t border-gray-200 dark:border-white/20
    pb-safe
    z-50
  "
>
  <view class="flex justify-around items-center py-2">
    <button
      v-for="item in navItems"
      :key="item.id"
      :aria-label="item.label"
      :class="{
        'text-primary': currentRoute === item.route,
        'text-gray-400': currentRoute !== item.route,
      }"
      class="
        flex flex-col items-center gap-1
        min-h-44px min-w-44px
        center
        cursor-pointer
        transition-colors duration-200
      "
      @click="navigateTo(item.route)"
    >
      <i :class="item.icon" class="w-24px h-24px"></i>
      <text class="text-12px">{{ item.label }}</text>
    </button>
  </view>
</view>

<script setup lang="ts">
const navItems = [
  { id: 1, label: '首页', icon: 'i-mdi-home', route: '/home' },
  { id: 2, label: '我的', icon: 'i-mdi-account', route: '/profile' },
]
</script>

Breadcrumbs

<nav aria-label="面包屑导航" class="mb-6">
  <ol class="flex items-center gap-2 text-14px">
    <li>
      <a href="/" class="text-primary hover:underline">首页</a>
    </li>
    <li class="text-gray-400">
      <i class="i-mdi-chevron-right w-16px h-16px"></i>
    </li>
    <li>
      <a href="/products" class="text-primary hover:underline">产品</a>
    </li>
    <li class="text-gray-400">
      <i class="i-mdi-chevron-right w-16px h-16px"></i>
    </li>
    <li class="text-content" aria-current="page">产品详情</li>
  </ol>
</nav>

📊 Data Display

List Item (Interactive)

<view
  class="
    flex items-center justify-between
    p-4
    bg-white dark:bg-[#2d2d2d]
    border-b border-gray-200 dark:border-gray-700
    min-h-44px
    transition-colors duration-200
    hover:bg-gray-50 dark:hover:bg-gray-800
    cursor-pointer
  "
  @click="handleItemClick(item)"
>
  <view class="flex items-center gap-3 flex-1">
    <i class="i-mdi-file-document w-24px h-24px text-primary"></i>
    <view class="flex-1">
      <text class="text-16px font-500 text-main block">{{ item.title }}</text>
      <text class="text-12px text-tips block mt-1">{{ item.subtitle }}</text>
    </view>
  </view>
  <i class="i-mdi-chevron-right w-24px h-24px text-gray-400"></i>
</view>

Badge/Tag

<!-- Status badge -->
<span
  :class="{
    'bg-success/10 text-success': status === 'active',
    'bg-warning/10 text-warning': status === 'pending',
    'bg-error/10 text-error': status === 'inactive',
  }"
  class="
    inline-flex items-center gap-1
    px-3 py-1
    rounded-full
    text-12px font-500
  "
>
  <i
    :class="{
      'i-mdi-check-circle': status === 'active',
      'i-mdi-clock': status === 'pending',
      'i-mdi-alert-circle': status === 'inactive',
    }"
    class="w-14px h-14px"
  ></i>
  {{ statusText }}
</span>

Empty State

<view class="center flex-col py-16">
  <i class="i-mdi-inbox w-64px h-64px text-gray-300 mb-4"></i>
  <text class="text-18px font-500 text-main mb-2">暂无数据</text>
  <text class="text-14px text-tips mb-6">
    当前没有可显示的内容
  </text>
  <button
    class="
      bg-primary text-white
      px-6 py-3 rounded-8px
      min-h-44px
      cursor-pointer
    "
    @click="handleRefresh"
  >
    刷新
  </button>
</view>

Loading States

Skeleton Loader

<view class="animate-pulse space-y-4 p-4">
  <!-- Title skeleton -->
  <view class="h-20px bg-gray-200 dark:bg-gray-700 rounded w-3/4"></view>

  <!-- Content skeletons -->
  <view class="space-y-2">
    <view class="h-16px bg-gray-200 dark:bg-gray-700 rounded"></view>
    <view class="h-16px bg-gray-200 dark:bg-gray-700 rounded w-5/6"></view>
    <view class="h-16px bg-gray-200 dark:bg-gray-700 rounded w-4/6"></view>
  </view>

  <!-- Image skeleton -->
  <view class="h-200px bg-gray-200 dark:bg-gray-700 rounded"></view>
</view>

Loading Spinner

<!-- Inline spinner -->
<view class="center py-8">
  <i class="i-mdi-loading animate-spin w-32px h-32px text-primary"></i>
</view>

<!-- With text -->
<view class="center flex-col gap-2 py-8">
  <i class="i-mdi-loading animate-spin w-32px h-32px text-primary"></i>
  <text class="text-14px text-tips">加载中...</text>
</view>

<!-- Full screen overlay -->
<view
  v-if="loading"
  class="
    fixed inset-0
    bg-black/50
    center flex-col gap-3
    z-999
  "
>
  <i class="i-mdi-loading animate-spin w-48px h-48px text-white"></i>
  <text class="text-16px text-white">加载中...</text>
</view>

🔔 Alerts & Toasts

Alert Box

<view
  role="alert"
  :class="{
    'bg-success/10 border-success/30 text-success': type === 'success',
    'bg-warning/10 border-warning/30 text-warning': type === 'warning',
    'bg-error/10 border-error/30 text-error': type === 'error',
    'bg-primary/10 border-primary/30 text-primary': type === 'info',
  }"
  class="
    flex items-start gap-3
    p-4
    border-l-4
    rounded-8px
    mb-4
  "
>
  <i
    :class="{
      'i-mdi-check-circle': type === 'success',
      'i-mdi-alert': type === 'warning',
      'i-mdi-alert-circle': type === 'error',
      'i-mdi-information': type === 'info',
    }"
    class="w-24px h-24px flex-shrink-0 mt-0.5"
  ></i>

  <view class="flex-1">
    <text class="text-16px font-600 block mb-1">{{ title }}</text>
    <text class="text-14px opacity-80">{{ message }}</text>
  </view>

  <button
    aria-label="关闭"
    class="min-h-44px min-w-44px center cursor-pointer"
    @click="handleClose"
  >
    <i class="i-mdi-close w-20px h-20px"></i>
  </button>
</view>

🎨 Modal/Dialog

Modal Overlay

<!-- Modal -->
<view
  v-if="isOpen"
  class="fixed inset-0 z-999 center p-4"
  @click.self="handleClose"
>
  <!-- Backdrop -->
  <view
    class="absolute inset-0 bg-black/50"
    @click="handleClose"
  ></view>

  <!-- Modal Content -->
  <view
    class="
      relative
      bg-white dark:bg-[#1a1a1a]
      rounded-16px
      p-6
      max-w-md w-full
      shadow-2xl
      animate-fade-in
    "
    role="dialog"
    aria-labelledby="modal-title"
    aria-modal="true"
  >
    <!-- Close button -->
    <button
      aria-label="关闭"
      class="
        absolute top-4 right-4
        min-h-44px min-w-44px
        center
        cursor-pointer
      "
      @click="handleClose"
    >
      <i class="i-mdi-close w-24px h-24px text-gray-400"></i>
    </button>

    <!-- Title -->
    <text id="modal-title" class="text-24px font-700 text-main block mb-4">
      Modal Title
    </text>

    <!-- Content -->
    <text class="text-16px text-content block mb-6">
      Modal content goes here.
    </text>

    <!-- Actions -->
    <view class="flex gap-3">
      <button
        class="
          flex-1
          bg-transparent border-2 border-gray-200
          text-gray-600
          px-6 py-3 rounded-8px
          min-h-44px
          cursor-pointer
        "
        @click="handleClose"
      >
        取消
      </button>
      <button
        class="
          flex-1
          bg-primary text-white
          px-6 py-3 rounded-8px
          min-h-44px
          cursor-pointer
        "
        @click="handleConfirm"
      >
        确认
      </button>
    </view>
  </view>
</view>

📐 Layout Utilities

Container

<view class="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
  <!-- Centered content with responsive padding -->
</view>

Responsive Grid

<view class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
  <view v-for="item in items" :key="item.id">
    <!-- Grid item -->
  </view>
</view>

Flex Layouts

<!-- Horizontal stack -->
<view class="flex items-center gap-4">
  <div>Item 1</div>
  <div>Item 2</div>
</view>

<!-- Vertical stack -->
<view class="flex flex-col gap-4">
  <div>Item 1</div>
  <div>Item 2</div>
</view>

<!-- Space between -->
<view class="flex items-center justify-between">
  <div>Left</div>
  <div>Right</div>
</view>

<!-- Centered -->
<view class="center">
  <div>Centered content</div>
</view>

Full Documentation: See .claude/DESIGN-SYSTEM.md