beta 1.0.0 - 正式外测版本

This commit is contained in:
LINING-PC\lining
2025-06-29 20:21:53 +08:00
parent 5a9a55cd1f
commit c10cb7fef5
13 changed files with 1297 additions and 1220 deletions
-996
View File
@@ -1,996 +0,0 @@
# Gantt Chart API 使用指南
## 📖 概述
Jordium Gantt Vue3 组件提供了灵活的API接口,允许开发者自定义TaskBar双击事件的处理逻辑,可以完全替换默认的编辑行为。组件采用Vue3 + TypeScript构建,样式延续Element Plus设计风格但不依赖Element Plus组件库。
## 🚀 API 接口
### GanttChart Props
| 属性名 | 类型 | 默认值 | 描述 |
|--------|------|--------|------|
| `tasks` | `Task[]` | `[]` | 任务数据数组,支持嵌套结构(树形数据) |
| `onTaskDoubleClick` | `(task: Task) => void` | `undefined` | 自定义双击事件处理器,当TaskBar被双击时触发 |
| `editComponent` | `any` | `undefined` | 自定义编辑组件(预留接口,暂未实现) |
| `useDefaultDrawer` | `boolean` | `true` | 是否使用默认的TaskDrawer抽屉组件进行编辑 |
| `onTaskDelete` | `(task: Task) => void` | `undefined` | 自定义删除事件处理器,当删除按钮被点击时触发 |
| `showToolbar` | `boolean` | `true` | 是否显示工具栏 |
| `toolbarConfig` | `ToolbarConfig` | `{}` | 工具栏配置选项 |
| `onAddTask` | `() => void` | `undefined` | 新增任务按钮点击事件处理器 |
| `onExportCsv` | `() => void \| boolean` | `undefined` | 导出CSV按钮点击事件处理器,返回false使用默认实现 |
| `onExportPdf` | `() => void` | `undefined` | 导出PDF按钮点击事件处理器 |
| `onLanguageChange` | `(lang: 'zh' \| 'en') => void` | `undefined` | 语言切换事件处理器 |
| `onThemeChange` | `(isDark: boolean) => void` | `undefined` | 主题切换事件处理器 |
| `onFullscreenChange` | `(isFullscreen: boolean) => void` | `undefined` | 全屏切换事件处理器 |
### ToolbarConfig 接口定义
```typescript
interface ToolbarConfig {
showAddTask?: boolean // 是否显示新增任务按钮,默认true
showExportCsv?: boolean // 是否显示导出CSV按钮,默认true
showExportPdf?: boolean // 是否显示导出PDF按钮,默认true
showLanguage?: boolean // 是否显示语言切换按钮,默认true
showTheme?: boolean // 是否显示主题切换按钮,默认true
showFullscreen?: boolean // 是否显示全屏切换按钮,默认true
}
```
### API 工作机制
1. **双击优先级**: 当提供 `onTaskDoubleClick` 时,将优先调用自定义处理器,默认的TaskDrawer不会打开
2. **双击一致性**: TaskList中任务行的双击与Timeline中TaskBar的双击具有完全相同的效果和优先级
3. **删除优先级**: 当提供 `onTaskDelete` 时,将优先调用自定义删除处理器,否则使用默认删除行为
4. **默认行为**: 当未提供 `onTaskDoubleClick``useDefaultDrawer``true` 时,双击TaskBar或TaskRow会打开内置的TaskDrawer
5. **完全自定义**: 设置 `useDefaultDrawer: false` 可以完全禁用默认抽屉,只使用自定义处理器
6. **数据更新机制**:
- TaskDrawer更新任务 → Timeline本地更新 → 发送task-updated事件 → TaskList更新数据源
- TaskList数据更新 → 发送tasks-changed事件 → Timeline重新渲染TaskBar
- TaskBar位置变化 → 自动重新报告位置 → 依赖关系线自动重新计算
7. **CSV导出机制**:
- 当提供 `onExportCsv` 时,优先调用自定义处理器
- 如果自定义处理器返回 `false`,则使用内置的默认导出功能
- 默认导出支持UTF-8编码、多语言头部、安全字符转义等特性
- 导出内容包含所有任务字段,递归处理子任务
8. **工具栏功能**:
- 新增任务按钮:独立的主要操作按钮,具有醒目的主色调样式
- 导出按钮组:CSV和PDF导出按钮采用button group样式,左右相连,统一的视觉效果
- 设置按钮:语言、主题、全屏等图标按钮,位于右侧,支持响应式布局
- 国际化支持:内置中英文切换,所有按钮文本和提示自动适配
### Task 接口定义
```typescript
interface Task {
id: number // 任务唯一标识
name: string // 任务名称
predecessor?: string // 前置任务ID
assignee?: string // 负责人
startDate?: string // 开始日期 (YYYY-MM-DD格式)
endDate?: string // 结束日期 (YYYY-MM-DD格式)
progress?: number // 完成进度 (0-100)
estimatedHours?: number // 预估工时
actualHours?: number // 实际工时
children?: Task[] // 子任务数组(支持嵌套结构)
collapsed?: boolean // 是否折叠子任务
isParent?: boolean // 是否为父级任务
type?: string // 任务类型 (task/story/bug/milestone)
description?: string // 任务描述
}
```
## 💡 使用示例
### 1. 默认模式(使用内置TaskDrawer
```vue
<template>
<!-- 最简单的使用方式双击TaskBar会打开默认的编辑抽屉 -->
<GanttChart :tasks="tasks" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import GanttChart from './components/GanttChart.vue'
// 定义任务数据
const tasks = ref([
{
id: 1,
name: '项目启动',
assignee: '张三',
startDate: '2025-06-15',
endDate: '2025-06-25',
progress: 100,
type: 'story',
children: [
{
id: 2,
name: '需求分析',
assignee: '李四',
startDate: '2025-06-16',
endDate: '2025-06-20',
progress: 100,
type: 'task'
}
]
}
])
</script>
```
### 2. 自定义双击处理器
```vue
<script setup lang="ts">
import { ref } from 'vue'
import GanttChart from './components/GanttChart.vue'
// 定义Task接口(与组件内部保持一致)
interface Task {
id: number
name: string
startDate: string
endDate: string
progress?: number
isParent?: boolean
predecessor?: number
type?: string
}
// 自定义双击事件处理器
const handleTaskDoubleClick = (task: Task) => {
console.log('双击了任务:', task)
// 注意:此处理器会同时响应TaskBar和TaskList中任务行的双击事件
// 自定义逻辑示例:
// 1. 打开自定义模态框
openCustomModal(task)
// 2. 跳转到详情页面
// router.push(`/task/${task.id}`)
// 3. 调用外部API
// await updateTaskAPI(task)
// 4. 显示自定义消息
// showCustomMessage(task)
}
const openCustomModal = (task: Task) => {
// 实现自定义模态框逻辑
alert(`自定义处理: ${task.name}`)
}
</script>
<template>
<!-- 使用自定义处理器禁用默认抽屉 -->
<GanttChart
:on-task-double-click="handleTaskDoubleClick"
:use-default-drawer="false"
/>
</template>
```
### 3. TaskList与TaskBar双击一致性
```vue
<script setup lang="ts">
import { ref } from 'vue'
import GanttChart from './components/GanttChart.vue'
const handleTaskDoubleClick = (task: Task) => {
console.log('任务双击事件:', task)
// 此处理器会同时响应:
// 1. Timeline中TaskBar的双击
// 2. TaskList中任务行的双击
// 无论用户双击任务条还是任务行,都会执行相同的逻辑
openTaskEditor(task)
}
const openTaskEditor = (task: Task) => {
// 打开自定义任务编辑器
console.log(`正在编辑任务: ${task.name}`)
}
</script>
<template> <!-- TaskList和Timeline的双击行为保持一致 -->
<GanttChart
:tasks="tasks"
:on-task-double-click="handleTaskDoubleClick"
:use-default-drawer="false"
/>
</template>
```
### 4. CSV导出功能使用
CSV导出功能支持自定义处理器和默认实现,具有完整的多语言和UTF-8编码支持。详细文档请参考 [CSV_EXPORT.md](./CSV_EXPORT.md)
#### 基本使用
```vue
<script setup lang="ts">
import { ref } from 'vue'
const tasks = ref([/* 你的任务数据 */])
// 启用CSV导出
const toolbarConfig = {
showExportCsv: true,
showLanguage: true, // 启用语言切换,影响CSV头部语言
}
// 可选:自定义CSV导出处理器
const handleCsvExport = () => {
console.log('CSV导出')
// 返回false使用默认实现
return false
}
</script>
<template>
<GanttChart
:tasks="tasks"
:toolbar-config="toolbarConfig"
:on-export-csv="handleCsvExport"
/>
</template>
```
### 5. 工具栏配置与使用
工具栏采用了全新的设计,新增任务按钮独立显示,导出CSV和PDF按钮采用button group样式相连,右侧为设置类按钮。
```vue
<script setup lang="ts">
import { ref } from 'vue'
import GanttChart from './components/GanttChart.vue'
// 工具栏配置
const toolbarConfig = ref({
showAddTask: true, // 显示新增任务按钮(主色调独立按钮)
showExportCsv: true, // 显示导出CSV按钮(button group左侧)
showExportPdf: true, // 显示导出PDF按钮(button group右侧)
showLanguage: true, // 显示语言切换按钮
showTheme: true, // 显示主题切换按钮
showFullscreen: true // 显示全屏切换按钮
})
// 工具栏事件处理器
const handleAddTask = () => {
console.log('新增任务按钮被点击')
// 实现新增任务逻辑
// 例如:打开新增任务对话框
openAddTaskDialog()
}
const handleExportCsv = () => {
console.log('导出CSV按钮被点击')
// 实现CSV导出逻辑
exportToCsv()
}
const handleExportPdf = () => {
console.log('导出PDF按钮被点击')
// 实现PDF导出逻辑
exportToPdf()
}
const handleLanguageChange = (lang: 'zh' | 'en') => {
console.log('语言切换为:', lang)
// 实现语言切换逻辑
}
const handleThemeChange = (isDark: boolean) => {
console.log('主题切换为:', isDark ? '暗色' : '明亮')
// 实现主题切换逻辑
}
const handleFullscreenChange = (isFullscreen: boolean) => {
console.log('全屏状态:', isFullscreen ? '已进入' : '已退出')
// 实现全屏状态处理逻辑
}
// 业务逻辑函数示例
const openAddTaskDialog = () => {
// 打开新增任务对话框的具体实现
}
const exportToCsv = () => {
// 导出CSV的具体实现
// 可以调用后端API或者前端处理
}
const exportToPdf = () => {
// 导出PDF的具体实现
// 可以调用后端API或者前端处理
}
</script>
<template>
<GanttChart
:tasks="tasks"
:show-toolbar="true"
:toolbar-config="toolbarConfig"
:on-add-task="handleAddTask"
:on-export-csv="handleExportCsv"
:on-export-pdf="handleExportPdf"
:on-language-change="handleLanguageChange"
:on-theme-change="handleThemeChange"
:on-fullscreen-change="handleFullscreenChange"
/>
</template>
```
#### 工具栏样式特点
- **Button Group设计**: 导出CSV和PDF按钮采用连接式设计,视觉上更加统一
- **主操作突出**: 新增任务按钮使用主色调,突出重要操作
- **图标化右侧**: 设置类按钮采用纯图标设计,节省空间
- **响应式布局**: 在小屏幕设备上自动调整按钮尺寸和间距
- **国际化支持**: 所有按钮文本和提示自动适配当前语言
#### 选择性显示工具栏项
```vue
<script setup lang="ts">
// 仅显示新增任务和导出功能,隐藏其他按钮
const minimalToolbarConfig = ref({
showAddTask: true,
showExportCsv: true,
showExportPdf: true,
showLanguage: false, // 隐藏语言切换
showTheme: false, // 隐藏主题切换
showFullscreen: false // 隐藏全屏切换
})
</script>
<template>
<!-- 最小化工具栏配置 -->
<GanttChart
:toolbar-config="minimalToolbarConfig"
:on-add-task="handleAddTask"
:on-export-csv="handleExportCsv"
:on-export-pdf="handleExportPdf"
/>
</template>
```
#### 完全隐藏工具栏
```vue
<template>
<!-- 隐藏工具栏使用纯甘特图 -->
<GanttChart
:show-toolbar="false"
:tasks="tasks"
/>
</template>
```
> **说明**: TaskList中任务行的双击与Timeline中TaskBar的双击具有完全相同的API行为和优先级机制,确保用户体验的一致性。
### 4. 条件性使用不同处理器
```vue
<script setup lang="ts">
import { ref, computed } from 'vue'
import GanttChart from './components/GanttChart.vue'
const useCustomHandler = ref(false)
const handleCustomTaskDoubleClick = (task: Task) => {
// 自定义处理逻辑
console.log('使用自定义处理器:', task)
}
// 根据条件决定使用哪种处理器
const doubleClickHandler = computed(() => {
return useCustomHandler.value ? handleCustomTaskDoubleClick : undefined
})
</script>
<template>
<div>
<label>
<input type="checkbox" v-model="useCustomHandler" />
使用自定义处理器
</label>
<GanttChart
:on-task-double-click="doubleClickHandler"
:use-default-drawer="!useCustomHandler"
/>
</div>
</template>
```
### 4. 自定义删除处理器
```vue
<script setup lang="ts">
import { ref } from 'vue'
import GanttChart from './components/GanttChart.vue'
// 自定义删除事件处理器
const handleCustomTaskDelete = async (task: Task) => {
console.log('自定义删除处理器被调用:', task)
// 自定义删除逻辑示例:
try {
// 1. 调用后端API删除
await fetch(`/api/tasks/${task.id}`, { method: 'DELETE' })
// 2. 更新本地状态
// store.dispatch('deleteTask', task.id)
// 3. 显示自定义成功消息
showNotification(`任务 "${task.name}" 已成功删除`, 'success')
// 4. 可以触发其他业务逻辑
// trackDeleteEvent(task)
} catch (error) {
console.error('删除失败:', error)
showNotification('删除失败,请重试', 'error')
}
}
const showNotification = (message: string, type: string) => {
// 实现自定义通知逻辑
console.log(`${type}: ${message}`)
}
</script>
<template>
<GanttChart
:on-task-delete="handleCustomTaskDelete"
/>
</template>
```
### 5. 完整的自定义处理器组合
```vue
<script setup lang="ts">
import { ref } from 'vue'
import GanttChart from './components/GanttChart.vue'
// 自定义双击处理器
const handleTaskDoubleClick = (task: Task) => {
// 打开自定义编辑界面
router.push(`/tasks/${task.id}/edit`)
}
// 自定义删除处理器
const handleTaskDelete = async (task: Task) => {
// 使用自定义确认对话框
const confirmed = await showCustomConfirm(
'删除确认',
`确定要删除任务 "${task.name}" 吗?`,
'warning'
)
if (confirmed) {
await api.deleteTask(task.id)
showMessage('删除成功', 'success')
}
}
</script>
<template>
<GanttChart
:on-task-double-click="handleTaskDoubleClick"
:on-task-delete="handleTaskDelete"
:use-default-drawer="false"
/>
</template>
```
## 🏗️ 组件架构
### 组件层次结构
```
GanttChart (入口组件,API配置)
├── TaskList (左侧任务列表)
├── Timeline (右侧时间轴区域)
├── TaskBar (任务条,支持双击事件)
├── Milestone (里程碑)
└── TaskDrawer (默认编辑抽屉)
```
### API 数据流
1. `GanttChart` 接收API配置props
2. 透传给 `Timeline` 组件
3. `Timeline` 将API配置传递给 `TaskBar`
4. `TaskBar` 双击时调用API处理器或触发默认行为
### 内置功能
- ✅ 拖拽调整任务时间
- ✅ 调整任务条长度
- ✅ 任务进度显示
- ✅ 前置任务依赖关系
- ✅ 父子任务层级结构
- ✅ 时间轴缩放和导航
- ✅ 自定义双击事件API
## 🔧 技术特性
- **Framework**: Vue 3 + TypeScript + Vite
- **样式**: 延续Element Plus设计风格,无外部依赖
- **响应式**: 完全使用Vue 3 Composition API
- **类型安全**: 完整的TypeScript接口定义
- **可扩展**: 灵活的API设计,支持自定义扩展
## 🛠️ 高级用法
### 完整示例:集成自定义编辑功能
```vue
<template>
<div class="gantt-container">
<!-- Gantt图表 -->
<GanttChart
:on-task-double-click="handleTaskDoubleClick"
:use-default-drawer="!useCustomEditor"
/>
<!-- 自定义编辑模态框 -->
<div v-if="showCustomModal" class="custom-modal-overlay" @click="closeModal">
<div class="custom-modal" @click.stop>
<div class="modal-header">
<h3>编辑任务: {{ currentTask?.name }}</h3>
<button @click="closeModal" class="close-btn">×</button>
</div>
<div class="modal-body">
<form @submit.prevent="saveTask">
<div class="form-group">
<label>任务名称:</label>
<input v-model="editingTask.name" type="text" required />
</div>
<div class="form-group">
<label>开始日期:</label>
<input v-model="editingTask.startDate" type="date" required />
</div>
<div class="form-group">
<label>结束日期:</label>
<input v-model="editingTask.endDate" type="date" required />
</div>
<div class="form-group">
<label>完成进度:</label>
<input v-model.number="editingTask.progress" type="number" min="0" max="100" />
</div>
</form>
</div>
<div class="modal-footer">
<button @click="closeModal" class="btn-cancel">取消</button>
<button @click="saveTask" class="btn-save">保存</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import GanttChart from './components/GanttChart.vue'
interface Task {
id: number
name: string
startDate: string
endDate: string
progress?: number
isParent?: boolean
predecessor?: number
type?: string
}
// 状态管理
const useCustomEditor = ref(true)
const showCustomModal = ref(false)
const currentTask = ref<Task | null>(null)
const editingTask = reactive<Partial<Task>>({})
// 自定义双击处理器
const handleTaskDoubleClick = (task: Task) => {
currentTask.value = task
// 复制任务数据到编辑状态
Object.assign(editingTask, {
id: task.id,
name: task.name,
startDate: task.startDate,
endDate: task.endDate,
progress: task.progress || 0
})
showCustomModal.value = true
}
// 关闭模态框
const closeModal = () => {
showCustomModal.value = false
currentTask.value = null
Object.keys(editingTask).forEach(key => {
delete editingTask[key as keyof typeof editingTask]
})
}
// 保存任务
const saveTask = () => {
if (!currentTask.value) return
// 这里可以调用API保存数据
console.log('保存任务:', editingTask)
// 触发任务更新事件
window.dispatchEvent(new CustomEvent('task-updated', {
detail: { ...currentTask.value, ...editingTask }
}))
closeModal()
}
</script>
<style scoped>
.gantt-container {
width: 100%;
height: 100%;
position: relative;
}
.custom-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.custom-modal {
background: white;
border-radius: 8px;
width: 500px;
max-width: 90vw;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #eee;
}
.modal-header h3 {
margin: 0;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
}
.modal-body {
padding: 20px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 4px;
color: #333;
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 20px;
border-top: 1px solid #eee;
}
.btn-cancel, .btn-save {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn-cancel {
background: #f5f5f5;
color: #666;
}
.btn-save {
background: #409eff;
color: white;
}
</style>
```
## ⚡ 性能优化建议
1. **事件处理器缓存**: 使用 `computed``useMemo` 缓存事件处理器,避免不必要的重新渲染
2. **异步处理**: 对于复杂的双击处理逻辑,建议使用异步函数
```vue
<script setup lang="ts">
const handleTaskDoubleClick = async (task: Task) => {
try {
// 显示加载状态
loading.value = true
// 异步处理
await processTask(task)
} catch (error) {
console.error('处理任务失败:', error)
} finally {
loading.value = false
}
}
</script>
```
3. **防抖处理**: 对于可能触发频繁操作的场景,考虑添加防抖
```vue
<script setup lang="ts">
import { debounce } from 'lodash-es'
const debouncedHandler = debounce((task: Task) => {
// 实际处理逻辑
console.log('处理任务:', task)
}, 300)
</script>
```
## 🔧 故障排除
### 常见问题
1. **Q: 双击事件不触发?**
- A: 检查是否正确传递了 `onTaskDoubleClick` 属性,并确保 `useDefaultDrawer` 设置正确
2. **Q: 同时使用自定义处理器和默认Drawer?**
- A: 当 `onTaskDoubleClick` 存在时,会优先调用自定义处理器,默认Drawer不会打开
3. **Q: 如何在自定义处理器中获取更多任务信息?**
- A: 可以通过全局状态管理或者父组件传递更多上下文信息
4. **Q: 自定义处理器中的异步操作报错?**
- A: 确保在异步操作中添加try-catch错误处理,避免未捕获的异常
### 调试技巧
```vue
<script setup lang="ts">
// 启用调试模式
const debugMode = ref(true)
const handleTaskDoubleClick = (task: Task) => {
if (debugMode.value) {
console.log('双击事件触发:', {
task,
timestamp: new Date().toISOString(),
component: 'TaskBar'
})
}
// 你的处理逻辑...
}
</script>
```
## ⚡ 性能优化
### 1. 事件处理器缓存
```vue
<script setup lang="ts">
import { computed } from 'vue'
// 使用computed缓存处理器,避免不必要的重新渲染
const taskDoubleClickHandler = computed(() => {
return (task: Task) => {
// 处理逻辑
}
})
</script>
<template>
<GanttChart :on-task-double-click="taskDoubleClickHandler" />
</template>
```
### 2. 异步处理优化
```vue
<script setup lang="ts">
const loading = ref(false)
const handleTaskDoubleClick = async (task: Task) => {
try {
// 显示加载状态
loading.value = true
// 异步处理
await processTaskAsync(task)
} catch (error) {
console.error('处理任务失败:', error)
// 显示错误提示
} finally {
loading.value = false
}
}
</script>
```
### 3. 防抖处理
```vue
<script setup lang="ts">
import { debounce } from 'lodash-es'
const debouncedHandler = debounce((task: Task) => {
// 实际处理逻辑
console.log('处理任务:', task)
}, 300)
</script>
```
## 🎯 最佳实践
### 1. 代码组织
- **保持处理器简洁**: 双击处理器应该保持轻量,复杂逻辑建议抽取到单独的函数中
- **类型安全**: 充分利用TypeScript的类型检查,确保Task接口一致性
- **职责分离**: 将业务逻辑与UI逻辑分离
### 2. 用户体验
- **即时反馈**: 为用户操作提供即时的视觉反馈
- **错误处理**: 始终为异步操作添加错误处理和用户友好的错误提示
- **加载状态**: 为长时间运行的操作提供适当的加载提示
- **可访问性**: 确保自定义交互也支持键盘导航和屏幕阅读器
### 3. 性能考虑
- **避免内存泄漏**: 及时清理事件监听器和定时器
- **合理使用响应式**: 不要过度使用reactive,对于简单数据使用ref
- **组件懒加载**: 对于复杂的自定义组件考虑懒加载
### 4. 兼容性设计
```vue
<script setup lang="ts">
// 提供降级方案
const handleTaskDoubleClick = (task: Task) => {
try {
// 主要逻辑
processTask(task)
} catch (error) {
// 降级处理
console.warn('使用降级方案')
fallbackHandler(task)
}
}
</script>
```
## 🚀 扩展开发
### 自定义事件系统
```vue
<script setup lang="ts">
// 定义自定义事件
const emit = defineEmits<{
'task-clicked': [task: Task]
'task-updated': [task: Task]
'task-deleted': [taskId: number]
}>()
const handleTaskDoubleClick = (task: Task) => {
// 处理双击逻辑
// 发出自定义事件
emit('task-clicked', task)
}
</script>
```
### 插件化架构
```typescript
// 定义插件接口
interface GanttPlugin {
name: string
onTaskDoubleClick?: (task: Task) => void
onTaskCreate?: (task: Task) => void
onTaskUpdate?: (task: Task) => void
}
// 使用插件
const ganttPlugins: GanttPlugin[] = [
{
name: 'analytics',
onTaskDoubleClick: (task) => {
// 分析统计逻辑
}
}
]
```
## 📝 更新日志
- **v0.2.0-beta**: 工具栏集成与Button Group优化
- ✅ 新增GanttToolbar工具栏组件,支持完整的功能配置
- ✅ 导出CSV和PDF按钮采用Button Group样式,视觉统一
- ✅ 新增任务按钮独立显示,采用主色调突出重要操作
- ✅ 右侧设置按钮采用图标化设计,支持语言、主题、全屏切换
- ✅ 工具栏支持国际化,内置中英文切换
- ✅ 响应式设计,在小屏幕设备上自动调整布局
- ✅ 支持选择性显示工具栏项,完全可配置
- **v1.0.0**: 初始API发布
- ✅ 支持自定义双击处理器 (`onTaskDoubleClick`)
- ✅ 支持禁用默认编辑抽屉 (`useDefaultDrawer`)
- ✅ 完整的TypeScript类型支持
- ✅ Element Plus风格设计,无外部依赖
- **规划中**:
- 🔄 自定义编辑组件支持 (`editComponent`)
- 🔄 更多事件API(创建、删除、拖拽等)
- 🔄 主题定制API
- 🔄 插件系统
## 🤝 贡献指南
欢迎提交Issue和Pull Request来改进这个组件!
### 开发环境设置
```bash
# 克隆项目
git clone <repository-url>
# 安装依赖
npm install
# 启动开发服务器
npm run dev
# 构建项目
npm run build
```
### 代码规范
- 使用TypeScript进行类型安全开发
- 遵循Vue 3 Composition API最佳实践
- 保持代码简洁和可读性
- 添加适当的注释和文档
---
🎉 **通过这些API,您可以完全自定义TaskBar的交互行为,打造符合您项目需求的甘特图体验!**
> 如有问题或建议,欢迎提交Issue或联系开发团队。
-88
View File
@@ -1,88 +0,0 @@
# <svg width="28" height="28" viewBox="0 0 24 24" style="vertical-align:middle;"><rect x="2" y="4" width="20" height="2" rx="1" fill="#409eff"/><rect x="2" y="8" width="12" height="2" rx="1" fill="#67c23a"/><rect x="2" y="12" width="16" height="2" rx="1" fill="#e6a23c"/><rect x="2" y="16" width="8" height="2" rx="1" fill="#f56c6c"/><rect x="2" y="20" width="14" height="2" rx="1" fill="#909399"/><circle cx="22" cy="5" r="1" fill="#409eff"/><circle cx="16" cy="9" r="1" fill="#67c23a"/><circle cx="20" cy="13" r="1" fill="#e6a23c"/><circle cx="12" cy="17" r="1" fill="#f56c6c"/><circle cx="18" cy="21" r="1" fill="#909399"/></svg> Jordium Gantt Vue3
> Modern, open-source, and high-performance Gantt chart component library for Vue3 + TypeScript
---
## 🚀 Product Positioning
Jordium Gantt Vue3 is a high-performance Gantt chart component library for modern web applications, focusing on excellent interaction, flexible extension, and open-source compliance. Ideal for project management, progress visualization, R&D collaboration, and more.
- **Tech Stack**: Vue 3 + TypeScript
- **Design Language**: Element Plus style (no dependency)
- **Use Cases**: Enterprise project management, scheduling, resource allocation, etc.
## ✨ Key Features
- **Modern UI/UX**: Minimalist, clean, responsive, supports light/dark themes, auto system adaptation
- **High Performance**: Virtual scrolling, auto-expanding timeline, smooth with large datasets
- **Task Management**: Multi-level tasks, milestones, drag & resize, progress, dependencies
- **Enhanced Interaction**: Today locator, fullscreen, version history, toolbar, quick actions
- **Internationalization**: Built-in Chinese/English, easy to extend to more languages
- **Flexible Extension**: Custom editors, event handling, data sources, export, etc.
- **Open Source Compliance**: MIT License, suitable for secondary development and commercial use
## 🖌️ Design Philosophy
- **Ultimate Experience**: Interaction details match top-tier products, smooth animation, positioning, and theme switching
- **No External Dependencies**: Only Vue3/TS required, Element Plus style but zero dependency
- **Easy to Use & Extend**: Friendly API, supports slots, events, and deep prop customization
- **Ready to Use**: Comprehensive documentation, easy integration
## 📦 Installation & Usage
```bash
npm install jordium-gantt-vue3
```
### Basic Usage
```vue
<template>
<GanttChart :tasks="tasks" />
</template>
<script setup lang="ts">
import GanttChart from 'jordium-gantt-vue3'
const tasks = [/* ...task data... */]
</script>
```
### Advanced Usage
- Custom task editor (disable default drawer, handle double-click events)
- Custom toolbar buttons, export, theme switching
- Listen to task/milestone changes, drag, dependency events
- Support for external data sources, async loading
See [API_USAGE.md](./API_USAGE.md) for details.
## 🎨 Theming & Adaptation
- Supports light/dark themes, auto system detection
- Theme variables customizable for easy brand adaptation
- Fully responsive, works on desktop and mobile
## 📝 Version History
See [version-history.json](./demo/version-history.json) for details.
- **0.9.x-ALPHA**: Productization refactor, full interaction & visual upgrade, MIT License
- **0.2 Beta**: API improvements, tasks/milestones/dependencies/toolbar/i18n/theme
- **0.1 Beta**: Basic features, Element Plus style
## 📚 API Documentation
See [API_USAGE.md](./API_USAGE.md) for full API reference.
## 🤝 Contribution & Community
Issues, PRs, suggestions, and stars are welcome! For custom development, enterprise support, or bug reports, contact [jordium.com](https://jordium.com).
## 📄 License
MIT License © Jordium
# README.en.md
This file has been deprecated. Please refer to `README.md` for the latest documentation.
+1014 -5
View File
File diff suppressed because it is too large Load Diff
+54 -57
View File
@@ -8,24 +8,10 @@ import packageInfo from '../package.json'
// 导入主题变量
import '../src/styles/theme-variables.css'
import VersionHistoryDrawer from './VersionHistoryDrawer.vue'
import { useMessage } from '../src/composables/useMessage'
import type { Task } from '../src/models/Task'
interface Task {
id: number
name: string
predecessor?: string
assignee?: string
startDate?: string
endDate?: string
progress?: number
estimatedHours?: number
actualHours?: number
children?: Task[]
collapsed?: boolean
isParent?: boolean
type?: string
description?: string
parentId?: number // 上级任务ID
}
const { showMessage } = useMessage()
const tasks = ref<Task[]>([])
const milestones = ref<Task[]>([])
@@ -56,9 +42,7 @@ const toolbarConfig = {
// 自定义CSV导出处理器(可选)
const handleCustomCsvExport = () => {
console.log('自定义CSV导出被调用')
console.log('当前任务数据:', tasks.value)
console.log('当前里程碑数据:', milestones.value)
showMessage('自定义CSV导出被调用', 'info', { closable: true })
// 这里可以实现自定义的CSV导出逻辑
// 例如:添加额外的数据处理、格式化、或发送到服务器等
@@ -86,11 +70,11 @@ const handleAddMilestone = () => {
}
const handleLanguageChange = (lang: 'zh' | 'en') => {
console.log('语言切换到:', lang)
showMessage(`语言切换到:${lang}`, 'info', { closable: true })
}
const handleThemeChange = (isDark: boolean) => {
console.log('主题切换到:', isDark ? '暗黑模式' : '明亮模式')
showMessage(`主题切换到:${isDark ? '暗黑模式' : '明亮模式'}`, 'info', { closable: true })
}
// 里程碑保存处理器示例
@@ -116,13 +100,11 @@ const handleMilestoneSave = (milestone: Task) => {
// 里程碑删除处理器
const handleMilestoneDelete = async (milestoneId: number) => {
console.log('删除里程碑,ID', milestoneId)
// 从里程碑数据中删除
const milestoneIndex = milestones.value.findIndex(m => m.id === milestoneId)
if (milestoneIndex !== -1) {
milestones.value.splice(milestoneIndex, 1)
console.log('里程碑删除成功')
showMessage('里程碑删除成功', 'success', { closable: false })
// 等待DOM更新完成
await nextTick()
@@ -148,10 +130,6 @@ const handleMilestoneDelete = async (milestoneId: number) => {
// 任务更新处理器
const handleTaskUpdate = (updatedTask: Task) => {
console.log('Demo App 接收到任务更新:', updatedTask)
console.log('updatedTask.type:', updatedTask.type)
console.log('updatedTask.parentId:', updatedTask.parentId)
// 先找到原任务,检查parentId是否改变了
const findOriginalTask = (taskArray: Task[]): Task | null => {
for (const task of taskArray) {
@@ -168,7 +146,7 @@ const handleTaskUpdate = (updatedTask: Task) => {
const originalTask = findOriginalTask(tasks.value)
if (!originalTask) {
console.log('未找到要更新的任务,ID', updatedTask.id)
showMessage(`未找到要更新的任务,ID ${updatedTask.id}`, 'warning', { closable: true })
return
}
@@ -177,13 +155,10 @@ const handleTaskUpdate = (updatedTask: Task) => {
if (parentIdChanged) {
// parentId改变了,需要移除任务并重新添加到新位置
console.log('任务父级关系改变,执行移动操作')
const removeTaskFromArray = (taskArray: Task[]): Task | null => {
for (let i = 0; i < taskArray.length; i++) {
if (taskArray[i].id === updatedTask.id) {
const removedTask = taskArray.splice(i, 1)[0]
console.log('从数组中移除任务:', removedTask)
return removedTask
}
@@ -218,7 +193,6 @@ const handleTaskUpdate = (updatedTask: Task) => {
if (!task.children) task.children = []
task.children.push(taskToAdd)
task.isParent = true
console.log('已将任务添加到新父任务的children中,父任务ID', taskToAdd.parentId)
return true
}
if (task.children && task.children.length > 0) {
@@ -229,17 +203,16 @@ const handleTaskUpdate = (updatedTask: Task) => {
}
if (!addToParentChildren(tasks.value)) {
console.warn('未找到新父任务,ID', taskToAdd.parentId, ',将作为顶级任务添加')
showMessage(`未找到新父任务,ID ${taskToAdd.parentId},将作为顶级任务添加`, 'warning', {
closable: true,
})
tasks.value.push(taskToAdd)
}
} else {
tasks.value.push(taskToAdd)
console.log('已将任务添加为顶级任务')
}
} else {
// parentId没有改变,只是就地更新任务数据
console.log('任务父级关系未变,执行就地更新')
const updateTaskInPlace = (taskArray: Task[]): boolean => {
for (let i = 0; i < taskArray.length; i++) {
if (taskArray[i].id === updatedTask.id) {
@@ -251,7 +224,6 @@ const handleTaskUpdate = (updatedTask: Task) => {
updatedTask.type === 'story' ||
(taskArray[i].children && taskArray[i].children.length > 0),
}
console.log('已就地更新任务数据:', taskArray[i])
return true
}
@@ -265,18 +237,15 @@ const handleTaskUpdate = (updatedTask: Task) => {
}
if (!updateTaskInPlace(tasks.value)) {
console.log('就地更新失败,未找到任务,ID', updatedTask.id)
showMessage(`就地更新失败,未找到任务,ID ${updatedTask.id}`, 'warning', { closable: true })
}
}
console.log('任务更新完成,当前任务数据:', tasks.value)
showMessage('任务更新完成', 'success', { closable: false })
}
// 任务添加处理器
const handleTaskAdd = (newTask: Task) => {
console.log('Demo App 接收到新增任务:', newTask)
console.log('newTask.type:', newTask.type)
// 为新任务生成ID(如果没有的话)
if (!newTask.id) {
// 找到当前最大的ID,然后+1
@@ -304,7 +273,6 @@ const handleTaskAdd = (newTask: Task) => {
}
// 将新任务添加到父任务的children中
task.children.push({ ...newTask })
console.log('已将子任务添加到父任务的children中,父任务ID', newTask.parentId)
return true
}
// 递归查找父任务(支持多层嵌套)
@@ -331,15 +299,13 @@ const handleTaskAdd = (newTask: Task) => {
// 任务删除处理器
const handleTaskDelete = (taskToDelete: Task) => {
console.log('删除任务:', taskToDelete)
// 递归查找和删除任务(支持嵌套结构)
const deleteTaskFromArray = (taskArray: Task[]): boolean => {
for (let i = 0; i < taskArray.length; i++) {
if (taskArray[i].id === taskToDelete.id) {
// 找到任务,删除它
taskArray.splice(i, 1)
console.log('已删除任务')
showMessage('已删除任务', 'success', { closable: false })
return true
}
@@ -354,20 +320,17 @@ const handleTaskDelete = (taskToDelete: Task) => {
}
if (!deleteTaskFromArray(tasks.value)) {
console.log('未找到要删除的任务,ID', taskToDelete.id)
showMessage(`未找到要删除的任务,ID ${taskToDelete.id}`, 'warning', { closable: true })
}
}
// 里程碑图标变更处理器
const handleMilestoneIconChange = (milestoneId: number, icon: string) => {
console.log('里程碑图标变更:', { milestoneId, icon })
const milestoneIndex = milestones.value.findIndex(m => m.id === milestoneId)
if (milestoneIndex !== -1) {
milestones.value[milestoneIndex].icon = icon
console.log('已更新里程碑图标')
} else {
console.log('未找到要更新图标的里程碑,ID', milestoneId)
showMessage(`未找到要更新图标的里程碑,ID ${milestoneId}`, 'warning', { closable: true })
}
}
@@ -411,14 +374,45 @@ const handleGiteeDocsClick = (event: Event) => {
window.open('https://gitee.com/jordium-gantt/jordium-gantt-vue3#readme', '_blank')
}
// 任务拖拽/拉伸/里程碑拖拽监听
function handleTaskbarDragOrResizeEnd(newTask) {
const oldTask = findTaskDeep(tasks.value, newTask.id)
if (!oldTask) return
showMessage(
`任务【${newTask.name}\n` +
`开始: ${oldTask.startDate}${newTask.startDate}\n` +
`结束: ${oldTask.endDate}${newTask.endDate}`,
'info',
{ closable: true },
)
}
function handleMilestoneDragEnd(newMilestone) {
const oldMilestone = findTaskDeep(milestones.value, newMilestone.id)
if (!oldMilestone) return
showMessage(
`里程碑【${newMilestone.name}\n` +
`开始: ${oldMilestone.endDate}${newMilestone.startDate}`,
'info',
{ closable: true },
)
}
onMounted(() => {
tasks.value = demoData.tasks as Task[]
milestones.value = demoData.milestones as Task[]
// 调试信息:打印原始任务数据
console.log('原始任务数据:', tasks.value)
console.log('里程碑数据:', milestones.value)
})
// 递归查找任务/里程碑,因为原始结构一致
function findTaskDeep(taskArray: Task[], id: number): Task | null {
for (const task of taskArray) {
if (task.id === id) return task
if (task.children && task.children.length > 0) {
const found = findTaskDeep(task.children, id)
if (found) return found
}
}
return null
}
</script>
<template>
@@ -469,6 +463,9 @@ onMounted(() => {
:on-task-add="handleTaskAdd"
:on-task-delete="handleTaskDelete"
:on-milestone-icon-change="handleMilestoneIconChange"
@taskbar-drag-end="handleTaskbarDragOrResizeEnd"
@taskbar-resize-end="handleTaskbarDragOrResizeEnd"
@milestone-drag-end="handleMilestoneDragEnd"
/>
</div>
<div class="license-info">
-3
View File
@@ -1,3 +0,0 @@
# README.md
This file has been deprecated. Please refer to the main project `README.md` for documentation.
+12
View File
@@ -68,5 +68,17 @@
"version": "alpha 0.9.8",
"date": "2025-06-28",
"notes": ["按钮样式统一管理"]
},
{
"version": "beta 1.0.0",
"date": "2025-06-29",
"notes": [
"GanttChart接口增强",
"添加GanttChart的拖拽事件回调",
"添加GanttChart的里程碑拖拽事件回调",
"修复里程碑点的样式问题",
"更新README文档",
"重构项目目录结构"
]
}
]
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "jordium-gantt-vue3",
"private": true,
"version": "0.9.8-alpha",
"version": "1.0.0-beta",
"type": "module",
"scripts": {
"dev": "vite",
+1 -1
View File
@@ -97,7 +97,7 @@ watch(
singleValue.value = (newValue as string) || ''
}
},
{ immediate: true }
{ immediate: true },
)
// 处理单日期输入变化(预留,当前版本不使用)
+53 -38
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, onUnmounted, onMounted, computed, watch, nextTick } from 'vue'
import { ref, onUnmounted, onMounted, computed, watch, nextTick, defineEmits } from 'vue'
import TaskList from './TaskList.vue'
import Timeline from './Timeline.vue'
import GanttToolbar from './GanttToolbar.vue'
@@ -8,6 +8,36 @@ import jsPDF from 'jspdf'
import html2canvas from 'html2canvas'
import type { Task } from '../models/classes/Task'
import type { ToolbarConfig } from '../models/configs/ToolbarConfig'
import { useMessage } from '../composables/useMessage'
const props = withDefaults(defineProps<Props>(), {
tasks: () => [],
milestones: () => [],
onTaskDoubleClick: undefined,
editComponent: undefined,
useDefaultDrawer: true,
onTaskDelete: undefined,
onMilestoneSave: undefined,
onMilestoneDelete: undefined,
onTaskUpdate: undefined,
onTaskAdd: undefined,
onMilestoneIconChange: undefined,
toolbarConfig: () => ({}),
showToolbar: true,
onAddTask: undefined,
onAddMilestone: undefined,
onTodayLocate: undefined,
onExportCsv: undefined,
onExportPdf: undefined,
onLanguageChange: undefined,
onThemeChange: undefined,
onFullscreenChange: undefined,
localeMessages: undefined,
})
const emit = defineEmits(['taskbar-drag-end', 'taskbar-resize-end', 'milestone-drag-end'])
const { showMessage } = useMessage()
interface Props {
// 任务数据
@@ -55,31 +85,6 @@ interface Props {
localeMessages?: Partial<import('../composables/useI18n').Messages['zh-CN']>
}
const props = withDefaults(defineProps<Props>(), {
tasks: () => [],
milestones: () => [],
onTaskDoubleClick: undefined,
editComponent: undefined,
useDefaultDrawer: true,
onTaskDelete: undefined,
onMilestoneSave: undefined,
onMilestoneDelete: undefined,
onTaskUpdate: undefined,
onTaskAdd: undefined,
onMilestoneIconChange: undefined,
toolbarConfig: () => ({}),
showToolbar: true,
onAddTask: undefined,
onAddMilestone: undefined,
onTodayLocate: undefined,
onExportCsv: undefined,
onExportPdf: undefined,
onLanguageChange: undefined,
onThemeChange: undefined,
onFullscreenChange: undefined,
localeMessages: undefined,
})
const leftPanelWidth = ref(320)
// Timeline组件的引用
@@ -229,10 +234,29 @@ const handleToggleTaskList = (event: CustomEvent) => {
isTaskListVisible.value = event.detail
}
// --- 事件链路:监听 Timeline 传递上来的拖拽/拉伸事件,并通过 props 回调暴露 ---
function handleTaskBarDragEnd(event: CustomEvent) {
emit('taskbar-drag-end', event.detail)
}
function handleTaskBarResizeEnd(event: CustomEvent) {
emit('taskbar-resize-end', event.detail)
}
function handleMilestoneDragEnd(event: CustomEvent) {
emit('milestone-drag-end', event.detail)
}
onMounted(() => {
window.addEventListener('taskbar-drag-end', handleTaskBarDragEnd as EventListener)
window.addEventListener('taskbar-resize-end', handleTaskBarResizeEnd as EventListener)
window.addEventListener('milestone-drag-end', handleMilestoneDragEnd as EventListener)
})
onUnmounted(() => {
window.removeEventListener('taskbar-drag-end', handleTaskBarDragEnd as EventListener)
window.removeEventListener('taskbar-resize-end', handleTaskBarResizeEnd as EventListener)
window.removeEventListener('milestone-drag-end', handleMilestoneDragEnd as EventListener)
})
// 处理TaskList的任务折叠状态变化
const handleTaskCollapseChange = (task: Task) => {
console.log('任务折叠状态变化:', task)
// 递归查找并更新原始数据中的任务折叠状态
const updateTaskCollapsedState = (
tasks: Task[],
@@ -242,7 +266,6 @@ const handleTaskCollapseChange = (task: Task) => {
for (const t of tasks) {
if (t.id === targetId) {
t.collapsed = collapsed
console.log(`已更新任务 ${t.name} 的折叠状态为: ${collapsed}`)
return true
}
if (t.children && t.children.length > 0) {
@@ -385,7 +408,6 @@ const tasksForTaskList = computed(() => {
result.push(...props.tasks)
}
console.log('TaskList数据(保持层级结构):', result)
return result
})
@@ -489,7 +511,6 @@ const tasksForTimeline = computed(() => {
startDate,
endDate,
}
console.log(`父任务 ${task.name} 时间范围已更新: ${startDate} - ${endDate}`)
}
return updatedTask
@@ -528,11 +549,9 @@ const tasksForTimeline = computed(() => {
// 因为tasksForTaskList返回的是props.tasks的副本,我们需要确保Timeline能看到最新的折叠状态
// 通过触发器强制重新计算来实现状态同步
const flattenedTasks = smartFlattenTasks(tasksWithUpdatedDates)
console.log('Timeline智能扁平化任务数据(考虑折叠状态):', flattenedTasks)
result.push(...flattenedTasks)
}
console.log('Timeline最终数据:', result)
return result
})
@@ -645,6 +664,7 @@ const defaultExportCsv = () => {
URL.revokeObjectURL(url)
} catch (error) {
console.error('CSV导出失败:', error)
showMessage('CSV导出失败', 'error', { closable: false })
}
}
@@ -877,8 +897,6 @@ const handleMilestoneSave = (milestone: Task) => {
// 如果没有外部处理器,执行默认的更新逻辑
// 这里可以添加默认的里程碑数据更新逻辑,比如更新本地数据或者触发重新渲染
console.log('Milestone saved:', milestone)
}
// 处理任务更新事件
@@ -907,7 +925,6 @@ const handleMilestoneIconChangeEvent = (event: CustomEvent) => {
if (props.onMilestoneIconChange && typeof props.onMilestoneIconChange === 'function') {
props.onMilestoneIconChange(milestoneId, icon)
}
console.log('Milestone icon changed:', { milestoneId, icon })
}
// 处理里程碑删除事件
@@ -980,8 +997,6 @@ const defaultAddMilestone = () => {
// 默认今日定位功能
const defaultTodayLocate = () => {
console.log('执行默认今日定位逻辑defaultTodayLocate')
// 优先使用Timeline组件的scrollToToday方法(已优化的定位逻辑)
if (timelineRef.value && typeof timelineRef.value.scrollToToday === 'function') {
timelineRef.value.scrollToToday()
+2 -7
View File
@@ -17,6 +17,7 @@ const props = defineProps<Props>()
const emit = defineEmits<{
'milestone-double-click': [milestone: Milestone]
'update:milestone': [milestone: Milestone] // 新增里程碑更新事件
'drag-end': [milestone: Milestone] // 新增
}>()
// 拖拽相关状态
@@ -89,17 +90,11 @@ const handleMouseUp = () => {
...props.milestone,
...tempMilestoneData.value,
}
console.log('里程碑拖拽完成,提交数据更新:', updatedMilestone)
emit('update:milestone', updatedMilestone)
// 清空临时数据
emit('drag-end', updatedMilestone) // 新增
tempMilestoneData.value = null
}
isDragging.value = false
// 移除全局事件监听器
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
}
+13 -2
View File
@@ -13,7 +13,13 @@ interface Props {
const props = defineProps<Props>()
const emit = defineEmits(['update:task', 'bar-mounted', 'dblclick'])
const emit = defineEmits([
'update:task',
'bar-mounted',
'dblclick',
'drag-end', // 新增
'resize-end', // 新增
])
// 日期工具函数 - 处理时区安全的日期创建和操作
const createLocalDate = (dateString: string | Date | undefined | null): Date | null => {
@@ -255,7 +261,12 @@ const handleMouseUp = () => {
...tempTaskData.value,
}
console.log('TaskBar操作完成,提交数据更新:', updatedTask)
// 判断是拖拽还是拉伸
if (isDragging.value) {
emit('drag-end', updatedTask)
} else if (isResizingLeft.value || isResizingRight.value) {
emit('resize-end', updatedTask)
}
emit('update:task', updatedTask)
// 清空临时数据
+19 -22
View File
@@ -230,8 +230,6 @@ const handleMilestoneDelete = (milestoneId: number) => {
// 处理里程碑拖拽更新事件
const handleMilestoneUpdate = (updatedMilestone: any) => {
console.log('Timeline 接收到里程碑拖拽更新事件:', updatedMilestone)
// 通知父组件里程碑数据已更新
if (props.onMilestoneSave && typeof props.onMilestoneSave === 'function') {
props.onMilestoneSave(updatedMilestone)
@@ -345,15 +343,6 @@ const scrollToTodayCenter = (retry = 0) => {
const scrollContainer = document.querySelector('.timeline') as HTMLElement
const containerWidth = scrollContainer.clientWidth
console.log('[scrollToTodayCenter]', {
retry,
today: todayNormalized,
timelineStart: startNormalized,
daysDiff,
todayPosition,
containerWidth,
containerClass: scrollContainer.className,
})
// 若宽度为0,递归重试,最多10次
if (containerWidth === 0 && retry < 10) {
setTimeout(() => scrollToTodayCenter(retry + 1), 60)
@@ -367,19 +356,8 @@ const scrollToTodayCenter = (retry = 0) => {
} else {
scrollContainer.scrollLeft = Math.max(0, centeredScrollPosition)
}
console.log(
'[scrollToTodayCenter] scrollLeft set:',
Math.max(0, centeredScrollPosition),
'on',
scrollContainer.className,
)
console.log(
'[scrollToTodayCenter] scrollContainer.scrollLeft updated:',
scrollContainer.scrollLeft,
)
}
// 聚焦到任务所在位置
const scrollToTasks = () => {
if (tasks.value.length === 0) {
// 如果没有任务,滚动到今天
@@ -565,6 +543,22 @@ function handleBarMounted(payload: {
updateSvgSize()
}
// 向上传递 TaskBar 拖拽/拉伸事件
const handleTaskBarDragEnd = (updatedTask: Task) => {
// 通过全局事件或 emit/props 回调传递给 GanttChart
window.dispatchEvent(new CustomEvent('taskbar-drag-end', { detail: updatedTask }))
// TODO: 你可以在这里加 emit/props 回调
}
const handleTaskBarResizeEnd = (updatedTask: Task) => {
window.dispatchEvent(new CustomEvent('taskbar-resize-end', { detail: updatedTask }))
// TODO: 你可以在这里加 emit/props 回调
}
// 向上传递 MilestonePoint 拖拽事件
const handleMilestoneDragEnd = (updatedMilestone: any) => {
window.dispatchEvent(new CustomEvent('milestone-drag-end', { detail: updatedMilestone }))
// TODO: 你可以在这里加 emit/props 回调
}
// 计算所有连线
const links = computed(() => {
const result: { from: number; to: number; path: string }[] = []
@@ -987,6 +981,7 @@ const convertTaskToMilestone = (task: Task): Milestone => {
:milestone="convertTaskToMilestone(milestone)"
@milestone-double-click="handleMilestoneDoubleClick"
@update:milestone="handleMilestoneUpdate"
@drag-end="handleMilestoneDragEnd"
/>
</template>
<!-- 普通任务条 - 排除里程碑分组和普通里程碑 -->
@@ -1002,6 +997,8 @@ const convertTaskToMilestone = (task: Task): Milestone => {
@update:task="updateTask"
@bar-mounted="handleBarMounted"
@dblclick="handleTaskBarDoubleClick(task)"
@drag-end="handleTaskBarDragEnd"
@resize-end="handleTaskBarResizeEnd"
/>
</div>
</div>
+128
View File
@@ -0,0 +1,128 @@
// src/composables/useMessage.ts
export type MessageType = 'success' | 'error' | 'warning' | 'info'
// 消息类型对应的颜色,参考 element plus
const typeColorMap: Record<MessageType, string> = {
success: '#67c23a', // 绿色
warning: '#e6a23c', // 黄色
error: '#f56c6c', // 红色
info: '#909399', // 灰色
}
// 全局消息容器
let container: HTMLDivElement | null = null
function getContainer() {
if (!container) {
container = document.createElement('div')
container.className = 'message-container'
container.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 12px;
pointer-events: none;
`
document.body.appendChild(container)
}
return container
}
export function useMessage() {
/**
* @param message
* @param type
* @param options
* - duration: 自动关闭的毫秒数30000
* - closable: 是否显示关闭按钮true=false=
*/
const showMessage = (
message: string,
type: MessageType = 'success',
options?: { duration?: number; closable?: boolean },
) => {
const messageEl = document.createElement('div')
messageEl.className = `message ${type}`
messageEl.textContent = message
messageEl.style.cssText = `
min-width: 180px;
max-width: 360px;
margin: 0;
padding: 12px 20px 12px 20px;
border-radius: 4px;
color: white;
font-size: 14px;
background: ${typeColorMap[type]};
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
pointer-events: auto;
opacity: 0;
display: flex;
align-items: center;
gap: 12px;
transform: translateY(-20px);
transition: opacity 0.25s, transform 0.25s;
position: relative;
`
let closed = false
// 关闭按钮
const closable = options?.closable ?? false
if (closable) {
const closeBtn = document.createElement('button')
closeBtn.innerHTML =
'<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>'
closeBtn.style.cssText = `
background: transparent;
border: none;
color: white;
cursor: pointer;
margin-left: 8px;
padding: 0;
display: flex;
align-items: center;
opacity: 0.7;
transition: opacity 0.2s;
`
closeBtn.onmouseenter = () => (closeBtn.style.opacity = '1')
closeBtn.onmouseleave = () => (closeBtn.style.opacity = '0.7')
closeBtn.onclick = () => close()
messageEl.appendChild(closeBtn)
}
// 插入到容器(始终插入到末尾,保证新消息在下方)
const parent = getContainer()
parent.appendChild(messageEl)
// 入场动画
setTimeout(() => {
messageEl.style.opacity = '0.98'
messageEl.style.transform = 'translateY(0)'
}, 10)
// 自动关闭
const duration = options?.duration ?? (closable ? 0 : 3000)
let timer: number | undefined
if (!closable && duration > 0) {
timer = window.setTimeout(() => close(), duration)
}
// 关闭逻辑,带动画
function close() {
if (closed) return
closed = true
messageEl.style.opacity = '0'
messageEl.style.transform = 'translateY(-20px)'
if (timer) clearTimeout(timer)
setTimeout(() => {
if (messageEl.parentNode) {
parent.removeChild(messageEl)
// 关闭后自动上移(flex布局已保证)
// 若无消息,移除容器
if (parent.childElementCount === 0) {
parent.remove()
container = null
}
}
}, 250)
}
}
return { showMessage }
}