<think>我们需要封装一个横向的Timeline组件,基于ElementPlus的Timeline。关键点:1.使用el-timeline并设置direction="horizontal"2.由于ElementPlus的Timeline默认是垂直的,改为横向后需要调整样式3.封装成组件,允许传入数据、自定义内容等组件功能设计:-支持传入数据数组,每个数据项包含时间戳、标题、描述等-支持自定义节点内容(通过插槽)-支持点击事件-支持设置当前激活项注意:横向时间线在ElementPlus中已经通过direction属性支持,但样式可能需要调整,特别是时间戳和连接线的位置。我们将创建一个名为HorizontalTimeline的组件,使用<scriptsetup>语法。组件代码:</think>
以下是在 Vue3 中将 Element Plus 的 Timeline 时间线改为横向布局并封装成可复用组件的完整解决方案:
### 横向时间线组件封装
```vue
<template>
<div class="horizontal-timeline-container">
<!-- 使用 el-timeline 并设置方向为 horizontal -->
<el-timeline direction="horizontal" :reverse="reverse">
<!-- 遍历传入的时间线数据 -->
<el-timeline-item
v-for="(item, index) in items"
:key="item.key || index"
:timestamp="item.timestamp"
:hollow="item.hollow || hollow"
:color="item.color || color"
:size="size"
:class="{ 'is-active': activeIndex === index }"
class="timeline-item"
@click="handleItemClick(index)"
>
<!-- 自定义内容插槽 -->
<slot name="content" :item="item" :index="index">
<!-- 默认内容模板 -->
<div class="timeline-content">
<div class="timeline-title">{{ item.title }}</div>
<div v-if="item.description" class="timeline-description">
{{ item.description }}
</div>
</div>
</slot>
</el-timeline-item>
</el-timeline>
</div>
</template>
<script setup>
import { ref, watch, computed } from 'vue'
// 组件属性定义
const props = defineProps({
items: {
type: Array,
required: true,
default: () => [
{ title: '事件1', timestamp: '2023-01-01', description: '描述内容1' },
{ title: '事件2', timestamp: '2023-02-01', description: '描述内容2' }
]
},
reverse: {
type: Boolean,
default: false
},
hollow: {
type: Boolean,
default: false
},
color: {
type: String,
default: '#0bbd87' // Element Plus 默认的时间线颜色
},
size: {
type: String,
default: 'large',
validator: (value) => ['normal', 'large'].includes(value)
},
activeIndex: {
type: Number,
default: -1
},
clickable: {
type: Boolean,
default: true
}
})
// 触发事件
const emit = defineEmits(['item-click', 'update:active-index'])
// 当前激活项索引
const activeItemIndex = ref(props.activeIndex)
// 监听父组件传入的 activeIndex 变化
watch(() => props.activeIndex, (newVal) => {
activeItemIndex.value = newVal
})
// 处理时间线项点击
const handleItemClick = (index) => {
if (!props.clickable) return
activeItemIndex.value = index
emit('update:active-index', index)
emit('item-click', props.items[index], index)
}
</script>
<style scoped>
.horizontal-timeline-container {
overflow-x: auto;
padding: 20px 0;
position: relative;
}
/* 横向时间线布局优化 */
:deep(.el-timeline) {
white-space: nowrap;
display: flex;
padding: 0;
position: relative;
}
:deep(.el-timeline-item) {
display: inline-flex;
flex-direction: column;
align-items: center;
flex: 1;
min-width: 150px;
padding: 0 10px;
position: relative;
}
/* 时间戳样式 */
:deep(.el-timeline-item__timestamp) {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
margin-bottom: 8px;
text-align: center;
white-space: nowrap;
font-size: 0.9em;
}
/* 节点容器 */
:deep(.el-timeline-item__node) {
position: relative;
z-index: 1;
margin: 0 auto;
}
/* 连接线样式 */
:deep(.el-timeline-item__tail) {
position: absolute;
top: 50%;
left: calc(100% - 12px);
right: calc(-100% + 12px);
height: 2px;
transform: translateY(-50%);
background-color: var(--el-timeline-node-color, #e4e7ed);
}
/* 去掉最后一个时间项的连接线 */
:deep(.el-timeline-item:last-child .el-timeline-item__tail) {
display: none;
}
/* 内容区域样式 */
.timeline-content {
text-align: center;
margin-top: 15px;
padding: 10px;
border-radius: 4px;
transition: all 0.3s;
cursor: pointer;
}
.timeline-content:hover {
background-color: #f5f7fa;
}
.timeline-title {
font-weight: bold;
margin-bottom: 5px;
}
.timeline-description {
font-size: 0.85em;
color: #606266;
white-space: normal;
text-align: center;
}
/* 激活项样式 */
:deep(.is-active .el-timeline-item__node) {
transform: scale(1.2);
box-shadow: 0 0 0 4px rgba(11, 189, 135, 0.2);
}
:deep(.is-active .timeline-content) {
background-color: #ecf5ff;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
transform: translateY(-5px);
}
</style>
```
### 组件使用示例
```vue
<template>
<div class="demo-container">
<!-- 使用横向时间线组件 -->
<HorizontalTimeline
:items="timelineItems"
:active-index="activeIndex"
@update:active-index="activeIndex = $event"
@item-click="handleItemClick"
>
<!-- 自定义内容模板 -->
<template #content="{ item }">
<div class="custom-content" :class="{ 'is-active': item.isActive }">
<div class="custom-icon" :style="{ backgroundColor: item.iconBg }">
<el-icon :size="24">
<component :is="item.icon" />
</el-icon>
</div>
<div class="custom-title">{{ item.title }}</div>
<div class="custom-status" :style="{ color: item.statusColor }">
{{ item.status }}
</div>
</div>
</template>
</HorizontalTimeline>
<!-- 显示活动项详情 -->
<div v-if="activeItem" class="timeline-details">
<h3>当前活动项: {{ activeItem.title }}</h3>
<p>{{ activeItem.description }}</p>
<p>状态: <span :style="{ color: activeItem.statusColor }">{{ activeItem.status }}</span></p>
</div>
</div>
</template>
<script setup>
import HorizontalTimeline from './components/HorizontalTimeline.vue'
import { Calendar, Check, Document, User, Coin } from '@element-plus/icons-vue'
import { ref, computed } from 'vue'
// 当前激活项索引
const activeIndex = ref(1)
// 时间线数据
const timelineItems = ref([
{
title: '项目启动',
timestamp: '2023-01-15',
description: '项目正式启动,成立专项小组',
status: '已完成',
statusColor: '#67c23a',
icon: Calendar,
iconBg: '#f0f7ff'
},
{
title: '需求分析',
timestamp: '2023-02-20',
description: '收集用户需求并完成需求文档',
status: '进行中',
statusColor: '#409eff',
icon: Document,
iconBg: '#f0f7ff',
color: '#409eff',
isActive: true
},
{
title: '原型设计',
timestamp: '2023-03-25',
description: '完成界面原型和用户流程设计',
status: '未开始',
statusColor: '#909399',
icon: User,
iconBg: '#f0f7ff'
},
{
title: '开发阶段',
timestamp: '2023-04-30',
description: '进入开发编码阶段,分模块实施',
status: '未开始',
statusColor: '#909399',
icon: Coin,
iconBg: '#f0f7ff'
},
{
title: '测试验收',
timestamp: '2023-06-10',
description: '系统测试和用户验收测试',
status: '未开始',
statusColor: '#909399',
icon: Check,
iconBg: '#f0f7ff'
}
])
// 计算当前活动项
const activeItem = computed(() => {
return activeIndex.value >= 0 ? timelineItems.value[activeIndex.value] : null
})
// 处理时间线项点击事件
const handleItemClick = (item, index) => {
console.log('点击了时间线项:', item.title, '索引:', index)
}
</script>
<style scoped>
.demo-container {
max-width: 1200px;
margin: 40px auto;
padding: 20px;
}
.custom-content {
padding: 10px;
border-radius: 8px;
transition: all 0.3s;
}
.custom-icon {
margin: 0 auto 10px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.custom-title {
font-weight: bold;
margin: 5px 0;
}
.custom-status {
font-size: 0.85rem;
}
.timeline-details {
margin-top: 30px;
padding: 20px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background-color: #f8f9fa;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
</style>
```
### 关键实现说明
1. **横向布局转换**
- 使用 Element Plus Timeline 的 `direction="horizontal"` 属性实现基础横向布局
- 通过 CSS Flexbox 布局实现项的水平排列
- 调整时间戳位置,使其显示在节点上方
- 修改连接线为水平直线
2. **组件封装设计**
- 使用 `defineProps` 定义可配置属性:
- `items`:时间线数据数组(必填)
- `reverse`:是否倒序显示
- `hollow`:是否空心节点
- `color`:节点颜色
- `size`:节点尺寸
- `activeIndex`:当前激活项的索引
- `clickable`:是否可点击
- 通过 `defineEmits` 定义事件:
- `item-click`:点击项时触发
- `update:active-index`:用于实现活动索引的双向绑定
3. **交互功能实现**
- 支持点击时间线项切换激活状态
- 实现活动索引的双向绑定(v-model:active-index)
- 提供默认插槽和具名插槽支持自定义内容
4. **样式优化**
- 使用 `:deep()` 穿透选择器修改 Element Plus 组件内部样式
- 为激活项添加特殊样式效果(放大节点、背景高亮)
- 添加悬浮效果增强交互体验
- 确保响应式设计,在小屏设备上自动添加水平滚动
### 使用场景
此组件特别适用于:
- 项目进度追踪(如示例所示)
- 操作流程展示(如订单状态跟踪)
- 横向历史时间轴
- 步骤引导流程
- 任何需要水平展示时间顺序的场景