组件设计中的一致性原则
一致性是优秀用户体验的基石,也是设计系统成功的关键要素。本文将深入探讨如何在组件设计中建立和维护一致性,确保用户在使用产品时获得连贯、可预测的体验。
一致性的层次
1. 视觉一致性
视觉一致性是用户最直观感受到的一致性层面:
typescript
interface VisualConsistency {
colors: {
primary: string
secondary: string
semantic: {
success: string
warning: string
error: string
info: string
}
neutral: string[]
}
typography: {
fontFamily: string[]
fontSizes: number[]
fontWeights: number[]
lineHeights: number[]
letterSpacing: number[]
}
spacing: {
base: number
scale: number[]
margins: number[]
paddings: number[]
}
borders: {
radius: number[]
width: number[]
style: string[]
}
shadows: {
elevation: string[]
focus: string
hover: string
}
}
// Ghuo Design 视觉规范
const ghuoVisualSystem: VisualConsistency = {
colors: {
primary: '#1890ff',
secondary: '#722ed1',
semantic: {
success: '#52c41a',
warning: '#faad14',
error: '#f5222d',
info: '#1890ff'
},
neutral: [
'#ffffff', '#fafafa', '#f5f5f5', '#f0f0f0',
'#d9d9d9', '#bfbfbf', '#8c8c8c', '#595959',
'#434343', '#262626', '#1f1f1f', '#141414'
]
},
typography: {
fontFamily: ['-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto'],
fontSizes: [12, 14, 16, 18, 20, 24, 30, 38, 46, 56, 68],
fontWeights: [400, 500, 600, 700],
lineHeights: [1.2, 1.4, 1.5, 1.6],
letterSpacing: [-0.02, 0, 0.02, 0.04]
},
spacing: {
base: 4,
scale: [0, 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96],
margins: [0, 8, 16, 24, 32, 48],
paddings: [4, 8, 12, 16, 20, 24]
},
borders: {
radius: [0, 2, 4, 6, 8, 12, 16, 24],
width: [0, 1, 2, 4],
style: ['solid', 'dashed', 'dotted']
},
shadows: {
elevation: [
'none',
'0 1px 2px rgba(0, 0, 0, 0.05)',
'0 1px 3px rgba(0, 0, 0, 0.1)',
'0 4px 6px rgba(0, 0, 0, 0.1)',
'0 10px 15px rgba(0, 0, 0, 0.1)',
'0 20px 25px rgba(0, 0, 0, 0.1)'
],
focus: '0 0 0 2px rgba(24, 144, 255, 0.2)',
hover: '0 2px 4px rgba(0, 0, 0, 0.1)'
}
}2. 交互一致性
交互一致性确保用户在不同组件间有相似的操作体验:
typescript
interface InteractionConsistency {
states: {
default: ComponentState
hover: ComponentState
active: ComponentState
focus: ComponentState
disabled: ComponentState
loading: ComponentState
}
behaviors: {
clickable: ClickableBehavior
hoverable: HoverableBehavior
focusable: FocusableBehavior
draggable: DraggableBehavior
}
feedback: {
visual: VisualFeedback
audio: AudioFeedback
haptic: HapticFeedback
}
timing: {
transitions: number[]
animations: number[]
delays: number[]
}
}
interface ComponentState {
backgroundColor?: string
borderColor?: string
textColor?: string
opacity?: number
transform?: string
cursor?: string
}
interface ClickableBehavior {
preventDefault: boolean
stopPropagation: boolean
debounceTime: number
rippleEffect: boolean
}
// 统一的交互规范
const ghuoInteractionSystem: InteractionConsistency = {
states: {
default: {
opacity: 1,
cursor: 'default'
},
hover: {
opacity: 0.8,
cursor: 'pointer',
transform: 'translateY(-1px)'
},
active: {
opacity: 0.9,
transform: 'translateY(0)'
},
focus: {
opacity: 1,
cursor: 'pointer'
},
disabled: {
opacity: 0.5,
cursor: 'not-allowed'
},
loading: {
opacity: 0.7,
cursor: 'wait'
}
},
behaviors: {
clickable: {
preventDefault: false,
stopPropagation: false,
debounceTime: 300,
rippleEffect: true
},
hoverable: {
enterDelay: 100,
leaveDelay: 100
},
focusable: {
tabIndex: 0,
outlineStyle: 'solid',
outlineWidth: 2,
outlineOffset: 2
},
draggable: {
threshold: 5,
axis: 'both',
containment: 'parent'
}
},
feedback: {
visual: {
duration: 200,
easing: 'ease-out'
},
audio: {
enabled: false,
volume: 0.5
},
haptic: {
enabled: true,
intensity: 'light'
}
},
timing: {
transitions: [150, 200, 300, 500],
animations: [200, 300, 500, 800],
delays: [0, 100, 200, 300]
}
}3. 语义一致性
语义一致性确保相同的概念在不同场景下有相同的表达:
typescript
interface SemanticConsistency {
terminology: {
actions: Record<string, string>
states: Record<string, string>
objects: Record<string, string>
properties: Record<string, string>
}
iconography: {
actions: Record<string, string>
states: Record<string, string>
categories: Record<string, string>
}
messaging: {
success: string[]
warning: string[]
error: string[]
info: string[]
}
patterns: {
confirmation: string
deletion: string
saving: string
loading: string
}
}
const ghuoSemanticSystem: SemanticConsistency = {
terminology: {
actions: {
create: '创建',
edit: '编辑',
delete: '删除',
save: '保存',
cancel: '取消',
confirm: '确认',
submit: '提交',
reset: '重置'
},
states: {
loading: '加载中',
success: '成功',
error: '错误',
warning: '警告',
pending: '待处理',
completed: '已完成'
},
objects: {
user: '用户',
project: '项目',
task: '任务',
file: '文件',
folder: '文件夹',
document: '文档'
},
properties: {
name: '名称',
title: '标题',
description: '描述',
status: '状态',
date: '日期',
time: '时间'
}
},
iconography: {
actions: {
add: 'plus',
edit: 'edit',
delete: 'delete',
search: 'search',
filter: 'filter',
sort: 'sort',
download: 'download',
upload: 'upload'
},
states: {
success: 'check-circle',
error: 'close-circle',
warning: 'exclamation-circle',
info: 'info-circle',
loading: 'loading'
},
categories: {
user: 'user',
settings: 'setting',
dashboard: 'dashboard',
analytics: 'bar-chart',
messages: 'message'
}
},
messaging: {
success: [
'操作成功',
'保存成功',
'创建成功',
'更新成功',
'删除成功'
],
warning: [
'请注意',
'操作不可逆',
'数据可能丢失',
'权限不足'
],
error: [
'操作失败',
'网络错误',
'服务器错误',
'参数错误',
'权限不足'
],
info: [
'提示信息',
'操作指南',
'功能说明',
'版本更新'
]
},
patterns: {
confirmation: '确定要{action}{object}吗?',
deletion: '删除后无法恢复,确定要删除{object}吗?',
saving: '正在保存{object}...',
loading: '正在加载{object}...'
}
}组件一致性实现
基础组件规范
vue
<!-- 按钮组件一致性实现 -->
<template>
<button
:class="buttonClasses"
:disabled="disabled"
:type="htmlType"
@click="handleClick"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
@focus="handleFocus"
@blur="handleBlur"
>
<span v-if="loading" class="ghuo-button__loading">
<ghuo-icon name="loading" />
</span>
<span v-if="icon && !loading" class="ghuo-button__icon">
<ghuo-icon :name="icon" />
</span>
<span class="ghuo-button__content">
<slot />
</span>
</button>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useConsistentBehavior } from '@/composables/useConsistentBehavior'
interface ButtonProps {
type?: 'primary' | 'secondary' | 'danger' | 'ghost'
size?: 'small' | 'medium' | 'large'
disabled?: boolean
loading?: boolean
icon?: string
htmlType?: 'button' | 'submit' | 'reset'
block?: boolean
}
const props = withDefaults(defineProps<ButtonProps>(), {
type: 'primary',
size: 'medium',
htmlType: 'button'
})
const emit = defineEmits<{
click: [event: MouseEvent]
}>()
// 使用一致性行为组合函数
const {
isHovered,
isFocused,
handleMouseEnter,
handleMouseLeave,
handleFocus,
handleBlur
} = useConsistentBehavior()
const buttonClasses = computed(() => [
'ghuo-button',
`ghuo-button--${props.type}`,
`ghuo-button--${props.size}`,
{
'ghuo-button--disabled': props.disabled,
'ghuo-button--loading': props.loading,
'ghuo-button--block': props.block,
'ghuo-button--hovered': isHovered.value,
'ghuo-button--focused': isFocused.value
}
])
const handleClick = (event: MouseEvent) => {
if (props.disabled || props.loading) return
emit('click', event)
}
</script>
<style scoped>
.ghuo-button {
/* 基础样式 */
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid transparent;
border-radius: v-bind('ghuoVisualSystem.borders.radius[1]');
font-family: v-bind('ghuoVisualSystem.typography.fontFamily.join(", ")');
font-weight: v-bind('ghuoVisualSystem.typography.fontWeights[1]');
cursor: pointer;
transition: all v-bind('ghuoInteractionSystem.timing.transitions[1]')ms ease-out;
user-select: none;
/* 尺寸变体 */
&--small {
height: 24px;
padding: 0 v-bind('ghuoVisualSystem.spacing.paddings[1]')px;
font-size: v-bind('ghuoVisualSystem.typography.fontSizes[0]')px;
}
&--medium {
height: 32px;
padding: 0 v-bind('ghuoVisualSystem.spacing.paddings[3]')px;
font-size: v-bind('ghuoVisualSystem.typography.fontSizes[1]')px;
}
&--large {
height: 40px;
padding: 0 v-bind('ghuoVisualSystem.spacing.paddings[4]')px;
font-size: v-bind('ghuoVisualSystem.typography.fontSizes[2]')px;
}
/* 类型变体 */
&--primary {
background-color: v-bind('ghuoVisualSystem.colors.primary');
border-color: v-bind('ghuoVisualSystem.colors.primary');
color: #ffffff;
&:hover:not(.ghuo-button--disabled) {
opacity: v-bind('ghuoInteractionSystem.states.hover.opacity');
transform: v-bind('ghuoInteractionSystem.states.hover.transform');
}
}
&--secondary {
background-color: transparent;
border-color: v-bind('ghuoVisualSystem.colors.neutral[4]');
color: v-bind('ghuoVisualSystem.colors.neutral[8]');
&:hover:not(.ghuo-button--disabled) {
border-color: v-bind('ghuoVisualSystem.colors.primary');
color: v-bind('ghuoVisualSystem.colors.primary');
}
}
/* 状态样式 */
&--disabled {
opacity: v-bind('ghuoInteractionSystem.states.disabled.opacity');
cursor: v-bind('ghuoInteractionSystem.states.disabled.cursor');
}
&--loading {
opacity: v-bind('ghuoInteractionSystem.states.loading.opacity');
cursor: v-bind('ghuoInteractionSystem.states.loading.cursor');
}
&--focused {
box-shadow: v-bind('ghuoVisualSystem.shadows.focus');
}
/* 布局变体 */
&--block {
width: 100%;
}
}
.ghuo-button__loading {
margin-right: v-bind('ghuoVisualSystem.spacing.margins[1]')px;
animation: spin 1s linear infinite;
}
.ghuo-button__icon {
margin-right: v-bind('ghuoVisualSystem.spacing.margins[1]')px;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>一致性行为组合函数
typescript
// composables/useConsistentBehavior.ts
import { ref } from 'vue'
export function useConsistentBehavior() {
const isHovered = ref(false)
const isFocused = ref(false)
const isActive = ref(false)
const handleMouseEnter = () => {
isHovered.value = true
}
const handleMouseLeave = () => {
isHovered.value = false
}
const handleFocus = () => {
isFocused.value = true
}
const handleBlur = () => {
isFocused.value = false
}
const handleMouseDown = () => {
isActive.value = true
}
const handleMouseUp = () => {
isActive.value = false
}
return {
isHovered,
isFocused,
isActive,
handleMouseEnter,
handleMouseLeave,
handleFocus,
handleBlur,
handleMouseDown,
handleMouseUp
}
}一致性验证工具
自动化检查
typescript
// tools/consistency-checker.ts
interface ConsistencyRule {
name: string
description: string
check: (component: ComponentDefinition) => ConsistencyViolation[]
}
interface ConsistencyViolation {
rule: string
severity: 'error' | 'warning' | 'info'
message: string
suggestion?: string
}
interface ComponentDefinition {
name: string
props: Record<string, any>
styles: Record<string, any>
behaviors: Record<string, any>
}
class ConsistencyChecker {
private rules: ConsistencyRule[] = []
constructor() {
this.initializeRules()
}
private initializeRules() {
this.rules = [
{
name: 'color-consistency',
description: '检查颜色使用是否符合设计系统',
check: this.checkColorConsistency.bind(this)
},
{
name: 'spacing-consistency',
description: '检查间距使用是否符合设计系统',
check: this.checkSpacingConsistency.bind(this)
},
{
name: 'typography-consistency',
description: '检查字体使用是否符合设计系统',
check: this.checkTypographyConsistency.bind(this)
},
{
name: 'interaction-consistency',
description: '检查交互行为是否一致',
check: this.checkInteractionConsistency.bind(this)
}
]
}
checkComponent(component: ComponentDefinition): ConsistencyViolation[] {
const violations: ConsistencyViolation[] = []
for (const rule of this.rules) {
const ruleViolations = rule.check(component)
violations.push(...ruleViolations)
}
return violations
}
private checkColorConsistency(component: ComponentDefinition): ConsistencyViolation[] {
const violations: ConsistencyViolation[] = []
const allowedColors = Object.values(ghuoVisualSystem.colors).flat()
// 检查组件中使用的颜色是否在设计系统中定义
for (const [property, value] of Object.entries(component.styles)) {
if (property.includes('color') && typeof value === 'string') {
if (!allowedColors.includes(value) && !value.startsWith('var(--')) {
violations.push({
rule: 'color-consistency',
severity: 'warning',
message: `组件 ${component.name} 使用了未定义的颜色: ${value}`,
suggestion: `请使用设计系统中定义的颜色或CSS变量`
})
}
}
}
return violations
}
private checkSpacingConsistency(component: ComponentDefinition): ConsistencyViolation[] {
const violations: ConsistencyViolation[] = []
const allowedSpacing = ghuoVisualSystem.spacing.scale
// 检查间距值是否符合设计系统
for (const [property, value] of Object.entries(component.styles)) {
if (['margin', 'padding', 'gap'].some(prop => property.includes(prop))) {
const numericValue = parseInt(value as string)
if (!isNaN(numericValue) && !allowedSpacing.includes(numericValue)) {
violations.push({
rule: 'spacing-consistency',
severity: 'warning',
message: `组件 ${component.name} 使用了不规范的间距值: ${value}`,
suggestion: `请使用设计系统中定义的间距值: ${allowedSpacing.join(', ')}`
})
}
}
}
return violations
}
private checkTypographyConsistency(component: ComponentDefinition): ConsistencyViolation[] {
const violations: ConsistencyViolation[] = []
// 检查字体大小
if (component.styles.fontSize) {
const fontSize = parseInt(component.styles.fontSize)
if (!ghuoVisualSystem.typography.fontSizes.includes(fontSize)) {
violations.push({
rule: 'typography-consistency',
severity: 'warning',
message: `组件 ${component.name} 使用了不规范的字体大小: ${fontSize}px`,
suggestion: `请使用设计系统中定义的字体大小`
})
}
}
// 检查字体粗细
if (component.styles.fontWeight) {
const fontWeight = parseInt(component.styles.fontWeight)
if (!ghuoVisualSystem.typography.fontWeights.includes(fontWeight)) {
violations.push({
rule: 'typography-consistency',
severity: 'warning',
message: `组件 ${component.name} 使用了不规范的字体粗细: ${fontWeight}`,
suggestion: `请使用设计系统中定义的字体粗细`
})
}
}
return violations
}
private checkInteractionConsistency(component: ComponentDefinition): ConsistencyViolation[] {
const violations: ConsistencyViolation[] = []
// 检查是否有一致的hover状态
if (component.behaviors.clickable && !component.styles[':hover']) {
violations.push({
rule: 'interaction-consistency',
severity: 'error',
message: `可点击组件 ${component.name} 缺少hover状态样式`,
suggestion: `请添加hover状态样式以保持交互一致性`
})
}
// 检查是否有一致的focus状态
if (component.behaviors.focusable && !component.styles[':focus']) {
violations.push({
rule: 'interaction-consistency',
severity: 'error',
message: `可聚焦组件 ${component.name} 缺少focus状态样式`,
suggestion: `请添加focus状态样式以保持无障碍性`
})
}
return violations
}
generateReport(components: ComponentDefinition[]): string {
let report = '# 组件一致性检查报告\n\n'
for (const component of components) {
const violations = this.checkComponent(component)
if (violations.length > 0) {
report += `## ${component.name}\n\n`
for (const violation of violations) {
const icon = violation.severity === 'error' ? '❌' :
violation.severity === 'warning' ? '⚠️' : 'ℹ️'
report += `${icon} **${violation.rule}**: ${violation.message}\n`
if (violation.suggestion) {
report += ` 💡 建议: ${violation.suggestion}\n`
}
report += '\n'
}
}
}
return report
}
}
export const consistencyChecker = new ConsistencyChecker()视觉回归测试
typescript
// tests/visual-regression.test.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import { toMatchImageSnapshot } from 'jest-image-snapshot'
expect.extend({ toMatchImageSnapshot })
describe('Visual Regression Tests', () => {
const components = [
'Button',
'Input',
'Select',
'Card',
'Modal'
]
const variants = [
'default',
'hover',
'focus',
'active',
'disabled'
]
const sizes = ['small', 'medium', 'large']
const types = ['primary', 'secondary', 'danger']
components.forEach(componentName => {
describe(componentName, () => {
variants.forEach(variant => {
sizes.forEach(size => {
types.forEach(type => {
it(`should match snapshot for ${variant} ${size} ${type}`, async () => {
const Component = await import(`@/components/${componentName.toLowerCase()}`)
const wrapper = mount(Component.default, {
props: {
size,
type,
[variant]: variant !== 'default'
}
})
// 等待组件渲染完成
await wrapper.vm.$nextTick()
// 截图并比较
const screenshot = await takeScreenshot(wrapper.element)
expect(screenshot).toMatchImageSnapshot({
customSnapshotIdentifier: `${componentName}-${variant}-${size}-${type}`,
threshold: 0.1,
customDiffConfig: {
threshold: 0.1
}
})
})
})
})
})
})
})
})
async function takeScreenshot(element: Element): Promise<Buffer> {
// 使用 Puppeteer 或其他工具截图
// 这里是伪代码,实际实现需要根据具体工具
return Buffer.from('screenshot-data')
}一致性文档化
设计规范文档
markdown
# Ghuo Design 一致性规范
## 视觉一致性
### 颜色使用规范
- **主色调**: 用于主要操作按钮、链接、选中状态
- **辅助色**: 用于次要操作、装饰元素
- **语义色**: 用于状态反馈、消息提示
- **中性色**: 用于文本、背景、边框
### 间距使用规范
- **基础单位**: 4px
- **组件内间距**: 8px, 12px, 16px
- **组件间间距**: 16px, 24px, 32px
- **布局间距**: 32px, 48px, 64px
### 字体使用规范
- **标题**: 24px, 20px, 18px (粗体)
- **正文**: 16px, 14px (常规)
- **辅助文本**: 12px (常规)
- **行高**: 1.4-1.6倍字体大小
## 交互一致性
### 状态反馈
- **hover**: 透明度变化 + 轻微位移
- **active**: 透明度变化 + 按下效果
- **focus**: 外边框高亮
- **disabled**: 透明度降低 + 禁用光标
- **loading**: 透明度降低 + 加载动画
### 动画时长
- **快速反馈**: 150ms
- **常规过渡**: 200ms
- **复杂动画**: 300ms
- **页面切换**: 500ms
## 语义一致性
### 术语统一
- 操作动词: 创建、编辑、删除、保存
- 状态描述: 成功、失败、警告、信息
- 对象名称: 用户、项目、任务、文件
### 图标使用
- 操作图标: 加号(添加)、铅笔(编辑)、垃圾桶(删除)
- 状态图标: 对勾(成功)、叉号(错误)、感叹号(警告)
- 导航图标: 箭头(方向)、房子(首页)、齿轮(设置)组件使用指南
typescript
// 组件使用指南生成器
class ComponentGuideGenerator {
generateGuide(componentName: string): string {
const component = this.getComponentDefinition(componentName)
return `
# ${componentName} 使用指南
## 基本用法
\`\`\`vue
<template>
<ghuo-${componentName.toLowerCase()}>
默认内容
</ghuo-${componentName.toLowerCase()}>
</template>
\`\`\`
## 属性说明
${this.generatePropsTable(component.props)}
## 样式变体
${this.generateVariantsExamples(component.variants)}
## 状态示例
${this.generateStatesExamples(component.states)}
## 一致性要求
- ✅ 使用设计系统定义的颜色
- ✅ 遵循统一的间距规范
- ✅ 保持一致的交互行为
- ✅ 提供完整的状态反馈
## 常见错误
- ❌ 使用自定义颜色值
- ❌ 不规范的间距设置
- ❌ 缺少状态反馈
- ❌ 不一致的交互行为
## 最佳实践
- ✅ 始终使用设计令牌
- ✅ 遵循组件API规范
- ✅ 提供完整的状态支持
- ✅ 保持语义化命名
## 总结
组件设计中的一致性是一个系统工程,需要从多个维度进行考虑和实施:
### 关键要素
1. **多层次一致性**:视觉、交互、语义三个层面的统一
2. **系统化规范**:建立完整的设计令牌和规范体系
3. **自动化工具**:使用工具确保一致性的执行
4. **团队协作**:建立有效的协作流程和培训机制
5. **持续改进**:建立反馈机制和版本演进策略
### 实施建议
1. **从基础开始**:先建立基础的设计令牌系统
2. **渐进式推进**:分阶段实施,避免一次性大规模变更
3. **工具先行**:优先建立自动化检查和监控工具
4. **文档完善**:提供详细的使用指南和最佳实践
5. **持续优化**:基于反馈持续改进和完善
### 成功指标
- **定量指标**:一致性评分、违规数量、采用率
- **定性指标**:团队满意度、用户体验、开发效率
- **业务指标**:产品质量、上市时间、维护成本
通过系统化的方法和持续的努力,我们可以建立起真正一致、高质量的组件设计体系,为用户提供优秀的产品体验,为团队提供高效的开发工具。
---
*发布于 2024年8月30日*