13 KiB
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