文章目录
前言
今天我将分享一个基于 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 的多种特性应用:
- 使用
<script setup>
简化组合式 API 代码 - 动态样式绑定和 CSS 变量实现主题化
- 完善的交互状态管理
- 响应式布局设计
- 可访问性最佳实践
组件可以轻松扩展:
- 添加更多课程只需在数据中增加配置
- 可以通过 props 接收外部课程数据
- 可以添加 emit 事件实现父子组件通信
希望这个实现案例对你的 Vue 开发有所帮助,你可以根据实际需求调整样式和功能。