beta 1.0.0 - 正式外测版本
This commit is contained in:
-996
@@ -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或联系开发团队。
|
||||
@@ -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.
|
||||
+54
-57
@@ -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">
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# README.md
|
||||
|
||||
This file has been deprecated. Please refer to the main project `README.md` for documentation.
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -97,7 +97,7 @@ watch(
|
||||
singleValue.value = (newValue as string) || ''
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 处理单日期输入变化(预留,当前版本不使用)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,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
@@ -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>
|
||||
|
||||
@@ -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: 自动关闭的毫秒数(默认3000,0表示不自动关闭)
|
||||
* - 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 }
|
||||
}
|
||||
Reference in New Issue
Block a user