视觉美学与前端技术的完美结合,为你的界面增添精致细节

🌟 前言:为什么四角装饰效果如此重要?

在UI设计领域,细节决定成败。四角装饰效果不仅能够提升界面的视觉层次感,还能有效地引导用户注意力,营造特定的氛围和品牌调性。从传统书籍装帧的边角花纹,到现代数字界面的装饰性边框,这种设计手法始终散发着独特的魅力。

本文将深入探索如何在Vue中实现灵活、高性能的四角装饰图片效果,涵盖从基础实现到高级动画的完整解决方案。

📐 第一章:设计分析与技术选型

1.1 四角装饰的设计心理学

四角装饰不仅仅是为了美观,它还具有重要的功能性:

  • 聚焦视线:将用户注意力集中在内容区域
  • 品牌强化:通过独特的装饰元素传递品牌形象
  • 状态指示:不同装饰样式表示不同的内容状态
  • 空间定义:明确划分内容边界,提升可读性

1.2 技术实现方案对比

方案 优点 缺点 适用场景
纯CSS 性能好,无依赖 样式固定,灵活性差 简单静态装饰
背景图片 实现简单,兼容性好 难以动态控制,响应式困难 固定装饰图案
Vue组件 高度可定制,动态控制 有一定复杂度 需要交互的动态装饰
Canvas/SVG 极致性能,矢量缩放 实现复杂,学习成本高 复杂动画和游戏场景

🚀 第二章:基础实现 - Vue组件化方案

2.1 基础组件结构设计

<!-- CornerDecorations.vue -->
<template>
  <div class="corner-decorations">
    <!-- 主内容插槽 -->
    <slot></slot>
    
    <!-- 四个角的装饰 -->
    <div class="corner corner-top-left">
      <img 
        v-if="topLeftSrc" 
        :src="topLeftSrc" 
        :style="getCornerStyle('top-left')"
        :class="['corner-image', `corner-effect-${effect}`]"
      />
    </div>
    
    <div class="corner corner-top-right">
      <img 
        v-if="topRightSrc" 
        :src="topRightSrc" 
        :style="getCornerStyle('top-right')"
        :class="['corner-image', `corner-effect-${effect}`]"
      />
    </div>
    
    <div class="corner corner-bottom-left">
      <img 
        v-if="bottomLeftSrc" 
        :src="bottomLeftSrc" 
        :style="getCornerStyle('bottom-left')"
        :class="['corner-image', `corner-effect-${effect}`]"
      />
    </div>
    
    <div class="corner corner-bottom-right">
      <img 
        v-if="bottomRightSrc" 
        :src="bottomRightSrc" 
        :style="getCornerStyle('bottom-right')"
        :class="['corner-image', `corner-effect-${effect}`]"
      />
    </div>
  </div>
</template>

<script setup>
import { computed } from 'vue'

const props = defineProps({
  // 各角图片源
  topLeftSrc: String,
  topRightSrc: String,
  bottomLeftSrc: String,
  bottomRightSrc: String,
  
  // 统一图片源(如果四个角相同)
  src: String,
  
  // 尺寸控制
  size: {
    type: [String, Number],
    default: '40px'
  },
  
  // 偏移量
  offset: {
    type: [String, Number],
    default: '0px'
  },
  
  // 旋转角度
  rotation: {
    type: [String, Number],
    default: 0
  },
  
  // 透明度
  opacity: {
    type: Number,
    default: 1,
    validator: (value) => value >= 0 && value <= 1
  },
  
  // 动画效果
  effect: {
    type: String,
    default: 'none',
    validator: (value) => [
      'none', 'pulse', 'rotate', 'glow', 
      'float', 'bounce', 'fade'
    ].includes(value)
  },
  
  // 悬停效果
  hoverEffect: {
    type: Boolean,
    default: false
  },
  
  // 响应式配置
  responsive: {
    type: Boolean,
    default: true
  },
  
  // 自定义类名
  customClass: String
})

// 计算实际使用的图片源
const effectiveTopLeftSrc = computed(() => 
  props.topLeftSrc || props.src
)
const effectiveTopRightSrc = computed(() => 
  props.topRightSrc || props.src
)
const effectiveBottomLeftSrc = computed(() => 
  props.bottomLeftSrc || props.src
)
const effectiveBottomRightSrc = computed(() => 
  props.bottomRightSrc || props.src
)

// 获取样式对象
const getCornerStyle = (position) => {
  const style = {
    width: typeof props.size === 'number' 
      ? `${props.size}px` 
      : props.size,
    height: typeof props.size === 'number' 
      ? `${props.size}px` 
      : props.size,
    opacity: props.opacity,
    transform: `rotate(${props.rotation}deg)`
  }
  
  // 根据位置添加偏移
  if (position.includes('top')) {
    style.top = props.offset
  } else {
    style.bottom = props.offset
  }
  
  if (position.includes('left')) {
    style.left = props.offset
  } else {
    style.right = props.offset
  }
  
  return style
}

// 响应式尺寸计算
const responsiveSize = computed(() => {
  if (!props.responsive) return props.size
  
  // 基础尺寸
  const baseSize = typeof props.size === 'string' 
    ? parseInt(props.size) 
    : props.size
  
  // 根据视口宽度动态调整
  const vw = window.innerWidth
  if (vw < 768) {
    return `${baseSize * 0.7}px`
  } else if (vw < 1024) {
    return `${baseSize * 0.85}px`
  }
  return `${baseSize}px`
})
</script>

<style scoped>
.corner-decorations {
  position: relative;
  display: inline-block;
  box-sizing: border-box;
}

.corner {
  position: absolute;
  pointer-events: none;
  z-index: 10;
}

.corner-top-left {
  top: 0;
  left: 0;
}

.corner-top-right {
  top: 0;
  right: 0;
}

.corner-bottom-left {
  bottom: 0;
  left: 0;
}

.corner-bottom-right {
  bottom: 0;
  right: 0;
}

.corner-image {
  display: block;
  object-fit: contain;
  transition: all 0.3s ease;
}

/* 悬停效果 */
.corner-image.hoverable {
  pointer-events: auto;
  cursor: pointer;
}

.corner-image.hoverable:hover {
  transform: scale(1.1) rotate(5deg);
  filter: brightness(1.2);
}

/* 动画效果 */
@keyframes pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.1); }
}

@keyframes rotate {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

@keyframes glow {
  0%, 100% { filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.7)); }
  50% { filter: drop-shadow(0 0 15px rgba(255, 255, 255, 0.9)); }
}

@keyframes float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-10px); }
}

@keyframes bounce {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-15px); }
}

@keyframes fade {
  0%, 100% { opacity: 0.5; }
  50% { opacity: 1; }
}

.corner-effect-pulse {
  animation: pulse 2s infinite ease-in-out;
}

.corner-effect-rotate {
  animation: rotate 10s infinite linear;
}

.corner-effect-glow {
  animation: glow 2s infinite ease-in-out;
}

.corner-effect-float {
  animation: float 3s infinite ease-in-out;
}

.corner-effect-bounce {
  animation: bounce 1.5s infinite ease-in-out;
}

.corner-effect-fade {
  animation: fade 2s infinite ease-in-out;
}

/* 响应式调整 */
@media (max-width: 768px) {
  .corner-image {
    max-width: 30px;
    max-height: 30px;
  }
}
</style>

2.2 高级组件 - 支持动态数据绑定

<!-- SmartCorners.vue -->
<template>
  <div 
    class="smart-corners-wrapper"
    :class="[customClass, { 'has-hover': hoverEffect }]"
    :style="wrapperStyle"
  >
    <div class="content-area">
      <slot></slot>
    </div>
    
    <template v-for="corner in activeCorners" :key="corner.position">
      <div 
        v-if="shouldShowCorner(corner)"
        class="corner-item"
        :class="[
          `corner-${corner.position}`,
          `effect-${corner.effect || defaultEffect}`,
          { 'interactive': corner.interactive }
        ]"
        :style="getCornerItemStyle(corner)"
        @mouseenter="onCornerEnter(corner)"
        @mouseleave="onCornerLeave(corner)"
        @click="onCornerClick(corner)"
      >
        <!-- 支持多种内容类型 -->
        <img 
          v-if="corner.type === 'image' && corner.src"
          :src="corner.src"
          :alt="corner.alt || 'corner decoration'"
          class="corner-content"
        />
        
        <div 
          v-else-if="corner.type === 'icon' && corner.icon"
          class="corner-content icon-wrapper"
          v-html="corner.icon"
        />
        
        <span 
          v-else-if="corner.type === 'text' && corner.text"
          class="corner-content text-content"
        >
          {{ corner.text }}
        </span>
        
        <div 
          v-else-if="corner.type === 'slot'"
          class="corner-content slot-content"
        >
          <slot :name="`corner-${corner.position}`"></slot>
        </div>
        
        <!-- 加载状态 -->
        <div 
          v-else-if="corner.loading"
          class="corner-content loading"
        >
          <div class="loading-spinner"></div>
        </div>
        
        <!-- 角标数字 -->
        <div 
          v-if="corner.badge"
          class="corner-badge"
          :style="getBadgeStyle(corner)"
        >
          {{ corner.badge }}
        </div>
      </div>
    </template>
  </div>
</template>

<script setup>
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'

const props = defineProps({
  corners: {
    type: Array,
    default: () => [
      {
        position: 'top-left',
        type: 'image',
        src: '',
        size: '40px',
        visible: true,
        effect: 'none',
        rotation: 0,
        opacity: 1,
        offsetX: '0px',
        offsetY: '0px',
        zIndex: 10,
        interactive: false,
        badge: null,
        tooltip: ''
      }
    ]
  },
  
  // 布局配置
  layout: {
    type: String,
    default: 'absolute', // 'absolute' | 'fixed' | 'sticky'
    validator: (value) => ['absolute', 'fixed', 'sticky'].includes(value)
  },
  
  // 动画配置
  animation: {
    type: Object,
    default: () => ({
      duration: '0.3s',
      timing: 'ease',
      delay: '0s',
      enterClass: 'corner-enter',
      leaveClass: 'corner-leave'
    })
  },
  
  // 响应式断点
  breakpoints: {
    type: Object,
    default: () => ({
      mobile: 768,
      tablet: 1024,
      desktop: 1200
    })
  },
  
  // 自定义样式
  wrapperStyle: Object,
  
  // 事件处理器
  onCornerEnter: Function,
  onCornerLeave: Function,
  onCornerClick: Function
})

const emit = defineEmits([
  'corner-enter',
  'corner-leave',
  'corner-click',
  'corner-loaded',
  'corner-error'
])

// 响应式状态
const windowWidth = ref(window.innerWidth)
const isMobile = computed(() => windowWidth.value < props.breakpoints.mobile)
const isTablet = computed(() => 
  windowWidth.value >= props.breakpoints.mobile && 
  windowWidth.value < props.breakpoints.tablet
)
const isDesktop = computed(() => windowWidth.value >= props.breakpoints.desktop)

// 过滤显示的角标
const activeCorners = computed(() => {
  return props.corners.filter(corner => {
    if (!corner.visible) return false
    
    // 响应式显示控制
    if (corner.responsive) {
      if (isMobile.value && !corner.showOnMobile) return false
      if (isTablet.value && !corner.showOnTablet) return false
      if (isDesktop.value && !corner.showOnDesktop) return false
    }
    
    return true
  })
})

// 计算样式
const getCornerItemStyle = (corner) => {
  const style = {
    position: props.layout,
    width: getComputedSize(corner.size),
    height: getComputedSize(corner.size),
    opacity: corner.opacity,
    transform: `rotate(${corner.rotation}deg)`,
    zIndex: corner.zIndex,
    transition: `all ${props.animation.duration} ${props.animation.timing} ${props.animation.delay}`
  }
  
  // 位置计算
  const positionStyles = {
    'top-left': { top: corner.offsetY, left: corner.offsetX },
    'top-right': { top: corner.offsetY, right: corner.offsetX },
    'bottom-left': { bottom: corner.offsetY, left: corner.offsetX },
    'bottom-right': { bottom: corner.offsetY, right: corner.offsetX }
  }
  
  Object.assign(style, positionStyles[corner.position])
  
  // 自定义样式合并
  if (corner.style) {
    Object.assign(style, corner.style)
  }
  
  return style
}

const getBadgeStyle = (corner) => {
  return {
    backgroundColor: corner.badgeColor || '#ff4757',
    color: corner.badgeTextColor || '#ffffff',
    fontSize: corner.badgeSize || '12px'
  }
}

const getComputedSize = (size) => {
  if (typeof size === 'number') return `${size}px`
  
  // 响应式尺寸计算
  if (typeof size === 'object') {
    if (isMobile.value && size.mobile) return size.mobile
    if (isTablet.value && size.tablet) return size.tablet
    return size.desktop || size.default || '40px'
  }
  
  return size
}

const shouldShowCorner = (corner) => {
  // 根据条件显示
  if (corner.condition) {
    return typeof corner.condition === 'function' 
      ? corner.condition() 
      : corner.condition
  }
  return true
}

// 事件处理
const onCornerEnter = (corner) => {
  emit('corner-enter', corner)
  if (props.onCornerEnter) {
    props.onCornerEnter(corner)
  }
}

const onCornerLeave = (corner) => {
  emit('corner-leave', corner)
  if (props.onCornerLeave) {
    props.onCornerLeave(corner)
  }
}

const onCornerClick = (corner) => {
  if (corner.interactive) {
    emit('corner-click', corner)
    if (props.onCornerClick) {
      props.onCornerClick(corner)
    }
    
    // 执行自定义点击动作
    if (corner.onClick) {
      corner.onClick(corner)
    }
  }
}

// 响应式监听
const handleResize = () => {
  windowWidth.value = window.innerWidth
}

onMounted(() => {
  window.addEventListener('resize', handleResize)
})

onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
})

// 图片预加载
const preloadImages = (corners) => {
  corners.forEach(corner => {
    if (corner.type === 'image' && corner.src) {
      const img = new Image()
      img.onload = () => {
        emit('corner-loaded', { ...corner, loaded: true })
      }
      img.onerror = () => {
        emit('corner-error', { ...corner, error: true })
      }
      img.src = corner.src
    }
  })
}

watch(() => props.corners, preloadImages, { immediate: true })
</script>

<style scoped>
.smart-corners-wrapper {
  position: relative;
  display: inline-block;
  box-sizing: border-box;
  overflow: hidden;
}

.content-area {
  position: relative;
  z-index: 1;
}

.corner-item {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  transition: inherit;
}

.corner-item.interactive {
  pointer-events: auto;
  cursor: pointer;
}

.corner-content {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

.corner-content img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.icon-wrapper {
  font-size: 0.8em;
}

.text-content {
  font-size: 0.7em;
  font-weight: bold;
  text-align: center;
  word-break: break-all;
}

.corner-badge {
  position: absolute;
  top: -5px;
  right: -5px;
  min-width: 18px;
  height: 18px;
  border-radius: 9px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 10px;
  font-weight: bold;
  padding: 2px;
  z-index: 20;
}

/* 动画效果 */
@keyframes spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

.loading-spinner {
  width: 50%;
  height: 50%;
  border: 2px solid rgba(255, 255, 255, 0.3);
  border-radius: 50%;
  border-top-color: #fff;
  animation: spin 1s ease-in-out infinite;
}

/* 过渡动画 */
.corner-enter-active,
.corner-leave-active {
  transition: all 0.3s ease;
}

.corner-enter-from,
.corner-leave-to {
  opacity: 0;
  transform: scale(0.5) rotate(90deg);
}

/* 悬停效果 */
.has-hover .corner-item.interactive:hover {
  transform: scale(1.15);
  filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.5));
}

/* 响应式调整 */
@media (max-width: 768px) {
  .corner-badge {
    min-width: 16px;
    height: 16px;
    font-size: 8px;
  }
  
  .corner-item {
    transform-origin: center;
  }
}
</style>

🎭 第三章:创意效果实现

3.1 动态数据驱动的装饰系统

// corner-effects.js
export class CornerEffectManager {
  constructor(options = {}) {
    this.options = {
      maxCorners: 8, // 支持8个位置(4角+4边中点)
      autoArrange: true,
      collisionDetection: true,
      ...options
    }
    
    this.corners = new Map()
    this.animations = new Map()
    this.effectQueue = []
  }
  
  // 注册新角标
  registerCorner(cornerConfig) {
    const id = cornerConfig.id || `corner-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
    
    const corner = {
      id,
      position: cornerConfig.position || 'top-left',
      type: cornerConfig.type || 'image',
      content: cornerConfig.content,
      size: cornerConfig.size || 40,
      rotation: cornerConfig.rotation || 0,
      opacity: cornerConfig.opacity || 1,
      animation: cornerConfig.animation,
      state: 'idle',
      priority: cornerConfig.priority || 0,
      ...cornerConfig
    }
    
    this.corners.set(id, corner)
    
    // 自动排列检查
    if (this.options.autoArrange) {
      this.arrangeCorners()
    }
    
    return id
  }
  
  // 排列角标(防止重叠)
  arrangeCorners() {
    const positions = this.getAvailablePositions()
    const cornersArray = Array.from(this.corners.values())
      .sort((a, b) => b.priority - a.priority)
    
    cornersArray.forEach((corner, index) => {
      const position = positions[index % positions.length]
      corner.position = position
      
      // 偏移计算避免重叠
      if (this.options.collisionDetection) {
        this.adjustOffsetForCollision(corner, cornersArray.slice(0, index))
      }
    })
  }
  
  // 添加动画效果
  addEffect(cornerId, effectType, options = {}) {
    const corner = this.corners.get(cornerId)
    if (!corner) return
    
    const effect = this.createEffect(effectType, options)
    this.animations.set(cornerId, {
      effect,
      options,
      startTime: Date.now(),
      running: true
    })
    
    corner.state = 'animating'
    
    // 开始动画循环
    this.animateCorner(cornerId)
  }
  
  // 创建效果
  createEffect(type, options) {
    const effects = {
      pulse: () => ({
        update: (progress) => {
          const scale = 1 + 0.2 * Math.sin(progress * Math.PI * 2)
          return { transform: `scale(${scale})` }
        },
        duration: options.duration || 1000
      }),
      
      rotate: () => ({
        update: (progress) => {
          const rotation = progress * 360
          return { transform: `rotate(${rotation}deg)` }
        },
        duration: options.duration || 2000
      }),
      
      float: () => ({
        update: (progress) => {
          const y = Math.sin(progress * Math.PI * 2) * 10
          return { transform: `translateY(${y}px)` }
        },
        duration: options.duration || 1500
      }),
      
      glow: () => ({
        update: (progress) => {
          const intensity = 0.5 + 0.5 * Math.sin(progress * Math.PI * 2)
          return { 
            filter: `drop-shadow(0 0 ${5 + intensity * 10}px rgba(255, 255, 255, ${intensity}))`
          }
        },
        duration: options.duration || 2000
      }),
      
      bounce: () => ({
        update: (progress) => {
          // 弹性函数
          const bounce = (t) => {
            const n1 = 7.5625
            const d1 = 2.75
            
            if (t < 1 / d1) {
              return n1 * t * t
            } else if (t < 2 / d1) {
              return n1 * (t -= 1.5 / d1) * t + 0.75
            } else if (t < 2.5 / d1) {
              return n1 * (t -= 2.25 / d1) * t + 0.9375
            } else {
              return n1 * (t -= 2.625 / d1) * t + 0.984375
            }
          }
          
          const scale = 1 + 0.2 * bounce(progress % 1)
          return { transform: `scale(${scale})` }
        },
        duration: options.duration || 800
      }),
      
      custom: () => ({
        update: options.update,
        duration: options.duration || 1000
      })
    }
    
    return effects[type] ? effects[type]() : effects.pulse()
  }
  
  // 动画循环
  animateCorner(cornerId) {
    const animation = this.animations.get(cornerId)
    if (!animation || !animation.running) return
    
    const now = Date.now()
    const elapsed = now - animation.startTime
    const progress = (elapsed % animation.effect.duration) / animation.effect.duration
    
    const updates = animation.effect.update(progress)
    
    // 触发更新回调
    if (animation.options.onUpdate) {
      animation.options.onUpdate(updates, progress)
    }
    
    // 继续下一帧
    requestAnimationFrame(() => this.animateCorner(cornerId))
  }
  
  // 获取可用位置
  getAvailablePositions() {
    return [
      'top-left', 'top-center', 'top-right',
      'middle-left', 'middle-right',
      'bottom-left', 'bottom-center', 'bottom-right'
    ]
  }
  
  // 碰撞检测调整
  adjustOffsetForCollision(corner, existingCorners) {
    const baseOffset = 10
    let offsetX = baseOffset
    let offsetY = baseOffset
    
    existingCorners.forEach(existing => {
      if (existing.position === corner.position) {
        // 简单偏移策略
        offsetX += 5
        offsetY += 5
      }
    })
    
    corner.offsetX = `${offsetX}px`
    corner.offsetY = `${offsetY}px`
  }
  
  // 批量更新
  updateCorners(updates) {
    updates.forEach(update => {
      const corner = this.corners.get(update.id)
      if (corner) {
        Object.assign(corner, update)
      }
    })
  }
  
  // 移除角标
  removeCorner(cornerId) {
    const animation = this.animations.get(cornerId)
    if (animation) {
      animation.running = false
      this.animations.delete(cornerId)
    }
    
    this.corners.delete(cornerId)
  }
  
  // 获取所有角标状态
  getState() {
    return {
      corners: Array.from(this.corners.values()),
      activeAnimations: this.animations.size,
      totalCorners: this.corners.size
    }
  }
}

// 使用示例
export const createCornerSystem = (container) => {
  const manager = new CornerEffectManager({
    maxCorners: 8,
    autoArrange: true
  })
  
  // 添加装饰角标
  const corner1 = manager.registerCorner({
    type: 'image',
    content: '/images/corner-leaf.png',
    position: 'top-left',
    size: 50,
    priority: 1
  })
  
  const corner2 = manager.registerCorner({
    type: 'icon',
    content: '✨',
    position: 'bottom-right',
    size: 40,
    priority: 2
  })
  
  // 添加动画效果
  manager.addEffect(corner1, 'pulse', {
    duration: 2000,
    onUpdate: (updates) => {
      // 更新DOM
      const element = document.querySelector(`[data-corner-id="${corner1}"]`)
      if (element) {
        Object.assign(element.style, updates)
      }
    }
  })
  
  manager.addEffect(corner2, 'rotate', {
    duration: 5000,
    onUpdate: (updates) => {
      const element = document.querySelector(`[data-corner-id="${corner2}"]`)
      if (element) {
        Object.assign(element.style, updates)
      }
    }
  })
  
  return manager
}

3.2 与Vue状态管理集成

// corner-store.js
import { reactive, computed } from 'vue'

export const useCornerStore = () => {
  const state = reactive({
    corners: [],
    activeEffects: {},
    settings: {
      enabled: true,
      theme: 'default',
      animationSpeed: 1.0,
      responsive: true
    },
    presets: {
      elegant: [
        { position: 'top-left', src: '/corners/elegant-tl.svg', size: 45 },
        { position: 'top-right', src: '/corners/elegant-tr.svg', size: 45 },
        { position: 'bottom-left', src: '/corners/elegant-bl.svg', size: 45 },
        { position: 'bottom-right', src: '/corners/elegant-br.svg', size: 45 }
      ],
      modern: [
        { position: 'top-left', src: '/corners/modern-tl.png', size: 40, effect: 'glow' },
        { position: 'bottom-right', src: '/corners/modern-br.png', size: 40, effect: 'glow' }
      ],
      minimal: [
        { position: 'top-left', src: '/corners/minimal-tl.svg', size: 30, opacity: 0.5 },
        { position: 'bottom-right', src: '/corners/minimal-br.svg', size: 30, opacity: 0.5 }
      ]
    }
  })
  
  // 计算属性
  const activeCorners = computed(() => {
    return state.corners.filter(corner => 
      state.settings.enabled && corner.visible !== false
    )
  })
  
  const cornerCount = computed(() => activeCorners.value.length)
  
  const hasAnimations = computed(() => {
    return Object.keys(state.activeEffects).length > 0
  })
  
  // 方法
  const applyPreset = (presetName) => {
    const preset = state.presets[presetName]
    if (preset) {
      state.corners = [...preset]
    }
  }
  
  const addCorner = (cornerConfig) => {
    const corner = {
      id: `corner-${Date.now()}`,
      visible: true,
      createdAt: new Date().toISOString(),
      ...cornerConfig
    }
    
    state.corners.push(corner)
    return corner.id
  }
  
  const updateCorner = (cornerId, updates) => {
    const index = state.corners.findIndex(c => c.id === cornerId)
    if (index !== -1) {
      Object.assign(state.corners[index], updates)
    }
  }
  
  const removeCorner = (cornerId) => {
    const index = state.corners.findIndex(c => c.id === cornerId)
    if (index !== -1) {
      state.corners.splice(index, 1)
      
      // 清理相关动画
      if (state.activeEffects[cornerId]) {
        delete state.activeEffects[cornerId]
      }
    }
  }
  
  const toggleVisibility = (cornerId) => {
    const corner = state.corners.find(c => c.id === cornerId)
    if (corner) {
      corner.visible = !corner.visible
    }
  }
  
  const startAnimation = (cornerId, effectType, options = {}) => {
    state.activeEffects[cornerId] = {
      type: effectType,
      startTime: Date.now(),
      options,
      active: true
    }
  }
  
  const stopAnimation = (cornerId) => {
    if (state.activeEffects[cornerId]) {
      state.activeEffects[cornerId].active = false
    }
  }
  
  const getCornerStyle = (corner) => {
    const style = {
      width: `${corner.size}px`,
      height: `${corner.size}px`,
      opacity: corner.opacity || 1
    }
    
    // 如果有动画
    const animation = state.activeEffects[corner.id]
    if (animation && animation.active) {
      const elapsed = Date.now() - animation.startTime
      const duration = animation.options.duration || 1000
      const progress = (elapsed % duration) / duration
      
      switch (animation.type) {
        case 'pulse':
          const scale = 1 + 0.1 * Math.sin(progress * Math.PI * 2)
          style.transform = `scale(${scale})`
          break
        case 'rotate':
          const rotation = progress * 360
          style.transform = `rotate(${rotation}deg)`
          break
      }
    } else if (corner.rotation) {
      style.transform = `rotate(${corner.rotation}deg)`
    }
    
    return style
  }
  
  // 响应式配置
  const updateSettings = (newSettings) => {
    Object.assign(state.settings, newSettings)
  }
  
  // 导入/导出配置
  const exportConfig = () => {
    return {
      corners: [...state.corners],
      settings: { ...state.settings },
      version: '1.0.0',
      exportedAt: new Date().toISOString()
    }
  }
  
  const importConfig = (config) => {
    if (config.version === '1.0.0') {
      state.corners = config.corners || []
      state.settings = config.settings || state.settings
    }
  }
  
  return {
    state,
    activeCorners,
    cornerCount,
    hasAnimations,
    applyPreset,
    addCorner,
    updateCorner,
    removeCorner,
    toggleVisibility,
    startAnimation,
    stopAnimation,
    getCornerStyle,
    updateSettings,
    exportConfig,
    importConfig
  }
}

🎨 第四章:创意应用场景

4.1 电商商品卡片装饰

<!-- ProductCard.vue -->
<template>
  <div class="product-card">
    <SmartCorners
      :corners="cornerConfigs"
      :hover-effect="true"
      @corner-click="handleCornerAction"
    >
      <div class="card-content">
        <!-- 商品图片 -->
        <div class="product-image">
          <img :src="product.image" :alt="product.name" />
          
          <!-- 角标显示状态 -->
          <div class="status-overlay">
            <span v-if="product.isNew" class="badge new">NEW</span>
            <span v-if="product.discount" class="badge discount">
              -{{ product.discount }}%
            </span>
          </div>
        </div>
        
        <!-- 商品信息 -->
        <div class="product-info">
          <h3 class="product-name">{{ product.name }}</h3>
          <div class="price-section">
            <span class="current-price">${{ product.price }}</span>
            <span v-if="product.originalPrice" class="original-price">
              ${{ product.originalPrice }}
            </span>
          </div>
          
          <!-- 评分 -->
          <div class="rating">
            <span class="stars">★★★★☆</span>
            <span class="rating-count">({{ product.ratingCount }})</span>
          </div>
        </div>
      </div>
    </SmartCorners>
    
    <!-- 角标提示工具 -->
    <div v-if="activeTooltip" class="corner-tooltip" :style="tooltipStyle">
      {{ activeTooltip }}
    </div>
  </div>
</template>

<script setup>
import { ref, computed, reactive } from 'vue'
import SmartCorners from './SmartCorners.vue'

const props = defineProps({
  product: {
    type: Object,
    required: true
  },
  theme: {
    type: String,
    default: 'default'
  }
})

const activeTooltip = ref('')
const tooltipPosition = reactive({ x: 0, y: 0 })

// 动态角标配置
const cornerConfigs = computed(() => {
  const corners = []
  
  // 左上角:收藏按钮
  corners.push({
    position: 'top-left',
    type: 'icon',
    icon: props.product.isFavorite ? '❤️' : '🤍',
    size: 35,
    interactive: true,
    tooltip: props.product.isFavorite ? '取消收藏' : '加入收藏',
    onClick: () => toggleFavorite()
  })
  
  // 右上角:分享按钮
  corners.push({
    position: 'top-right',
    type: 'icon',
    icon: '📤',
    size: 35,
    interactive: true,
    tooltip: '分享商品',
    onClick: () => shareProduct()
  })
  
  // 左下角:新品标识
  if (props.product.isNew) {
    corners.push({
      position: 'bottom-left',
      type: 'image',
      src: '/decorations/new-badge.svg',
      size: 40,
      effect: 'pulse',
      tooltip: '新品上市'
    })
  }
  
  // 右下角:折扣标识
  if (props.product.discount) {
    corners.push({
      position: 'bottom-right',
      type: 'text',
      text: `-${props.product.discount}%`,
      size: 40,
      style: {
        backgroundColor: '#ff4757',
        color: '#ffffff',
        borderRadius: '50%',
        fontSize: '12px',
        fontWeight: 'bold'
      },
      effect: 'bounce',
      tooltip: '限时折扣'
    })
  }
  
  return corners
})

// 事件处理
const toggleFavorite = () => {
  // 切换收藏状态
  emit('toggle-favorite', props.product.id)
}

const shareProduct = () => {
  // 分享逻辑
  if (navigator.share) {
    navigator.share({
      title: props.product.name,
      text: `Check out this product: ${props.product.name}`,
      url: window.location.href
    })
  } else {
    // 回退方案
    activeTooltip.value = '链接已复制到剪贴板'
    setTimeout(() => {
      activeTooltip.value = ''
    }, 2000)
  }
}

const handleCornerAction = (corner) => {
  if (corner.tooltip) {
    activeTooltip.value = corner.tooltip
    tooltipPosition.x = event.clientX
    tooltipPosition.y = event.clientY - 30
    
    setTimeout(() => {
      activeTooltip.value = ''
    }, 1500)
  }
  
  if (corner.onClick) {
    corner.onClick()
  }
}

const tooltipStyle = computed(() => ({
  left: `${tooltipPosition.x}px`,
  top: `${tooltipPosition.y}px`
}))
</script>

<style scoped>
.product-card {
  position: relative;
  width: 280px;
  background: white;
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
  transition: transform 0.3s ease, box-shadow 0.3s ease;
}

.product-card:hover {
  transform: translateY(-5px);
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
}

.card-content {
  padding: 20px;
}

.product-image {
  position: relative;
  width: 100%;
  height: 200px;
  overflow: hidden;
  border-radius: 8px;
  margin-bottom: 15px;
}

.product-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.5s ease;
}

.product-card:hover .product-image img {
  transform: scale(1.05);
}

.status-overlay {
  position: absolute;
  top: 10px;
  right: 10px;
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.badge {
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 12px;
  font-weight: bold;
  color: white;
}

.badge.new {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

.badge.discount {
  background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}

.product-info {
  text-align: center;
}

.product-name {
  font-size: 16px;
  font-weight: 600;
  margin-bottom: 10px;
  color: #333;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.price-section {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
}

.current-price {
  font-size: 20px;
  font-weight: bold;
  color: #ff6b6b;
}

.original-price {
  font-size: 14px;
  color: #999;
  text-decoration: line-through;
}

.rating {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 5px;
  font-size: 14px;
  color: #666;
}

.stars {
  color: #ffd700;
}

.corner-tooltip {
  position: fixed;
  background: rgba(0, 0, 0, 0.8);
  color: white;
  padding: 8px 12px;
  border-radius: 6px;
  font-size: 12px;
  z-index: 1000;
  pointer-events: none;
  transform: translateX(-50%);
  white-space: nowrap;
  animation: tooltipFade 1.5s ease-in-out;
}

@keyframes tooltipFade {
  0%, 100% { opacity: 0; transform: translateX(-50%) translateY(10px); }
  20%, 80% { opacity: 1; transform: translateX(-50%) translateY(0); }
}
</style>

4.2 仪表板数据卡片

<!-- DashboardCard.vue -->
<template>
  <div 
    class="dashboard-card"
    :class="[`card-${type}`, { 'has-alert': hasAlert }]"
  >
    <SmartCorners
      :corners="decorations"
      :animation="animationConfig"
      @corner-enter="onDecorationHover"
    >
      <div class="card-header">
        <div class="header-left">
          <div class="card-icon" :style="iconStyle">
            <component :is="iconComponent" v-if="iconComponent" />
            <span v-else class="icon-placeholder">{{ icon }}</span>
          </div>
          <h3 class="card-title">{{ title }}</h3>
        </div>
        
        <div class="card-actions">
          <button class="action-btn" @click="refreshData">
            <RefreshIcon />
          </button>
          <button class="action-btn" @click="toggleExpand">
            <ExpandIcon />
          </button>
        </div>
      </div>
      
      <div class="card-body">
        <div class="main-value">
          <animated-number
            :value="currentValue"
            :format="valueFormat"
            :duration="500"
          />
          <span class="value-unit">{{ unit }}</span>
        </div>
        
        <div class="value-trend">
          <trend-indicator :value="trendValue" />
          <span class="trend-text">{{ trendText }}</span>
        </div>
        
        <div v-if="showChart" class="mini-chart">
          <svg width="100%" height="40">
            <path 
              :d="chartPath" 
              fill="none" 
              stroke="currentColor" 
              stroke-width="2"
            />
          </svg>
        </div>
      </div>
      
      <div class="card-footer">
        <div class="footer-text">{{ footerText }}</div>
        <div class="timestamp">{{ formattedTime }}</div>
      </div>
    </SmartCorners>
    
    <!-- 装饰效果 -->
    <div v-if="particleEffect" class="particles">
      <div 
        v-for="(particle, index) in particles"
        :key="index"
        class="particle"
        :style="particle.style"
      ></div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { RefreshIcon, ExpandIcon, TrendingUpIcon, TrendingDownIcon } from './Icons.vue'
import AnimatedNumber from './AnimatedNumber.vue'
import TrendIndicator from './TrendIndicator.vue'

const props = defineProps({
  title: String,
  value: [Number, String],
  unit: String,
  trendValue: Number,
  type: {
    type: String,
    default: 'default',
    validator: (val) => ['default', 'primary', 'success', 'warning', 'danger'].includes(val)
  },
  icon: String,
  footerText: String,
  showChart: Boolean,
  dataPoints: Array
})

// 装饰配置
const decorations = computed(() => {
  const baseDecorations = [
    {
      position: 'top-left',
      type: 'icon',
      icon: '⚡',
      size: 20,
      opacity: 0.7,
      effect: 'glow'
    },
    {
      position: 'top-right',
      type: 'icon',
      icon: props.trendValue >= 0 ? '📈' : '📉',
      size: 20,
      effect: 'float'
    }
  ]
  
  // 根据卡片类型添加特殊装饰
  if (props.type === 'danger' && props.trendValue < -10) {
    baseDecorations.push({
      position: 'bottom-left',
      type: 'icon',
      icon: '⚠️',
      size: 25,
      effect: 'pulse',
      interactive: true,
      tooltip: '需要关注'
    })
  }
  
  if (props.type === 'success' && props.trendValue > 20) {
    baseDecorations.push({
      position: 'bottom-right',
      type: 'icon',
      icon: '🎯',
      size: 25,
      effect: 'bounce'
    })
  }
  
  return baseDecorations
})

// 动画配置
const animationConfig = ref({
  duration: '0.4s',
  timing: 'cubic-bezier(0.4, 0, 0.2, 1)',
  enterClass: 'card-enter'
})

// 粒子效果
const particles = ref([])
const particleEffect = ref(false)

const generateParticles = () => {
  const newParticles = []
  const particleCount = 15
  
  for (let i = 0; i < particleCount; i++) {
    const angle = (Math.PI * 2 * i) / particleCount
    const radius = 150 + Math.random() * 50
    
    newParticles.push({
      style: {
        left: '50%',
        top: '50%',
        width: `${Math.random() * 4 + 2}px`,
        height: `${Math.random() * 4 + 2}px`,
        backgroundColor: `rgba(255, 255, 255, ${Math.random() * 0.5 + 0.3})`,
        animation: `particleFlow ${Math.random() * 2 + 1}s ease-out forwards`,
        '--angle': `${angle}rad`,
        '--radius': `${radius}px`,
        '--delay': `${Math.random() * 0.5}s`
      }
    })
  }
  
  particles.value = newParticles
}

const startParticleEffect = () => {
  particleEffect.value = true
  generateParticles()
  
  setTimeout(() => {
    particleEffect.value = false
    particles.value = []
  }, 1000)
}

// 计算属性
const currentValue = ref(0)
const formattedTime = ref('')

const valueFormat = computed(() => {
  if (typeof props.value === 'number') {
    return props.value > 1000 ? '0,0' : '0,0.00'
  }
  return null
})

const trendText = computed(() => {
  if (props.trendValue > 0) {
    return `+${props.trendValue}% 较昨日`
  } else if (props.trendValue < 0) {
    return `${props.trendValue}% 较昨日`
  }
  return '无变化'
})

const hasAlert = computed(() => {
  return props.type === 'danger' || Math.abs(props.trendValue) > 15
})

// 图表路径
const chartPath = computed(() => {
  if (!props.dataPoints || props.dataPoints.length < 2) return ''
  
  const points = props.dataPoints
  const width = 200
  const height = 40
  const max = Math.max(...points)
  const min = Math.min(...points)
  const range = max - min || 1
  
  let path = `M 0 ${height - ((points[0] - min) / range) * height}`
  
  for (let i = 1; i < points.length; i++) {
    const x = (i / (points.length - 1)) * width
    const y = height - ((points[i] - min) / range) * height
    path += ` L ${x} ${y}`
  }
  
  return path
})

// 事件处理
const onDecorationHover = (corner) => {
  if (corner.tooltip) {
    // 显示工具提示
    console.log('显示提示:', corner.tooltip)
  }
}

const refreshData = () => {
  // 触发刷新动画
  startParticleEffect()
  emit('refresh')
}

const toggleExpand = () => {
  emit('expand')
}

// 更新时间
const updateTime = () => {
  const now = new Date()
  formattedTime.value = now.toLocaleTimeString('zh-CN', {
    hour: '2-digit',
    minute: '2-digit'
  })
}

onMounted(() => {
  updateTime()
  const timer = setInterval(updateTime, 60000)
  
  onUnmounted(() => {
    clearInterval(timer)
  })
})
</script>

<style scoped>
.dashboard-card {
  position: relative;
  background: linear-gradient(135deg, var(--card-bg, #ffffff) 0%, rgba(255, 255, 255, 0.9) 100%);
  border-radius: 16px;
  padding: 20px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
  overflow: hidden;
  transition: all 0.3s ease;
}

.dashboard-card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 4px;
  background: linear-gradient(90deg, 
    var(--card-accent, #667eea) 0%,
    var(--card-accent-secondary, #764ba2) 100%
  );
}

.dashboard-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
}

.has-alert {
  animation: alertPulse 2s infinite;
}

@keyframes alertPulse {
  0%, 100% { box-shadow: 0 4px 20px rgba(255, 107, 107, 0.2); }
  50% { box-shadow: 0 4px 30px rgba(255, 107, 107, 0.4); }
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}

.header-left {
  display: flex;
  align-items: center;
  gap: 12px;
}

.card-icon {
  width: 40px;
  height: 40px;
  border-radius: 10px;
  background: linear-gradient(135deg, 
    var(--icon-bg-start, #667eea) 0%,
    var(--icon-bg-end, #764ba2) 100%
  );
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-size: 18px;
}

.card-title {
  font-size: 16px;
  font-weight: 600;
  color: var(--text-primary, #333);
  margin: 0;
}

.card-actions {
  display: flex;
  gap: 8px;
}

.action-btn {
  width: 32px;
  height: 32px;
  border-radius: 8px;
  border: none;
  background: rgba(0, 0, 0, 0.05);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: all 0.2s ease;
  color: #666;
}

.action-btn:hover {
  background: rgba(0, 0, 0, 0.1);
  transform: scale(1.05);
}

.card-body {
  margin-bottom: 20px;
}

.main-value {
  font-size: 36px;
  font-weight: 700;
  color: var(--text-primary, #333);
  margin-bottom: 10px;
  display: flex;
  align-items: baseline;
  gap: 4px;
}

.value-unit {
  font-size: 16px;
  color: #666;
  font-weight: 500;
}

.value-trend {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 15px;
}

.trend-text {
  font-size: 14px;
  color: #666;
}

.mini-chart {
  height: 40px;
  color: var(--card-accent, #667eea);
}

.card-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-top: 15px;
  border-top: 1px solid rgba(0, 0, 0, 0.1);
  font-size: 12px;
  color: #888;
}

/* 粒子效果 */
.particles {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  pointer-events: none;
  z-index: 1;
}

.particle {
  position: absolute;
  border-radius: 50%;
  animation: particleFlow var(--duration) ease-out forwards;
}

@keyframes particleFlow {
  0% {
    transform: translate(0, 0) scale(1);
    opacity: 1;
  }
  100% {
    transform: 
      translate(
        calc(cos(var(--angle)) * var(--radius)),
        calc(sin(var(--angle)) * var(--radius))
      )
      scale(0);
    opacity: 0;
  }
}

/* 卡片类型样式 */
.card-primary {
  --card-bg: #f8f9ff;
  --card-accent: #667eea;
  --card-accent-secondary: #764ba2;
  --icon-bg-start: #667eea;
  --icon-bg-end: #764ba2;
}

.card-success {
  --card-bg: #f0fff4;
  --card-accent: #48bb78;
  --card-accent-secondary: #38a169;
  --icon-bg-start: #48bb78;
  --icon-bg-end: #38a169;
}

.card-warning {
  --card-bg: #fffaf0;
  --card-accent: #ed8936;
  --card-accent-secondary: #dd6b20;
  --icon-bg-start: #ed8936;
  --icon-bg-end: #dd6b20;
}

.card-danger {
  --card-bg: #fff5f5;
  --card-accent: #f56565;
  --card-accent-secondary: #e53e3e;
  --icon-bg-start: #f56565;
  --icon-bg-end: #e53e3e;
}
</style>

🎯 第五章:性能优化与最佳实践

5.1 图片优化策略

// image-optimizer.js
export class CornerImageOptimizer {
  constructor(options = {}) {
    this.options = {
      lazyLoad: true,
      preloadLimit: 4,
      cacheSize: 10,
      webpSupport: true,
      ...options
    }
    
    this.imageCache = new Map()
    this.pendingRequests = new Map()
  }
  
  // 获取优化后的图片URL
  async getOptimizedImage(src, size = 40) {
    const cacheKey = `${src}-${size}`
    
    // 检查缓存
    if (this.imageCache.has(cacheKey)) {
      return this.imageCache.get(cacheKey)
    }
    
    // 检查是否正在加载
    if (this.pendingRequests.has(cacheKey)) {
      return this.pendingRequests.get(cacheKey)
    }
    
    // 创建加载Promise
    const loadPromise = this.loadImage(src, size)
    this.pendingRequests.set(cacheKey, loadPromise)
    
    try {
      const result = await loadPromise
      
      // 缓存结果
      this.imageCache.set(cacheKey, result)
      
      // 清理缓存
      if (this.imageCache.size > this.options.cacheSize) {
        const firstKey = this.imageCache.keys().next().value
        this.imageCache.delete(firstKey)
      }
      
      return result
    } finally {
      this.pendingRequests.delete(cacheKey)
    }
  }
  
  // 加载图片并优化
  async loadImage(src, size) {
    // 检查WebP支持
    const supportsWebP = this.options.webpSupport && 
      await this.checkWebPSupport()
    
    // 构建优化URL(如果有图片处理服务)
    const optimizedSrc = this.getOptimizedSrc(src, size, supportsWebP)
    
    return new Promise((resolve, reject) => {
      const img = new Image()
      
      img.onload = () => {
        // 图片加载成功
        resolve({
          src: optimizedSrc,
          width: img.width,
          height: img.height,
          loaded: true
        })
      }
      
      img.onerror = () => {
        // 加载失败,使用备用方案
        console.warn(`Failed to load corner image: ${src}`)
        resolve({
          src: this.getFallbackImage(size),
          width: size,
          height: size,
          loaded: false,
          isFallback: true
        })
      }
      
      img.src = optimizedSrc
    })
  }
  
  // 获取优化后的图片地址
  getOptimizedSrc(src, size, supportsWebP) {
    // 如果是外部URL,可能无法优化
    if (src.startsWith('http')) {
      return src
    }
    
    // 这里可以集成图片处理服务,如Cloudinary、imgix等
    const params = new URLSearchParams({
      w: size,
      h: size,
      fit: 'contain',
      quality: '80',
      format: supportsWebP ? 'webp' : 'png'
    })
    
    return `${src}?${params.toString()}`
  }
  
  // 获取备用图片
  getFallbackImage(size) {
    // 创建一个简单的SVG占位图
    return `data:image/svg+xml;base64,${btoa(`
      <svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" 
           xmlns="http://www.w3.org/2000/svg">
        <rect width="100%" height="100%" fill="#f0f0f0"/>
        <path d="M${size/2} ${size/4} L${size*3/4} ${size*3/4} L${size/4} ${size*3/4} Z" 
              fill="#ccc"/>
      </svg>
    `)}`
  }
  
  // 检查WebP支持
  async checkWebPSupport() {
    if (!this.options.webpSupport) return false
    
    return new Promise((resolve) => {
      const webP = new Image()
      webP.onload = webP.onerror = () => {
        resolve(webP.height === 2)
      }
      webP.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA'
    })
  }
  
  // 预加载角标图片
  async preloadCorners(cornerConfigs) {
    const loadPromises = cornerConfigs
      .filter(corner => corner.type === 'image' && corner.src)
      .slice(0, this.options.preloadLimit)
      .map(corner => 
        this.getOptimizedImage(corner.src, corner.size || 40)
      )
    
    await Promise.all(loadPromises)
  }
  
  // 清理缓存
  clearCache() {
    this.imageCache.clear()
    this.pendingRequests.clear()
  }
}

// Vue组合式函数
export const useImageOptimizer = () => {
  const optimizer = new CornerImageOptimizer({
    lazyLoad: true,
    cacheSize: 20
  })
  
  // 批量优化角标
  const optimizeCorners = async (corners) => {
    const optimizedCorners = []
    
    for (const corner of corners) {
      if (corner.type === 'image' && corner.src) {
        try {
          const optimized = await optimizer.getOptimizedImage(
            corner.src, 
            corner.size || 40
          )
          
          optimizedCorners.push({
            ...corner,
            optimizedSrc: optimized.src,
            optimized: true
          })
        } catch (error) {
          console.error('Failed to optimize corner image:', error)
          optimizedCorners.push(corner)
        }
      } else {
        optimizedCorners.push(corner)
      }
    }
    
    return optimizedCorners
  }
  
  // 图片懒加载指令
  const vLazyCorner = {
    mounted(el, binding) {
      const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            const img = el.querySelector('img')
            if (img && img.dataset.src) {
              img.src = img.dataset.src
              img.removeAttribute('data-src')
            }
            observer.unobserve(el)
          }
        })
      }, {
        rootMargin: '50px',
        threshold: 0.1
      })
      
      observer.observe(el)
    }
  }
  
  return {
    optimizer,
    optimizeCorners,
    vLazyCorner
  }
}

5.2 性能监控与优化

// performance-monitor.js
export class CornerPerformanceMonitor {
  constructor() {
    this.metrics = {
      renderTime: 0,
      fps: 0,
      memoryUsage: 0,
      cornerCount: 0
    }
    
    this.renderTimes = []
    this.frameCount = 0
    this.lastTime = performance.now()
  }
  
  // 开始监控
  startMonitoring() {
    this.measureFrameRate()
    this.measureMemory()
  }
  
  // 测量帧率
  measureFrameRate() {
    const now = performance.now()
    this.frameCount++
    
    if (now >= this.lastTime + 1000) {
      this.metrics.fps = Math.round(
        (this.frameCount * 1000) / (now - this.lastTime)
      )
      this.frameCount = 0
      this.lastTime = now
    }
    
    requestAnimationFrame(() => this.measureFrameRate())
  }
  
  // 测量内存使用
  measureMemory() {
    if (performance.memory) {
      this.metrics.memoryUsage = 
        performance.memory.usedJSHeapSize / 1024 / 1024 // MB
    }
    
    setTimeout(() => this.measureMemory(), 1000)
  }
  
  // 记录渲染时间
  recordRenderTime(startTime) {
    const renderTime = performance.now() - startTime
    this.renderTimes.push(renderTime)
    
    // 保持最近100次记录
    if (this.renderTimes.length > 100) {
      this.renderTimes.shift()
    }
    
    // 计算平均渲染时间
    this.metrics.renderTime = this.renderTimes.reduce((a, b) => a + b, 0) / 
      this.renderTimes.length
  }
  
  // 获取性能报告
  getPerformanceReport() {
    const warnings = []
    
    if (this.metrics.fps < 30) {
      warnings.push('帧率过低,可能影响动画流畅度')
    }
    
    if (this.metrics.renderTime > 16) {
      warnings.push('渲染时间过长,可能影响性能')
    }
    
    if (this.metrics.memoryUsage > 50) {
      warnings.push('内存使用过高,建议优化')
    }
    
    return {
      metrics: { ...this.metrics },
      warnings,
      suggestions: this.getOptimizationSuggestions()
    }
  }
  
  // 获取优化建议
  getOptimizationSuggestions() {
    const suggestions = []
    
    if (this.metrics.cornerCount > 8) {
      suggestions.push('减少角标数量,当前数量:' + this.metrics.cornerCount)
    }
    
    if (this.metrics.renderTime > 10) {
      suggestions.push('考虑使用CSS替代图片装饰')
      suggestions.push('减少装饰动画复杂度')
    }
    
    if (this.metrics.fps < 45) {
      suggestions.push('降低动画帧率或使用will-change优化')
    }
    
    return suggestions
  }
}

// Vue性能优化组件
export const PerformanceOptimizedCorners = {
  name: 'PerformanceOptimizedCorners',
  
  props: {
    corners: Array,
    maxCorners: {
      type: Number,
      default: 8
    },
    enableThrottling: {
      type: Boolean,
      default: true
    }
  },
  
  data() {
    return {
      visibleCorners: [],
      lastScrollTime: 0,
      throttleDelay: 100,
      observer: null
    }
  },
  
  computed: {
    // 性能优化:限制显示数量
    optimizedCorners() {
      return this.corners.slice(0, this.maxCorners)
    }
  },
  
  methods: {
    // 节流更新
    updateVisibleCorners() {
      if (!this.enableThrottling) {
        this.updateCornersImmediately()
        return
      }
      
      const now = Date.now()
      if (now - this.lastScrollTime > this.throttleDelay) {
        this.updateCornersImmediately()
        this.lastScrollTime = now
      } else {
        clearTimeout(this.updateTimeout)
        this.updateTimeout = setTimeout(() => {
          this.updateCornersImmediately()
        }, this.throttleDelay)
      }
    },
    
    updateCornersImmediately() {
      // 根据视口位置更新可见角标
      this.visibleCorners = this.optimizedCorners.filter(corner => {
        if (!corner.lazyLoad) return true
        
        // 简单视口检测
        const element = this.$refs[`corner-${corner.id}`]
        if (!element) return false
        
        const rect = element.getBoundingClientRect()
        return (
          rect.top < window.innerHeight &&
          rect.bottom > 0 &&
          rect.left < window.innerWidth &&
          rect.right > 0
        )
      })
    },
    
    // 使用Intersection Observer
    setupIntersectionObserver() {
      if ('IntersectionObserver' in window) {
        this.observer = new IntersectionObserver((entries) => {
          entries.forEach(entry => {
            const cornerId = entry.target.dataset.cornerId
            const corner = this.corners.find(c => c.id === cornerId)
            
            if (corner) {
              corner.isVisible = entry.isIntersecting
              
              // 延迟加载图片
              if (entry.isIntersecting && corner.type === 'image') {
                this.loadImage(corner)
              }
            }
          })
        }, {
          rootMargin: '50px',
          threshold: 0.1
        })
        
        // 观察所有角标元素
        this.$nextTick(() => {
          this.optimizedCorners.forEach(corner => {
            const element = this.$refs[`corner-${corner.id}`]
            if (element) {
              this.observer.observe(element)
            }
          })
        })
      }
    },
    
    // 图片懒加载
    loadImage(corner) {
      if (corner.loaded || corner.loading) return
      
      corner.loading = true
      const img = new Image()
      
      img.onload = () => {
        corner.loaded = true
        corner.loading = false
        this.$emit('image-loaded', corner)
      }
      
      img.onerror = () => {
        corner.loaded = false
        corner.loading = false
        this.$emit('image-error', corner)
      }
      
      img.src = corner.src
    }
  },
  
  mounted() {
    this.updateVisibleCorners()
    this.setupIntersectionObserver()
    
    // 监听滚动
    if (this.enableThrottling) {
      window.addEventListener('scroll', this.updateVisibleCorners, { passive: true })
    }
  },
  
  beforeUnmount() {
    if (this.observer) {
      this.observer.disconnect()
    }
    
    window.removeEventListener('scroll', this.updateVisibleCorners)
    clearTimeout(this.updateTimeout)
  },
  
  render(h) {
    const cornerElements = this.visibleCorners.map(corner => {
      return h('div', {
        ref: `corner-${corner.id}`,
        'data-corner-id': corner.id,
        class: [
          'corner',
          `corner-${corner.position}`,
          {
            'corner-visible': corner.isVisible,
            'corner-loading': corner.loading
          }
        ],
        style: this.getCornerStyle(corner)
      }, [
        // 根据类型渲染内容
        this.renderCornerContent(h, corner)
      ])
    })
    
    return h('div', {
      class: 'performance-corners'
    }, [
      this.$slots.default,
      ...cornerElements
    ])
  }
}

🎨 第六章:设计系统集成

6.1 与设计系统整合

// design-system-integration.js
export const createCornerDesignSystem = (designSystem) => {
  const { colors, typography, spacing, shadows, borderRadius } = designSystem
  
  return {
    // 预定义角标主题
    themes: {
      light: {
        cornerBg: colors.white,
        cornerBorder: colors.gray[200],
        cornerShadow: shadows.sm,
        textColor: colors.gray[800]
      },
      dark: {
        cornerBg: colors.gray[800],
        cornerBorder: colors.gray[700],
        cornerShadow: shadows.lg,
        textColor: colors.white
      },
      brand: {
        cornerBg: colors.primary[50],
        cornerBorder: colors.primary[200],
        cornerShadow: shadows.md,
        textColor: colors.primary[700]
      }
    },
    
    // 角标尺寸系统
    sizes: {
      xs: {
        size: '20px',
        fontSize: typography.fontSize.xs,
        padding: spacing[1],
        borderRadius: borderRadius.sm
      },
      sm: {
        size: '30px',
        fontSize: typography.fontSize.sm,
        padding: spacing[2],
        borderRadius: borderRadius.md
      },
      md: {
        size: '40px',
        fontSize: typography.fontSize.base,
        padding: spacing[3],
        borderRadius: borderRadius.lg
      },
      lg: {
        size: '50px',
        fontSize: typography.fontSize.lg,
        padding: spacing[4],
        borderRadius: borderRadius.xl
      },
      xl: {
        size: '60px',
        fontSize: typography.fontSize.xl,
        padding: spacing[5],
        borderRadius: borderRadius['2xl']
      }
    },
    
    // 动画配置
    animations: {
      duration: {
        fast: '150ms',
        normal: '300ms',
        slow: '500ms'
      },
      easing: {
        default: 'cubic-bezier(0.4, 0, 0.2, 1)',
        bounce: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
        elastic: 'cubic-bezier(0.68, -0.6, 0.32, 1.6)'
      }
    },
    
    // 生成角标样式
    generateCornerStyle(cornerConfig, theme = 'light', size = 'md') {
      const themeConfig = this.themes[theme]
      const sizeConfig = this.sizes[size]
      
      return {
        width: cornerConfig.size || sizeConfig.size,
        height: cornerConfig.size || sizeConfig.size,
        backgroundColor: cornerConfig.bgColor || themeConfig.cornerBg,
        border: cornerConfig.border ? `1px solid ${themeConfig.cornerBorder}` : 'none',
        borderRadius: cornerConfig.borderRadius || sizeConfig.borderRadius,
        boxShadow: cornerConfig.shadow || themeConfig.cornerShadow,
        color: cornerConfig.textColor || themeConfig.textColor,
        fontSize: sizeConfig.fontSize,
        padding: sizeConfig.padding,
        ...cornerConfig.customStyle
      }
    },
    
    // 生成动画关键帧
    generateKeyframes(effectName) {
      const keyframes = {
        pulse: `
          @keyframes pulse {
            0%, 100% { transform: scale(1); }
            50% { transform: scale(1.1); }
          }
        `,
        float: `
          @keyframes float {
            0%, 100% { transform: translateY(0); }
            50% { transform: translateY(-10px); }
          }
        `,
        spin: `
          @keyframes spin {
            from { transform: rotate(0deg); }
            to { transform: rotate(360deg); }
          }
        `
      }
      
      return keyframes[effectName] || ''
    },
    
    // 创建CSS变量
    createCSSVariables(theme = 'light') {
      const themeConfig = this.themes[theme]
      
      return Object.entries(themeConfig).map(([key, value]) => {
        return `--corner-${key}: ${value};`
      }).join('\n')
    }
  }
}

// Vue插件
export const CornerDesignSystemPlugin = {
  install(app, designSystem) {
    const cornerDS = createCornerDesignSystem(designSystem)
    
    // 全局提供设计系统
    app.provide('cornerDesignSystem', cornerDS)
    
    // 全局组件
    app.component('DesignSystemCorner', {
      props: {
        config: Object,
        theme: {
          type: String,
          default: 'light'
        },
        size: {
          type: String,
          default: 'md'
        }
      },
      
      inject: ['cornerDesignSystem'],
      
      computed: {
        cornerStyle() {
          return this.cornerDesignSystem.generateCornerStyle(
            this.config, 
            this.theme, 
            this.size
          )
        }
      },
      
      template: `
        <div 
          class="design-system-corner"
          :style="cornerStyle"
        >
          <slot></slot>
        </div>
      `
    })
    
    // 全局指令
    app.directive('corner-animate', {
      mounted(el, binding) {
        const { effect, duration } = binding.value || {}
        const ds = cornerDS
        
        if (effect && ds.animations.duration[duration]) {
          el.style.animation = `
            ${effect} ${ds.animations.duration[duration]} ${ds.animations.easing.default} infinite
          `
        }
      }
    })
  }
}

📱 第七章:移动端优化

7.1 触摸交互优化

<!-- MobileTouchCorners.vue -->
<template>
  <div 
    class="mobile-corners-container"
    :class="{ 'touch-active': isTouching }"
    @touchstart="onTouchStart"
    @touchmove="onTouchMove"
    @touchend="onTouchEnd"
    @touchcancel="onTouchCancel"
  >
    <!-- 内容区域 -->
    <div class="content-wrapper">
      <slot></slot>
    </div>
    
    <!-- 触摸反馈 -->
    <div 
      v-if="touchFeedback.visible"
      class="touch-feedback"
      :style="touchFeedback.style"
    >
      <div class="ripple-effect"></div>
    </div>
    
    <!-- 移动端优化的角标 -->
    <div 
      v-for="corner in mobileOptimizedCorners"
      :key="corner.id"
      class="mobile-corner"
      :class="[
        `corner-${corner.position}`,
        { 'corner-touchable': corner.interactive }
      ]"
      :style="getMobileCornerStyle(corner)"
      @click="onMobileCornerClick(corner, $event)"
    >
      <!-- 角标内容 -->
      <div class="corner-content">
        <!-- 图片使用更小的尺寸 -->
        <img 
          v-if="corner.type === 'image'"
          :src="corner.mobileSrc || corner.src"
          :alt="corner.alt"
          loading="lazy"
          @load="onImageLoad(corner)"
        />
        
        <!-- 图标 -->
        <i 
          v-else-if="corner.type === 'icon'"
          :class="corner.icon"
        ></i>
        
        <!-- 文本 -->
        <span 
          v-else-if="corner.type === 'text'"
          class="corner-text"
        >
          {{ corner.text }}
        </span>
        
        <!-- 角标徽章 -->
        <div 
          v-if="corner.badge && corner.badge > 0"
          class="mobile-badge"
        >
          {{ corner.badge > 99 ? '99+' : corner.badge }}
        </div>
      </div>
      
      <!-- 长按菜单 -->
      <div 
        v-if="corner.longPressMenu && activeLongPress === corner.id"
        class="long-press-menu"
        :style="getMenuPosition(corner)"
      >
        <div 
          v-for="item in corner.longPressMenu"
          :key="item.id"
          class="menu-item"
          @click.stop="onMenuItemClick(item, corner)"
        >
          <i :class="item.icon"></i>
          <span>{{ item.label }}</span>
        </div>
      </div>
    </div>
    
    <!-- 手势提示 -->
    <div 
      v-if="showGestureHint"
      class="gesture-hint"
      :class="`hint-${gestureHint.type}`"
    >
      <div class="hint-icon">👆</div>
      <div class="hint-text">{{ gestureHint.text }}</div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'

const props = defineProps({
  corners: Array,
  mobileConfig: {
    type: Object,
    default: () => ({
      minTouchSize: 44, // iOS建议的最小触摸尺寸
      longPressDelay: 500,
      vibration: false,
      hapticFeedback: true,
      swipeThreshold: 50,
      gestureHints: true
    })
  }
})

const emit = defineEmits([
  'corner-tap',
  'corner-long-press',
  'corner-swipe',
  'gesture-complete'
])

// 移动端状态
const isTouching = ref(false)
const touchStartTime = ref(0)
const touchStartPosition = ref({ x: 0, y: 0 })
const activeLongPress = ref(null)
const touchFeedback = ref({
  visible: false,
  style: {}
})

// 手势提示
const showGestureHint = ref(true)
const gestureHint = ref({
  type: 'tap',
  text: '点击角标查看详情'
})

// 移动端优化的角标配置
const mobileOptimizedCorners = computed(() => {
  return props.corners.map(corner => {
    // 移动端特定的调整
    const mobileCorner = {
      ...corner,
      size: corner.mobileSize || Math.max(corner.size || 40, props.mobileConfig.minTouchSize),
      interactive: corner.interactive !== false,
      // 减少动画复杂度
      effect: corner.mobileEffect || (corner.effect === 'rotate' ? 'pulse' : corner.effect)
    }
    
    // 确保触摸区域足够大
    if (mobileCorner.interactive) {
      mobileCorner.touchPadding = '10px'
    }
    
    return mobileCorner
  })
})

// 触摸事件处理
const onTouchStart = (event) => {
  isTouching.value = true
  touchStartTime.value = Date.now()
  touchStartPosition.value = {
    x: event.touches[0].clientX,
    y: event.touches[0].clientY
  }
  
  // 显示触摸反馈
  const touch = event.touches[0]
  showTouchFeedback(touch.clientX, touch.clientY)
  
  // 开始长按检测
  startLongPressDetection(event)
}

const onTouchMove = (event) => {
  // 取消长按检测
  clearLongPressDetection()
  
  // 计算滑动距离
  const currentX = event.touches[0].clientX
  const currentY = event.touches[0].clientY
  
  const deltaX = currentX - touchStartPosition.value.x
  const deltaY = currentY - touchStartPosition.value.y
  
  // 如果滑动距离超过阈值,触发滑动事件
  if (Math.abs(deltaX) > props.mobileConfig.swipeThreshold || 
      Math.abs(deltaY) > props.mobileConfig.swipeThreshold) {
    emit('corner-swipe', {
      deltaX,
      deltaY,
      direction: getSwipeDirection(deltaX, deltaY)
    })
    
    // 隐藏触摸反馈
    hideTouchFeedback()
  }
}

const onTouchEnd = () => {
  isTouching.value = false
  hideTouchFeedback()
  clearLongPressDetection()
  
  const touchDuration = Date.now() - touchStartTime.value
  
  // 如果触摸时间很短,认为是点击
  if (touchDuration < 200) {
    // 触觉反馈
    if (props.mobileConfig.hapticFeedback && window.navigator.vibrate) {
      window.navigator.vibrate(10)
    }
  }
}

const onTouchCancel = () => {
  isTouching.value = false
  hideTouchFeedback()
  clearLongPressDetection()
}

// 长按处理
let longPressTimer = null

const startLongPressDetection = (event) => {
  const touch = event.touches[0]
  const element = document.elementFromPoint(touch.clientX, touch.clientY)
  const cornerElement = element?.closest('.mobile-corner')
  
  if (cornerElement) {
    const cornerId = cornerElement.dataset.cornerId
    const corner = mobileOptimizedCorners.value.find(c => c.id === cornerId)
    
    if (corner?.longPressMenu) {
      longPressTimer = setTimeout(() => {
        activeLongPress.value = cornerId
        
        // 触觉反馈
        if (props.mobileConfig.hapticFeedback && window.navigator.vibrate) {
          window.navigator.vibrate(50)
        }
        
        emit('corner-long-press', corner)
      }, props.mobileConfig.longPressDelay)
    }
  }
}

const clearLongPressDetection = () => {
  if (longPressTimer) {
    clearTimeout(longPressTimer)
    longPressTimer = null
  }
}

// 触摸反馈
const showTouchFeedback = (x, y) => {
  touchFeedback.value = {
    visible: true,
    style: {
      left: `${x}px`,
      top: `${y}px`
    }
  }
}

const hideTouchFeedback = () => {
  touchFeedback.value.visible = false
}

// 角标点击
const onMobileCornerClick = (corner, event) => {
  if (activeLongPress.value === corner.id) {
    // 如果有长按菜单显示,不触发点击
    activeLongPress.value = null
    return
  }
  
  // 阻止事件冒泡
  event.stopPropagation()
  
  // 触觉反馈
  if (corner.interactive && props.mobileConfig.hapticFeedback && window.navigator.vibrate) {
    window.navigator.vibrate(20)
  }
  
  emit('corner-tap', corner)
  
  // 隐藏手势提示
  if (showGestureHint.value) {
    showGestureHint.value = false
  }
}

// 获取移动端样式
const getMobileCornerStyle = (corner) => {
  const style = {
    width: `${corner.size}px`,
    height: `${corner.size}px`,
    minWidth: `${props.mobileConfig.minTouchSize}px`,
    minHeight: `${props.mobileConfig.minTouchSize}px`
  }
  
  // 触摸反馈
  if (corner.touchPadding) {
    style.padding = corner.touchPadding
  }
  
  // 位置
  const positions = {
    'top-left': { top: '10px', left: '10px' },
    'top-right': { top: '10px', right: '10px' },
    'bottom-left': { bottom: '10px', left: '10px' },
    'bottom-right': { bottom: '10px', right: '10px' }
  }
  
  Object.assign(style, positions[corner.position] || positions['top-left'])
  
  return style
}

// 获取菜单位置
const getMenuPosition = (corner) => {
  const basePosition = corner.position.split('-')
  const isTop = basePosition[0] === 'top'
  const isLeft = basePosition[1] === 'left'
  
  return {
    [isTop ? 'top' : 'bottom']: '100%',
    [isLeft ? 'left' : 'right']: '0'
  }
}

// 手势方向判断
const getSwipeDirection = (deltaX, deltaY) => {
  if (Math.abs(deltaX) > Math.abs(deltaY)) {
    return deltaX > 0 ? 'right' : 'left'
  } else {
    return deltaY > 0 ? 'down' : 'up'
  }
}

// 图片加载完成
const onImageLoad = (corner) => {
  corner.loaded = true
}

// 初始化手势提示
const initGestureHints = () => {
  if (props.mobileConfig.gestureHints) {
    setTimeout(() => {
      if (showGestureHint.value) {
        gestureHint.value = {
          type: 'long-press',
          text: '长按角标查看更多选项'
        }
      }
    }, 3000)
    
    // 8秒后自动隐藏提示
    setTimeout(() => {
      showGestureHint.value = false
    }, 8000)
  }
}

onMounted(() => {
  initGestureHints()
})

onUnmounted(() => {
  clearLongPressDetection()
})
</script>

<style scoped>
.mobile-corners-container {
  position: relative;
  touch-action: pan-y;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

.content-wrapper {
  position: relative;
  z-index: 1;
}

.mobile-corner {
  position: absolute;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.9);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  transition: all 0.2s ease;
  overflow: visible;
}

.mobile-corner.corner-touchable {
  cursor: pointer;
}

.mobile-corner.corner-touchable:active {
  transform: scale(0.95);
  opacity: 0.8;
}

.corner-content {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.corner-content img {
  width: 70%;
  height: 70%;
  object-fit: contain;
}

.mobile-badge {
  position: absolute;
  top: -5px;
  right: -5px;
  min-width: 20px;
  height: 20px;
  background: #ff4757;
  color: white;
  border-radius: 10px;
  font-size: 10px;
  font-weight: bold;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 4px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}

/* 触摸反馈 */
.touch-feedback {
  position: absolute;
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: 1000;
}

.ripple-effect {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.3);
  animation: ripple 0.6s ease-out;
}

@keyframes ripple {
  0% {
    transform: scale(0.1);
    opacity: 1;
  }
  100% {
    transform: scale(1);
    opacity: 0;
  }
}

/* 长按菜单 */
.long-press-menu {
  position: absolute;
  background: white;
  border-radius: 12px;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
  padding: 8px;
  min-width: 120px;
  z-index: 1001;
  animation: menuAppear 0.2s ease;
}

@keyframes menuAppear {
  from {
    opacity: 0;
    transform: scale(0.9) translateY(-10px);
  }
  to {
    opacity: 1;
    transform: scale(1) translateY(0);
  }
}

.menu-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-radius: 8px;
  font-size: 14px;
  color: #333;
  transition: background-color 0.2s;
}

.menu-item:active {
  background-color: #f5f5f5;
}

.menu-item i {
  font-size: 16px;
}

/* 手势提示 */
.gesture-hint {
  position: absolute;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(0, 0, 0, 0.8);
  color: white;
  padding: 12px 16px;
  border-radius: 20px;
  display: flex;
  align-items: center;
  gap: 10px;
  animation: hintFloat 3s ease-in-out infinite;
  z-index: 100;
}

@keyframes hintFloat {
  0%, 100% { transform: translateX(-50%) translateY(0); }
  50% { transform: translateX(-50%) translateY(-10px); }
}

.hint-icon {
  font-size: 20px;
}

.hint-text {
  font-size: 14px;
  font-weight: 500;
}

/* 适配深色模式 */
@media (prefers-color-scheme: dark) {
  .mobile-corner {
    background: rgba(40, 40, 40, 0.9);
  }
  
  .long-press-menu {
    background: #2d2d2d;
    color: white;
  }
  
  .menu-item:active {
    background-color: #3d3d3d;
  }
}

/* 适配小屏幕 */
@media (max-width: 320px) {
  .mobile-corner {
    min-width: 40px;
    min-height: 40px;
  }
  
  .mobile-badge {
    min-width: 16px;
    height: 16px;
    font-size: 8px;
  }
}
</style>

📊 第八章:测试与质量保证

8.1 单元测试示例

// corners.spec.js
import { mount } from '@vue/test-utils'
import CornerDecorations from './CornerDecorations.vue'
import SmartCorners from './SmartCorners.vue'

describe('CornerDecorations', () => {
  test('渲染四个角标', () => {
    const wrapper = mount(CornerDecorations, {
      props: {
        src: '/test-image.png',
        size: 40
      }
    })
    
    const corners = wrapper.findAll('.corner')
    expect(corners).toHaveLength(4)
  })
  
  test('自定义各角图片源', () => {
    const wrapper = mount(CornerDecorations, {
      props: {
        topLeftSrc: '/tl.png',
        topRightSrc: '/tr.png',
        bottomLeftSrc: '/bl.png',
        bottomRightSrc: '/br.png'
      }
    })
    
    const images = wrapper.findAll('img')
    expect(images).toHaveLength(4)
    
    const srcs = images.map(img => img.attributes('src'))
    expect(srcs).toEqual(['/tl.png', '/tr.png', '/bl.png', '/br.png'])
  })
  
  test('响应式尺寸调整', async () => {
    const wrapper = mount(CornerDecorations, {
      props: {
        size: 40,
        responsive: true
      }
    })
    
    // 模拟窗口大小变化
    Object.defineProperty(window, 'innerWidth', {
      writable: true,
      configurable: true,
      value: 375 // 移动端宽度
    })
    
    window.dispatchEvent(new Event('resize'))
    await wrapper.vm.$nextTick()
    
    const image = wrapper.find('img')
    expect(image.attributes('style')).toContain('width')
  })
})

describe('SmartCorners', () => {
  test('根据条件显示角标', () => {
    const corners = [
      {
        id: '1',
        position: 'top-left',
        type: 'image',
        src: '/test.png',
        visible: true
      },
      {
        id: '2',
        position: 'top-right',
        type: 'icon',
        icon: '★',
        visible: false // 隐藏
      }
    ]
    
    const wrapper = mount(SmartCorners, {
      props: { corners }
    })
    
    const cornerElements = wrapper.findAll('.corner-item')
    expect(cornerElements).toHaveLength(1) // 只显示一个
  })
  
  test('交互事件触发', async () => {
    const onClick = jest.fn()
    const corners = [{
      id: '1',
      position: 'top-left',
      type: 'icon',
      icon: '★',
      interactive: true,
      onClick
    }]
    
    const wrapper = mount(SmartCorners, {
      props: { corners }
    })
    
    await wrapper.find('.corner-item').trigger('click')
    expect(onClick).toHaveBeenCalled()
  })
  
  test('动画效果应用', () => {
    const corners = [{
      id: '1',
      position: 'top-left',
      type: 'icon',
      icon: '★',
      effect: 'pulse'
    }]
    
    const wrapper = mount(SmartCorners, {
      props: { corners }
    })
    
    const corner = wrapper.find('.corner-item')
    expect(corner.classes()).toContain('effect-pulse')
  })
})

// 性能测试
describe('性能测试', () => {
  test('大量角标渲染性能', () => {
    const startTime = performance.now()
    
    const corners = Array.from({ length: 20 }, (_, i) => ({
      id: `corner-${i}`,
      position: ['top-left', 'top-right', 'bottom-left', 'bottom-right'][i % 4],
      type: 'image',
      src: `/image-${i}.png`,
      size: 30 + (i % 10)
    }))
    
    const wrapper = mount(SmartCorners, {
      props: { corners }
    })
    
    const renderTime = performance.now() - startTime
    console.log(`渲染20个角标耗时: ${renderTime}ms`)
    
    // 性能断言
    expect(renderTime).toBeLessThan(100) // 应少于100ms
  })
  
  test('内存使用检查', () => {
    const initialMemory = performance.memory?.usedJSHeapSize || 0
    
    const wrapper = mount(SmartCorners, {
      props: {
        corners: Array.from({ length: 50 }, (_, i) => ({
          id: `corner-${i}`,
          position: 'top-left',
          type: 'text',
          text: i.toString()
        }))
      }
    })
    
    wrapper.unmount()
    
    // 强制垃圾回收(如果可用)
    if (global.gc) {
      global.gc()
    }
    
    const finalMemory = performance.memory?.usedJSHeapSize || 0
    const memoryIncrease = finalMemory - initialMemory
    
    console.log(`内存增加: ${memoryIncrease} bytes`)
    expect(memoryIncrease).toBeLessThan(5 * 1024 * 1024) // 应少于5MB
  })
})

🚀 总结与最佳实践

9.1 核心要点总结

  1. 组件化思维:将四角装饰抽象为可复用的Vue组件
  2. 性能优先:懒加载、图片优化、节流防抖
  3. 移动端友好:触摸优化、手势支持、响应式设计
  4. 可访问性:确保装饰不影响内容访问
  5. 设计系统集成:保持视觉一致性

9.2 推荐的最佳实践

// 最佳实践示例
export const cornerBestPractices = {
  1: '使用SVG图标替代PNG,获得更好的缩放质量和更小的文件体积',
  2: '为移动端提供更大的触摸区域(至少44x44px)',
  3: '实现懒加载,特别是当页面有大量装饰时',
  4: '提供适当的替代文本和ARIA标签',
  5: '考虑性能影响,限制动画复杂度',
  6: '与设计系统集成,保持一致性',
  7: '提供主题支持,适应深色/浅色模式',
  8: '实现正确的错误处理和降级方案',
  9: '编写单元测试和性能测试',
  10: '提供详细的文档和使用示例'
}

9.3 未来发展方向

  1. Web Components:将角标组件转换为原生Web Components
  2. 3D效果:使用WebGL或CSS 3D实现立体装饰
  3. AI生成:根据内容自动生成合适的装饰图案
  4. AR集成:在增强现实中显示装饰效果
  5. 语音交互:支持语音控制角标显示和隐藏

通过本文的深入探讨,我们不仅学会了如何在Vue中实现四角装饰效果,更重要的是掌握了构建高质量、高性能、可维护的UI组件的完整方法论。四角装饰虽然是一个小功能,但它体现了现代前端开发的核心理念:组件化、性能优化、用户体验和可访问性

记住,最好的技术实现总是服务于用户体验。在追求炫酷效果的同时,不要忘记设计的初衷——提升用户体验,而不是分散注意力

装饰是艺术,实现是科学,而优秀的开发者在两者之间找到了完美的平衡。 🎨✨

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐