eagleye

Quasar QTimeline 组件企业级完整示例

# Quasar QTimeline 组件企业级完整示例

下面是一个完整的示例,展示了如何使用TimelineComponent组件的全部功能:

```html
<template>
<div class="q-pa-md">
<!-- 页面标题和操作区 -->
<div class="row items-center justify-between q-mb-md">
<div>
<h2 class="text-h4">项目时间线管理系统</h2>
<p class="text-grey-8">跟踪项目进度和重要事件</p>
</div>

<div class="row q-gutter-sm">
<q-btn
color="primary"
icon="add"
label="添加事件"
@click="showAddDialog = true"
/>
<q-btn
color="secondary"
icon="refresh"
label="刷新时间线"
@click="handleRefresh"
/>
<q-btn
color="grey-7"
icon="download"
label="导出数据"
@click="exportTimelineData"
/>
</div>
</div>

<!-- 统计信息卡片 -->
<div class="row q-col-gutter-md q-mb-md">
<q-card class="col-12 col-sm-4">
<q-card-section class="bg-primary text-white">
<div class="text-h6">总事件数</div>
<div class="text-h4">{{ timelineStats.total }}</div>
</q-card-section>
</q-card>

<q-card class="col-12 col-sm-4">
<q-card-section class="bg-positive text-white">
<div class="text-h6">本周新增</div>
<div class="text-h4">{{ timelineStats.thisWeek }}</div>
</q-card-section>
</q-card>

<q-card class="col-12 col-sm-4">
<q-card-section class="bg-info text-white">
<div class="text-h6">待处理</div>
<div class="text-h4">{{ timelineStats.pending }}</div>
</q-card-section>
</q-card>
</div>

<!-- 时间线组件 -->
<TimelineComponent
:auto-load="true"
:group-by-date="true"
:page-size="pageSize"
:initial-entries="initialEntries"
@entry-click="handleEntryClick"
@action-handler="handleAction"
@refresh="handleRefresh"
ref="timelineRef"
/>

<!-- 添加事件对话框 -->
<q-dialog v-model="showAddDialog" persistent>
<q-card style="min-width: 400px">
<q-card-section class="row items-center q-pb-none">
<div class="text-h6">添加新事件</div>
<q-space />
<q-btn icon="close" flat round dense v-close-popup />
</q-card-section>

<q-card-section>
<q-form @submit="addNewEntry" class="q-gutter-md">
<q-input
v-model="newEntry.title"
label="事件标题"
filled
:rules="[val => !!val || '标题不能为空']"
/>

<q-input
v-model="newEntry.body"
label="事件描述"
filled
type="textarea"
rows="3"
/>

<div class="row q-col-gutter-sm">
<div class="col-6">
<q-select
v-model="newEntry.icon"
:options="iconOptions"
label="选择图标"
filled
emit-value
map-options
/>
</div>
<div class="col-6">
<q-select
v-model="newEntry.color"
:options="colorOptions"
label="选择颜色"
filled
emit-value
map-options
/>
</div>
</div>

<div class="row q-col-gutter-sm">
<div class="col-6">
<q-select
v-model="newEntry.side"
:options="sideOptions"
label="显示位置"
filled
emit-value
map-options
/>
</div>
<div class="col-6">
<q-input
v-model="newEntry.userName"
label="负责人"
filled
/>
</div>
</div>

<q-select
v-model="newEntry.tags"
multiple
:options="tagOptions"
label="选择标签"
filled
use-chips
/>

<div class="row justify-end q-gutter-sm">
<q-btn label="取消" color="negative" flat v-close-popup />
<q-btn label="添加" type="submit" color="primary" />
</div>
</q-form>
</q-card-section>
</q-card>
</q-dialog>

<!-- 事件详情对话框 -->
<q-dialog v-model="showDetailDialog">
<q-card style="min-width: 500px">
<q-card-section>
<div class="text-h6">{{ selectedEntry?.title }}</div>
</q-card-section>

<q-card-section class="q-pt-none">
<div class="row q-col-gutter-sm q-mb-md">
<div class="col-6">
<div class="text-caption text-grey-7">时间</div>
<div>{{ formatDateTime(selectedEntry?.timestamp) }}</div>
</div>
<div class="col-6">
<div class="text-caption text-grey-7">负责人</div>
<div>{{ selectedEntry?.userName || '未指定' }}</div>
</div>
</div>

<q-separator class="q-my-md" />

<div class="text-caption text-grey-7">事件描述</div>
<div class="q-mt-xs">{{ selectedEntry?.body }}</div>

<div v-if="selectedEntry?.tags && selectedEntry.tags.length" class="q-mt-md">
<div class="text-caption text-grey-7">标签</div>
<div class="q-mt-xs">
<q-chip
v-for="(tag, index) in selectedEntry.tags"
:key="index"
size="sm"
:color="tag.color || 'primary'"
text-color="white"
>
{{ tag.label }}
</q-chip>
</div>
</div>

<div v-if="selectedEntry?.attachments && selectedEntry.attachments.length" class="q-mt-md">
<div class="text-caption text-grey-7">附件</div>
<div class="q-mt-xs">
<q-list dense>
<q-item
v-for="(attachment, index) in selectedEntry.attachments"
:key="index"
clickable
@click="openAttachment(attachment)"
>
<q-item-section avatar>
<q-icon :name="getAttachmentIcon(attachment.type)" />
</q-item-section>
<q-item-section>
<q-item-label>{{ attachment.name }}</q-item-label>
<q-item-label caption v-if="attachment.size">
{{ formatFileSize(attachment.size) }}
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
</div>
</q-card-section>

<q-card-actions align="right">
<q-btn
v-if="selectedEntry"
label="删除"
color="negative"
@click="deleteEntry(selectedEntry.id)"
/>
<q-btn label="关闭" color="primary" v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
</div>
</template>

<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { date, useQuasar } from 'quasar'
import TimelineComponent from 'components/examples/timeline/TimelineComponent.vue'
import type { ExtendedTimelineEntry, TimelineAction, TimelineTag, TimelineAttachment } from './timelineTypes'

const $q = useQuasar()
const timelineRef = ref<InstanceType<typeof TimelineComponent>>()

// 响应式状态
const showAddDialog = ref(false)
const showDetailDialog = ref(false)
const selectedEntry = ref<ExtendedTimelineEntry | null>(null)
const pageSize = ref(10)

// 新事件表单
const newEntry = reactive({
title: '',
body: '',
icon: 'event',
color: 'primary',
side: 'right',
userName: '',
tags: [] as string[]
})

// 统计信息
const timelineStats = reactive({
total: 0,
thisWeek: 0,
pending: 0
})

// 初始数据
const initialEntries = ref<ExtendedTimelineEntry[]>([
{
id: '1',
title: '项目启动会议',
body: '项目正式启动,确定了项目目标和里程碑',
timestamp: new Date('2024-01-15T09:00:00'),
icon: 'flag',
color: 'positive',
userName: '张三',
tags: [
{ label: '会议', color: 'blue' },
{ label: '重要', color: 'red' }
]
},
{
id: '2',
title: '需求分析完成',
body: '完成了所有用户需求的分析和文档编写',
timestamp: new Date('2024-01-20T14:30:00'),
icon: 'check_circle',
color: 'primary',
userName: '李四',
tags: [
{ label: '完成', color: 'green' }
]
},
{
id: '3',
title: '技术架构设计',
body: '完成了系统技术架构设计,确定了技术栈',
timestamp: new Date('2024-01-25T16:45:00'),
icon: 'architecture',
color: 'info',
userName: '王五',
tags: [
{ label: '设计', color: 'purple' }
],
attachments: [
{
id: 'att1',
name: '系统架构图.pdf',
type: 'pdf',
url: '/downloads/architecture.pdf',
size: 1024 * 1024 * 2.5 // 2.5MB
}
]
},
{
id: '4',
title: '开发环境搭建',
body: '完成了开发、测试和生产环境的搭建和配置',
timestamp: new Date('2024-02-01T11:20:00'),
icon: 'settings',
color: 'amber',
userName: '赵六',
tags: [
{ label: '基础设施', color: 'orange' },
{ label: '完成', color: 'green' }
]
},
{
id: '5',
title: '第一阶段开发完成',
body: '完成了项目第一阶段的全部开发任务',
timestamp: new Date('2024-02-10T17:00:00'),
icon: 'celebration',
color: 'positive',
userName: '前端团队',
tags: [
{ label: '里程碑', color: 'red' },
{ label: '完成', color: 'green' }
]
}
])

// 选项数据
const iconOptions = [
{ label: '事件', value: 'event' },
{ label: '旗帜', value: 'flag' },
{ label: '检查', value: 'check_circle' },
{ label: '架构', value: 'architecture' },
{ label: '设置', value: 'settings' },
{ label: '庆祝', value: 'celebration' },
{ label: '警告', value: 'warning' },
{ label: '错误', value: 'error' }
]

const colorOptions = [
{ label: '主要', value: 'primary' },
{ label: '正面', value: 'positive' },
{ label: '负面', value: 'negative' },
{ label: '信息', value: 'info' },
{ label: '警告', value: 'warning' },
{ label: '琥珀色', value: 'amber' },
{ label: '青色', value: 'teal' },
{ label: '紫色', value: 'purple' }
]

const sideOptions = [
{ label: '左侧', value: 'left' },
{ label: '右侧', value: 'right' }
]

const tagOptions = [
'重要', '完成', '会议', '设计', '开发', '测试', '里程碑', '问题', '基础设施'
]

// 计算属性
const currentUser = computed(() => {
return '当前用户' // 在实际应用中,这里可以从store或auth模块获取
})

// 方法
const handleEntryClick = (entry: ExtendedTimelineEntry): void => {
console.log('时间线条目点击:', entry)
selectedEntry.value = entry
showDetailDialog.value = true
}

const handleAction = (action: TimelineAction, entry: ExtendedTimelineEntry): void => {
console.log('操作处理:', action, entry)

switch (action.id) {
case 'edit':
editEntry(entry)
break
case 'delete':
deleteEntry(entry.id)
break
case 'share':
shareEntry(entry)
break
default:
console.log('未知操作:', action.id)
}
}

const handleRefresh = (): void => {
console.log('时间线刷新事件')
if (timelineRef.value) {
timelineRef.value.refresh()
updateStats()
}
}

const addNewEntry = (): void => {
if (timelineRef.value) {
const tags: TimelineTag[] = newEntry.tags.map(tag => ({ label: tag }))

const entry: ExtendedTimelineEntry = {
id: `entry-${Date.now()}`,
title: newEntry.title,
body: newEntry.body,
timestamp: new Date(),
icon: newEntry.icon,
color: newEntry.color,
side: newEntry.side as 'left' | 'right',
userName: newEntry.userName || currentUser.value,
tags: tags
}

timelineRef.value.addEntry(entry)
showAddDialog.value = false
resetNewEntryForm()
updateStats()

$q.notify({
type: 'positive',
message: '事件添加成功'
})
}
}

const editEntry = (entry: ExtendedTimelineEntry): void => {
console.log('编辑事件:', entry)
// 在实际应用中,这里可以打开编辑对话框
$q.notify({
type: 'info',
message: '编辑功能待实现'
})
}

const deleteEntry = (id: string): void => {
if (timelineRef.value) {
timelineRef.value.removeEntry(id)
showDetailDialog.value = false
updateStats()

$q.notify({
type: 'positive',
message: '事件删除成功'
})
}
}

const shareEntry = (entry: ExtendedTimelineEntry): void => {
console.log('分享事件:', entry)
// 在实际应用中,这里可以实现分享逻辑
$q.notify({
type: 'info',
message: '分享功能待实现'
})
}

const exportTimelineData = (): void => {
console.log('导出时间线数据')
// 在实际应用中,这里可以实现导出逻辑
$q.notify({
type: 'info',
message: '导出功能待实现'
})
}

const openAttachment = (attachment: TimelineAttachment): void => {
console.log('打开附件:', attachment)
window.open(attachment.url, '_blank')
}

const getAttachmentIcon = (type: string): string => {
const iconMap: Record<string, string> = {
'pdf': 'picture_as_pdf',
'image': 'image',
'document': 'description',
'spreadsheet': 'table_chart',
'archive': 'folder_zip',
'video': 'videocam',
'audio': 'audiotrack'
}

return iconMap[type] || 'insert_drive_file'
}

const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 Bytes'

const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))

return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}

const formatDateTime = (timestamp?: Date): string => {
if (!timestamp) return ''
return date.formatDate(timestamp, 'YYYY-MM-DD HH:mm')
}

const resetNewEntryForm = (): void => {
newEntry.title = ''
newEntry.body = ''
newEntry.icon = 'event'
newEntry.color = 'primary'
newEntry.side = 'right'
newEntry.userName = ''
newEntry.tags = []
}

const updateStats = (): void => {
// 在实际应用中,这里可以根据时间线数据计算统计信息
timelineStats.total = initialEntries.value.length + 1 // 模拟增加
timelineStats.thisWeek = 2 // 模拟本周新增
timelineStats.pending = 3 // 模拟待处理
}

// 生命周期钩子
onMounted(() => {
updateStats()
})
</script>

<style scoped lang="scss">
.timeline-container {
width: 100%;
height: 100%;
}

// 响应式调整
@media (max-width: 600px) {
.row.items-center.justify-between {
flex-direction: column;
align-items: flex-start;

.row.q-gutter-sm {
margin-top: 16px;
align-self: flex-end;
}
}

.row.q-col-gutter-md .col-12.col-sm-4 {
margin-bottom: 16px;
}
}
</style>
```

## 功能说明

这个完整示例展示了TimelineComponent组件的全部功能:

### 1. 基本时间线展示
- 显示带有图标、颜色、标签和时间戳的时间线条目
- 支持左右交替显示
- 按日期分组显示

### 2. 交互功能
- 点击条目显示详细信息对话框
- 支持添加新事件
- 支持删除事件
- 支持刷新时间线
- 支持导出数据

### 3. 搜索和过滤
- 通过TimelineComponent内置的搜索框进行文本搜索
- 通过过滤器按类型筛选事件

### 4. 分页功能
- 支持分页显示大量时间线数据
- 可配置每页显示数量

### 5. 统计信息
- 显示总事件数、本周新增和待处理事件的统计卡片
- 统计信息会根据时间线的变化自动更新

### 6. 附件支持
- 时间线条目可以包含附件
- 支持点击附件进行下载或查看

### 7. 标签系统
- 支持为事件添加多个标签
- 标签可以有不同的颜色,用于分类和优先级标识

### 8. 响应式设计
- 适配不同屏幕尺寸
- 在移动设备上优化布局

## 使用说明

1. **查看事件**:点击时间线上的任意条目可以查看详细信息
2. **添加事件**:点击"添加事件"按钮,填写表单后提交
3. **删除事件**:在事件详情对话框中点击"删除"按钮
4. **搜索事件**:使用时间线顶部的搜索框进行关键词搜索
5. **过滤事件**:使用过滤器按类型筛选事件
6. **刷新数据**:点击"刷新时间线"按钮重新加载数据
7. **导出数据**:点击"导出数据"按钮将时间线数据导出

这个示例展示了如何充分利用TimelineComponent组件的所有功能,您可以根据实际需求进一步定制和扩展。

posted on 2025-09-09 21:12  GoGrid  阅读(12)  评论(0)    收藏  举报

导航