【前端】Vue 3 课程选择组件开发实战:从设计到实现

发布于:2025-07-27 ⋅ 阅读:(12) ⋅ 点赞:(0)


前言

今天我将分享一个基于 Vue 3 和 Element Plus 的课程选择组件的开发过程。这个组件不仅实现了基本的课程选择功能,还包含了精美的UI设计和良好的交互体验。下面我们将从组件设计、功能实现到样式优化进行全面解析。

一、组件设计思路

1. 需求分析

我们需要实现一个课程选择界面,要求:

  • 以卡片形式展示多个课程
  • 点击卡片时显示课程详情
  • 有视觉反馈的交互效果
  • 良好的可访问性支持
  • 响应式布局适配不同设备

2. 技术选型

  • Vue 3:使用 <script setup> 语法简化代码
  • Element Plus:利用其 ElMessage 组件提供用户反馈
  • CSS 变量:实现动态主题色
  • 绝对定位:创建自由布局的卡片位置

二、核心功能实现

1. 数据结构设计

const courses = [
  {
    id: 'math',
    name: '高等数学',
    description: '涵盖微积分、线性代数等核心数学知识',
    color: '#f7b100',
    activeColor: '#ffcc00',
    position: { top: '0%', left: '0.2%', width: '18%', height: '48%' }
  },
  // 其他课程...
]

每个课程对象包含:

  • 基础信息:id、name、description
  • 样式配置:color、activeColor
  • 布局参数:position 对象定义绝对定位属性

2. 动态样式计算

const getCourseStyle = (course) => {
  const isActive = activeId.value === course.id
  const baseColor = isActive ? course.activeColor : course.color
  const darkenColor = adjustColor(baseColor, isActive ? -25 : -15)
  
  return {
    top: course.position.top,
    left: course.position.left,
    width: course.position.width,
    height: course.position.height,
    '--base-color': baseColor,
    '--darken-color': darkenColor,
    '--text-color': getContrastColor(baseColor)
  }
}

这里使用了 CSS 变量来实现动态主题色,通过计算属性返回样式对象。

3. 颜色处理工具函数

组件中实现了两个实用的颜色处理函数:

adjustColor 函数:调整颜色亮度

const adjustColor = (hexColor, percent) => {
  // 处理3位或6位hex颜色
  // 转换为RGB数值
  // 按百分比调整亮度
  // 返回新的hex颜色
}

getContrastColor 函数:自动计算对比色

const getContrastColor = (hexColor) => {
  // 计算颜色亮度
  // 根据亮度返回黑色或白色
}

三、交互实现

1. 点击事件处理

const handleCourseClick = (course) => {
  activeId.value = course.id
  ElMessage.success({
    message: `已选中课程: ${course.name}`,
    duration: 1500
  })
}

使用 Element Plus 的 ElMessage 提供用户反馈,增强交互体验。

2. 键盘可访问性

<div 
  @keydown.enter="handleCourseClick(course)"
  tabindex="0"
  role="button"
  :aria-label="`选择课程${course.name}`"
>

添加了键盘事件支持和 ARIA 属性,使组件可以通过键盘操作。

3. 状态管理

使用 activeId ref 来跟踪当前选中的课程,通过计算属性动态应用样式类:

:class="{ active: activeId === course.id }"

四、样式设计

1. 基础布局

.course-selection-container {
  position: relative;
  width: 100%;
  height: 600px;
  margin: 20px auto;
}

.course-box {
  position: absolute;
  /* 其他样式... */
}

使用绝对定位创建自由布局,容器设置固定高度。

2. 卡片效果

.course-box {
  border-radius: 12px;
  cursor: pointer;
  transition: all 0.3s ease;
  background: linear-gradient(135deg, var(--base-color), var(--darken-color));
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
  • 圆角边框
  • 平滑过渡动画
  • 渐变背景
  • 阴影效果

3. 交互状态

.course-box:hover {
  transform: translateY(-5px) scale(1.02);
}

.course-box.active {
  transform: scale(1.05);
  z-index: 10;
  box-shadow: 0 12px 24px rgba(0, 0, 0, 0.25);
}
  • 悬停效果:轻微上浮和放大
  • 激活状态:更大缩放和更高阴影

4. 响应式设计

@media (max-width: 768px) {
  .course-selection-container {
    height: 1000px;
  }
  
  .course-box {
    position: relative;
    width: 90% !important;
    height: 120px !important;
    margin: 10px auto;
  }
}

在小屏幕设备上,将绝对定位改为常规布局,垂直排列卡片。

五、完整页面代码

<template>
  <div class="course-selection-container">
    <!-- 课程卡片组件 -->
    <div
        v-for="course in courses"
        :key="course.id"
        class="course-box"
        :style="getCourseStyle(course)"
        :class="{ active: activeId === course.id }"
        @click="handleCourseClick(course)"
        @keydown.enter="handleCourseClick(course)"
        tabindex="0"
        role="button"
        :aria-label="`选择课程${course.name}`"
    >
      <div class="course-name">{{ course.name }}</div>
      <div class="course-desc" v-if="activeId === course.id">{{ course.description }}</div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'

// 当前选中的课程ID
const activeId = ref('math')

// 课程数据配置
const courses = [
  {
    id: 'math',
    name: '高等数学',
    description: '涵盖微积分、线性代数等核心数学知识',
    color: '#f7b100',
    activeColor: '#ffcc00',
    position: { top: '0%', left: '0.2%', width: '18%', height: '48%' }
  },
  {
    id: 'physics',
    name: '大学物理',
    description: '力学、电磁学、热力学等物理基础',
    color: '#0096ff',
    activeColor: '#00b4ff',
    position: { top: '0%', left: '20.2%', width: '18%', height: '48%' }
  },
  {
    id: 'chemistry',
    name: '有机化学',
    description: '有机化合物结构与反应机理研究',
    color: '#32c832',
    activeColor: '#4ce64c',
    position: { top: '0%', left: '40.2%', width: '18%', height: '48%' }
  },
  {
    id: 'programming',
    name: '程序设计',
    description: '编程基础与算法思维训练',
    color: '#ff6400',
    activeColor: '#ff8200',
    position: { top: '0%', left: '60.2%', width: '18%', height: '48%' }
  },
  {
    id: 'english',
    name: '学术英语',
    description: '学术写作与专业英语能力提升',
    color: '#6464ff',
    activeColor: '#8282ff',
    position: { top: '50%', left: '0.2%', width: '18%', height: '48%' }
  },
  {
    id: 'history',
    name: '世界历史',
    description: '全球文明发展与历史事件分析',
    color: '#b464b4',
    activeColor: '#d282d2',
    position: { top: '50%', left: '20.2%', width: '18%', height: '48%' }
  },
  {
    id: 'art',
    name: '艺术设计',
    description: '视觉艺术原理与创意设计实践',
    color: '#ff3296',
    activeColor: '#ff50b4',
    position: { top: '50%', left: '40.2%', width: '18%', height: '48%' }
  }
]

// 计算课程卡片样式
const getCourseStyle = (course) => {
  const isActive = activeId.value === course.id
  const baseColor = isActive ? course.activeColor : course.color
  const darkenColor = adjustColor(baseColor, isActive ? -25 : -15)

  return {
    top: course.position.top,
    left: course.position.left,
    width: course.position.width,
    height: course.position.height,
    '--base-color': baseColor,
    '--darken-color': darkenColor,
    '--text-color': getContrastColor(baseColor)
  }
}

// 处理课程点击
const handleCourseClick = (course) => {
  activeId.value = course.id
  ElMessage.success({
    message: `已选中课程: ${course.name}`,
    duration: 1500
  })
}

// 颜色调整工具函数(优化版)
const adjustColor = (hexColor, percent) => {
  // 确保hexColor是6位十六进制格式
  let hex = hexColor.replace('#', '')
  if (hex.length === 3) {
    hex = hex.split('').map(x => x + x).join('')
  }

  // 转换为RGB
  let r = parseInt(hex.substring(0, 2), 16)
  let g = parseInt(hex.substring(2, 4), 16)
  let b = parseInt(hex.substring(4, 6), 16)

  // 调整亮度
  r = Math.min(255, Math.max(0, r + Math.round(r * percent / 100)))
  g = Math.min(255, Math.max(0, g + Math.round(g * percent / 100)))
  b = Math.min(255, Math.max(0, b + Math.round(b * percent / 100)))

  // 返回hex格式
  return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`
}

// 获取对比色(确保文字可读性)
const getContrastColor = (hexColor) => {
  const hex = hexColor.replace('#', '')
  const r = parseInt(hex.substring(0, 2), 16)
  const g = parseInt(hex.substring(2, 4), 16)
  const b = parseInt(hex.substring(4, 6), 16)
  const brightness = (r * 299 + g * 587 + b * 114) / 1000
  return brightness > 128 ? '#333' : '#fff'
}
</script>

<style scoped>
.course-selection-container {
  position: relative;
  width: 100%;
  height: 600px;
  margin: 20px auto;
}

.course-box {
  position: absolute;
  border-radius: 12px;
  cursor: pointer;
  transition: all 0.3s ease;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  background: linear-gradient(135deg, var(--base-color), var(--darken-color));
  color: var(--text-color);
  overflow: hidden;
  outline: none;
}

.course-box:hover {
  transform: translateY(-5px) scale(1.02);
  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
}

.course-box.active {
  transform: scale(1.05);
  z-index: 10;
  box-shadow: 0 12px 24px rgba(0, 0, 0, 0.25);
}

.course-box:focus-visible {
  outline: 2px solid var(--darken-color);
  outline-offset: 2px;
}

.course-name {
  font-size: 1.25rem;
  font-weight: bold;
  text-align: center;
  padding: 12px;
  transition: all 0.3s ease;
}

.course-desc {
  font-size: 0.875rem;
  padding: 0 12px 12px;
  text-align: center;
  max-width: 90%;
  opacity: 0;
  max-height: 0;
  transition: all 0.3s ease;
}

.course-box.active .course-desc {
  opacity: 1;
  max-height: 100px;
}

@media (max-width: 768px) {
  .course-selection-container {
    height: 1000px;
  }

  .course-box {
    position: relative;
    top: auto !important;
    left: auto !important;
    width: 90% !important;
    height: 120px !important;
    margin: 10px auto;
  }
}
</style>

六、实现效果

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

七、优化与改进

1. 性能优化

  • 使用 CSS 变量减少重复计算
  • 避免在模板中进行复杂计算
  • 合理使用 transition 实现平滑动画

2. 可访问性增强

  • 添加 tabindex 使元素可聚焦
  • 使用 ARIA 属性描述元素
  • 键盘事件支持
  • 高对比度文字颜色

3. 代码组织

  • 将样式计算逻辑提取到单独函数
  • 使用计算属性缓存结果
  • 清晰的代码注释

总结

这个课程选择组件展示了 Vue 3 的多种特性应用:

  1. 使用 <script setup> 简化组合式 API 代码
  2. 动态样式绑定和 CSS 变量实现主题化
  3. 完善的交互状态管理
  4. 响应式布局设计
  5. 可访问性最佳实践

组件可以轻松扩展:

  • 添加更多课程只需在数据中增加配置
  • 可以通过 props 接收外部课程数据
  • 可以添加 emit 事件实现父子组件通信

希望这个实现案例对你的 Vue 开发有所帮助,你可以根据实际需求调整样式和功能。


网站公告

今日签到

点亮在社区的每一天
去签到