Merge branch 'master' into my-feature-partial
This commit is contained in:
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.2.1] - 2025-07-22
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- SonarQube代码质量检查问题修改
|
||||||
|
|
||||||
|
## [1.2.0] - 2025-07-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- 增加TaskRow/TaskBar的右键菜单
|
||||||
|
- 增加Task的执行定时
|
||||||
|
- 增加添加前置任务功能
|
||||||
|
- 增加添加后置任务功能
|
||||||
|
- 增加TaskRow/TaskBar的删除功能
|
||||||
|
- GanttChart增加@predecessor-added(添加前置任务完成)事件
|
||||||
|
- GanttChart增加@successor-added(添加后置任务完成)事件
|
||||||
|
- GanttChart增加@task-deleted(删除任务完成)事件
|
||||||
|
- GanttChart增加@task-added(添加任务完成)事件
|
||||||
|
- GanttChart增加@task-updated(编辑任务完成)事件
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- 升级TaskDrawer统一在GanttChart中管理
|
||||||
|
|
||||||
## [1.1.0] - 2025-07-08
|
## [1.1.0] - 2025-07-08
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+87
-26
@@ -55,12 +55,6 @@ pnpm add jordium-gantt-vue3
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
> 💡 **Badge Downloads**:
|
|
||||||
> - npm version badge:https://img.shields.io/npm/v/jordium-gantt-vue3.svg
|
|
||||||
> - MIT license badge:https://img.shields.io/badge/license-MIT-blue.svg
|
|
||||||
> - Vue version badge:https://img.shields.io/badge/vue-3.x-green.svg
|
|
||||||
> - TypeScript badge:https://img.shields.io/badge/typescript-5.x-blue.svg
|
|
||||||
|
|
||||||
## 📁 Project Structure
|
## 📁 Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -116,11 +110,72 @@ jordium-gantt-vue3/
|
|||||||
|
|
||||||
### GanttChart Events
|
### GanttChart Events
|
||||||
|
|
||||||
| Event | Parameters | Description |
|
| Event | Parameters | Description |
|
||||||
|-------|------------|-------------|
|
|--------------------|----------------------------|------------------------------------|
|
||||||
| `taskbar-drag-end` | `task: Task` | Task bar drag end |
|
| `taskbar-drag-end` | `task: Task` | Task bar drag end |
|
||||||
| `taskbar-resize-end` | `task: Task` | Task bar resize end |
|
| `taskbar-resize-end` | `task: Task` | Task bar resize end |
|
||||||
| `milestone-drag-end` | `milestone: Task` | Milestone drag end |
|
| `milestone-drag-end` | `milestone: Task` | Milestone drag end |
|
||||||
|
| `predecessor-added`| `{ targetTask, newTask }` | Triggered after adding predecessor.<br>Parameters:<br>• `targetTask`: The task to which a predecessor was added (Task object)<br>• `newTask`: The newly added predecessor task (Task object) |
|
||||||
|
| `successor-added` | `{ targetTask, newTask }` | Triggered after adding successor.<br>Parameters:<br>• `targetTask`: The task to which a successor was added (Task object)<br>• `newTask`: The newly added successor task (Task object) |
|
||||||
|
| `task-deleted` | `{ task }` | Triggered after deleting a task |
|
||||||
|
| `task-added` | `{ task }` | Triggered after creating a task |
|
||||||
|
| `task-updated` | `{ task }` | Triggered after updating a task |
|
||||||
|
|
||||||
|
#### Timer Event Usage Example
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<GanttChart
|
||||||
|
...
|
||||||
|
@timer-started="onTimerStarted"
|
||||||
|
@timer-stopped="onTimerStopped"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
function onTimerStarted(task) {
|
||||||
|
// Custom notification, logging, or business logic
|
||||||
|
alert(`Task [${task.name}] started at: ${new Date(task.timerStartTime).toLocaleString()}`)
|
||||||
|
}
|
||||||
|
function onTimerStopped(task) {
|
||||||
|
alert(`Task [${task.name}] stopped`)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Task Event Usage Example
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<GanttChart
|
||||||
|
...
|
||||||
|
@predecessor-added="onPredecessorAdded"
|
||||||
|
@successor-added="onSuccessorAdded"
|
||||||
|
@task-deleted="onTaskDeleted"
|
||||||
|
@task-added="onTaskAdded"
|
||||||
|
@task-updated="onTaskUpdated"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
function onPredecessorAdded(e) {
|
||||||
|
// e: { targetTask: Task, newTask: Task }
|
||||||
|
alert(`Task [${e.targetTask.name}] predecessor added [${e.newTask.name}]`)
|
||||||
|
}
|
||||||
|
function onSuccessorAdded(e) {
|
||||||
|
// e: { targetTask: Task, newTask: Task }
|
||||||
|
alert(`Task [${e.targetTask.name}] successor added [${e.newTask.name}]`)
|
||||||
|
}
|
||||||
|
function onTaskDeleted(e) {
|
||||||
|
// e: { task: Task }
|
||||||
|
alert(`Task [${e.task.name}] deleted`)
|
||||||
|
}
|
||||||
|
function onTaskAdded(e) {
|
||||||
|
// e: { task: Task }
|
||||||
|
alert(`Task [${e.task.name}] created`)
|
||||||
|
}
|
||||||
|
function onTaskUpdated(e) {
|
||||||
|
// e: { task: Task }
|
||||||
|
alert(`Task [${e.task.name}] updated`)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
### Data Types
|
### Data Types
|
||||||
|
|
||||||
@@ -128,24 +183,30 @@ jordium-gantt-vue3/
|
|||||||
|
|
||||||
**Task Type**
|
**Task Type**
|
||||||
```typescript
|
```typescript
|
||||||
interface Task {
|
export interface Task {
|
||||||
id: number // Unique task identifier
|
id: number // Unique task ID
|
||||||
name: string // Task name
|
name: string // Task name
|
||||||
predecessor?: string // Predecessor task ID
|
predecessor?: number[] // Predecessor task ID array
|
||||||
assignee?: string // Assignee
|
assignee?: string // Assignee
|
||||||
startDate?: string // Start date (YYYY-MM-DD format)
|
startDate?: string // Start date (ISO string)
|
||||||
endDate?: string // End date (YYYY-MM-DD format)
|
endDate?: string // End date (ISO string)
|
||||||
progress?: number // Completion progress (0-100)
|
progress?: number // Progress percentage 0-100
|
||||||
estimatedHours?: number // Estimated hours
|
estimatedHours?: number // Estimated hours
|
||||||
actualHours?: number // Actual hours
|
actualHours?: number // Actual hours
|
||||||
parentId?: number // Parent task ID
|
parentId?: number // Parent task ID
|
||||||
children?: Task[] // Child tasks array (supports nested structure)
|
children?: Task[] // Subtask array
|
||||||
collapsed?: boolean // Whether child tasks are collapsed
|
collapsed?: boolean // Collapsed state
|
||||||
isParent?: boolean // Whether it's a parent task
|
isParent?: boolean // Is parent task
|
||||||
type?: string // Task type (task/story/bug/milestone)
|
type?: string // Task type (e.g. task, story, milestone)
|
||||||
description?: string // Task description
|
description?: string // Task description
|
||||||
icon?: string // Task icon
|
icon?: string // Icon
|
||||||
level?: number // Task level
|
level?: number // Level
|
||||||
|
// Timer related fields
|
||||||
|
isTimerRunning?: boolean // Is timer running
|
||||||
|
timerStartTime?: number // Timer start timestamp
|
||||||
|
timerEndTime?: number // Timer end timestamp
|
||||||
|
timerStartDesc?: string // Timer start description
|
||||||
|
timerElapsedTime?: number // Accumulated timer duration (seconds)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -56,36 +56,33 @@ pnpm add jordium-gantt-vue3
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
> 💡 **徽章下载**:
|
|
||||||
> - npm 版本徽章:https://img.shields.io/npm/v/jordium-gantt-vue3.svg
|
|
||||||
> - MIT 许可证徽章:https://img.shields.io/badge/license-MIT-blue.svg
|
|
||||||
> - Vue 版本徽章:https://img.shields.io/badge/vue-3.x-green.svg
|
|
||||||
> - TypeScript 徽章:https://img.shields.io/badge/typescript-5.x-blue.svg
|
|
||||||
|
|
||||||
## 📁 项目结构
|
## 📁 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
jordium-gantt-vue3/
|
jordium-gantt-vue3/
|
||||||
├── src/ # 源码目录
|
├── src/ # 组件源码与核心逻辑
|
||||||
│ ├── components/ # 核心组件
|
│ ├── components/ # 主要 Vue 组件
|
||||||
│ │ ├── GanttChart.vue # 主入口组件
|
│ ├── models/ # 数据类型与配置
|
||||||
│ │ ├── TaskList.vue # 任务列表
|
│ ├── composables/ # 组合式函数
|
||||||
│ │ ├── Timeline.vue # 时间轴
|
│ ├── styles/ # 样式文件
|
||||||
│ │ ├── TaskBar.vue # 任务条
|
│ └── index.ts # 入口导出
|
||||||
│ │ ├── MilestonePoint.vue # 里程碑
|
├── demo/ # 组件开发与交互演示(本地开发/预览用)
|
||||||
│ │ └── ... # 其他组件
|
├── packageDemo/ # npm 包集成演示(模拟外部项目集成效果)
|
||||||
│ ├── models/ # 数据模型
|
├── dist/ # 构建产物(发布/静态站点/打包输出)
|
||||||
│ │ ├── classes/ # 类定义
|
├── docs/ # 相关文档(如部署、API 说明等)
|
||||||
│ │ └── configs/ # 配置接口
|
├── design/ # 设计资源与截图
|
||||||
│ ├── composables/ # 组合式函数
|
├── public/ # 公共静态资源
|
||||||
│ ├── styles/ # 样式文件
|
├── README.md # 中文说明文档
|
||||||
│ └── index.ts # 导出入口
|
├── README-EN.md # 英文说明文档
|
||||||
├── demo/ # 开发演示
|
└── ... # 其他配置、脚本与元数据
|
||||||
├── dist/ # 构建产物
|
|
||||||
├── docs/ # 文档
|
|
||||||
└── package.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- `demo/`:用于本地开发和功能演示,包含完整的交互页面。
|
||||||
|
- `packageDemo/`:用于模拟 npm 包在外部项目中的集成与使用场景。
|
||||||
|
- `dist/`:构建输出目录,包含发布到 npm 或静态站点的产物。
|
||||||
|
- `docs/`:项目相关文档,如部署说明、API 参考等。
|
||||||
|
- 其余目录请参考注释。
|
||||||
|
|
||||||
## 🔧 API 参考
|
## 🔧 API 参考
|
||||||
|
|
||||||
### GanttChart 属性
|
### GanttChart 属性
|
||||||
@@ -117,11 +114,72 @@ jordium-gantt-vue3/
|
|||||||
|
|
||||||
### GanttChart 事件
|
### GanttChart 事件
|
||||||
|
|
||||||
| 事件名 | 参数 | 说明 |
|
| 事件名 | 参数 | 说明 |
|
||||||
|--------|------|------|
|
|----------------------|----------------------------|------------------------------|
|
||||||
| `taskbar-drag-end` | `task: Task` | 任务条拖拽结束 |
|
| `taskbar-drag-end` | `task: Task` | 任务条拖拽结束 |
|
||||||
| `taskbar-resize-end` | `task: Task` | 任务条大小调整结束 |
|
| `taskbar-resize-end` | `task: Task` | 任务条大小调整结束 |
|
||||||
| `milestone-drag-end` | `milestone: Task` | 里程碑拖拽结束 |
|
| `milestone-drag-end` | `milestone: Task` | 里程碑拖拽结束 |
|
||||||
|
| `predecessor-added` | `{ targetTask, newTask }` | 添加前置任务后触发。<br>参数说明:<br>• `targetTask`:被添加前置任务的目标任务(Task对象)<br>• `newTask`:新添加的前置任务(Task对象) |
|
||||||
|
| `successor-added` | `{ targetTask, newTask }` | 添加后置任务后触发。<br>参数说明:<br>• `targetTask`:被添加后置任务的目标任务(Task对象)<br>• `newTask`:新添加的后置任务(Task对象) |
|
||||||
|
| `task-deleted` | `{ task }` | 删除任务后触发 |
|
||||||
|
| `task-added` | `{ task }` | 新建任务后触发 |
|
||||||
|
| `task-updated` | `{ task }` | 更新任务后触发 |
|
||||||
|
|
||||||
|
#### 计时事件用法示例
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<GanttChart
|
||||||
|
...
|
||||||
|
@timer-started="onTimerStarted"
|
||||||
|
@timer-stopped="onTimerStopped"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
function onTimerStarted(task) {
|
||||||
|
// 这里可以自定义提示、日志或业务逻辑
|
||||||
|
alert(`任务【${task.name}】开始计时:${new Date(task.timerStartTime).toLocaleString()}`)
|
||||||
|
}
|
||||||
|
function onTimerStopped(task) {
|
||||||
|
alert(`任务【${task.name}】停止计时`)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 任务事件用法示例
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<GanttChart
|
||||||
|
...
|
||||||
|
@predecessor-added="onPredecessorAdded"
|
||||||
|
@successor-added="onSuccessorAdded"
|
||||||
|
@task-deleted="onTaskDeleted"
|
||||||
|
@task-added="onTaskAdded"
|
||||||
|
@task-updated="onTaskUpdated"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
function onPredecessorAdded(e) {
|
||||||
|
// e: { targetTask: Task, newTask: Task }
|
||||||
|
alert(`任务【${e.targetTask.name}】添加前置任务【${e.newTask.name}】`)
|
||||||
|
}
|
||||||
|
function onSuccessorAdded(e) {
|
||||||
|
// e: { targetTask: Task, newTask: Task }
|
||||||
|
alert(`任务【${e.targetTask.name}】添加后置任务【${e.newTask.name}】`)
|
||||||
|
}
|
||||||
|
function onTaskDeleted(e) {
|
||||||
|
// e: { task: Task }
|
||||||
|
alert(`任务【${e.task.name}】已删除`)
|
||||||
|
}
|
||||||
|
function onTaskAdded(e) {
|
||||||
|
// e: { task: Task }
|
||||||
|
alert(`任务【${e.task.name}】已创建`)
|
||||||
|
}
|
||||||
|
function onTaskUpdated(e) {
|
||||||
|
// e: { task: Task }
|
||||||
|
alert(`任务【${e.task.name}】已更新`)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
### 数据类型
|
### 数据类型
|
||||||
|
|
||||||
@@ -129,24 +187,30 @@ jordium-gantt-vue3/
|
|||||||
|
|
||||||
**Task 任务类型**
|
**Task 任务类型**
|
||||||
```typescript
|
```typescript
|
||||||
interface Task {
|
export interface Task {
|
||||||
id: number // 任务唯一标识
|
id: number // 任务唯一ID
|
||||||
name: string // 任务名称
|
name: string // 任务名称
|
||||||
predecessor?: string // 前置任务ID
|
predecessor?: number[] // 前置任务ID数组
|
||||||
assignee?: string // 负责人
|
assignee?: string // 负责人
|
||||||
startDate?: string // 开始日期 (YYYY-MM-DD格式)
|
startDate?: string // 开始日期(ISO字符串)
|
||||||
endDate?: string // 结束日期 (YYYY-MM-DD格式)
|
endDate?: string // 结束日期(ISO字符串)
|
||||||
progress?: number // 完成进度 (0-100)
|
progress?: number // 进度百分比 0-100
|
||||||
estimatedHours?: number // 预估工时
|
estimatedHours?: number // 预估工时
|
||||||
actualHours?: number // 实际工时
|
actualHours?: number // 实际工时
|
||||||
parentId?: number // 上级任务ID
|
parentId?: number // 上级任务ID
|
||||||
children?: Task[] // 子任务数组(支持嵌套结构)
|
children?: Task[] // 子任务数组
|
||||||
collapsed?: boolean // 是否折叠子任务
|
collapsed?: boolean // 是否折叠
|
||||||
isParent?: boolean // 是否为父级任务
|
isParent?: boolean // 是否为父任务
|
||||||
type?: string // 任务类型 (task/story/bug/milestone)
|
type?: string // 任务类型(如 task、story、milestone 等)
|
||||||
description?: string // 任务描述
|
description?: string // 任务描述
|
||||||
icon?: string // 任务图标
|
icon?: string // 图标
|
||||||
level?: number // 任务层级
|
level?: number // 层级
|
||||||
|
// 计时相关字段
|
||||||
|
isTimerRunning?: boolean // 计时是否进行中
|
||||||
|
timerStartTime?: number // 计时开始时间(时间戳)
|
||||||
|
timerEndTime?: number // 计时结束时间(时间戳)
|
||||||
|
timerStartDesc?: string // 计时开始时的描述
|
||||||
|
timerElapsedTime?: number // 已累计计时时长(秒)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+50
-42
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, nextTick } from 'vue'
|
import { ref, onMounted, nextTick } from 'vue'
|
||||||
import GanttChart from '../src/components/GanttChart.vue'
|
import GanttChart from '../src/components/GanttChart.vue'
|
||||||
import TaskDrawer from '../src/components/TaskDrawer.vue'
|
// import TaskDrawer from '../src/components/TaskDrawer.vue' // 移除
|
||||||
import MilestoneDialog from '../src/components/MilestoneDialog.vue'
|
import MilestoneDialog from '../src/components/MilestoneDialog.vue'
|
||||||
import demoData from './data.json'
|
import demoData from './data.json'
|
||||||
import packageInfo from '../package.json'
|
import packageInfo from '../package.json'
|
||||||
@@ -19,11 +19,6 @@ const { t, formatTranslation } = useI18n()
|
|||||||
const tasks = ref<Task[]>([])
|
const tasks = ref<Task[]>([])
|
||||||
const milestones = ref<Task[]>([])
|
const milestones = ref<Task[]>([])
|
||||||
|
|
||||||
// TaskDrawer状态管理
|
|
||||||
const showTaskDrawer = ref(false)
|
|
||||||
const currentTask = ref<Task | null>(null)
|
|
||||||
const isEditMode = ref(false)
|
|
||||||
|
|
||||||
// MilestoneDialog状态管理
|
// MilestoneDialog状态管理
|
||||||
const showMilestoneDialog = ref(false)
|
const showMilestoneDialog = ref(false)
|
||||||
const currentMilestone = ref<Task | null>(null)
|
const currentMilestone = ref<Task | null>(null)
|
||||||
@@ -123,14 +118,14 @@ const handleMilestoneDelete = async (milestoneId: number) => {
|
|||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent('milestone-deleted', {
|
new CustomEvent('milestone-deleted', {
|
||||||
detail: { milestoneId },
|
detail: { milestoneId },
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
// 触发强制更新事件,确保Timeline重新渲染
|
// 触发强制更新事件,确保Timeline重新渲染
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent('milestone-data-changed', {
|
new CustomEvent('milestone-data-changed', {
|
||||||
detail: { milestones: milestones.value },
|
detail: { milestones: milestones.value },
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,6 +135,12 @@ const handleMilestoneDelete = async (milestoneId: number) => {
|
|||||||
|
|
||||||
// 任务更新处理器
|
// 任务更新处理器
|
||||||
const handleTaskUpdate = (updatedTask: Task) => {
|
const handleTaskUpdate = (updatedTask: Task) => {
|
||||||
|
// 计时信息展示(无论来源于 TaskBar/TaskRow 还是 TaskDrawer header)
|
||||||
|
if (updatedTask.timerStartTime) {
|
||||||
|
const msg = `任务【${updatedTask.name}】已更新`
|
||||||
|
showMessage(msg, 'success', { closable: true })
|
||||||
|
}
|
||||||
|
|
||||||
// 先找到原任务,检查parentId是否改变了
|
// 先找到原任务,检查parentId是否改变了
|
||||||
const findOriginalTask = (taskArray: Task[]): Task | null => {
|
const findOriginalTask = (taskArray: Task[]): Task | null => {
|
||||||
for (const task of taskArray) {
|
for (const task of taskArray) {
|
||||||
@@ -218,7 +219,7 @@ const handleTaskUpdate = (updatedTask: Task) => {
|
|||||||
showMessage(
|
showMessage(
|
||||||
formatTranslation('newParentTaskNotFound', { parentId: taskToAdd.parentId }),
|
formatTranslation('newParentTaskNotFound', { parentId: taskToAdd.parentId }),
|
||||||
'warning',
|
'warning',
|
||||||
{ closable: true },
|
{ closable: true }
|
||||||
)
|
)
|
||||||
tasks.value.push(taskToAdd)
|
tasks.value.push(taskToAdd)
|
||||||
}
|
}
|
||||||
@@ -266,7 +267,7 @@ const handleTaskAdd = (newTask: Task) => {
|
|||||||
const maxId = Math.max(
|
const maxId = Math.max(
|
||||||
...tasks.value.map(t => t.id || 0),
|
...tasks.value.map(t => t.id || 0),
|
||||||
...milestones.value.map(m => m.id || 0),
|
...milestones.value.map(m => m.id || 0),
|
||||||
0,
|
0
|
||||||
)
|
)
|
||||||
newTask.id = maxId + 1
|
newTask.id = maxId + 1
|
||||||
}
|
}
|
||||||
@@ -361,7 +362,7 @@ const handleStoryDeleteWithChildren = (storyToDelete: Task) => {
|
|||||||
'success',
|
'success',
|
||||||
{
|
{
|
||||||
closable: false,
|
closable: false,
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -423,7 +424,7 @@ const handleStoryDeleteOnly = (storyToDelete: Task) => {
|
|||||||
'success',
|
'success',
|
||||||
{
|
{
|
||||||
closable: false,
|
closable: false,
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -460,24 +461,6 @@ const handleMilestoneIconChange = (milestoneId: number, icon: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TaskDrawer事件处理器
|
|
||||||
const handleTaskDrawerSubmit = (task: Task) => {
|
|
||||||
if (isEditMode.value) {
|
|
||||||
// 编辑模式:更新任务
|
|
||||||
handleTaskUpdate(task)
|
|
||||||
} else {
|
|
||||||
// 新建模式:添加任务
|
|
||||||
handleTaskAdd(task)
|
|
||||||
}
|
|
||||||
showTaskDrawer.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTaskDrawerClose = () => {
|
|
||||||
showTaskDrawer.value = false
|
|
||||||
currentTask.value = null
|
|
||||||
isEditMode.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTaskDrawerDelete = (task: Task, deleteChildren?: boolean) => {
|
const handleTaskDrawerDelete = (task: Task, deleteChildren?: boolean) => {
|
||||||
if (task.type === 'story' && deleteChildren === true) {
|
if (task.type === 'story' && deleteChildren === true) {
|
||||||
// 删除story及其所有子任务
|
// 删除story及其所有子任务
|
||||||
@@ -515,7 +498,7 @@ function handleTaskbarDragOrResizeEnd(newTask) {
|
|||||||
`开始: ${oldTask.startDate} → ${newTask.startDate}\n` +
|
`开始: ${oldTask.startDate} → ${newTask.startDate}\n` +
|
||||||
`结束: ${oldTask.endDate} → ${newTask.endDate}`,
|
`结束: ${oldTask.endDate} → ${newTask.endDate}`,
|
||||||
'info',
|
'info',
|
||||||
{ closable: true },
|
{ closable: true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
function handleMilestoneDragEnd(newMilestone) {
|
function handleMilestoneDragEnd(newMilestone) {
|
||||||
@@ -525,7 +508,7 @@ function handleMilestoneDragEnd(newMilestone) {
|
|||||||
`里程碑【${newMilestone.name}】\n` +
|
`里程碑【${newMilestone.name}】\n` +
|
||||||
`开始: ${oldMilestone.endDate} → ${newMilestone.startDate}`,
|
`开始: ${oldMilestone.endDate} → ${newMilestone.startDate}`,
|
||||||
'info',
|
'info',
|
||||||
{ closable: true },
|
{ closable: true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,6 +569,26 @@ const collectAllTaskIds = (task: Task): number[] => {
|
|||||||
}
|
}
|
||||||
return ids
|
return ids
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Timer事件处理
|
||||||
|
function onTimerStarted(task: Task) {
|
||||||
|
showMessage(
|
||||||
|
`Demo 任务【${task.name}】\n开始计时:${new Date(task.timerStartTime).toLocaleString()}\n计时说明:${task.timerStartDesc ? task.timerStartDesc : ''}`,
|
||||||
|
'info',
|
||||||
|
{ closable: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
function onTimerStopped(task: Task) {
|
||||||
|
let msg = `Demo 任务【${task.name}】`
|
||||||
|
if (task.timerStartTime) {
|
||||||
|
msg += `\n开始计时:${new Date(task.timerStartTime).toLocaleString()}`
|
||||||
|
msg += `\n结束计时:${new Date().toLocaleString()}`
|
||||||
|
if (task.timerStartDesc) msg += `\n计时说明:${task.timerStartDesc}`
|
||||||
|
} else {
|
||||||
|
msg += `\n结束计时:${new Date().toLocaleString()}`
|
||||||
|
}
|
||||||
|
showMessage(msg, 'info', { closable: true })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -639,22 +642,27 @@ const collectAllTaskIds = (task: Task): number[] => {
|
|||||||
@taskbar-drag-end="handleTaskbarDragOrResizeEnd"
|
@taskbar-drag-end="handleTaskbarDragOrResizeEnd"
|
||||||
@taskbar-resize-end="handleTaskbarDragOrResizeEnd"
|
@taskbar-resize-end="handleTaskbarDragOrResizeEnd"
|
||||||
@milestone-drag-end="handleMilestoneDragEnd"
|
@milestone-drag-end="handleMilestoneDragEnd"
|
||||||
|
@edit-task="task => showMessage(`进入任务编辑:${task.name}`)"
|
||||||
|
@close="() => showMessage('已关闭任务编辑', 'info')"
|
||||||
|
@timer-started="onTimerStarted"
|
||||||
|
@timer-stopped="onTimerStopped"
|
||||||
|
@predecessor-added="
|
||||||
|
e =>
|
||||||
|
showMessage(`Demo 任务[${e.targetTask.name}] 添加前置任务 [${e.newTask.name}]`, 'info')
|
||||||
|
"
|
||||||
|
@successor-added="
|
||||||
|
e =>
|
||||||
|
showMessage(`Demo 任务[${e.targetTask.name}] 添加后置任务 [${e.newTask.name}]`, 'info')
|
||||||
|
"
|
||||||
|
@task-deleted="e => showMessage(`Demo 任务[${e.task.name}] 已删除`, 'info')"
|
||||||
|
@task-added="e => showMessage(`Demo 任务[${e.task.name}] 已创建`, 'info')"
|
||||||
|
@task-updated="e => showMessage(`Demo 任务[${e.task.name}] 已更新`, 'info')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="license-info">
|
<div class="license-info">
|
||||||
<p>MIT License @JORDIUM.COM</p>
|
<p>MIT License @JORDIUM.COM</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TaskDrawer用于新建/编辑任务 -->
|
|
||||||
<TaskDrawer
|
|
||||||
v-model:visible="showTaskDrawer"
|
|
||||||
:task="currentTask"
|
|
||||||
:is-edit="isEditMode"
|
|
||||||
@submit="handleTaskDrawerSubmit"
|
|
||||||
@close="handleTaskDrawerClose"
|
|
||||||
@delete="handleTaskDrawerDelete"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- MilestoneDialog用于新建/编辑里程碑 -->
|
<!-- MilestoneDialog用于新建/编辑里程碑 -->
|
||||||
<MilestoneDialog
|
<MilestoneDialog
|
||||||
:visible="showMilestoneDialog"
|
:visible="showMilestoneDialog"
|
||||||
|
|||||||
@@ -140,5 +140,27 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"date": "2025-07-08",
|
"date": "2025-07-08",
|
||||||
"notes": ["TaskBar拖拽触发Timeline水平滑动", "增加日|周|月视图切换", "问题修复"]
|
"notes": ["TaskBar拖拽触发Timeline水平滑动", "增加日|周|月视图切换", "问题修复"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.2.0",
|
||||||
|
"date": "2025-07-21",
|
||||||
|
"notes": [
|
||||||
|
"增加TaskRow/TaskBar的右键菜单",
|
||||||
|
"增加Task的执行定时",
|
||||||
|
"增加添加前置任务功能",
|
||||||
|
"增加添加后置任务功能",
|
||||||
|
"增加TaskRow/TaskBar的删除功能",
|
||||||
|
"GanttChart增加@predecessor-added(添加前置任务完成)事件",
|
||||||
|
"GanttChart增加@successor-added(添加后置任务完成)事件",
|
||||||
|
"GanttChart增加@task-deleted(删除任务完成)事件",
|
||||||
|
"GanttChart增加@task-added(添加任务完成)事件",
|
||||||
|
"GanttChart增加@task-updated(编辑任务完成)事件",
|
||||||
|
"升级TaskDrawer统一在GanttChart中管理"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.2.1",
|
||||||
|
"date": "2025-07-22",
|
||||||
|
"notes": ["SonarQube代码质量检查问题修改"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
# GitHub Pages Deployment Guide
|
|
||||||
|
|
||||||
This project has been configured with automatic deployment of GitHub Pages, and users can experience the complete Gantt charting function online.
|
|
||||||
|
|
||||||
## 🌐 Online visit
|
|
||||||
|
|
||||||
**Demo Address**: https://nelson820125.github.io/jordium-gantt-vue3/
|
|
||||||
|
|
||||||
## 🔧 Deployment Configuration
|
|
||||||
|
|
||||||
### Automated deployment
|
|
||||||
|
|
||||||
Projects are automatically deployed to GitHub Pages using GitHub Actions:
|
|
||||||
|
|
||||||
- **Triggering condition**: Push to `main` or `master` branch
|
|
||||||
- **Build Command**: `npm run build:pages`
|
|
||||||
- **Deployment Directory**: `./dist`
|
|
||||||
- **Workflows**: `.github/workflows/deploy-github-pages.yml`
|
|
||||||
|
|
||||||
### Enable GitHub Pages manually
|
|
||||||
|
|
||||||
1. Enter the GitHub repository settings page
|
|
||||||
2. Find the "Pages" setting options
|
|
||||||
3. Select "Source" as "GitHub Actions"
|
|
||||||
4. Push code to the main branch and automatically trigger deployment
|
|
||||||
|
|
||||||
## 📁 Construct the product
|
|
||||||
|
|
||||||
- **Development and Construction**: `npm run build` → `dist/`
|
|
||||||
- **GitHub Pages**: `npm run build:pages` → `dist/` (contains the correct base path)
|
|
||||||
- **NPM package build**: `npm run build:lib` → `npm-package/dist/`
|
|
||||||
|
|
||||||
## 🛠️ Local Preview
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Installation dependencies
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# Start the development server
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# Build and preview the production version
|
|
||||||
npm run build:pages
|
|
||||||
npm run preview
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔄 Update deployment
|
|
||||||
|
|
||||||
GitHub Actions will automatically:
|
|
||||||
|
|
||||||
1. Check out the code
|
|
||||||
2. Install Node.js and dependencies
|
|
||||||
3. Build demo application
|
|
||||||
4. Deploy to GitHub Pages
|
|
||||||
|
|
||||||
## 📝 Notes
|
|
||||||
|
|
||||||
- Deployment usually takes 1-5 minutes to take effect
|
|
||||||
- Make sure GitHub Pages is enabled in repository settings
|
|
||||||
- Custom domain names need to be configured in the `demo/public/CNAME` file
|
|
||||||
- Static resource paths use relative paths to ensure normal loading in the Pages environment
|
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "jordium-gantt-vue3",
|
"name": "jordium-gantt-vue3",
|
||||||
"version": "1.0.10",
|
"version": "1.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "jordium-gantt-vue3",
|
"name": "jordium-gantt-vue3",
|
||||||
"version": "1.0.10",
|
"version": "1.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jordium-gantt-vue3",
|
"name": "jordium-gantt-vue3",
|
||||||
"version": "1.1.0",
|
"version": "1.2.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/jordium-gantt-vue3.cjs.js",
|
"main": "dist/jordium-gantt-vue3.cjs.js",
|
||||||
"module": "dist/jordium-gantt-vue3.es.js",
|
"module": "dist/jordium-gantt-vue3.es.js",
|
||||||
|
|||||||
+51
-42
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, nextTick } from 'vue'
|
import { ref, onMounted, nextTick } from 'vue'
|
||||||
import GanttChart from '../src/components/GanttChart.vue'
|
import GanttChart from '../src/components/GanttChart.vue'
|
||||||
import TaskDrawer from '../src/components/TaskDrawer.vue'
|
// import TaskDrawer from '../src/components/TaskDrawer.vue' // 移除
|
||||||
import MilestoneDialog from '../src/components/MilestoneDialog.vue'
|
import MilestoneDialog from '../src/components/MilestoneDialog.vue'
|
||||||
import demoData from './data.json'
|
import demoData from './data.json'
|
||||||
import packageInfo from '../package.json'
|
import packageInfo from '../package.json'
|
||||||
@@ -19,11 +19,6 @@ const { t, formatTranslation } = useI18n()
|
|||||||
const tasks = ref<Task[]>([])
|
const tasks = ref<Task[]>([])
|
||||||
const milestones = ref<Task[]>([])
|
const milestones = ref<Task[]>([])
|
||||||
|
|
||||||
// TaskDrawer状态管理
|
|
||||||
const showTaskDrawer = ref(false)
|
|
||||||
const currentTask = ref<Task | null>(null)
|
|
||||||
const isEditMode = ref(false)
|
|
||||||
|
|
||||||
// MilestoneDialog状态管理
|
// MilestoneDialog状态管理
|
||||||
const showMilestoneDialog = ref(false)
|
const showMilestoneDialog = ref(false)
|
||||||
const currentMilestone = ref<Task | null>(null)
|
const currentMilestone = ref<Task | null>(null)
|
||||||
@@ -41,6 +36,7 @@ const toolbarConfig = {
|
|||||||
showLanguage: true,
|
showLanguage: true,
|
||||||
showTheme: true,
|
showTheme: true,
|
||||||
showFullscreen: true,
|
showFullscreen: true,
|
||||||
|
showTimeScale: true, // 控制日|周|月时间刻度按钮组的可见性
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自定义CSV导出处理器(可选)
|
// 自定义CSV导出处理器(可选)
|
||||||
@@ -122,14 +118,14 @@ const handleMilestoneDelete = async (milestoneId: number) => {
|
|||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent('milestone-deleted', {
|
new CustomEvent('milestone-deleted', {
|
||||||
detail: { milestoneId },
|
detail: { milestoneId },
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
// 触发强制更新事件,确保Timeline重新渲染
|
// 触发强制更新事件,确保Timeline重新渲染
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent('milestone-data-changed', {
|
new CustomEvent('milestone-data-changed', {
|
||||||
detail: { milestones: milestones.value },
|
detail: { milestones: milestones.value },
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +135,12 @@ const handleMilestoneDelete = async (milestoneId: number) => {
|
|||||||
|
|
||||||
// 任务更新处理器
|
// 任务更新处理器
|
||||||
const handleTaskUpdate = (updatedTask: Task) => {
|
const handleTaskUpdate = (updatedTask: Task) => {
|
||||||
|
// 计时信息展示(无论来源于 TaskBar/TaskRow 还是 TaskDrawer header)
|
||||||
|
if (updatedTask.timerStartTime) {
|
||||||
|
const msg = `任务【${updatedTask.name}】已更新`
|
||||||
|
showMessage(msg, 'success', { closable: true })
|
||||||
|
}
|
||||||
|
|
||||||
// 先找到原任务,检查parentId是否改变了
|
// 先找到原任务,检查parentId是否改变了
|
||||||
const findOriginalTask = (taskArray: Task[]): Task | null => {
|
const findOriginalTask = (taskArray: Task[]): Task | null => {
|
||||||
for (const task of taskArray) {
|
for (const task of taskArray) {
|
||||||
@@ -217,7 +219,7 @@ const handleTaskUpdate = (updatedTask: Task) => {
|
|||||||
showMessage(
|
showMessage(
|
||||||
formatTranslation('newParentTaskNotFound', { parentId: taskToAdd.parentId }),
|
formatTranslation('newParentTaskNotFound', { parentId: taskToAdd.parentId }),
|
||||||
'warning',
|
'warning',
|
||||||
{ closable: true },
|
{ closable: true }
|
||||||
)
|
)
|
||||||
tasks.value.push(taskToAdd)
|
tasks.value.push(taskToAdd)
|
||||||
}
|
}
|
||||||
@@ -265,7 +267,7 @@ const handleTaskAdd = (newTask: Task) => {
|
|||||||
const maxId = Math.max(
|
const maxId = Math.max(
|
||||||
...tasks.value.map(t => t.id || 0),
|
...tasks.value.map(t => t.id || 0),
|
||||||
...milestones.value.map(m => m.id || 0),
|
...milestones.value.map(m => m.id || 0),
|
||||||
0,
|
0
|
||||||
)
|
)
|
||||||
newTask.id = maxId + 1
|
newTask.id = maxId + 1
|
||||||
}
|
}
|
||||||
@@ -360,7 +362,7 @@ const handleStoryDeleteWithChildren = (storyToDelete: Task) => {
|
|||||||
'success',
|
'success',
|
||||||
{
|
{
|
||||||
closable: false,
|
closable: false,
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -422,7 +424,7 @@ const handleStoryDeleteOnly = (storyToDelete: Task) => {
|
|||||||
'success',
|
'success',
|
||||||
{
|
{
|
||||||
closable: false,
|
closable: false,
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -459,24 +461,6 @@ const handleMilestoneIconChange = (milestoneId: number, icon: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TaskDrawer事件处理器
|
|
||||||
const handleTaskDrawerSubmit = (task: Task) => {
|
|
||||||
if (isEditMode.value) {
|
|
||||||
// 编辑模式:更新任务
|
|
||||||
handleTaskUpdate(task)
|
|
||||||
} else {
|
|
||||||
// 新建模式:添加任务
|
|
||||||
handleTaskAdd(task)
|
|
||||||
}
|
|
||||||
showTaskDrawer.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTaskDrawerClose = () => {
|
|
||||||
showTaskDrawer.value = false
|
|
||||||
currentTask.value = null
|
|
||||||
isEditMode.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTaskDrawerDelete = (task: Task, deleteChildren?: boolean) => {
|
const handleTaskDrawerDelete = (task: Task, deleteChildren?: boolean) => {
|
||||||
if (task.type === 'story' && deleteChildren === true) {
|
if (task.type === 'story' && deleteChildren === true) {
|
||||||
// 删除story及其所有子任务
|
// 删除story及其所有子任务
|
||||||
@@ -514,7 +498,7 @@ function handleTaskbarDragOrResizeEnd(newTask) {
|
|||||||
`开始: ${oldTask.startDate} → ${newTask.startDate}\n` +
|
`开始: ${oldTask.startDate} → ${newTask.startDate}\n` +
|
||||||
`结束: ${oldTask.endDate} → ${newTask.endDate}`,
|
`结束: ${oldTask.endDate} → ${newTask.endDate}`,
|
||||||
'info',
|
'info',
|
||||||
{ closable: true },
|
{ closable: true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
function handleMilestoneDragEnd(newMilestone) {
|
function handleMilestoneDragEnd(newMilestone) {
|
||||||
@@ -524,7 +508,7 @@ function handleMilestoneDragEnd(newMilestone) {
|
|||||||
`里程碑【${newMilestone.name}】\n` +
|
`里程碑【${newMilestone.name}】\n` +
|
||||||
`开始: ${oldMilestone.endDate} → ${newMilestone.startDate}`,
|
`开始: ${oldMilestone.endDate} → ${newMilestone.startDate}`,
|
||||||
'info',
|
'info',
|
||||||
{ closable: true },
|
{ closable: true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,6 +569,26 @@ const collectAllTaskIds = (task: Task): number[] => {
|
|||||||
}
|
}
|
||||||
return ids
|
return ids
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Timer事件处理
|
||||||
|
function onTimerStarted(task: Task) {
|
||||||
|
showMessage(
|
||||||
|
`Demo 任务【${task.name}】\n开始计时:${new Date(task.timerStartTime).toLocaleString()}\n计时说明:${task.timerStartDesc ? task.timerStartDesc : ''}`,
|
||||||
|
'info',
|
||||||
|
{ closable: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
function onTimerStopped(task: Task) {
|
||||||
|
let msg = `Demo 任务【${task.name}】`
|
||||||
|
if (task.timerStartTime) {
|
||||||
|
msg += `\n开始计时:${new Date(task.timerStartTime).toLocaleString()}`
|
||||||
|
msg += `\n结束计时:${new Date().toLocaleString()}`
|
||||||
|
if (task.timerStartDesc) msg += `\n计时说明:${task.timerStartDesc}`
|
||||||
|
} else {
|
||||||
|
msg += `\n结束计时:${new Date().toLocaleString()}`
|
||||||
|
}
|
||||||
|
showMessage(msg, 'info', { closable: true })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -638,22 +642,27 @@ const collectAllTaskIds = (task: Task): number[] => {
|
|||||||
@taskbar-drag-end="handleTaskbarDragOrResizeEnd"
|
@taskbar-drag-end="handleTaskbarDragOrResizeEnd"
|
||||||
@taskbar-resize-end="handleTaskbarDragOrResizeEnd"
|
@taskbar-resize-end="handleTaskbarDragOrResizeEnd"
|
||||||
@milestone-drag-end="handleMilestoneDragEnd"
|
@milestone-drag-end="handleMilestoneDragEnd"
|
||||||
|
@edit-task="task => showMessage(`进入任务编辑:${task.name}`)"
|
||||||
|
@close="() => showMessage('已关闭任务编辑', 'info')"
|
||||||
|
@timer-started="onTimerStarted"
|
||||||
|
@timer-stopped="onTimerStopped"
|
||||||
|
@predecessor-added="
|
||||||
|
e =>
|
||||||
|
showMessage(`Demo 任务[${e.targetTask.name}] 添加前置任务 [${e.newTask.name}]`, 'info')
|
||||||
|
"
|
||||||
|
@successor-added="
|
||||||
|
e =>
|
||||||
|
showMessage(`Demo 任务[${e.targetTask.name}] 添加后置任务 [${e.newTask.name}]`, 'info')
|
||||||
|
"
|
||||||
|
@task-deleted="e => showMessage(`Demo 任务[${e.task.name}] 已删除`, 'info')"
|
||||||
|
@task-added="e => showMessage(`Demo 任务[${e.task.name}] 已创建`, 'info')"
|
||||||
|
@task-updated="e => showMessage(`Demo 任务[${e.task.name}] 已更新`, 'info')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="license-info">
|
<div class="license-info">
|
||||||
<p>MIT License @JORDIUM.COM</p>
|
<p>MIT License @JORDIUM.COM</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TaskDrawer用于新建/编辑任务 -->
|
|
||||||
<TaskDrawer
|
|
||||||
v-model:visible="showTaskDrawer"
|
|
||||||
:task="currentTask"
|
|
||||||
:is-edit="isEditMode"
|
|
||||||
@submit="handleTaskDrawerSubmit"
|
|
||||||
@close="handleTaskDrawerClose"
|
|
||||||
@delete="handleTaskDrawerDelete"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- MilestoneDialog用于新建/编辑里程碑 -->
|
<!-- MilestoneDialog用于新建/编辑里程碑 -->
|
||||||
<MilestoneDialog
|
<MilestoneDialog
|
||||||
:visible="showMilestoneDialog"
|
:visible="showMilestoneDialog"
|
||||||
|
|||||||
@@ -135,5 +135,32 @@
|
|||||||
"Timeline中sub-task隐藏后,关系线也随之隐藏",
|
"Timeline中sub-task隐藏后,关系线也随之隐藏",
|
||||||
"增强TaskBar可以存在多个前置任务"
|
"增强TaskBar可以存在多个前置任务"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.1.0",
|
||||||
|
"date": "2025-07-08",
|
||||||
|
"notes": ["TaskBar拖拽触发Timeline水平滑动", "增加日|周|月视图切换", "问题修复"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.2.0",
|
||||||
|
"date": "2025-07-21",
|
||||||
|
"notes": [
|
||||||
|
"增加TaskRow/TaskBar的右键菜单",
|
||||||
|
"增加Task的执行定时",
|
||||||
|
"增加添加前置任务功能",
|
||||||
|
"增加添加后置任务功能",
|
||||||
|
"增加TaskRow/TaskBar的删除功能",
|
||||||
|
"GanttChart增加@predecessor-added(添加前置任务完成)事件",
|
||||||
|
"GanttChart增加@successor-added(添加后置任务完成)事件",
|
||||||
|
"GanttChart增加@task-deleted(删除任务完成)事件",
|
||||||
|
"GanttChart增加@task-added(添加任务完成)事件",
|
||||||
|
"GanttChart增加@task-updated(编辑任务完成)事件",
|
||||||
|
"升级TaskDrawer统一在GanttChart中管理"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.2.1",
|
||||||
|
"date": "2025-07-22",
|
||||||
|
"notes": ["SonarQube代码质量检查问题修改"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, defineProps, defineEmits, computed } from 'vue'
|
||||||
|
import { useI18n } from '../composables/useI18n'
|
||||||
|
const props = defineProps({
|
||||||
|
visible: Boolean,
|
||||||
|
title: { type: String, default: '确认开始计时' },
|
||||||
|
message: { type: String, default: '' },
|
||||||
|
defaultDesc: { type: String, default: '' },
|
||||||
|
placeholder: { type: String, default: '请输入计时说明' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['confirm', 'cancel'])
|
||||||
|
|
||||||
|
const { getTranslation: t } = useI18n()
|
||||||
|
|
||||||
|
const desc = ref(props.defaultDesc)
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
v => {
|
||||||
|
if (v) desc.value = props.defaultDesc
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const onConfirm = () => emit('confirm', desc.value)
|
||||||
|
const messageTaskName = computed(() => {
|
||||||
|
if (props.message) {
|
||||||
|
// 支持多语言下的任务名提取
|
||||||
|
const match = props.message.match(/任务([\S\s]+?)计时|Task ([\S\s]+?) timing/)
|
||||||
|
if (match && (match[1] || match[2])) return match[1] || match[2]
|
||||||
|
}
|
||||||
|
return props.defaultDesc
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="confirm-timer-dialog-overlay">
|
||||||
|
<div class="confirm-timer-dialog">
|
||||||
|
<div class="dialog-message">
|
||||||
|
<span>{{ t('timerConfirmPrefix') }}</span>
|
||||||
|
<span class="task-name-highlight">{{ messageTaskName }}</span>
|
||||||
|
<span>{{ t('timerConfirmSuffix') }}</span>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
v-model="desc"
|
||||||
|
class="dialog-textarea"
|
||||||
|
:placeholder="t('timerConfirmPlaceholder')"
|
||||||
|
rows="3"
|
||||||
|
></textarea>
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<button class="btn btn-default" @click="$emit('cancel')">{{ t('cancel') }}</button>
|
||||||
|
<button class="btn btn-confirm" @click="onConfirm">{{ t('startTimer') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.confirm-timer-dialog-overlay {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 99999;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.confirm-timer-dialog {
|
||||||
|
background: var(--gantt-bg-primary, #fff);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
padding: 24px 32px 18px 32px; /* 左右padding完全一致 */
|
||||||
|
min-width: 340px;
|
||||||
|
max-width: 90vw;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .confirm-timer-dialog {
|
||||||
|
background: var(--gantt-bg-primary, #6b6b6b);
|
||||||
|
}
|
||||||
|
.dialog-message {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
.dialog-message .task-name-highlight {
|
||||||
|
color: #f44336;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0 2px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.dialog-textarea {
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 60px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.dialog-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
min-width: 96px;
|
||||||
|
padding: 10px 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
text-align: center;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.btn-confirm {
|
||||||
|
background: #4caf50;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.btn-confirm:hover {
|
||||||
|
background: #43a047;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,7 +5,6 @@ import { useI18n } from '../composables/useI18n'
|
|||||||
|
|
||||||
const { t} = useI18n()
|
const { t} = useI18n()
|
||||||
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue?: string | [string, string]
|
modelValue?: string | [string, string]
|
||||||
type?: 'date' | 'daterange'
|
type?: 'date' | 'daterange'
|
||||||
@@ -72,9 +71,6 @@ const formatDisplayDate = (dateStr: string) => {
|
|||||||
const date = new Date(dateStr)
|
const date = new Date(dateStr)
|
||||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 显示值
|
// 显示值
|
||||||
const displayValue = computed(() => {
|
const displayValue = computed(() => {
|
||||||
if (props.type === 'daterange') {
|
if (props.type === 'daterange') {
|
||||||
@@ -471,7 +467,6 @@ const yearList = computed(() => {
|
|||||||
return years
|
return years
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
|
||||||
// 月份名称
|
// 月份名称
|
||||||
const monthNames = [
|
const monthNames = [
|
||||||
'一月',
|
'一月',
|
||||||
@@ -488,7 +483,6 @@ const monthNames = [
|
|||||||
'十二月',
|
'十二月',
|
||||||
]
|
]
|
||||||
const weekDays = ['日', '一', '二', '三', '四', '五', '六']
|
const weekDays = ['日', '一', '二', '三', '四', '五', '六']
|
||||||
*/
|
|
||||||
|
|
||||||
// 生命周期
|
// 生命周期
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -745,7 +739,7 @@ const panelStyle = computed(() => {
|
|||||||
<<
|
<<
|
||||||
</button>
|
</button>
|
||||||
<span class="el-month-picker__header-label" @click="showYearSelector($event)">
|
<span class="el-month-picker__header-label" @click="showYearSelector($event)">
|
||||||
{{ currentYear }}
|
{{ currentYear }}年
|
||||||
</span>
|
</span>
|
||||||
<button type="button" class="el-picker-panel__icon-btn" @click="nextYear">
|
<button type="button" class="el-picker-panel__icon-btn" @click="nextYear">
|
||||||
>>
|
>>
|
||||||
@@ -753,7 +747,7 @@ const panelStyle = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="el-month-picker__content">
|
<div class="el-month-picker__content">
|
||||||
<div
|
<div
|
||||||
v-for="(monthName, index) in t.monthNames"
|
v-for="(monthName, index) in monthNames"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="el-month-picker__item"
|
class="el-month-picker__item"
|
||||||
:class="{ 'is-current': index === currentMonth }"
|
:class="{ 'is-current': index === currentMonth }"
|
||||||
@@ -775,10 +769,10 @@ const panelStyle = computed(() => {
|
|||||||
</button>
|
</button>
|
||||||
<span class="el-date-picker__header-label">
|
<span class="el-date-picker__header-label">
|
||||||
<span class="el-date-picker__header-year" @click="showYearSelector($event)">
|
<span class="el-date-picker__header-year" @click="showYearSelector($event)">
|
||||||
{{ currentYear }}
|
{{ currentYear }}年
|
||||||
</span>
|
</span>
|
||||||
<span class="el-date-picker__header-month" @click="showMonthSelector($event)">
|
<span class="el-date-picker__header-month" @click="showMonthSelector($event)">
|
||||||
{{ t.monthNames[currentMonth] }}
|
{{ monthNames[currentMonth] }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<button type="button" class="el-picker-panel__icon-btn" @click="nextMonth">
|
<button type="button" class="el-picker-panel__icon-btn" @click="nextMonth">
|
||||||
@@ -792,7 +786,7 @@ const panelStyle = computed(() => {
|
|||||||
<div class="el-date-picker__content">
|
<div class="el-date-picker__content">
|
||||||
<!-- 星期标题 -->
|
<!-- 星期标题 -->
|
||||||
<div class="el-date-table__header">
|
<div class="el-date-table__header">
|
||||||
<div v-for="day in t.weekDays" :key="day" class="el-date-table__header-cell">
|
<div v-for="day in weekDays" :key="day" class="el-date-table__header-cell">
|
||||||
{{ day }}
|
{{ day }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+331
-10
@@ -3,6 +3,7 @@ import { ref, onUnmounted, onMounted, computed, watch, nextTick, defineEmits } f
|
|||||||
import TaskList from './TaskList.vue'
|
import TaskList from './TaskList.vue'
|
||||||
import Timeline from './Timeline.vue'
|
import Timeline from './Timeline.vue'
|
||||||
import GanttToolbar from './GanttToolbar.vue'
|
import GanttToolbar from './GanttToolbar.vue'
|
||||||
|
import TaskDrawer from './TaskDrawer.vue'
|
||||||
import { useI18n, setCustomMessages } from '../composables/useI18n'
|
import { useI18n, setCustomMessages } from '../composables/useI18n'
|
||||||
import { formatPredecessorDisplay } from '../utils/predecessorUtils'
|
import { formatPredecessorDisplay } from '../utils/predecessorUtils'
|
||||||
import jsPDF from 'jspdf'
|
import jsPDF from 'jspdf'
|
||||||
@@ -37,7 +38,18 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
localeMessages: undefined,
|
localeMessages: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['taskbar-drag-end', 'taskbar-resize-end', 'milestone-drag-end'])
|
const emit = defineEmits([
|
||||||
|
'taskbar-drag-end',
|
||||||
|
'taskbar-resize-end',
|
||||||
|
'milestone-drag-end',
|
||||||
|
'timer-started',
|
||||||
|
'timer-stopped',
|
||||||
|
'predecessor-added',
|
||||||
|
'successor-added',
|
||||||
|
'task-deleted',
|
||||||
|
'task-added',
|
||||||
|
'task-updated',
|
||||||
|
])
|
||||||
|
|
||||||
const { showMessage } = useMessage()
|
const { showMessage } = useMessage()
|
||||||
|
|
||||||
@@ -237,7 +249,7 @@ const toggleTaskList = () => {
|
|||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent('timeline-container-resized', {
|
new CustomEvent('timeline-container-resized', {
|
||||||
detail: { source: 'manual-task-list-toggle' },
|
detail: { source: 'manual-task-list-toggle' },
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}, 400)
|
}, 400)
|
||||||
@@ -253,7 +265,7 @@ const handleToggleTaskList = (event: CustomEvent) => {
|
|||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent('timeline-container-resized', {
|
new CustomEvent('timeline-container-resized', {
|
||||||
detail: { source: 'task-list-toggle' },
|
detail: { source: 'task-list-toggle' },
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -268,6 +280,7 @@ function handleTaskBarResizeEnd(event: CustomEvent) {
|
|||||||
function handleMilestoneDragEnd(event: CustomEvent) {
|
function handleMilestoneDragEnd(event: CustomEvent) {
|
||||||
emit('milestone-drag-end', event.detail)
|
emit('milestone-drag-end', event.detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('taskbar-drag-end', handleTaskBarDragEnd as EventListener)
|
window.addEventListener('taskbar-drag-end', handleTaskBarDragEnd as EventListener)
|
||||||
window.addEventListener('taskbar-resize-end', handleTaskBarResizeEnd as EventListener)
|
window.addEventListener('taskbar-resize-end', handleTaskBarResizeEnd as EventListener)
|
||||||
@@ -285,7 +298,7 @@ const handleTaskCollapseChange = (task: Task) => {
|
|||||||
const updateTaskCollapsedState = (
|
const updateTaskCollapsedState = (
|
||||||
tasks: Task[],
|
tasks: Task[],
|
||||||
targetId: number,
|
targetId: number,
|
||||||
collapsed: boolean,
|
collapsed: boolean
|
||||||
): boolean => {
|
): boolean => {
|
||||||
for (const t of tasks) {
|
for (const t of tasks) {
|
||||||
if (t.id === targetId) {
|
if (t.id === targetId) {
|
||||||
@@ -356,7 +369,7 @@ watch(
|
|||||||
notifyTaskListUpdated()
|
notifyTaskListUpdated()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{ deep: true, immediate: true },
|
{ deep: true, immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -375,6 +388,8 @@ onMounted(() => {
|
|||||||
window.addEventListener('request-task-list', handleRequestTaskList as EventListener)
|
window.addEventListener('request-task-list', handleRequestTaskList as EventListener)
|
||||||
// 监听窗口大小变化
|
// 监听窗口大小变化
|
||||||
window.addEventListener('resize', handleWindowResize)
|
window.addEventListener('resize', handleWindowResize)
|
||||||
|
// 监听TaskBar的右键菜单事件
|
||||||
|
window.addEventListener('context-menu', handleTaskContextMenu as EventListener)
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (timelineRef.value && typeof timelineRef.value.scrollToTodayCenter === 'function') {
|
if (timelineRef.value && typeof timelineRef.value.scrollToTodayCenter === 'function') {
|
||||||
@@ -392,12 +407,13 @@ onUnmounted(() => {
|
|||||||
window.removeEventListener('task-added', handleTaskAdd as EventListener)
|
window.removeEventListener('task-added', handleTaskAdd as EventListener)
|
||||||
window.removeEventListener(
|
window.removeEventListener(
|
||||||
'milestone-icon-changed',
|
'milestone-icon-changed',
|
||||||
handleMilestoneIconChangeEvent as EventListener,
|
handleMilestoneIconChangeEvent as EventListener
|
||||||
)
|
)
|
||||||
window.removeEventListener('milestone-deleted', handleMilestoneDeleted as EventListener)
|
window.removeEventListener('milestone-deleted', handleMilestoneDeleted as EventListener)
|
||||||
window.removeEventListener('milestone-data-changed', handleMilestoneDataChanged as EventListener)
|
window.removeEventListener('milestone-data-changed', handleMilestoneDataChanged as EventListener)
|
||||||
window.removeEventListener('request-task-list', handleRequestTaskList as EventListener)
|
window.removeEventListener('request-task-list', handleRequestTaskList as EventListener)
|
||||||
window.removeEventListener('resize', handleWindowResize)
|
window.removeEventListener('resize', handleWindowResize)
|
||||||
|
window.removeEventListener('context-menu', handleTaskContextMenu as EventListener)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 全屏状态管理
|
// 全屏状态管理
|
||||||
@@ -1167,8 +1183,289 @@ watch(
|
|||||||
val => {
|
val => {
|
||||||
if (val) setCustomMessages(locale.value, val)
|
if (val) setCustomMessages(locale.value, val)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 右键菜单状态管理
|
||||||
|
const contextMenuPosition = ref({ x: 0, y: 0 })
|
||||||
|
const contextMenuVisible = ref(false)
|
||||||
|
const contextMenuTask = ref<Task | null>(null)
|
||||||
|
|
||||||
|
// TaskDrawer 相关变量
|
||||||
|
const taskDrawerVisible = ref(false)
|
||||||
|
const taskDrawerTask = ref<Task | null>(null)
|
||||||
|
const taskDrawerEditMode = ref(false)
|
||||||
|
|
||||||
|
// 添加前置任务功能相关变量
|
||||||
|
const taskToAddPredecessorTo = ref<Task | null>(null) // 要添加前置任务的目标任务
|
||||||
|
// 添加后置任务功能相关变量
|
||||||
|
const taskToAddSuccessorTo = ref<Task | null>(null) // 要添加后置任务的目标任务
|
||||||
|
|
||||||
|
// 处理任务条的右键菜单事件
|
||||||
|
const handleTaskContextMenu = (event: CustomEvent) => {
|
||||||
|
// 显示右键菜单
|
||||||
|
const { task, position } = event.detail
|
||||||
|
|
||||||
|
// 显示右键菜单
|
||||||
|
contextMenuTask.value = task
|
||||||
|
contextMenuPosition.value = position
|
||||||
|
contextMenuVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭右键菜单
|
||||||
|
const closeContextMenu = () => {
|
||||||
|
contextMenuVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具栏新建任务事件处理
|
||||||
|
function handleToolbarAddTask() {
|
||||||
|
// 构造一个空的新任务对象
|
||||||
|
const newTask: Task = {
|
||||||
|
id: Date.now(), // 临时id,实际保存时应由后端分配
|
||||||
|
name: '',
|
||||||
|
type: 'task',
|
||||||
|
assignee: '',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
predecessor: [],
|
||||||
|
estimatedHours: 0,
|
||||||
|
actualHours: 0,
|
||||||
|
progress: 0,
|
||||||
|
description: '',
|
||||||
|
parentId: undefined,
|
||||||
|
children: [],
|
||||||
|
}
|
||||||
|
taskDrawerTask.value = newTask
|
||||||
|
taskDrawerEditMode.value = false
|
||||||
|
taskDrawerVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听TaskDrawer、TaskList、Timeline的计时事件,统一处理
|
||||||
|
const handleStartTimer = (task: Task) => {
|
||||||
|
// 任务树内状态同步
|
||||||
|
if (props.tasks) {
|
||||||
|
const updateTask = (tasks: Task[]): boolean => {
|
||||||
|
for (let i = 0; i < tasks.length; i++) {
|
||||||
|
if (tasks[i].id === task.id) {
|
||||||
|
tasks[i].isTimerRunning = true
|
||||||
|
tasks[i].timerStartTime = task.timerStartTime || Date.now()
|
||||||
|
tasks[i].timerEndTime = undefined
|
||||||
|
tasks[i].timerElapsedTime = 0
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (tasks[i].children?.length) {
|
||||||
|
if (updateTask(tasks[i].children as Task[])) return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
updateTask(props.tasks)
|
||||||
|
}
|
||||||
|
closeContextMenu()
|
||||||
|
emit('timer-started', task)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStopTimer = (task: Task) => {
|
||||||
|
// 任务树内状态同步
|
||||||
|
if (props.tasks) {
|
||||||
|
const updateTask = (tasks: Task[]): boolean => {
|
||||||
|
for (let i = 0; i < tasks.length; i++) {
|
||||||
|
if (tasks[i].id === task.id) {
|
||||||
|
if (tasks[i].isTimerRunning && tasks[i].timerStartTime !== undefined) {
|
||||||
|
const elapsed = tasks[i].timerElapsedTime || 0
|
||||||
|
tasks[i].timerElapsedTime = elapsed + (Date.now() - tasks[i].timerStartTime!)
|
||||||
|
tasks[i].timerEndTime = Date.now()
|
||||||
|
}
|
||||||
|
tasks[i].isTimerRunning = false
|
||||||
|
if (
|
||||||
|
taskDrawerVisible.value &&
|
||||||
|
taskDrawerTask.value &&
|
||||||
|
taskDrawerTask.value.id === task.id
|
||||||
|
) {
|
||||||
|
taskDrawerTask.value.isTimerRunning = false
|
||||||
|
taskDrawerTask.value.timerEndTime = Date.now()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (tasks[i].children?.length) {
|
||||||
|
if (updateTask(tasks[i].children as Task[])) return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
updateTask(props.tasks)
|
||||||
|
}
|
||||||
|
closeContextMenu()
|
||||||
|
emit('timer-stopped', task)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听来自Timeline的任务编辑事件
|
||||||
|
function handleTimelineEditTask(task: Task) {
|
||||||
|
taskDrawerTask.value = task
|
||||||
|
taskDrawerEditMode.value = true
|
||||||
|
taskDrawerVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理添加前置任务事件
|
||||||
|
function handleAddPredecessor(targetTask: Task) {
|
||||||
|
if (!targetTask) return
|
||||||
|
|
||||||
|
// 1. 记录要添加前置任务的目标任务
|
||||||
|
taskToAddPredecessorTo.value = targetTask
|
||||||
|
|
||||||
|
// 2. 打开TaskDrawer,进入新增模式
|
||||||
|
// 新建任务,parentId与目标任务一致
|
||||||
|
const newTask: Task = {
|
||||||
|
id: Date.now(), // 临时id,实际保存时应由后端分配
|
||||||
|
name: '',
|
||||||
|
type: 'task',
|
||||||
|
assignee: '',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
predecessor: [],
|
||||||
|
estimatedHours: 0,
|
||||||
|
actualHours: 0,
|
||||||
|
progress: 0,
|
||||||
|
description: '',
|
||||||
|
parentId: targetTask.parentId,
|
||||||
|
children: [],
|
||||||
|
isTimerRunning: false,
|
||||||
|
timerStartTime: undefined,
|
||||||
|
timerEndTime: undefined,
|
||||||
|
timerElapsedTime: 0,
|
||||||
|
}
|
||||||
|
taskDrawerTask.value = newTask
|
||||||
|
taskDrawerEditMode.value = false
|
||||||
|
taskDrawerVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理添加后置任务事件
|
||||||
|
function handleAddSuccessor(targetTask: Task) {
|
||||||
|
if (!targetTask) return
|
||||||
|
// 记录要添加后置任务的目标任务
|
||||||
|
taskToAddSuccessorTo.value = targetTask
|
||||||
|
// 构造新任务,parentId 与目标任务一致,predecessor 仅包含目标任务 id
|
||||||
|
const newTask: Task = {
|
||||||
|
id: Date.now(), // 临时id,实际保存时应由后端分配
|
||||||
|
name: '',
|
||||||
|
type: 'task',
|
||||||
|
assignee: '',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
predecessor: [targetTask.id],
|
||||||
|
estimatedHours: 0,
|
||||||
|
actualHours: 0,
|
||||||
|
progress: 0,
|
||||||
|
description: '',
|
||||||
|
parentId: targetTask.parentId,
|
||||||
|
children: [],
|
||||||
|
isTimerRunning: false,
|
||||||
|
timerStartTime: undefined,
|
||||||
|
timerEndTime: undefined,
|
||||||
|
timerElapsedTime: 0,
|
||||||
|
}
|
||||||
|
taskDrawerTask.value = newTask
|
||||||
|
taskDrawerEditMode.value = false
|
||||||
|
taskDrawerVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增Task插入到任务树中
|
||||||
|
// 插入新任务到任务树(parentId 已在打开 TaskDrawer 时预设好)
|
||||||
|
const insertTask = (tasks: Task[], newTask: Task) => {
|
||||||
|
if (!newTask.parentId) {
|
||||||
|
tasks.push(newTask)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for (const t of tasks) {
|
||||||
|
if (t.id === newTask.parentId) {
|
||||||
|
if (!t.children) t.children = []
|
||||||
|
t.children.push(newTask)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (t.children && t.children.length > 0) {
|
||||||
|
if (insertTask(t.children, newTask)) return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑模式:递归查找并更新任务树节点
|
||||||
|
const updateTaskInTree = (tasks: Task[], updatedTask: Task): boolean => {
|
||||||
|
for (let i = 0; i < tasks.length; i++) {
|
||||||
|
if (tasks[i].id === updatedTask.id) {
|
||||||
|
tasks[i] = { ...tasks[i], ...updatedTask }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (tasks[i].children && (tasks[i].children as Task[]).length > 0) {
|
||||||
|
if (updateTaskInTree(tasks[i].children as Task[], updatedTask)) return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在 handleTaskDrawerSubmit 里补充:如果是添加前置任务,自动将新任务id加入目标任务的 predecessor
|
||||||
|
function handleTaskDrawerSubmit(task: Task) {
|
||||||
|
if (!taskDrawerEditMode.value) {
|
||||||
|
if (props.tasks) {
|
||||||
|
insertTask(props.tasks, task)
|
||||||
|
}
|
||||||
|
// emit 新增任务事件
|
||||||
|
emit('task-added', { task })
|
||||||
|
if (taskToAddPredecessorTo.value) {
|
||||||
|
if (!taskToAddPredecessorTo.value.predecessor) {
|
||||||
|
taskToAddPredecessorTo.value.predecessor = []
|
||||||
|
}
|
||||||
|
taskToAddPredecessorTo.value.predecessor.push(task.id)
|
||||||
|
// emit 添加前置任务事件
|
||||||
|
emit('predecessor-added', { targetTask: taskToAddPredecessorTo.value, newTask: task })
|
||||||
|
taskToAddPredecessorTo.value = null
|
||||||
|
}
|
||||||
|
if (taskToAddSuccessorTo.value) {
|
||||||
|
// emit 添加后置任务事件
|
||||||
|
emit('successor-added', { targetTask: taskToAddSuccessorTo.value, newTask: task })
|
||||||
|
taskToAddSuccessorTo.value = null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (props.tasks) {
|
||||||
|
updateTaskInTree(props.tasks, task)
|
||||||
|
}
|
||||||
|
updateTaskTrigger.value++
|
||||||
|
// emit 任务更新事件
|
||||||
|
emit('task-updated', { task })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除任务的递归工具函数,支持 deleteChildren 逻辑
|
||||||
|
function removeTaskFromTree(tasks: Task[], taskId: number, deleteChildren?: boolean): boolean {
|
||||||
|
for (let i = 0; i < tasks.length; i++) {
|
||||||
|
if (tasks[i].id === taskId) {
|
||||||
|
if (deleteChildren) {
|
||||||
|
// 递归删除该节点及所有子节点(直接 splice 即可)
|
||||||
|
tasks.splice(i, 1)
|
||||||
|
} else {
|
||||||
|
// 只删除该节点,把 children 提升到同级
|
||||||
|
const children = tasks[i].children || []
|
||||||
|
tasks.splice(i, 1, ...children)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (tasks[i].children && (tasks[i].children as Task[]).length > 0) {
|
||||||
|
if (removeTaskFromTree(tasks[i].children as Task[], taskId, deleteChildren)) return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 Task 的删除事件
|
||||||
|
function handleTaskDelete(task: Task, deleteChildren?: boolean) {
|
||||||
|
if (props.tasks) {
|
||||||
|
removeTaskFromTree(props.tasks, task.id, deleteChildren)
|
||||||
|
}
|
||||||
|
taskDrawerVisible.value = false
|
||||||
|
taskDrawerTask.value = null
|
||||||
|
// emit 删除事件
|
||||||
|
emit('task-deleted', { task })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -1189,6 +1486,7 @@ watch(
|
|||||||
:on-theme-change="props.onThemeChange"
|
:on-theme-change="props.onThemeChange"
|
||||||
:on-fullscreen-change="props.onFullscreenChange"
|
:on-fullscreen-change="props.onFullscreenChange"
|
||||||
:on-time-scale-change="handleTimeScaleChange"
|
:on-time-scale-change="handleTimeScaleChange"
|
||||||
|
@add-task="handleToolbarAddTask"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 甘特图主体 -->
|
<!-- 甘特图主体 -->
|
||||||
@@ -1204,6 +1502,11 @@ watch(
|
|||||||
:edit-component="props.editComponent"
|
:edit-component="props.editComponent"
|
||||||
:use-default-drawer="props.useDefaultDrawer"
|
:use-default-drawer="props.useDefaultDrawer"
|
||||||
@task-collapse-change="handleTaskCollapseChange"
|
@task-collapse-change="handleTaskCollapseChange"
|
||||||
|
@start-timer="handleStartTimer"
|
||||||
|
@stop-timer="handleStopTimer"
|
||||||
|
@add-predecessor="handleAddPredecessor"
|
||||||
|
@add-successor="handleAddSuccessor"
|
||||||
|
@delete="handleTaskDelete"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="gantt-splitter" @mousedown="onMouseDown">
|
<div class="gantt-splitter" @mousedown="onMouseDown">
|
||||||
@@ -1243,12 +1546,30 @@ watch(
|
|||||||
:on-task-double-click="props.onTaskDoubleClick"
|
:on-task-double-click="props.onTaskDoubleClick"
|
||||||
:edit-component="props.editComponent"
|
:edit-component="props.editComponent"
|
||||||
:use-default-drawer="props.useDefaultDrawer"
|
:use-default-drawer="props.useDefaultDrawer"
|
||||||
:on-task-delete="props.onTaskDelete"
|
|
||||||
:on-milestone-save="handleMilestoneSave"
|
:on-milestone-save="handleMilestoneSave"
|
||||||
@timeline-scale-changed="handleTimelineScaleChanged"
|
@timeline-scale-changed="handleTimelineScaleChanged"
|
||||||
|
@edit-task="handleTimelineEditTask"
|
||||||
|
@start-timer="handleStartTimer"
|
||||||
|
@stop-timer="handleStopTimer"
|
||||||
|
@add-predecessor="handleAddPredecessor"
|
||||||
|
@add-successor="handleAddSuccessor"
|
||||||
|
@delete="handleTaskDelete"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 任务抽屉组件 - 用于添加前置任务 -->
|
||||||
|
<TaskDrawer
|
||||||
|
v-if="props.useDefaultDrawer"
|
||||||
|
v-model:visible="taskDrawerVisible"
|
||||||
|
:task="taskDrawerTask"
|
||||||
|
:is-edit="taskDrawerEditMode"
|
||||||
|
@submit="handleTaskDrawerSubmit"
|
||||||
|
@close="taskDrawerVisible = false"
|
||||||
|
@start-timer="handleStartTimer"
|
||||||
|
@stop-timer="handleStopTimer"
|
||||||
|
@delete="handleTaskDelete"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -1535,8 +1856,8 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.gantt-root.splitter-dragging .gantt-panel-right {
|
.gantt-root.splitter-dragging .gantt-panel-right {
|
||||||
/* 拖拽期间禁用Timeline区域的指针事件 */
|
/* 拖拽时高亮右侧面板 */
|
||||||
pointer-events: none;
|
background: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gantt-root.splitter-dragging * {
|
.gantt-root.splitter-dragging * {
|
||||||
|
|||||||
@@ -485,26 +485,26 @@ const handleMilestoneMouseLeave = () => {
|
|||||||
|
|
||||||
// 格式化日期显示
|
// 格式化日期显示
|
||||||
const formatDisplayDate = (dateStr: string): string => {
|
const formatDisplayDate = (dateStr: string): string => {
|
||||||
if (!dateStr) return t('dateNotSet') //Not Set
|
if (!dateStr) return '未设置'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const date = new Date(dateStr)
|
const date = new Date(dateStr)
|
||||||
if (isNaN(date.getTime())) return t('dateNotSet')
|
if (isNaN(date.getTime())) return '未设置'
|
||||||
|
|
||||||
const year = date.getFullYear()
|
const year = date.getFullYear()
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
const day = String(date.getDate()).padStart(2, '0')
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
return `${year}-${month}-${day}`
|
return `${year}-${month}-${day}`
|
||||||
} catch {
|
} catch {
|
||||||
return t('dateNotSet')
|
return '未设置'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tooltip内容 Target date
|
// Tooltip内容
|
||||||
const tooltipContent = computed(() => {
|
const tooltipContent = computed(() => {
|
||||||
const milestoneName = props.name || props.milestone?.name || t('milestone')
|
const milestoneName = props.name || props.milestone?.name || '里程碑'
|
||||||
const targetDate = formatDisplayDate(props.date || props.milestone?.startDate || '')
|
const targetDate = formatDisplayDate(props.date || props.milestone?.startDate || '')
|
||||||
return `${t('milestone')}:${milestoneName} <br> ${t('targetDate')}:${targetDate}`
|
return `里程碑:${milestoneName} - 目标日期:${targetDate}`
|
||||||
})
|
})
|
||||||
|
|
||||||
// 组件销毁时清理事件监听器
|
// 组件销毁时清理事件监听器
|
||||||
@@ -688,7 +688,8 @@ const calculateMilestonePositionFromTimelineData = (
|
|||||||
top: `${tooltipPosition.y}px`,
|
top: `${tooltipPosition.y}px`,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="tooltip-content" v-html="tooltipContent">
|
<div class="tooltip-content">
|
||||||
|
{{ tooltipContent }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const availableTasks = computed(() => {
|
|||||||
task =>
|
task =>
|
||||||
task.type === 'task' &&
|
task.type === 'task' &&
|
||||||
task.id !== props.currentTaskId &&
|
task.id !== props.currentTaskId &&
|
||||||
!selectedPredecessorIds.value.includes(task.id),
|
!selectedPredecessorIds.value.includes(task.id)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -69,13 +69,13 @@ watch(
|
|||||||
() => {
|
() => {
|
||||||
selectedValue.value = ''
|
selectedValue.value = ''
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="multi-select-predecessor">
|
<div class="multi-select-predecessor">
|
||||||
<label class="form-label">{{ label }}</label>
|
<label class="form-label" for="predecessor-select">{{ label }}</label>
|
||||||
<div class="predecessor-selector">
|
<div class="predecessor-selector">
|
||||||
<!-- 已选择的前置任务标签 -->
|
<!-- 已选择的前置任务标签 -->
|
||||||
<div v-if="selectedPredecessors.length > 0" class="selected-tags">
|
<div v-if="selectedPredecessors.length > 0" class="selected-tags">
|
||||||
@@ -89,7 +89,12 @@ watch(
|
|||||||
|
|
||||||
<!-- 下拉选择器 -->
|
<!-- 下拉选择器 -->
|
||||||
<div class="select-wrapper">
|
<div class="select-wrapper">
|
||||||
<select v-model="selectedValue" class="form-select" @change="addPredecessor">
|
<select
|
||||||
|
id="predecessor-select"
|
||||||
|
v-model="selectedValue"
|
||||||
|
class="form-select"
|
||||||
|
@change="addPredecessor"
|
||||||
|
>
|
||||||
<option value="">{{ placeholder }}</option>
|
<option value="">{{ placeholder }}</option>
|
||||||
<option v-for="task in availableTasks" :key="task.id" :value="task.id">
|
<option v-for="task in availableTasks" :key="task.id" :value="task.id">
|
||||||
{{ task.name }} (ID: {{ task.id }})
|
{{ task.name }} (ID: {{ task.id }})
|
||||||
|
|||||||
+69
-18
@@ -2,10 +2,7 @@
|
|||||||
import { ref, computed, onUnmounted, onMounted, nextTick, watch } from 'vue'
|
import { ref, computed, onUnmounted, onMounted, nextTick, watch } from 'vue'
|
||||||
import type { Task } from '../models/classes/Task'
|
import type { Task } from '../models/classes/Task'
|
||||||
import { TimelineScale } from '../models/types/TimelineScale'
|
import { TimelineScale } from '../models/types/TimelineScale'
|
||||||
|
import TaskContextMenu from './TaskContextMenu.vue'
|
||||||
|
|
||||||
import { useI18n } from '../composables/useI18n'
|
|
||||||
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
task: Task
|
task: Task
|
||||||
@@ -44,9 +41,14 @@ const emit = defineEmits([
|
|||||||
'update:task',
|
'update:task',
|
||||||
'bar-mounted',
|
'bar-mounted',
|
||||||
'dblclick',
|
'dblclick',
|
||||||
'drag-end', // 新增
|
'drag-end',
|
||||||
'resize-end', // 新增
|
'resize-end',
|
||||||
'scroll-to-position', // 新增:半圆点击定位事件
|
'scroll-to-position',
|
||||||
|
'start-timer',
|
||||||
|
'stop-timer',
|
||||||
|
'add-predecessor',
|
||||||
|
'add-successor',
|
||||||
|
'delete',
|
||||||
])
|
])
|
||||||
|
|
||||||
// 日期工具函数 - 处理时区安全的日期创建和操作
|
// 日期工具函数 - 处理时区安全的日期创建和操作
|
||||||
@@ -234,9 +236,7 @@ const taskStatus = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 判断是否已完成
|
// 判断是否已完成
|
||||||
const isCompleted = computed(() => {
|
const isCompleted = computed(() => (props.task.progress || 0) >= 100)
|
||||||
return (props.task.progress || 0) >= 100
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算完成部分的宽度
|
// 计算完成部分的宽度
|
||||||
const progressWidth = computed(() => {
|
const progressWidth = computed(() => {
|
||||||
@@ -448,10 +448,14 @@ onMounted(() => {
|
|||||||
window.addEventListener('timeline-scale-updated', handleTimelineScaleUpdate)
|
window.addEventListener('timeline-scale-updated', handleTimelineScaleUpdate)
|
||||||
window.addEventListener('timeline-force-recalculate', handleForceRecalculate)
|
window.addEventListener('timeline-force-recalculate', handleForceRecalculate)
|
||||||
|
|
||||||
|
// 监听全局关闭菜单事件
|
||||||
|
window.addEventListener('close-all-taskbar-menus', closeContextMenu)
|
||||||
|
|
||||||
// 清理函数
|
// 清理函数
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('timeline-scale-updated', handleTimelineScaleUpdate)
|
window.removeEventListener('timeline-scale-updated', handleTimelineScaleUpdate)
|
||||||
window.removeEventListener('timeline-force-recalculate', handleForceRecalculate)
|
window.removeEventListener('timeline-force-recalculate', handleForceRecalculate)
|
||||||
|
window.removeEventListener('close-all-taskbar-menus', closeContextMenu)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -835,9 +839,9 @@ const handleBubbleMouseDown = (event: MouseEvent) => {
|
|||||||
|
|
||||||
// 格式化日期显示
|
// 格式化日期显示
|
||||||
const formatDisplayDate = (dateStr: string | undefined): string => {
|
const formatDisplayDate = (dateStr: string | undefined): string => {
|
||||||
if (!dateStr) return t('dateNotSet')
|
if (!dateStr) return '未设置'
|
||||||
const date = createLocalDate(dateStr)
|
const date = createLocalDate(dateStr)
|
||||||
if (!date) return t('dateNotSet')
|
if (!date) return '未设置'
|
||||||
|
|
||||||
const year = date.getFullYear()
|
const year = date.getFullYear()
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
@@ -975,7 +979,41 @@ const calculatePositionFromTimelineData = (
|
|||||||
return cumulativePosition // 如果没找到,返回累计位置
|
return cumulativePosition // 如果没找到,返回累计位置
|
||||||
}
|
}
|
||||||
|
|
||||||
// ...existing code...
|
// 处理右键菜单
|
||||||
|
const contextMenuVisible = ref(false)
|
||||||
|
const contextMenuPosition = ref({ x: 0, y: 0 })
|
||||||
|
const contextMenuTask = computed(() => props.task)
|
||||||
|
|
||||||
|
function handleContextMenu(event: MouseEvent) {
|
||||||
|
// 先广播关闭所有TaskBar菜单
|
||||||
|
window.dispatchEvent(new CustomEvent('close-all-taskbar-menus'))
|
||||||
|
if (props.task.type !== 'task' && props.task.type !== 'story') {
|
||||||
|
// 为了排除里程碑类型
|
||||||
|
event.preventDefault()
|
||||||
|
contextMenuVisible.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
contextMenuVisible.value = true
|
||||||
|
contextMenuPosition.value = { x: event.clientX, y: event.clientY }
|
||||||
|
}
|
||||||
|
function closeContextMenu() {
|
||||||
|
contextMenuVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTaskDelete = (task: Task, deleteChildren?: boolean) => {
|
||||||
|
// 触发删除事件
|
||||||
|
emit('delete', task, deleteChildren)
|
||||||
|
closeContextMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听全局关闭菜单事件
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('close-all-taskbar-menus', closeContextMenu)
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('close-all-taskbar-menus', closeContextMenu)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -999,6 +1037,7 @@ const calculatePositionFromTimelineData = (
|
|||||||
'short-task-bar': isShortTaskBar,
|
'short-task-bar': isShortTaskBar,
|
||||||
'overflow-effect': needsOverflowEffect,
|
'overflow-effect': needsOverflowEffect,
|
||||||
}"
|
}"
|
||||||
|
@contextmenu="handleContextMenu"
|
||||||
@dblclick="handleTaskBarDoubleClick"
|
@dblclick="handleTaskBarDoubleClick"
|
||||||
>
|
>
|
||||||
<!-- 父级任务的标签 -->
|
<!-- 父级任务的标签 -->
|
||||||
@@ -1059,6 +1098,18 @@ const calculatePositionFromTimelineData = (
|
|||||||
@mousedown="handleBubbleMouseDown"
|
@mousedown="handleBubbleMouseDown"
|
||||||
@click="handleBubbleClick"
|
@click="handleBubbleClick"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
|
<TaskContextMenu
|
||||||
|
:visible="contextMenuVisible"
|
||||||
|
:task="contextMenuTask"
|
||||||
|
:position="contextMenuPosition"
|
||||||
|
@close="closeContextMenu"
|
||||||
|
@start-timer="$emit('start-timer', props.task)"
|
||||||
|
@stop-timer="$emit('stop-timer', props.task)"
|
||||||
|
@add-predecessor="$emit('add-predecessor', props.task)"
|
||||||
|
@add-successor="$emit('add-successor', props.task)"
|
||||||
|
@delete="handleTaskDelete"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tooltip 弹窗 -->
|
<!-- Tooltip 弹窗 -->
|
||||||
@@ -1075,23 +1126,23 @@ const calculatePositionFromTimelineData = (
|
|||||||
<div class="tooltip-title">{{ task.name }}</div>
|
<div class="tooltip-title">{{ task.name }}</div>
|
||||||
<div class="tooltip-content">
|
<div class="tooltip-content">
|
||||||
<div class="tooltip-row">
|
<div class="tooltip-row">
|
||||||
<span class="tooltip-label"> {{ t('startDate') }}:</span>
|
<span class="tooltip-label">计划开始:</span>
|
||||||
<span class="tooltip-value">{{ formatDisplayDate(task.startDate) }}</span>
|
<span class="tooltip-value">{{ formatDisplayDate(task.startDate) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="tooltip-row">
|
<div class="tooltip-row">
|
||||||
<span class="tooltip-label">{{ t('endDate') }}:</span>
|
<span class="tooltip-label">计划结束:</span>
|
||||||
<span class="tooltip-value">{{ formatDisplayDate(task.endDate) }}</span>
|
<span class="tooltip-value">{{ formatDisplayDate(task.endDate) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="tooltip-row">
|
<div class="tooltip-row">
|
||||||
<span class="tooltip-label">{{ t('estimatedHours') }}:</span>
|
<span class="tooltip-label">计划工时:</span>
|
||||||
<span class="tooltip-value">{{ workHourInfo.total }}h</span>
|
<span class="tooltip-value">{{ workHourInfo.total }}h</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="tooltip-row">
|
<div class="tooltip-row">
|
||||||
<span class="tooltip-label"> {{ t('actualHours') }}:</span>
|
<span class="tooltip-label">已用工时:</span>
|
||||||
<span class="tooltip-value">{{ workHourInfo.used }}h</span>
|
<span class="tooltip-value">{{ workHourInfo.used }}h</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="tooltip-row">
|
<div class="tooltip-row">
|
||||||
<span class="tooltip-label"> {{ t('progress') }}:</span>
|
<span class="tooltip-label">完成率:</span>
|
||||||
<span class="tooltip-value">{{ task.progress || 0 }}%</span>
|
<span class="tooltip-value">{{ task.progress || 0 }}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,591 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue'
|
||||||
|
import { useI18n } from '../composables/useI18n'
|
||||||
|
import type { Task } from '../models/classes/Task'
|
||||||
|
import ConfirmTimerDialog from './ConfirmTimerDialog.vue'
|
||||||
|
import GanttConfirmDialog from './GanttConfirmDialog.vue'
|
||||||
|
|
||||||
|
// 定义Props接口
|
||||||
|
interface Props {
|
||||||
|
visible: boolean
|
||||||
|
position: {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
task: Task | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
'start-timer',
|
||||||
|
'stop-timer',
|
||||||
|
'add-predecessor',
|
||||||
|
'add-successor',
|
||||||
|
'delete', // 新增delete事件
|
||||||
|
'close',
|
||||||
|
])
|
||||||
|
|
||||||
|
// 多语言支持
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// 菜单容器ref
|
||||||
|
const menuRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
// 调整菜单位置,确保不会超出视窗
|
||||||
|
const adjustedPosition = ref({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 箭头位置和方向
|
||||||
|
const arrowStyle = ref({
|
||||||
|
display: 'none', // 初始不显示
|
||||||
|
left: '50%',
|
||||||
|
top: '-8px',
|
||||||
|
transform: 'rotate(0deg)',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 确认对话框相关
|
||||||
|
const showTimerConfirm = ref(false)
|
||||||
|
const timerDesc = ref('')
|
||||||
|
|
||||||
|
// 删除确认相关
|
||||||
|
const showDeleteConfirm = ref(false)
|
||||||
|
|
||||||
|
// 根据任务类型确定dialog类型
|
||||||
|
const dialogType = computed(() => {
|
||||||
|
return props.task?.type === 'story' ? 'yes-no-cancel' : 'confirm-cancel'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 根据任务类型确定dialog消息
|
||||||
|
const dialogMessage = computed(() => {
|
||||||
|
if (props.task?.type === 'story') {
|
||||||
|
return t.value.confirmDeleteStory.replace('{name}', props.task?.name || '')
|
||||||
|
}
|
||||||
|
return t.value.confirmDeleteTask.replace('{name}', props.task?.name || '')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 打开确认对话框
|
||||||
|
function openTimerConfirm() {
|
||||||
|
timerDesc.value = props.task?.name || ''
|
||||||
|
showTimerConfirm.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消确认
|
||||||
|
function cancelTimerConfirm() {
|
||||||
|
showTimerConfirm.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认计时
|
||||||
|
function confirmTimer(desc: string) {
|
||||||
|
showTimerConfirm.value = false
|
||||||
|
if (props.task && typeof props.task === 'object') {
|
||||||
|
emit('close')
|
||||||
|
// 记录当前时间为开始时间
|
||||||
|
const now = Date.now()
|
||||||
|
Object.assign(props.task, {
|
||||||
|
timerStartDesc: desc || '',
|
||||||
|
isTimerRunning: true,
|
||||||
|
timerStartTime: now,
|
||||||
|
timerElapsedTime: 0, // 每次从0秒开始
|
||||||
|
})
|
||||||
|
emit('start-timer', props.task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理删除任务点击
|
||||||
|
function handleDeleteClick() {
|
||||||
|
showDeleteConfirm.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDelete = () => {
|
||||||
|
showDeleteConfirm.value = false
|
||||||
|
if (props.task) {
|
||||||
|
emit('delete', props.task)
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelDelete = () => {
|
||||||
|
showDeleteConfirm.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Story删除:选择"是" - 删除story及其所有子任务
|
||||||
|
const handleDeleteYes = () => {
|
||||||
|
showDeleteConfirm.value = false
|
||||||
|
if (props.task) {
|
||||||
|
// 传递true表示删除所有子任务
|
||||||
|
emit('delete', props.task, true) // 传递true表示删除所有子任务
|
||||||
|
closeMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Story删除:选择"否" - 仅删除story,保留子任务
|
||||||
|
const handleDeleteNo = () => {
|
||||||
|
showDeleteConfirm.value = false
|
||||||
|
if (props.task) {
|
||||||
|
// 传递false表示仅删除story
|
||||||
|
emit('delete', props.task, false) // 传递false表示仅删除story
|
||||||
|
closeMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:是否显示计时菜单项(非story类型才显示)
|
||||||
|
const showTimerMenu = computed(() => props.task?.type !== 'story')
|
||||||
|
|
||||||
|
// 监听菜单可见状态和位置变化
|
||||||
|
watch(
|
||||||
|
[() => props.visible, () => props.position],
|
||||||
|
([visible, newPosition]) => {
|
||||||
|
// 仅在菜单可见且位置有效时调整位置
|
||||||
|
if (visible && newPosition) {
|
||||||
|
nextTick(() => {
|
||||||
|
if (menuRef.value) {
|
||||||
|
// 获取视窗和菜单尺寸
|
||||||
|
const viewportWidth = window.innerWidth
|
||||||
|
const viewportHeight = window.innerHeight
|
||||||
|
const menuWidth = menuRef.value.offsetWidth
|
||||||
|
const menuHeight = menuRef.value.offsetHeight
|
||||||
|
|
||||||
|
// 初始位置:菜单位于鼠标指针下方,箭头指向上方
|
||||||
|
let adjustedX = newPosition.x - menuWidth / 2 // 居中对齐
|
||||||
|
let adjustedY = newPosition.y + 10 // 鼠标下方10px
|
||||||
|
let arrowLeft = '50%' // 箭头默认居中
|
||||||
|
let arrowTop = '-8px' // 箭头在菜单顶部
|
||||||
|
let arrowTransform = 'rotate(0deg)' // 箭头指向上方
|
||||||
|
|
||||||
|
// 检查右边界
|
||||||
|
if (adjustedX + menuWidth > viewportWidth) {
|
||||||
|
const overflow = adjustedX + menuWidth - viewportWidth + 5
|
||||||
|
adjustedX -= overflow
|
||||||
|
// 调整箭头位置
|
||||||
|
arrowLeft = `calc(50% + ${overflow}px)`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查左边界
|
||||||
|
if (adjustedX < 5) {
|
||||||
|
const overflow = 5 - adjustedX
|
||||||
|
adjustedX = 5
|
||||||
|
// 调整箭头位置
|
||||||
|
arrowLeft = `calc(50% - ${overflow}px)`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查下边界 - 如果超出,将菜单放在鼠标上方
|
||||||
|
if (adjustedY + menuHeight > viewportHeight) {
|
||||||
|
adjustedY = newPosition.y - menuHeight - 10
|
||||||
|
arrowTop = '100%' // 箭头在菜单底部
|
||||||
|
arrowTransform = 'rotate(180deg)' // 箭头指向下方
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新位置和箭头样式
|
||||||
|
adjustedPosition.value = {
|
||||||
|
x: adjustedX,
|
||||||
|
y: adjustedY,
|
||||||
|
}
|
||||||
|
|
||||||
|
arrowStyle.value = {
|
||||||
|
display: 'block',
|
||||||
|
left: arrowLeft,
|
||||||
|
top: arrowTop,
|
||||||
|
transform: arrowTransform,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
// 关闭菜单的方法
|
||||||
|
const closeMenu = () => {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理菜单项点击
|
||||||
|
const handleStartTimer = () => {
|
||||||
|
if (props.task?.isTimerRunning) {
|
||||||
|
emit('close')
|
||||||
|
// 统一格式,补充 timerEndTime 以便 formatTimerStopMessage 能正确显示周期
|
||||||
|
Object.assign(props.task, {
|
||||||
|
isTimerRunning: false,
|
||||||
|
timerEndTime: Date.now(),
|
||||||
|
timerElapsedTime:
|
||||||
|
(props.task.timerElapsedTime || 0) +
|
||||||
|
(Date.now() - (props.task.timerStartTime || Date.now())),
|
||||||
|
})
|
||||||
|
emit('stop-timer', props.task)
|
||||||
|
} else {
|
||||||
|
// 未计时,弹出确认弹窗
|
||||||
|
openTimerConfirm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddPredecessor = () => {
|
||||||
|
if (props.task) {
|
||||||
|
// 先关闭菜单,再触发事件
|
||||||
|
emit('close')
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
emit('add-predecessor', props.task)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddSuccessor = () => {
|
||||||
|
if (props.task) {
|
||||||
|
// 先关闭菜单,再触发事件
|
||||||
|
emit('close')
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
emit('add-successor', props.task)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理点击其他地方关闭菜单
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (menuRef.value && !menuRef.value.contains(event.target as Node)) {
|
||||||
|
closeMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理ESC键关闭菜单
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
closeMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 添加全局点击和按键事件监听
|
||||||
|
document.addEventListener('mousedown', handleClickOutside)
|
||||||
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// 移除事件监听
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside)
|
||||||
|
document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<teleport to="body">
|
||||||
|
<div
|
||||||
|
v-if="visible"
|
||||||
|
ref="menuRef"
|
||||||
|
class="task-context-menu"
|
||||||
|
:style="{
|
||||||
|
left: `${adjustedPosition.x}px`,
|
||||||
|
top: `${adjustedPosition.y}px`,
|
||||||
|
zIndex: 3000,
|
||||||
|
position: 'fixed',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- 气泡箭头 -->
|
||||||
|
<div
|
||||||
|
class="menu-arrow"
|
||||||
|
:style="{
|
||||||
|
display: arrowStyle.display,
|
||||||
|
left: arrowStyle.left,
|
||||||
|
top: arrowStyle.top,
|
||||||
|
transform: arrowStyle.transform,
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
<div v-if="showTimerMenu" class="menu-item" @click="handleStartTimer">
|
||||||
|
<div class="icon-wrapper">
|
||||||
|
<i class="menu-icon" :class="props.task?.isTimerRunning ? 'stop-icon' : 'timer-icon'"></i>
|
||||||
|
</div>
|
||||||
|
{{ props.task?.isTimerRunning ? t.stopTimer : t.startTimer }}
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" @click="handleAddPredecessor">
|
||||||
|
<div class="icon-wrapper">
|
||||||
|
<i class="menu-icon predecessor-icon"></i>
|
||||||
|
</div>
|
||||||
|
{{ t.addPredecessor }}
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" @click="handleAddSuccessor">
|
||||||
|
<div class="icon-wrapper">
|
||||||
|
<i class="menu-icon successor-icon"></i>
|
||||||
|
</div>
|
||||||
|
{{ t.addSuccessor }}
|
||||||
|
</div>
|
||||||
|
<div class="menu-divider"></div>
|
||||||
|
<div class="menu-item menu-item-danger" @click="handleDeleteClick">
|
||||||
|
<div class="icon-wrapper">
|
||||||
|
<i class="menu-icon delete-icon"></i>
|
||||||
|
</div>
|
||||||
|
{{ t.delete }}
|
||||||
|
</div>
|
||||||
|
<!-- 删除确认弹窗 -->
|
||||||
|
<GanttConfirmDialog
|
||||||
|
:visible="showDeleteConfirm"
|
||||||
|
:title="t.delete"
|
||||||
|
:type="dialogType"
|
||||||
|
:message="dialogMessage"
|
||||||
|
:confirm-text="t.confirm"
|
||||||
|
:cancel-text="t.cancel"
|
||||||
|
:yes-text="t.storyDeleteYes"
|
||||||
|
:no-text="t.storyDeleteNo"
|
||||||
|
@confirm="confirmDelete"
|
||||||
|
@yes="handleDeleteYes"
|
||||||
|
@no="handleDeleteNo"
|
||||||
|
@cancel="cancelDelete"
|
||||||
|
/>
|
||||||
|
<!-- 确认计时对话框 -->
|
||||||
|
<ConfirmTimerDialog
|
||||||
|
v-if="showTimerConfirm"
|
||||||
|
:visible="showTimerConfirm"
|
||||||
|
:title="'确认开始计时'"
|
||||||
|
:message="`即将为任务${props.task?.name}计时,若有特殊说明请完善下面的描述`"
|
||||||
|
:default-desc="props.task?.name || ''"
|
||||||
|
@confirm="confirmTimer"
|
||||||
|
@cancel="cancelTimerConfirm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.task-context-menu {
|
||||||
|
position: fixed;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
padding: 4px 0;
|
||||||
|
width: 180px; /* 调整固定宽度,确保文本不会被截断 */
|
||||||
|
z-index: 1000;
|
||||||
|
user-select: none;
|
||||||
|
animation: fadeIn 0.15s ease-out;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
padding: 6px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: #333;
|
||||||
|
gap: 10px; /* 增加图标与文本之间的间距 */
|
||||||
|
height: 36px; /* 保持高度以适应32px的图标 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-wrapper {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0; /* 防止图标被压缩 */
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 计时图标保持32px,前置后置任务图标使用20px */
|
||||||
|
.timer-icon,
|
||||||
|
.stop-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.predecessor-icon,
|
||||||
|
.successor-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-icon::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid currentColor;
|
||||||
|
border-radius: 50%;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右箭头样式 */
|
||||||
|
.timer-icon::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top: 5px solid transparent;
|
||||||
|
border-bottom: 5px solid transparent;
|
||||||
|
border-left: 8px solid currentColor;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-30%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.predecessor-icon::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 12px;
|
||||||
|
height: 2px;
|
||||||
|
background-color: currentColor;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-35%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.predecessor-icon::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
border-left: 2px solid currentColor;
|
||||||
|
border-bottom: 2px solid currentColor;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-120%, -50%) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.successor-icon::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 12px;
|
||||||
|
height: 2px;
|
||||||
|
background-color: currentColor;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-65%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.successor-icon::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
border-right: 2px solid currentColor;
|
||||||
|
border-top: 2px solid currentColor;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(20%, -50%) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 停止计时图标 */
|
||||||
|
.stop-icon::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid currentColor;
|
||||||
|
border-radius: 50%;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-icon::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background-color: currentColor;
|
||||||
|
border-radius: 0;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 菜单气泡箭头 */
|
||||||
|
.menu-arrow {
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 8px solid transparent;
|
||||||
|
border-right: 8px solid transparent;
|
||||||
|
border-bottom: 8px solid #fff; /* 匹配菜单背景色 */
|
||||||
|
transform-origin: center;
|
||||||
|
filter: drop-shadow(0 -1px 2px rgba(0, 0, 0, 0.1)); /* 为箭头添加阴影效果 */
|
||||||
|
z-index: 1001;
|
||||||
|
pointer-events: none; /* 确保箭头不会干扰鼠标事件 */
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题支持 */
|
||||||
|
:global(html[data-theme='dark']) .task-context-menu {
|
||||||
|
background-color: #2c2c2c;
|
||||||
|
border-color: #444444;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .menu-item {
|
||||||
|
color: #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .menu-item:hover {
|
||||||
|
background-color: #3a3a3a;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题下箭头颜色 */
|
||||||
|
:global(html[data-theme='dark']) .menu-arrow {
|
||||||
|
border-bottom-color: #2c2c2c; /* 匹配暗色菜单背景色 */
|
||||||
|
filter: drop-shadow(0 -1px 2px rgba(0, 0, 0, 0.25));
|
||||||
|
}
|
||||||
|
.menu-item-danger {
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
.menu-item-danger:hover {
|
||||||
|
background-color: #faeaea;
|
||||||
|
color: #c0392b;
|
||||||
|
}
|
||||||
|
.menu-icon.delete-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: inline-block;
|
||||||
|
background: none;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.menu-icon.delete-icon::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin: 2px auto;
|
||||||
|
background: url('data:image/svg+xml;utf8,<svg fill="%23e74c3c" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m2 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14z" stroke="%23e74c3c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>')
|
||||||
|
no-repeat center center;
|
||||||
|
background-size: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #ececec;
|
||||||
|
margin: 4px 0;
|
||||||
|
width: 92%;
|
||||||
|
margin-left: 4%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+314
-20
@@ -5,6 +5,7 @@ import { useMessage } from '../composables/useMessage'
|
|||||||
import DatePicker from './DatePicker.vue'
|
import DatePicker from './DatePicker.vue'
|
||||||
import GanttConfirmDialog from './GanttConfirmDialog.vue'
|
import GanttConfirmDialog from './GanttConfirmDialog.vue'
|
||||||
import MultiSelectPredecessor from './MultiSelectPredecessor.vue'
|
import MultiSelectPredecessor from './MultiSelectPredecessor.vue'
|
||||||
|
import ConfirmTimerDialog from './ConfirmTimerDialog.vue'
|
||||||
import type { Task } from '../models/classes/Task'
|
import type { Task } from '../models/classes/Task'
|
||||||
import '../styles/app.css'
|
import '../styles/app.css'
|
||||||
|
|
||||||
@@ -27,6 +28,8 @@ const emit = defineEmits<{
|
|||||||
submit: [task: Task]
|
submit: [task: Task]
|
||||||
close: []
|
close: []
|
||||||
delete: [task: Task, deleteChildren?: boolean]
|
delete: [task: Task, deleteChildren?: boolean]
|
||||||
|
'start-timer': [task: Task]
|
||||||
|
'stop-timer': [task: Task]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -36,6 +39,78 @@ const submitting = ref(false)
|
|||||||
const isVisible = ref(props.visible)
|
const isVisible = ref(props.visible)
|
||||||
const showDeleteConfirm = ref(false)
|
const showDeleteConfirm = ref(false)
|
||||||
|
|
||||||
|
// 计时器相关
|
||||||
|
const timerElapsed = ref(0)
|
||||||
|
const timerInterval = ref<number | null>(null)
|
||||||
|
|
||||||
|
const isTimerRunning = computed(() => props.task?.isTimerRunning)
|
||||||
|
const timerStartTime = computed(() => props.task?.timerStartTime)
|
||||||
|
const timerElapsedTime = computed(() => props.task?.timerElapsedTime || 0)
|
||||||
|
|
||||||
|
const formattedTimer = computed(() => {
|
||||||
|
const totalSeconds = Math.floor(timerElapsed.value / 1000)
|
||||||
|
const hours = Math.floor(totalSeconds / 3600)
|
||||||
|
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
||||||
|
const seconds = totalSeconds % 60
|
||||||
|
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateTimer = () => {
|
||||||
|
if (isTimerRunning.value && timerStartTime.value) {
|
||||||
|
timerElapsed.value = Date.now() - timerStartTime.value + timerElapsedTime.value
|
||||||
|
} else {
|
||||||
|
timerElapsed.value = timerElapsedTime.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:监听 props.task 的 isTimerRunning、timerStartTime、timerElapsedTime 变化,确保 header 区域按钮和计时器展示实时同步
|
||||||
|
watch(
|
||||||
|
() => [props.task?.isTimerRunning, props.task?.timerStartTime, props.task?.timerElapsedTime],
|
||||||
|
() => {
|
||||||
|
updateTimer()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 计时器本地状态,保证点击后UI立即切换
|
||||||
|
const localTimerRunning = ref(false)
|
||||||
|
|
||||||
|
// 只要 props.task 变化或全局事件变化,立即同步本地状态
|
||||||
|
watch(
|
||||||
|
() => props.task?.isTimerRunning,
|
||||||
|
val => {
|
||||||
|
localTimerRunning.value = !!val
|
||||||
|
if (!val && timerInterval.value) {
|
||||||
|
clearInterval(timerInterval.value)
|
||||||
|
timerInterval.value = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 修正计时器每秒递增逻辑,保证计时器正常跳动
|
||||||
|
watch(
|
||||||
|
[localTimerRunning, timerStartTime, timerElapsedTime],
|
||||||
|
() => {
|
||||||
|
if (timerInterval.value) {
|
||||||
|
clearInterval(timerInterval.value)
|
||||||
|
timerInterval.value = null
|
||||||
|
}
|
||||||
|
if (localTimerRunning.value && timerStartTime.value) {
|
||||||
|
updateTimer()
|
||||||
|
timerInterval.value = window.setInterval(updateTimer, 1000)
|
||||||
|
} else {
|
||||||
|
updateTimer()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timerInterval.value) {
|
||||||
|
clearInterval(timerInterval.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 根据任务类型确定dialog类型
|
// 根据任务类型确定dialog类型
|
||||||
const dialogType = computed(() => {
|
const dialogType = computed(() => {
|
||||||
return props.task?.type === 'story' ? 'yes-no-cancel' : 'confirm-cancel'
|
return props.task?.type === 'story' ? 'yes-no-cancel' : 'confirm-cancel'
|
||||||
@@ -63,6 +138,10 @@ const formData = reactive<Task>({
|
|||||||
progress: 0,
|
progress: 0,
|
||||||
description: '',
|
description: '',
|
||||||
parentId: undefined, // 上级任务ID
|
parentId: undefined, // 上级任务ID
|
||||||
|
isTimerRunning: false, // 是否正在计时
|
||||||
|
timerStartTime: undefined, // 计时器开始时间
|
||||||
|
timerEndTime: undefined, // 计时器结束时间
|
||||||
|
timerElapsedTime: 0, // 计时器已用时间
|
||||||
})
|
})
|
||||||
|
|
||||||
// 任务列表数据
|
// 任务列表数据
|
||||||
@@ -74,7 +153,7 @@ const availableParentTasks = computed(() => {
|
|||||||
.filter(
|
.filter(
|
||||||
task =>
|
task =>
|
||||||
task.id !== props.task?.id && // 排除当前任务自己
|
task.id !== props.task?.id && // 排除当前任务自己
|
||||||
(task.type === 'story' || task.type === 'task'), // 只显示story和task类型
|
(task.type === 'story' || task.type === 'task') // 只显示story和task类型
|
||||||
)
|
)
|
||||||
.map(task => ({
|
.map(task => ({
|
||||||
...task,
|
...task,
|
||||||
@@ -195,11 +274,16 @@ watch(
|
|||||||
if (props.task && props.isEdit) {
|
if (props.task && props.isEdit) {
|
||||||
// 编辑模式,填充表单数据
|
// 编辑模式,填充表单数据
|
||||||
Object.assign(formData, props.task)
|
Object.assign(formData, props.task)
|
||||||
|
} else if (props.task && !props.isEdit) {
|
||||||
|
// 新建模式,自动绑定上级任务
|
||||||
|
formData.parentId = props.task.parentId ?? undefined
|
||||||
|
// 新建模式,自动绑定前置任务
|
||||||
|
formData.predecessor = props.task.predecessor ?? []
|
||||||
}
|
}
|
||||||
// 抽屉显示时重新请求任务数据,确保前置任务列表是最新的
|
// 抽屉显示时重新请求任务数据,确保前置任务列表是最新的
|
||||||
window.dispatchEvent(new CustomEvent('request-task-list'))
|
window.dispatchEvent(new CustomEvent('request-task-list'))
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// 监听 isVisible 变化,同步到父组件
|
// 监听 isVisible 变化,同步到父组件
|
||||||
@@ -207,6 +291,18 @@ watch(isVisible, newVal => {
|
|||||||
emit('update:visible', newVal)
|
emit('update:visible', newVal)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 监听 task 变化,同步更新 parentId
|
||||||
|
watch(
|
||||||
|
() => props.task,
|
||||||
|
newTask => {
|
||||||
|
if (newTask && !props.isEdit) {
|
||||||
|
// 新建时,确保 parentId 与传入的 parentId 同步
|
||||||
|
formData.parentId = newTask.parentId ?? undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
// 重置表单
|
// 重置表单
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
Object.assign(formData, {
|
Object.assign(formData, {
|
||||||
@@ -221,6 +317,10 @@ const resetForm = () => {
|
|||||||
progress: 0,
|
progress: 0,
|
||||||
description: '',
|
description: '',
|
||||||
parentId: undefined,
|
parentId: undefined,
|
||||||
|
isTimerRunning: false,
|
||||||
|
timerStartTime: undefined,
|
||||||
|
timerEndTime: undefined,
|
||||||
|
timerElapsedTime: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 清除错误信息
|
// 清除错误信息
|
||||||
@@ -283,11 +383,9 @@ const handleSubmit = async () => {
|
|||||||
id: props.isEdit && props.task ? props.task.id : Date.now(),
|
id: props.isEdit && props.task ? props.task.id : Date.now(),
|
||||||
}
|
}
|
||||||
emit('submit', taskData)
|
emit('submit', taskData)
|
||||||
showMessage(props.isEdit ? t.value.taskUpdateSuccess : t.value.taskCreateSuccess, 'success')
|
|
||||||
handleClose()
|
handleClose()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 处理错误但不在控制台输出
|
// 处理错误但不在控制台输出
|
||||||
showMessage(t.value.operationFailed, 'error')
|
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
}
|
}
|
||||||
@@ -298,6 +396,21 @@ const handleDelete = () => {
|
|||||||
showDeleteConfirm.value = true
|
showDeleteConfirm.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleError = (error: unknown) => {
|
||||||
|
let msg = ''
|
||||||
|
if (
|
||||||
|
error &&
|
||||||
|
typeof error === 'object' &&
|
||||||
|
'message' in error &&
|
||||||
|
typeof (error as { message?: unknown }).message === 'string'
|
||||||
|
) {
|
||||||
|
msg = (error as { message: string }).message
|
||||||
|
} else {
|
||||||
|
msg = String(error)
|
||||||
|
}
|
||||||
|
showMessage(msg, 'error', { closable: true })
|
||||||
|
}
|
||||||
|
|
||||||
const confirmDelete = () => {
|
const confirmDelete = () => {
|
||||||
showDeleteConfirm.value = false
|
showDeleteConfirm.value = false
|
||||||
if (props.task && props.isEdit) {
|
if (props.task && props.isEdit) {
|
||||||
@@ -306,7 +419,7 @@ const confirmDelete = () => {
|
|||||||
emit('delete', props.task)
|
emit('delete', props.task)
|
||||||
handleClose()
|
handleClose()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showMessage(t.value.taskDeleteFailed, 'error')
|
handleError(error)
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
}
|
}
|
||||||
@@ -326,14 +439,13 @@ const handleDeleteYes = () => {
|
|||||||
emit('delete', props.task, true) // 传递true表示删除所有子任务
|
emit('delete', props.task, true) // 传递true表示删除所有子任务
|
||||||
handleClose()
|
handleClose()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showMessage(t.value.taskDeleteFailed, 'error')
|
handleError(error)
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Story删除:选择"否" - 仅删除story,保留子任务
|
|
||||||
// Story删除:选择"否" - 仅删除story,保留子任务
|
// Story删除:选择"否" - 仅删除story,保留子任务
|
||||||
const handleDeleteNo = () => {
|
const handleDeleteNo = () => {
|
||||||
showDeleteConfirm.value = false
|
showDeleteConfirm.value = false
|
||||||
@@ -343,7 +455,7 @@ const handleDeleteNo = () => {
|
|||||||
emit('delete', props.task, false) // 传递false表示仅删除story
|
emit('delete', props.task, false) // 传递false表示仅删除story
|
||||||
handleClose()
|
handleClose()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showMessage(t.value.taskDeleteFailed, 'error')
|
handleError(error)
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
}
|
}
|
||||||
@@ -373,18 +485,193 @@ watch(
|
|||||||
newValue => {
|
newValue => {
|
||||||
progressDisplayValue.value = (newValue || 0).toString()
|
progressDisplayValue.value = (newValue || 0).toString()
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 修正计时器首次启动不跳动问题:每次打开抽屉时重置 timerElapsed,且 timerStartTime 为空时立即赋值
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
visible => {
|
||||||
|
if (visible && props.task && props.task.type !== 'story') {
|
||||||
|
timerElapsed.value = props.task.timerElapsedTime || 0
|
||||||
|
// 若计时器未启动,重置本地interval
|
||||||
|
if (!props.task.isTimerRunning) {
|
||||||
|
if (timerInterval.value) {
|
||||||
|
clearInterval(timerInterval.value)
|
||||||
|
timerInterval.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleStartTimer = (desc?: string) => {
|
||||||
|
const now = Date.now()
|
||||||
|
if (props.task && typeof props.task === 'object') {
|
||||||
|
Object.assign(props.task, {
|
||||||
|
timerStartTime: now,
|
||||||
|
isTimerRunning: true,
|
||||||
|
timerElapsedTime: 0, // 每次从0秒开始
|
||||||
|
timerStartDesc: desc || '',
|
||||||
|
})
|
||||||
|
emit('start-timer', props.task)
|
||||||
|
}
|
||||||
|
timerElapsed.value = 0 // 启动时重置本地计时器
|
||||||
|
localTimerRunning.value = true
|
||||||
|
updateTimer()
|
||||||
|
if (timerInterval.value) {
|
||||||
|
clearInterval(timerInterval.value)
|
||||||
|
timerInterval.value = null
|
||||||
|
}
|
||||||
|
window.dispatchEvent(new CustomEvent('start-timer', { detail: props.task }))
|
||||||
|
}
|
||||||
|
const handleStopTimer = () => {
|
||||||
|
if (props.task && typeof props.task === 'object') {
|
||||||
|
emit('stop-timer', props.task)
|
||||||
|
}
|
||||||
|
localTimerRunning.value = false
|
||||||
|
if (timerInterval.value) {
|
||||||
|
clearInterval(timerInterval.value)
|
||||||
|
timerInterval.value = null
|
||||||
|
}
|
||||||
|
updateTimer()
|
||||||
|
window.dispatchEvent(new CustomEvent('stop-timer', { detail: props.task }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计时器确认弹窗
|
||||||
|
const showTimerConfirm = ref(false)
|
||||||
|
const timerDesc = ref('')
|
||||||
|
|
||||||
|
function openTimerConfirm() {
|
||||||
|
timerDesc.value = props.task?.name || ''
|
||||||
|
showTimerConfirm.value = true
|
||||||
|
}
|
||||||
|
function cancelTimerConfirm() {
|
||||||
|
showTimerConfirm.value = false
|
||||||
|
}
|
||||||
|
function confirmTimer(desc: string) {
|
||||||
|
showTimerConfirm.value = false
|
||||||
|
// desc 可用于后续业务
|
||||||
|
handleStartTimer(desc)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="isVisible" class="drawer-overlay" @click="handleOverlayClick">
|
<div v-if="isVisible" class="drawer-overlay" @click="handleOverlayClick">
|
||||||
<div class="drawer-container" @click.stop>
|
<div class="drawer-container" @click.stop>
|
||||||
<!-- Drawer Header -->
|
<!-- Drawer Header -->
|
||||||
<div class="drawer-header">
|
<div
|
||||||
<h3 class="drawer-title">{{ isEdit ? t.editTask : t.newTask }}</h3>
|
class="drawer-header"
|
||||||
|
style="display: flex; align-items: center; justify-content: flex-start; gap: 8px"
|
||||||
|
>
|
||||||
|
<h3 class="drawer-title" style="margin: 0">{{ isEdit ? t.editTask : t.newTask }}</h3>
|
||||||
|
<div
|
||||||
|
v-if="props.task?.type !== 'story' && isEdit"
|
||||||
|
class="drawer-timer"
|
||||||
|
style="display: flex; align-items: center; gap: 6px; margin-left: 8px"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="!localTimerRunning"
|
||||||
|
class="timer-btn start minimal"
|
||||||
|
title="开始计时"
|
||||||
|
style="
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background: #4caf50;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.2s;
|
||||||
|
"
|
||||||
|
@click.stop="openTimerConfirm"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
style="display: block; margin: 0 auto"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="11" stroke="#4caf50" stroke-width="2" fill="#4caf50" />
|
||||||
|
<polygon points="9,7 18,12 9,17" fill="#fff" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
class="timer-btn stop minimal"
|
||||||
|
title="停止计时"
|
||||||
|
style="
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background: #f44336;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.2s;
|
||||||
|
"
|
||||||
|
@click.stop="handleStopTimer"
|
||||||
|
>
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle cx="12" cy="12" r="11" stroke="#f44336" stroke-width="2" fill="#f44336" />
|
||||||
|
<rect x="7" y="7" width="10" height="10" fill="#fff" rx="1.5" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
v-if="localTimerRunning"
|
||||||
|
class="timer-badge"
|
||||||
|
:class="{ 'timer-active': localTimerRunning }"
|
||||||
|
style="
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fffbe6;
|
||||||
|
color: #e6a23c;
|
||||||
|
box-shadow: 0 0 0 1px #ffe58f;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 80px;
|
||||||
|
justify-content: center;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="localTimerRunning"
|
||||||
|
class="timer-dot"
|
||||||
|
style="
|
||||||
|
background: #67c23a;
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 5px;
|
||||||
|
animation: pulse 1s infinite;
|
||||||
|
"
|
||||||
|
></span>
|
||||||
|
{{ formattedTimer }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1"></div>
|
||||||
<button class="drawer-close-btn" type="button" @click="handleClose">
|
<button class="drawer-close-btn" type="button" @click="handleClose">
|
||||||
<svg class="close-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
<svg
|
||||||
|
class="close-icon"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
style="vertical-align: middle"
|
||||||
|
>
|
||||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -426,15 +713,15 @@ watch(
|
|||||||
<span v-if="errors.type" class="error-text">{{ errors.type }}</span>
|
<span v-if="errors.type" class="error-text">{{ errors.type }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="task-assignee">{{ t.assignee }}</label>
|
<label class="form-label" for="task-assignee">{{ t.assignee }}</label>
|
||||||
<select id="task-assignee" v-model="formData.assignee" class="form-select">
|
<select id="task-assignee" v-model="formData.assignee" class="form-select">
|
||||||
<option value="">{{ t.selectAssignee }}</option>
|
<option value="">{{ t.selectAssignee }}</option>
|
||||||
<!-- <option value="张三">张三</option>
|
<option value="张三">张三</option>
|
||||||
<option value="李四">李四</option>
|
<option value="李四">李四</option>
|
||||||
<option value="王五">王五</option>
|
<option value="王五">王五</option>
|
||||||
<option value="赵六">赵六</option>
|
<option value="赵六">赵六</option>
|
||||||
<option value="钱七">钱七</option>-->
|
<option value="钱七">钱七</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -606,6 +893,17 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 计时器确认弹窗 -->
|
||||||
|
<ConfirmTimerDialog
|
||||||
|
v-if="showTimerConfirm"
|
||||||
|
:visible="showTimerConfirm"
|
||||||
|
:title="'确认开始计时'"
|
||||||
|
:message="`即将为任务${props.task?.name}计时,若有特殊说明请完善下面的描述`"
|
||||||
|
:default-desc="props.task?.name || ''"
|
||||||
|
@confirm="confirmTimer"
|
||||||
|
@cancel="cancelTimerConfirm"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -819,10 +1117,6 @@ watch(
|
|||||||
background: var(--gantt-primary, #409eff);
|
background: var(--gantt-primary, #409eff);
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-slider::-moz-range-track {
|
|
||||||
background: var(--gantt-border-light, #e4e7ed);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 为更好的兼容性,添加一个伪元素来显示已完成部分 */
|
/* 为更好的兼容性,添加一个伪元素来显示已完成部分 */
|
||||||
.progress-slider::before {
|
.progress-slider::before {
|
||||||
content: '';
|
content: '';
|
||||||
|
|||||||
@@ -14,7 +14,18 @@ interface Props {
|
|||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
// 定义emit事件
|
// 定义emit事件
|
||||||
const emit = defineEmits(['task-collapse-change'])
|
// const emit = defineEmits(['task-collapse-change', 'start-timer', 'stop-timer'])
|
||||||
|
// +'add-predecessor': [task: Task] // 新增:添加前置任务事件
|
||||||
|
// 'add-successor': [task: Task] // 新增:添加后置任务事件
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'task-collapse-change': [task: Task]
|
||||||
|
'start-timer': [task: Task]
|
||||||
|
'stop-timer': [task: Task]
|
||||||
|
'add-predecessor': [task: Task] // 新增:添加前置任务事件
|
||||||
|
'add-successor': [task: Task] // 新增:添加后置任务事件
|
||||||
|
delete: [task: Task, deleteChildren?: boolean]
|
||||||
|
}>()
|
||||||
|
|
||||||
// 多语言支持
|
// 多语言支持
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -300,6 +311,44 @@ const handleTimelineVerticalScroll = (event: CustomEvent) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理任务行右键菜单事件
|
||||||
|
const handleTaskRowContextMenu = (event: { task: Task; position: { x: number; y: number } }) => {
|
||||||
|
// 直接从TaskRow触发全局事件,跳过TaskList中转
|
||||||
|
// 将事件转发为全局事件,让GanttChart组件处理
|
||||||
|
try {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('context-menu', {
|
||||||
|
detail: event,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('TaskList - Failed to dispatch context-menu event', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理TaskRow计时事件向上传递
|
||||||
|
const handleStartTimer = (task: Task) => {
|
||||||
|
emit('start-timer', task)
|
||||||
|
}
|
||||||
|
const handleStopTimer = (task: Task) => {
|
||||||
|
emit('stop-timer', task)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理添加前置任务事件
|
||||||
|
const handleAddPredecessor = (task: Task) => {
|
||||||
|
emit('add-predecessor', task)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理添加后置任务事件
|
||||||
|
const handleAddSuccessor = (task: Task) => {
|
||||||
|
emit('add-successor', task)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTaskDelete = (task: Task, deleteChildren?: boolean) => {
|
||||||
|
// 触发删除事件
|
||||||
|
emit('delete', task, deleteChildren)
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
window.addEventListener('task-updated', handleTaskUpdated as EventListener)
|
window.addEventListener('task-updated', handleTaskUpdated as EventListener)
|
||||||
window.addEventListener('task-added', handleTaskAdded as EventListener)
|
window.addEventListener('task-added', handleTaskAdded as EventListener)
|
||||||
@@ -354,6 +403,12 @@ onUnmounted(() => {
|
|||||||
:on-hover="handleTaskRowHover"
|
:on-hover="handleTaskRowHover"
|
||||||
@toggle="toggleCollapse"
|
@toggle="toggleCollapse"
|
||||||
@dblclick="handleTaskRowDoubleClick"
|
@dblclick="handleTaskRowDoubleClick"
|
||||||
|
@contextmenu="handleTaskRowContextMenu"
|
||||||
|
@start-timer="handleStartTimer"
|
||||||
|
@stop-timer="handleStopTimer"
|
||||||
|
@add-predecessor="handleAddPredecessor"
|
||||||
|
@add-successor="handleAddSuccessor"
|
||||||
|
@delete="handleTaskDelete"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+179
-2
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
|
||||||
import { useI18n } from '../composables/useI18n'
|
import { useI18n } from '../composables/useI18n'
|
||||||
import { formatPredecessorDisplay } from '../utils/predecessorUtils'
|
import { formatPredecessorDisplay } from '../utils/predecessorUtils'
|
||||||
import type { Task } from '../models/classes/Task'
|
import type { Task } from '../models/classes/Task'
|
||||||
|
import TaskContextMenu from './TaskContextMenu.vue'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
task: Task
|
task: Task
|
||||||
@@ -13,7 +14,16 @@ interface Props {
|
|||||||
onHover?: (taskId: number | null) => void
|
onHover?: (taskId: number | null) => void
|
||||||
}
|
}
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
const emit = defineEmits(['toggle', 'dblclick'])
|
const emit = defineEmits([
|
||||||
|
'toggle',
|
||||||
|
'dblclick',
|
||||||
|
'contextmenu',
|
||||||
|
'start-timer',
|
||||||
|
'stop-timer',
|
||||||
|
'add-predecessor',
|
||||||
|
'add-successor',
|
||||||
|
'delete',
|
||||||
|
])
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const overtimeText = computed(() => t.value?.overtime ?? '')
|
const overtimeText = computed(() => t.value?.overtime ?? '')
|
||||||
const overdueText = computed(() => t.value?.overdue ?? '')
|
const overdueText = computed(() => t.value?.overdue ?? '')
|
||||||
@@ -129,15 +139,101 @@ const handleSplitterDragEnd = () => {
|
|||||||
isSplitterDragging.value = false
|
isSplitterDragging.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 任务计时器状态
|
||||||
|
const timerElapsed = ref(0)
|
||||||
|
const timerInterval = ref<number | null>(null)
|
||||||
|
|
||||||
|
// 格式化计时器显示:转换为 HH:MM:SS 格式
|
||||||
|
const formattedTimer = computed(() => {
|
||||||
|
const totalSeconds = Math.floor(timerElapsed.value / 1000)
|
||||||
|
const hours = Math.floor(totalSeconds / 3600)
|
||||||
|
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
||||||
|
const seconds = totalSeconds % 60
|
||||||
|
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新计时器显示
|
||||||
|
const updateTimer = () => {
|
||||||
|
if (props.task.isTimerRunning && props.task.timerStartTime) {
|
||||||
|
// 计算已经运行的时间 = 当前时间 - 开始时间 + 之前累积的时间
|
||||||
|
const previousElapsed = props.task.timerElapsedTime || 0
|
||||||
|
timerElapsed.value = Date.now() - props.task.timerStartTime + previousElapsed
|
||||||
|
} else if (props.task.timerElapsedTime) {
|
||||||
|
// 如果任务不在运行中,但有累计时间,显示累计时间
|
||||||
|
timerElapsed.value = props.task.timerElapsedTime
|
||||||
|
} else {
|
||||||
|
// 默认情况下,计时器为0
|
||||||
|
timerElapsed.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听任务的计时状态变化
|
||||||
|
watch(
|
||||||
|
() => [props.task.isTimerRunning, props.task.timerStartTime, props.task.timerElapsedTime],
|
||||||
|
() => {
|
||||||
|
// 清除之前的计时器
|
||||||
|
if (timerInterval.value) {
|
||||||
|
clearInterval(timerInterval.value)
|
||||||
|
timerInterval.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果任务正在计时,开始计时器
|
||||||
|
if (props.task.isTimerRunning) {
|
||||||
|
updateTimer()
|
||||||
|
timerInterval.value = window.setInterval(updateTimer, 1000)
|
||||||
|
} else {
|
||||||
|
// 更新一次最终值
|
||||||
|
updateTimer()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
// 右键菜单相关状态
|
||||||
|
const contextMenuVisible = ref(false)
|
||||||
|
const contextMenuPosition = ref({ x: 0, y: 0 })
|
||||||
|
const contextMenuTask = computed(() => props.task)
|
||||||
|
|
||||||
|
// 处理右键菜单显示
|
||||||
|
function handleContextMenu(event: MouseEvent) {
|
||||||
|
// 先广播关闭所有TaskRow菜单
|
||||||
|
window.dispatchEvent(new CustomEvent('close-all-taskbar-menus'))
|
||||||
|
if (props.task.type !== 'task' && props.task.type !== 'story') {
|
||||||
|
// 为了排除里程碑类型
|
||||||
|
event.preventDefault()
|
||||||
|
contextMenuVisible.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
contextMenuVisible.value = true
|
||||||
|
contextMenuPosition.value = { x: event.clientX, y: event.clientY }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭右键菜单
|
||||||
|
function closeContextMenu() {
|
||||||
|
contextMenuVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTaskDelete = (task: Task, deleteChildren?: boolean) => {
|
||||||
|
// 触发删除事件
|
||||||
|
emit('delete', task, deleteChildren)
|
||||||
|
closeContextMenu()
|
||||||
|
}
|
||||||
|
|
||||||
// 生命周期钩子 - 注册事件监听器
|
// 生命周期钩子 - 注册事件监听器
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('splitter-drag-start', handleSplitterDragStart)
|
window.addEventListener('splitter-drag-start', handleSplitterDragStart)
|
||||||
window.addEventListener('splitter-drag-end', handleSplitterDragEnd)
|
window.addEventListener('splitter-drag-end', handleSplitterDragEnd)
|
||||||
|
window.addEventListener('close-all-taskbar-menus', closeContextMenu)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('splitter-drag-start', handleSplitterDragStart)
|
window.removeEventListener('splitter-drag-start', handleSplitterDragStart)
|
||||||
window.removeEventListener('splitter-drag-end', handleSplitterDragEnd)
|
window.removeEventListener('splitter-drag-end', handleSplitterDragEnd)
|
||||||
|
window.removeEventListener('close-all-taskbar-menus', closeContextMenu)
|
||||||
|
if (timerInterval.value) {
|
||||||
|
clearInterval(timerInterval.value)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -160,6 +256,7 @@ onUnmounted(() => {
|
|||||||
@dblclick="handleTaskRowDoubleClick"
|
@dblclick="handleTaskRowDoubleClick"
|
||||||
@mouseenter="handleMouseEnter"
|
@mouseenter="handleMouseEnter"
|
||||||
@mouseleave="handleMouseLeave"
|
@mouseleave="handleMouseLeave"
|
||||||
|
@contextmenu="handleContextMenu"
|
||||||
>
|
>
|
||||||
<div class="col col-name" :style="{ paddingLeft: indent }">
|
<div class="col col-name" :style="{ paddingLeft: indent }">
|
||||||
<span
|
<span
|
||||||
@@ -245,6 +342,15 @@ onUnmounted(() => {
|
|||||||
:title="props.task.name"
|
:title="props.task.name"
|
||||||
>
|
>
|
||||||
{{ props.task.name }}
|
{{ props.task.name }}
|
||||||
|
<!-- 计时器显示 -->
|
||||||
|
<span
|
||||||
|
v-if="props.task.isTimerRunning || props.task.timerElapsedTime"
|
||||||
|
class="timer-badge"
|
||||||
|
:class="{ 'timer-active': props.task.isTimerRunning }"
|
||||||
|
>
|
||||||
|
<span v-if="props.task.isTimerRunning" class="timer-dot"></span>
|
||||||
|
{{ formattedTimer }}
|
||||||
|
</span>
|
||||||
<span v-if="isOvertime()" class="status-badge overtime">{{ overtimeText }}</span>
|
<span v-if="isOvertime()" class="status-badge overtime">{{ overtimeText }}</span>
|
||||||
<span v-if="overdueDays() > 0" class="status-badge overdue">
|
<span v-if="overdueDays() > 0" class="status-badge overdue">
|
||||||
{{ overdueText }}{{ overdueDays() > 0 ? overdueDays() + daysText : '' }}
|
{{ overdueText }}{{ overdueDays() > 0 ? overdueDays() + daysText : '' }}
|
||||||
@@ -299,8 +405,25 @@ onUnmounted(() => {
|
|||||||
:on-hover="props.onHover"
|
:on-hover="props.onHover"
|
||||||
@toggle="emit('toggle', $event)"
|
@toggle="emit('toggle', $event)"
|
||||||
@dblclick="emit('dblclick', $event)"
|
@dblclick="emit('dblclick', $event)"
|
||||||
|
@start-timer="emit('start-timer', $event)"
|
||||||
|
@stop-timer="emit('stop-timer', $event)"
|
||||||
|
@add-predecessor="emit('add-predecessor', $event)"
|
||||||
|
@add-successor="emit('add-successor', $event)"
|
||||||
|
@delete="handleTaskDelete"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<TaskContextMenu
|
||||||
|
:visible="contextMenuVisible"
|
||||||
|
:task="contextMenuTask"
|
||||||
|
:position="contextMenuPosition"
|
||||||
|
@close="closeContextMenu"
|
||||||
|
@start-timer="$emit('start-timer', props.task)"
|
||||||
|
@stop-timer="$emit('stop-timer', props.task)"
|
||||||
|
@add-predecessor="$emit('add-predecessor', props.task)"
|
||||||
|
@add-successor="$emit('add-successor', props.task)"
|
||||||
|
@delete="handleTaskDelete"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -720,4 +843,58 @@ onUnmounted(() => {
|
|||||||
0 6px 16px rgba(246, 124, 124, 0.4),
|
0 6px 16px rgba(246, 124, 124, 0.4),
|
||||||
0 2px 8px rgba(255, 255, 255, 0.1);
|
0 2px 8px rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 计时器样式 */
|
||||||
|
.timer-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-badge.timer-active {
|
||||||
|
color: #e6a23c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #67c23a; /* 绿色 */
|
||||||
|
margin-right: 4px;
|
||||||
|
animation: pulse 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.8);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(0.8);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .timer-badge {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
color: var(--text-color-secondary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .timer-badge.timer-active {
|
||||||
|
color: #e6c07b;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .timer-dot {
|
||||||
|
background-color: #85ce61; /* 暗色主题下的绿色 */
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+47
-79
@@ -2,7 +2,6 @@
|
|||||||
import { ref, onMounted, onUnmounted, computed, watch, nextTick } from 'vue'
|
import { ref, onMounted, onUnmounted, computed, watch, nextTick } from 'vue'
|
||||||
import TaskBar from './TaskBar.vue'
|
import TaskBar from './TaskBar.vue'
|
||||||
import MilestonePoint from './MilestonePoint.vue'
|
import MilestonePoint from './MilestonePoint.vue'
|
||||||
import TaskDrawer from './TaskDrawer.vue'
|
|
||||||
import MilestoneDialog from './MilestoneDialog.vue'
|
import MilestoneDialog from './MilestoneDialog.vue'
|
||||||
import { useI18n } from '../composables/useI18n'
|
import { useI18n } from '../composables/useI18n'
|
||||||
import { getPredecessorIds } from '../utils/predecessorUtils'
|
import { getPredecessorIds } from '../utils/predecessorUtils'
|
||||||
@@ -47,6 +46,12 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
// 定义emits
|
// 定义emits
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'timeline-scale-changed': [scale: TimelineScale]
|
'timeline-scale-changed': [scale: TimelineScale]
|
||||||
|
'edit-task': [task: Task]
|
||||||
|
'start-timer': [task: Task]
|
||||||
|
'stop-timer': [task: Task]
|
||||||
|
'add-predecessor': [task: Task] // 新增:添加前置任务事件
|
||||||
|
'add-successor': [task: Task] // 新增:添加后置任务事件
|
||||||
|
delete: [task: Task, deleteChildren?: boolean]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// 多语言
|
// 多语言
|
||||||
@@ -176,11 +181,6 @@ const getMonthTimelineRange = () => {
|
|||||||
return { startDate, endDate }
|
return { startDate, endDate }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 抽屉状态管理
|
|
||||||
const drawerVisible = ref(false)
|
|
||||||
const currentTask = ref<Task | null>(null)
|
|
||||||
const isEditMode = ref(false)
|
|
||||||
|
|
||||||
// 里程碑对话框状态管理
|
// 里程碑对话框状态管理
|
||||||
const milestoneDialogVisible = ref(false)
|
const milestoneDialogVisible = ref(false)
|
||||||
const currentMilestone = ref<Milestone | null>(null)
|
const currentMilestone = ref<Milestone | null>(null)
|
||||||
@@ -861,44 +861,9 @@ const updateTask = (updatedTask: Task) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理TaskBar双击事件 - 支持API和默认行为
|
// 处理TaskBar双击事件 - 只emit事件
|
||||||
const handleTaskBarDoubleClick = (task: Task) => {
|
const handleTaskBarDoubleClick = (task: Task) => {
|
||||||
// 优先调用外部传入的双击处理器
|
emit('edit-task', task)
|
||||||
if (props.onTaskDoubleClick && typeof props.onTaskDoubleClick === 'function') {
|
|
||||||
props.onTaskDoubleClick(task)
|
|
||||||
} else if (props.useDefaultDrawer) {
|
|
||||||
// 默认行为:打开内置的TaskDrawer
|
|
||||||
currentTask.value = task
|
|
||||||
isEditMode.value = true
|
|
||||||
drawerVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果有自定义编辑组件,也需要处理
|
|
||||||
// 这里可以根据需要扩展
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理抽屉提交事件
|
|
||||||
const handleDrawerSubmit = (task: Task) => {
|
|
||||||
if (isEditMode.value) {
|
|
||||||
// 编辑模式 - 更新现有任务
|
|
||||||
updateTask(task)
|
|
||||||
} else {
|
|
||||||
// 新增模式 - 添加新任务,通过事件通知父组件
|
|
||||||
window.dispatchEvent(
|
|
||||||
new CustomEvent('task-added', {
|
|
||||||
detail: task,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭抽屉
|
|
||||||
drawerVisible.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理抽屉关闭事件
|
|
||||||
const handleDrawerClose = () => {
|
|
||||||
currentTask.value = null
|
|
||||||
isEditMode.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 存储所有TaskBar的位置信息
|
// 存储所有TaskBar的位置信息
|
||||||
@@ -946,6 +911,16 @@ const handleTaskBarResizeEnd = (updatedTask: Task) => {
|
|||||||
window.dispatchEvent(new CustomEvent('taskbar-resize-end', { detail: updatedTask }))
|
window.dispatchEvent(new CustomEvent('taskbar-resize-end', { detail: updatedTask }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理TaskBar右键菜单事件 - 将事件转发给父组件
|
||||||
|
const handleTaskBarContextMenu = (event: { task: Task; position: { x: number; y: number } }) => {
|
||||||
|
// 将事件转发为全局事件,让GanttChart组件处理
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('context-menu', {
|
||||||
|
detail: event,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// 处理TaskBar的滚动定位请求
|
// 处理TaskBar的滚动定位请求
|
||||||
const handleScrollToPosition = (targetScrollLeft: number) => {
|
const handleScrollToPosition = (targetScrollLeft: number) => {
|
||||||
if (timelineContainer.value) {
|
if (timelineContainer.value) {
|
||||||
@@ -1386,31 +1361,8 @@ onUnmounted(() => {
|
|||||||
document.removeEventListener('mouseup', handleMouseUp)
|
document.removeEventListener('mouseup', handleMouseUp)
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleTaskDelete = (taskId: number, deleteChildren?: boolean) => {
|
const handleTaskDelete = (task: Task, deleteChildren?: boolean) => {
|
||||||
// 调用父组件传入的删除处理器
|
emit('delete', task, deleteChildren)
|
||||||
if (props.onTaskDelete && typeof props.onTaskDelete === 'function') {
|
|
||||||
const taskToDelete = tasks.value.find(task => task.id === taskId)
|
|
||||||
if (taskToDelete) {
|
|
||||||
props.onTaskDelete(taskToDelete, deleteChildren)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 触发全局事件,通知其他组件任务已删除
|
|
||||||
window.dispatchEvent(
|
|
||||||
new CustomEvent('task-deleted', {
|
|
||||||
detail: taskId,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
// 关闭抽屉
|
|
||||||
drawerVisible.value = false
|
|
||||||
currentTask.value = null
|
|
||||||
isEditMode.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// TaskDrawer删除事件适配器
|
|
||||||
const handleDrawerTaskDelete = (task: Task, deleteChildren?: boolean) => {
|
|
||||||
handleTaskDelete(task.id, deleteChildren)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 月度视图中按年份分组的计算属性
|
// 月度视图中按年份分组的计算属性
|
||||||
@@ -1442,6 +1394,16 @@ defineExpose({
|
|||||||
// 时间刻度更新
|
// 时间刻度更新
|
||||||
updateTimeScale,
|
updateTimeScale,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 处理开始计时事件
|
||||||
|
const handleStartTimer = (task: Task) => {
|
||||||
|
emit('start-timer', task)
|
||||||
|
}
|
||||||
|
// 处理停止计时事件
|
||||||
|
const handleStopTimer = (task: Task) => {
|
||||||
|
emit('stop-timer', task)
|
||||||
|
}
|
||||||
|
|
||||||
// Task类型转换成Milestone类型, 需要返回一个Milestone对象
|
// Task类型转换成Milestone类型, 需要返回一个Milestone对象
|
||||||
const convertTaskToMilestone = (task: Task): Milestone => {
|
const convertTaskToMilestone = (task: Task): Milestone => {
|
||||||
// 保证 startDate 一定为 string,避免 undefined
|
// 保证 startDate 一定为 string,避免 undefined
|
||||||
@@ -1627,6 +1589,16 @@ const generateMonthTimelineData = () => {
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加前置任务事件
|
||||||
|
const handleAddPredecessor = (task: Task) => {
|
||||||
|
emit('add-predecessor', task)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加后置任务事件
|
||||||
|
const handleAddSuccessor = (task: Task) => {
|
||||||
|
emit('add-successor', task)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -1648,7 +1620,7 @@ const generateMonthTimelineData = () => {
|
|||||||
class="timeline-year"
|
class="timeline-year"
|
||||||
:style="{ width: '719px' }"
|
:style="{ width: '719px' }"
|
||||||
>
|
>
|
||||||
<div class="year-label">{{ yearValue }}</div>
|
<div class="year-label">{{ yearValue }}年</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1913,22 +1885,18 @@ const generateMonthTimelineData = () => {
|
|||||||
@drag-end="handleTaskBarDragEnd"
|
@drag-end="handleTaskBarDragEnd"
|
||||||
@resize-end="handleTaskBarResizeEnd"
|
@resize-end="handleTaskBarResizeEnd"
|
||||||
@scroll-to-position="handleScrollToPosition"
|
@scroll-to-position="handleScrollToPosition"
|
||||||
|
@context-menu="handleTaskBarContextMenu"
|
||||||
|
@start-timer="handleStartTimer"
|
||||||
|
@stop-timer="handleStopTimer"
|
||||||
|
@add-predecessor="handleAddPredecessor"
|
||||||
|
@add-successor="handleAddSuccessor"
|
||||||
|
@delete="handleTaskDelete"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Task Drawer 抽屉组件 - 仅在使用默认Drawer时显示 -->
|
|
||||||
<TaskDrawer
|
|
||||||
v-if="props.useDefaultDrawer"
|
|
||||||
v-model:visible="drawerVisible"
|
|
||||||
:task="currentTask"
|
|
||||||
:is-edit="isEditMode"
|
|
||||||
@submit="handleDrawerSubmit"
|
|
||||||
@close="handleDrawerClose"
|
|
||||||
@delete="handleDrawerTaskDelete"
|
|
||||||
/>
|
|
||||||
<!-- Milestone Dialog 里程碑对话框组件 -->
|
<!-- Milestone Dialog 里程碑对话框组件 -->
|
||||||
<MilestoneDialog
|
<MilestoneDialog
|
||||||
v-model:visible="milestoneDialogVisible"
|
v-model:visible="milestoneDialogVisible"
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ export { default as MilestonePoint } from './MilestonePoint.vue'
|
|||||||
export { default as MilestoneDialog } from './MilestoneDialog.vue'
|
export { default as MilestoneDialog } from './MilestoneDialog.vue'
|
||||||
export { default as TaskDrawer } from './TaskDrawer.vue'
|
export { default as TaskDrawer } from './TaskDrawer.vue'
|
||||||
export { default as DatePicker } from './DatePicker.vue'
|
export { default as DatePicker } from './DatePicker.vue'
|
||||||
|
export { default as TaskContextMenu } from './TaskContextMenu.vue'
|
||||||
|
|||||||
+22
-37
@@ -6,7 +6,6 @@ export type Locale = 'zh-CN' | 'en-US'
|
|||||||
// 多语言配置
|
// 多语言配置
|
||||||
const messages = {
|
const messages = {
|
||||||
'zh-CN': {
|
'zh-CN': {
|
||||||
dateNotSet: '未设置',
|
|
||||||
// TaskList Header
|
// TaskList Header
|
||||||
taskName: '任务名称',
|
taskName: '任务名称',
|
||||||
predecessor: '前置任务',
|
predecessor: '前置任务',
|
||||||
@@ -39,26 +38,8 @@ const messages = {
|
|||||||
// 月份格式
|
// 月份格式
|
||||||
monthFormat: (month: number) => `${month}月`,
|
monthFormat: (month: number) => `${month}月`,
|
||||||
|
|
||||||
// 月份名称
|
|
||||||
monthNames: [
|
|
||||||
'一月',
|
|
||||||
'二月',
|
|
||||||
'三月',
|
|
||||||
'四月',
|
|
||||||
'五月',
|
|
||||||
'六月',
|
|
||||||
'七月',
|
|
||||||
'八月',
|
|
||||||
'九月',
|
|
||||||
'十月',
|
|
||||||
'十一月',
|
|
||||||
'十二月',
|
|
||||||
],
|
|
||||||
weekDays:['日', '一', '二', '三', '四', '五', '六'],
|
|
||||||
|
|
||||||
// 其他
|
// 其他
|
||||||
milestone: '里程碑',
|
milestone: '里程碑',
|
||||||
targetDate: '目标日期',
|
|
||||||
today: '今天',
|
today: '今天',
|
||||||
// 里程碑对话框
|
// 里程碑对话框
|
||||||
milestoneDetails: '里程碑详情',
|
milestoneDetails: '里程碑详情',
|
||||||
@@ -116,6 +97,13 @@ const messages = {
|
|||||||
newMilestone: '新建里程碑',
|
newMilestone: '新建里程碑',
|
||||||
editMilestone: '编辑里程碑',
|
editMilestone: '编辑里程碑',
|
||||||
cancel: '取消',
|
cancel: '取消',
|
||||||
|
// 右键菜单
|
||||||
|
startTimer: '开始计时',
|
||||||
|
stopTimer: '停止计时',
|
||||||
|
timerStarted: '任务计时已开始',
|
||||||
|
timerStopped: '任务计时已停止',
|
||||||
|
addPredecessor: '添加前置任务',
|
||||||
|
addSuccessor: '添加后置任务',
|
||||||
|
|
||||||
// PDF导出相关
|
// PDF导出相关
|
||||||
pdfExportLoading: '正在生成PDF,请稍候...',
|
pdfExportLoading: '正在生成PDF,请稍候...',
|
||||||
@@ -175,9 +163,12 @@ const messages = {
|
|||||||
overtime: '超',
|
overtime: '超',
|
||||||
overdue: '逾期',
|
overdue: '逾期',
|
||||||
days: '天',
|
days: '天',
|
||||||
|
// 计时确认弹窗
|
||||||
|
timerConfirmPrefix: '即将为任务',
|
||||||
|
timerConfirmSuffix: '计时,若有特殊说明请完善下面的描述',
|
||||||
|
timerConfirmPlaceholder: '请输入计时说明',
|
||||||
},
|
},
|
||||||
'en-US': {
|
'en-US': {
|
||||||
dateNotSet:'Not set',
|
|
||||||
// TaskList Header
|
// TaskList Header
|
||||||
taskName: 'Task Name',
|
taskName: 'Task Name',
|
||||||
predecessor: 'Predecessor',
|
predecessor: 'Predecessor',
|
||||||
@@ -208,25 +199,8 @@ const messages = {
|
|||||||
// 月份格式
|
// 月份格式
|
||||||
monthFormat: (month: number) => `M${String(month).padStart(2, '0')}`,
|
monthFormat: (month: number) => `M${String(month).padStart(2, '0')}`,
|
||||||
|
|
||||||
// Month name
|
|
||||||
monthNames: [
|
|
||||||
'Jan',
|
|
||||||
'Feb',
|
|
||||||
'Mar',
|
|
||||||
'Apr',
|
|
||||||
'May',
|
|
||||||
'Jun',
|
|
||||||
'Jul',
|
|
||||||
'Aug',
|
|
||||||
'Sep',
|
|
||||||
'Oct',
|
|
||||||
'Nov',
|
|
||||||
'Dec',
|
|
||||||
],
|
|
||||||
weekDays:['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
|
||||||
// 其他
|
// 其他
|
||||||
milestone: 'Milestone',
|
milestone: 'Milestone',
|
||||||
targetDate: 'Target Date',
|
|
||||||
today: 'Today',
|
today: 'Today',
|
||||||
// 里程碑对话框
|
// 里程碑对话框
|
||||||
milestoneDetails: 'Milestone Details',
|
milestoneDetails: 'Milestone Details',
|
||||||
@@ -283,6 +257,13 @@ const messages = {
|
|||||||
newMilestone: 'New Milestone',
|
newMilestone: 'New Milestone',
|
||||||
editMilestone: 'Edit Milestone',
|
editMilestone: 'Edit Milestone',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
|
// Context menu
|
||||||
|
timerStarted: 'Timer Started',
|
||||||
|
timerStopped: 'Timer Stopped',
|
||||||
|
startTimer: 'Start Timer',
|
||||||
|
stopTimer: 'Stop Timer',
|
||||||
|
addPredecessor: 'Add Predecessor',
|
||||||
|
addSuccessor: 'Add Successor',
|
||||||
|
|
||||||
// PDF export
|
// PDF export
|
||||||
pdfExportLoading: 'Generating PDF, please wait...',
|
pdfExportLoading: 'Generating PDF, please wait...',
|
||||||
@@ -346,6 +327,10 @@ const messages = {
|
|||||||
overtime: 'Over',
|
overtime: 'Over',
|
||||||
overdue: 'Overdue',
|
overdue: 'Overdue',
|
||||||
days: ' days',
|
days: ' days',
|
||||||
|
// 计时确认弹窗
|
||||||
|
timerConfirmPrefix: 'About to start timing for',
|
||||||
|
timerConfirmSuffix: '. If there are special notes, please complete the description below.',
|
||||||
|
timerConfirmPlaceholder: 'Please enter timer description',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,4 +17,10 @@ export interface Task {
|
|||||||
description?: string
|
description?: string
|
||||||
icon?: string
|
icon?: string
|
||||||
level?: number
|
level?: number
|
||||||
|
// 计时相关字段
|
||||||
|
isTimerRunning?: boolean
|
||||||
|
timerStartTime?: number
|
||||||
|
timerEndTime?: number // 结束计时时间
|
||||||
|
timerStartDesc?: string // 计时开始时填写的描述
|
||||||
|
timerElapsedTime?: number
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user