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/),
|
||||
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
|
||||
|
||||
### 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
|
||||
|
||||
```
|
||||
@@ -116,11 +110,72 @@ jordium-gantt-vue3/
|
||||
|
||||
### GanttChart Events
|
||||
|
||||
| Event | Parameters | Description |
|
||||
|-------|------------|-------------|
|
||||
| Event | Parameters | Description |
|
||||
|--------------------|----------------------------|------------------------------------|
|
||||
| `taskbar-drag-end` | `task: Task` | Task bar drag end |
|
||||
| `taskbar-resize-end` | `task: Task` | Task bar resize 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
|
||||
|
||||
@@ -128,24 +183,30 @@ jordium-gantt-vue3/
|
||||
|
||||
**Task Type**
|
||||
```typescript
|
||||
interface Task {
|
||||
id: number // Unique task identifier
|
||||
name: string // Task name
|
||||
predecessor?: string // Predecessor task ID
|
||||
assignee?: string // Assignee
|
||||
startDate?: string // Start date (YYYY-MM-DD format)
|
||||
endDate?: string // End date (YYYY-MM-DD format)
|
||||
progress?: number // Completion progress (0-100)
|
||||
estimatedHours?: number // Estimated hours
|
||||
actualHours?: number // Actual hours
|
||||
parentId?: number // Parent task ID
|
||||
children?: Task[] // Child tasks array (supports nested structure)
|
||||
collapsed?: boolean // Whether child tasks are collapsed
|
||||
isParent?: boolean // Whether it's a parent task
|
||||
type?: string // Task type (task/story/bug/milestone)
|
||||
description?: string // Task description
|
||||
icon?: string // Task icon
|
||||
level?: number // Task level
|
||||
export interface Task {
|
||||
id: number // Unique task ID
|
||||
name: string // Task name
|
||||
predecessor?: number[] // Predecessor task ID array
|
||||
assignee?: string // Assignee
|
||||
startDate?: string // Start date (ISO string)
|
||||
endDate?: string // End date (ISO string)
|
||||
progress?: number // Progress percentage 0-100
|
||||
estimatedHours?: number // Estimated hours
|
||||
actualHours?: number // Actual hours
|
||||
parentId?: number // Parent task ID
|
||||
children?: Task[] // Subtask array
|
||||
collapsed?: boolean // Collapsed state
|
||||
isParent?: boolean // Is parent task
|
||||
type?: string // Task type (e.g. task, story, milestone)
|
||||
description?: string // Task description
|
||||
icon?: string // Icon
|
||||
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/
|
||||
├── src/ # 源码目录
|
||||
│ ├── components/ # 核心组件
|
||||
│ │ ├── GanttChart.vue # 主入口组件
|
||||
│ │ ├── TaskList.vue # 任务列表
|
||||
│ │ ├── Timeline.vue # 时间轴
|
||||
│ │ ├── TaskBar.vue # 任务条
|
||||
│ │ ├── MilestonePoint.vue # 里程碑
|
||||
│ │ └── ... # 其他组件
|
||||
│ ├── models/ # 数据模型
|
||||
│ │ ├── classes/ # 类定义
|
||||
│ │ └── configs/ # 配置接口
|
||||
│ ├── composables/ # 组合式函数
|
||||
│ ├── styles/ # 样式文件
|
||||
│ └── index.ts # 导出入口
|
||||
├── demo/ # 开发演示
|
||||
├── dist/ # 构建产物
|
||||
├── docs/ # 文档
|
||||
└── package.json
|
||||
├── src/ # 组件源码与核心逻辑
|
||||
│ ├── components/ # 主要 Vue 组件
|
||||
│ ├── models/ # 数据类型与配置
|
||||
│ ├── composables/ # 组合式函数
|
||||
│ ├── styles/ # 样式文件
|
||||
│ └── index.ts # 入口导出
|
||||
├── demo/ # 组件开发与交互演示(本地开发/预览用)
|
||||
├── packageDemo/ # npm 包集成演示(模拟外部项目集成效果)
|
||||
├── dist/ # 构建产物(发布/静态站点/打包输出)
|
||||
├── docs/ # 相关文档(如部署、API 说明等)
|
||||
├── design/ # 设计资源与截图
|
||||
├── public/ # 公共静态资源
|
||||
├── README.md # 中文说明文档
|
||||
├── README-EN.md # 英文说明文档
|
||||
└── ... # 其他配置、脚本与元数据
|
||||
```
|
||||
|
||||
- `demo/`:用于本地开发和功能演示,包含完整的交互页面。
|
||||
- `packageDemo/`:用于模拟 npm 包在外部项目中的集成与使用场景。
|
||||
- `dist/`:构建输出目录,包含发布到 npm 或静态站点的产物。
|
||||
- `docs/`:项目相关文档,如部署说明、API 参考等。
|
||||
- 其余目录请参考注释。
|
||||
|
||||
## 🔧 API 参考
|
||||
|
||||
### GanttChart 属性
|
||||
@@ -117,11 +114,72 @@ jordium-gantt-vue3/
|
||||
|
||||
### GanttChart 事件
|
||||
|
||||
| 事件名 | 参数 | 说明 |
|
||||
|--------|------|------|
|
||||
| 事件名 | 参数 | 说明 |
|
||||
|----------------------|----------------------------|------------------------------|
|
||||
| `taskbar-drag-end` | `task: Task` | 任务条拖拽结束 |
|
||||
| `taskbar-resize-end` | `task: 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 任务类型**
|
||||
```typescript
|
||||
interface Task {
|
||||
id: number // 任务唯一标识
|
||||
name: string // 任务名称
|
||||
predecessor?: string // 前置任务ID
|
||||
assignee?: string // 负责人
|
||||
startDate?: string // 开始日期 (YYYY-MM-DD格式)
|
||||
endDate?: string // 结束日期 (YYYY-MM-DD格式)
|
||||
progress?: number // 完成进度 (0-100)
|
||||
estimatedHours?: number // 预估工时
|
||||
actualHours?: number // 实际工时
|
||||
parentId?: number // 上级任务ID
|
||||
children?: Task[] // 子任务数组(支持嵌套结构)
|
||||
collapsed?: boolean // 是否折叠子任务
|
||||
isParent?: boolean // 是否为父级任务
|
||||
type?: string // 任务类型 (task/story/bug/milestone)
|
||||
description?: string // 任务描述
|
||||
icon?: string // 任务图标
|
||||
level?: number // 任务层级
|
||||
export interface Task {
|
||||
id: number // 任务唯一ID
|
||||
name: string // 任务名称
|
||||
predecessor?: number[] // 前置任务ID数组
|
||||
assignee?: string // 负责人
|
||||
startDate?: string // 开始日期(ISO字符串)
|
||||
endDate?: string // 结束日期(ISO字符串)
|
||||
progress?: number // 进度百分比 0-100
|
||||
estimatedHours?: number // 预估工时
|
||||
actualHours?: number // 实际工时
|
||||
parentId?: number // 上级任务ID
|
||||
children?: Task[] // 子任务数组
|
||||
collapsed?: boolean // 是否折叠
|
||||
isParent?: boolean // 是否为父任务
|
||||
type?: string // 任务类型(如 task、story、milestone 等)
|
||||
description?: string // 任务描述
|
||||
icon?: string // 图标
|
||||
level?: number // 层级
|
||||
// 计时相关字段
|
||||
isTimerRunning?: boolean // 计时是否进行中
|
||||
timerStartTime?: number // 计时开始时间(时间戳)
|
||||
timerEndTime?: number // 计时结束时间(时间戳)
|
||||
timerStartDesc?: string // 计时开始时的描述
|
||||
timerElapsedTime?: number // 已累计计时时长(秒)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+50
-42
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, nextTick } from '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 demoData from './data.json'
|
||||
import packageInfo from '../package.json'
|
||||
@@ -19,11 +19,6 @@ const { t, formatTranslation } = useI18n()
|
||||
const tasks = ref<Task[]>([])
|
||||
const milestones = ref<Task[]>([])
|
||||
|
||||
// TaskDrawer状态管理
|
||||
const showTaskDrawer = ref(false)
|
||||
const currentTask = ref<Task | null>(null)
|
||||
const isEditMode = ref(false)
|
||||
|
||||
// MilestoneDialog状态管理
|
||||
const showMilestoneDialog = ref(false)
|
||||
const currentMilestone = ref<Task | null>(null)
|
||||
@@ -123,14 +118,14 @@ const handleMilestoneDelete = async (milestoneId: number) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('milestone-deleted', {
|
||||
detail: { milestoneId },
|
||||
}),
|
||||
})
|
||||
)
|
||||
|
||||
// 触发强制更新事件,确保Timeline重新渲染
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('milestone-data-changed', {
|
||||
detail: { milestones: milestones.value },
|
||||
}),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@@ -140,6 +135,12 @@ const handleMilestoneDelete = async (milestoneId: number) => {
|
||||
|
||||
// 任务更新处理器
|
||||
const handleTaskUpdate = (updatedTask: Task) => {
|
||||
// 计时信息展示(无论来源于 TaskBar/TaskRow 还是 TaskDrawer header)
|
||||
if (updatedTask.timerStartTime) {
|
||||
const msg = `任务【${updatedTask.name}】已更新`
|
||||
showMessage(msg, 'success', { closable: true })
|
||||
}
|
||||
|
||||
// 先找到原任务,检查parentId是否改变了
|
||||
const findOriginalTask = (taskArray: Task[]): Task | null => {
|
||||
for (const task of taskArray) {
|
||||
@@ -218,7 +219,7 @@ const handleTaskUpdate = (updatedTask: Task) => {
|
||||
showMessage(
|
||||
formatTranslation('newParentTaskNotFound', { parentId: taskToAdd.parentId }),
|
||||
'warning',
|
||||
{ closable: true },
|
||||
{ closable: true }
|
||||
)
|
||||
tasks.value.push(taskToAdd)
|
||||
}
|
||||
@@ -266,7 +267,7 @@ const handleTaskAdd = (newTask: Task) => {
|
||||
const maxId = Math.max(
|
||||
...tasks.value.map(t => t.id || 0),
|
||||
...milestones.value.map(m => m.id || 0),
|
||||
0,
|
||||
0
|
||||
)
|
||||
newTask.id = maxId + 1
|
||||
}
|
||||
@@ -361,7 +362,7 @@ const handleStoryDeleteWithChildren = (storyToDelete: Task) => {
|
||||
'success',
|
||||
{
|
||||
closable: false,
|
||||
},
|
||||
}
|
||||
)
|
||||
return true
|
||||
}
|
||||
@@ -423,7 +424,7 @@ const handleStoryDeleteOnly = (storyToDelete: Task) => {
|
||||
'success',
|
||||
{
|
||||
closable: false,
|
||||
},
|
||||
}
|
||||
)
|
||||
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) => {
|
||||
if (task.type === 'story' && deleteChildren === true) {
|
||||
// 删除story及其所有子任务
|
||||
@@ -515,7 +498,7 @@ function handleTaskbarDragOrResizeEnd(newTask) {
|
||||
`开始: ${oldTask.startDate} → ${newTask.startDate}\n` +
|
||||
`结束: ${oldTask.endDate} → ${newTask.endDate}`,
|
||||
'info',
|
||||
{ closable: true },
|
||||
{ closable: true }
|
||||
)
|
||||
}
|
||||
function handleMilestoneDragEnd(newMilestone) {
|
||||
@@ -525,7 +508,7 @@ function handleMilestoneDragEnd(newMilestone) {
|
||||
`里程碑【${newMilestone.name}】\n` +
|
||||
`开始: ${oldMilestone.endDate} → ${newMilestone.startDate}`,
|
||||
'info',
|
||||
{ closable: true },
|
||||
{ closable: true }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -586,6 +569,26 @@ const collectAllTaskIds = (task: Task): number[] => {
|
||||
}
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -639,22 +642,27 @@ const collectAllTaskIds = (task: Task): number[] => {
|
||||
@taskbar-drag-end="handleTaskbarDragOrResizeEnd"
|
||||
@taskbar-resize-end="handleTaskbarDragOrResizeEnd"
|
||||
@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 class="license-info">
|
||||
<p>MIT License @JORDIUM.COM</p>
|
||||
</div>
|
||||
|
||||
<!-- TaskDrawer用于新建/编辑任务 -->
|
||||
<TaskDrawer
|
||||
v-model:visible="showTaskDrawer"
|
||||
:task="currentTask"
|
||||
:is-edit="isEditMode"
|
||||
@submit="handleTaskDrawerSubmit"
|
||||
@close="handleTaskDrawerClose"
|
||||
@delete="handleTaskDrawerDelete"
|
||||
/>
|
||||
|
||||
<!-- MilestoneDialog用于新建/编辑里程碑 -->
|
||||
<MilestoneDialog
|
||||
:visible="showMilestoneDialog"
|
||||
|
||||
@@ -140,5 +140,27 @@
|
||||
"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代码质量检查问题修改"]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
"version": "1.0.10",
|
||||
"version": "1.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "jordium-gantt-vue3",
|
||||
"version": "1.0.10",
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"date-fns": "^4.1.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jordium-gantt-vue3",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.1",
|
||||
"type": "module",
|
||||
"main": "dist/jordium-gantt-vue3.cjs.js",
|
||||
"module": "dist/jordium-gantt-vue3.es.js",
|
||||
|
||||
+51
-42
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, nextTick } from '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 demoData from './data.json'
|
||||
import packageInfo from '../package.json'
|
||||
@@ -19,11 +19,6 @@ const { t, formatTranslation } = useI18n()
|
||||
const tasks = ref<Task[]>([])
|
||||
const milestones = ref<Task[]>([])
|
||||
|
||||
// TaskDrawer状态管理
|
||||
const showTaskDrawer = ref(false)
|
||||
const currentTask = ref<Task | null>(null)
|
||||
const isEditMode = ref(false)
|
||||
|
||||
// MilestoneDialog状态管理
|
||||
const showMilestoneDialog = ref(false)
|
||||
const currentMilestone = ref<Task | null>(null)
|
||||
@@ -41,6 +36,7 @@ const toolbarConfig = {
|
||||
showLanguage: true,
|
||||
showTheme: true,
|
||||
showFullscreen: true,
|
||||
showTimeScale: true, // 控制日|周|月时间刻度按钮组的可见性
|
||||
}
|
||||
|
||||
// 自定义CSV导出处理器(可选)
|
||||
@@ -122,14 +118,14 @@ const handleMilestoneDelete = async (milestoneId: number) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('milestone-deleted', {
|
||||
detail: { milestoneId },
|
||||
}),
|
||||
})
|
||||
)
|
||||
|
||||
// 触发强制更新事件,确保Timeline重新渲染
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('milestone-data-changed', {
|
||||
detail: { milestones: milestones.value },
|
||||
}),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@@ -139,6 +135,12 @@ const handleMilestoneDelete = async (milestoneId: number) => {
|
||||
|
||||
// 任务更新处理器
|
||||
const handleTaskUpdate = (updatedTask: Task) => {
|
||||
// 计时信息展示(无论来源于 TaskBar/TaskRow 还是 TaskDrawer header)
|
||||
if (updatedTask.timerStartTime) {
|
||||
const msg = `任务【${updatedTask.name}】已更新`
|
||||
showMessage(msg, 'success', { closable: true })
|
||||
}
|
||||
|
||||
// 先找到原任务,检查parentId是否改变了
|
||||
const findOriginalTask = (taskArray: Task[]): Task | null => {
|
||||
for (const task of taskArray) {
|
||||
@@ -217,7 +219,7 @@ const handleTaskUpdate = (updatedTask: Task) => {
|
||||
showMessage(
|
||||
formatTranslation('newParentTaskNotFound', { parentId: taskToAdd.parentId }),
|
||||
'warning',
|
||||
{ closable: true },
|
||||
{ closable: true }
|
||||
)
|
||||
tasks.value.push(taskToAdd)
|
||||
}
|
||||
@@ -265,7 +267,7 @@ const handleTaskAdd = (newTask: Task) => {
|
||||
const maxId = Math.max(
|
||||
...tasks.value.map(t => t.id || 0),
|
||||
...milestones.value.map(m => m.id || 0),
|
||||
0,
|
||||
0
|
||||
)
|
||||
newTask.id = maxId + 1
|
||||
}
|
||||
@@ -360,7 +362,7 @@ const handleStoryDeleteWithChildren = (storyToDelete: Task) => {
|
||||
'success',
|
||||
{
|
||||
closable: false,
|
||||
},
|
||||
}
|
||||
)
|
||||
return true
|
||||
}
|
||||
@@ -422,7 +424,7 @@ const handleStoryDeleteOnly = (storyToDelete: Task) => {
|
||||
'success',
|
||||
{
|
||||
closable: false,
|
||||
},
|
||||
}
|
||||
)
|
||||
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) => {
|
||||
if (task.type === 'story' && deleteChildren === true) {
|
||||
// 删除story及其所有子任务
|
||||
@@ -514,7 +498,7 @@ function handleTaskbarDragOrResizeEnd(newTask) {
|
||||
`开始: ${oldTask.startDate} → ${newTask.startDate}\n` +
|
||||
`结束: ${oldTask.endDate} → ${newTask.endDate}`,
|
||||
'info',
|
||||
{ closable: true },
|
||||
{ closable: true }
|
||||
)
|
||||
}
|
||||
function handleMilestoneDragEnd(newMilestone) {
|
||||
@@ -524,7 +508,7 @@ function handleMilestoneDragEnd(newMilestone) {
|
||||
`里程碑【${newMilestone.name}】\n` +
|
||||
`开始: ${oldMilestone.endDate} → ${newMilestone.startDate}`,
|
||||
'info',
|
||||
{ closable: true },
|
||||
{ closable: true }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -585,6 +569,26 @@ const collectAllTaskIds = (task: Task): number[] => {
|
||||
}
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -638,22 +642,27 @@ const collectAllTaskIds = (task: Task): number[] => {
|
||||
@taskbar-drag-end="handleTaskbarDragOrResizeEnd"
|
||||
@taskbar-resize-end="handleTaskbarDragOrResizeEnd"
|
||||
@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 class="license-info">
|
||||
<p>MIT License @JORDIUM.COM</p>
|
||||
</div>
|
||||
|
||||
<!-- TaskDrawer用于新建/编辑任务 -->
|
||||
<TaskDrawer
|
||||
v-model:visible="showTaskDrawer"
|
||||
:task="currentTask"
|
||||
:is-edit="isEditMode"
|
||||
@submit="handleTaskDrawerSubmit"
|
||||
@close="handleTaskDrawerClose"
|
||||
@delete="handleTaskDrawerDelete"
|
||||
/>
|
||||
|
||||
<!-- MilestoneDialog用于新建/编辑里程碑 -->
|
||||
<MilestoneDialog
|
||||
:visible="showMilestoneDialog"
|
||||
|
||||
@@ -135,5 +135,32 @@
|
||||
"Timeline中sub-task隐藏后,关系线也随之隐藏",
|
||||
"增强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()
|
||||
|
||||
|
||||
interface Props {
|
||||
modelValue?: string | [string, string]
|
||||
type?: 'date' | 'daterange'
|
||||
@@ -72,9 +71,6 @@ const formatDisplayDate = (dateStr: string) => {
|
||||
const date = new Date(dateStr)
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 显示值
|
||||
const displayValue = computed(() => {
|
||||
if (props.type === 'daterange') {
|
||||
@@ -471,7 +467,6 @@ const yearList = computed(() => {
|
||||
return years
|
||||
})
|
||||
|
||||
/*
|
||||
// 月份名称
|
||||
const monthNames = [
|
||||
'一月',
|
||||
@@ -488,7 +483,6 @@ const monthNames = [
|
||||
'十二月',
|
||||
]
|
||||
const weekDays = ['日', '一', '二', '三', '四', '五', '六']
|
||||
*/
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
@@ -745,7 +739,7 @@ const panelStyle = computed(() => {
|
||||
<<
|
||||
</button>
|
||||
<span class="el-month-picker__header-label" @click="showYearSelector($event)">
|
||||
{{ currentYear }}
|
||||
{{ currentYear }}年
|
||||
</span>
|
||||
<button type="button" class="el-picker-panel__icon-btn" @click="nextYear">
|
||||
>>
|
||||
@@ -753,7 +747,7 @@ const panelStyle = computed(() => {
|
||||
</div>
|
||||
<div class="el-month-picker__content">
|
||||
<div
|
||||
v-for="(monthName, index) in t.monthNames"
|
||||
v-for="(monthName, index) in monthNames"
|
||||
:key="index"
|
||||
class="el-month-picker__item"
|
||||
:class="{ 'is-current': index === currentMonth }"
|
||||
@@ -775,10 +769,10 @@ const panelStyle = computed(() => {
|
||||
</button>
|
||||
<span class="el-date-picker__header-label">
|
||||
<span class="el-date-picker__header-year" @click="showYearSelector($event)">
|
||||
{{ currentYear }}
|
||||
{{ currentYear }}年
|
||||
</span>
|
||||
<span class="el-date-picker__header-month" @click="showMonthSelector($event)">
|
||||
{{ t.monthNames[currentMonth] }}
|
||||
{{ monthNames[currentMonth] }}
|
||||
</span>
|
||||
</span>
|
||||
<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-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 }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+331
-10
@@ -3,6 +3,7 @@ import { ref, onUnmounted, onMounted, computed, watch, nextTick, defineEmits } f
|
||||
import TaskList from './TaskList.vue'
|
||||
import Timeline from './Timeline.vue'
|
||||
import GanttToolbar from './GanttToolbar.vue'
|
||||
import TaskDrawer from './TaskDrawer.vue'
|
||||
import { useI18n, setCustomMessages } from '../composables/useI18n'
|
||||
import { formatPredecessorDisplay } from '../utils/predecessorUtils'
|
||||
import jsPDF from 'jspdf'
|
||||
@@ -37,7 +38,18 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
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()
|
||||
|
||||
@@ -237,7 +249,7 @@ const toggleTaskList = () => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('timeline-container-resized', {
|
||||
detail: { source: 'manual-task-list-toggle' },
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
}, 400)
|
||||
@@ -253,7 +265,7 @@ const handleToggleTaskList = (event: CustomEvent) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('timeline-container-resized', {
|
||||
detail: { source: 'task-list-toggle' },
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -268,6 +280,7 @@ function handleTaskBarResizeEnd(event: CustomEvent) {
|
||||
function handleMilestoneDragEnd(event: CustomEvent) {
|
||||
emit('milestone-drag-end', event.detail)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('taskbar-drag-end', handleTaskBarDragEnd as EventListener)
|
||||
window.addEventListener('taskbar-resize-end', handleTaskBarResizeEnd as EventListener)
|
||||
@@ -285,7 +298,7 @@ const handleTaskCollapseChange = (task: Task) => {
|
||||
const updateTaskCollapsedState = (
|
||||
tasks: Task[],
|
||||
targetId: number,
|
||||
collapsed: boolean,
|
||||
collapsed: boolean
|
||||
): boolean => {
|
||||
for (const t of tasks) {
|
||||
if (t.id === targetId) {
|
||||
@@ -356,7 +369,7 @@ watch(
|
||||
notifyTaskListUpdated()
|
||||
})
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
@@ -375,6 +388,8 @@ onMounted(() => {
|
||||
window.addEventListener('request-task-list', handleRequestTaskList as EventListener)
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', handleWindowResize)
|
||||
// 监听TaskBar的右键菜单事件
|
||||
window.addEventListener('context-menu', handleTaskContextMenu as EventListener)
|
||||
|
||||
nextTick(() => {
|
||||
if (timelineRef.value && typeof timelineRef.value.scrollToTodayCenter === 'function') {
|
||||
@@ -392,12 +407,13 @@ onUnmounted(() => {
|
||||
window.removeEventListener('task-added', handleTaskAdd as EventListener)
|
||||
window.removeEventListener(
|
||||
'milestone-icon-changed',
|
||||
handleMilestoneIconChangeEvent as EventListener,
|
||||
handleMilestoneIconChangeEvent as EventListener
|
||||
)
|
||||
window.removeEventListener('milestone-deleted', handleMilestoneDeleted as EventListener)
|
||||
window.removeEventListener('milestone-data-changed', handleMilestoneDataChanged as EventListener)
|
||||
window.removeEventListener('request-task-list', handleRequestTaskList as EventListener)
|
||||
window.removeEventListener('resize', handleWindowResize)
|
||||
window.removeEventListener('context-menu', handleTaskContextMenu as EventListener)
|
||||
})
|
||||
|
||||
// 全屏状态管理
|
||||
@@ -1167,8 +1183,289 @@ watch(
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -1189,6 +1486,7 @@ watch(
|
||||
:on-theme-change="props.onThemeChange"
|
||||
:on-fullscreen-change="props.onFullscreenChange"
|
||||
:on-time-scale-change="handleTimeScaleChange"
|
||||
@add-task="handleToolbarAddTask"
|
||||
/>
|
||||
|
||||
<!-- 甘特图主体 -->
|
||||
@@ -1204,6 +1502,11 @@ watch(
|
||||
:edit-component="props.editComponent"
|
||||
:use-default-drawer="props.useDefaultDrawer"
|
||||
@task-collapse-change="handleTaskCollapseChange"
|
||||
@start-timer="handleStartTimer"
|
||||
@stop-timer="handleStopTimer"
|
||||
@add-predecessor="handleAddPredecessor"
|
||||
@add-successor="handleAddSuccessor"
|
||||
@delete="handleTaskDelete"
|
||||
/>
|
||||
</div>
|
||||
<div class="gantt-splitter" @mousedown="onMouseDown">
|
||||
@@ -1243,12 +1546,30 @@ watch(
|
||||
:on-task-double-click="props.onTaskDoubleClick"
|
||||
:edit-component="props.editComponent"
|
||||
:use-default-drawer="props.useDefaultDrawer"
|
||||
:on-task-delete="props.onTaskDelete"
|
||||
:on-milestone-save="handleMilestoneSave"
|
||||
@timeline-scale-changed="handleTimelineScaleChanged"
|
||||
@edit-task="handleTimelineEditTask"
|
||||
@start-timer="handleStartTimer"
|
||||
@stop-timer="handleStopTimer"
|
||||
@add-predecessor="handleAddPredecessor"
|
||||
@add-successor="handleAddSuccessor"
|
||||
@delete="handleTaskDelete"
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -1535,8 +1856,8 @@ watch(
|
||||
}
|
||||
|
||||
.gantt-root.splitter-dragging .gantt-panel-right {
|
||||
/* 拖拽期间禁用Timeline区域的指针事件 */
|
||||
pointer-events: none;
|
||||
/* 拖拽时高亮右侧面板 */
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.gantt-root.splitter-dragging * {
|
||||
|
||||
@@ -485,26 +485,26 @@ const handleMilestoneMouseLeave = () => {
|
||||
|
||||
// 格式化日期显示
|
||||
const formatDisplayDate = (dateStr: string): string => {
|
||||
if (!dateStr) return t('dateNotSet') //Not Set
|
||||
if (!dateStr) return '未设置'
|
||||
|
||||
try {
|
||||
const date = new Date(dateStr)
|
||||
if (isNaN(date.getTime())) return t('dateNotSet')
|
||||
if (isNaN(date.getTime())) return '未设置'
|
||||
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
} catch {
|
||||
return t('dateNotSet')
|
||||
return '未设置'
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltip内容 Target date
|
||||
// Tooltip内容
|
||||
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 || '')
|
||||
return `${t('milestone')}:${milestoneName} <br> ${t('targetDate')}:${targetDate}`
|
||||
return `里程碑:${milestoneName} - 目标日期:${targetDate}`
|
||||
})
|
||||
|
||||
// 组件销毁时清理事件监听器
|
||||
@@ -688,7 +688,8 @@ const calculateMilestonePositionFromTimelineData = (
|
||||
top: `${tooltipPosition.y}px`,
|
||||
}"
|
||||
>
|
||||
<div class="tooltip-content" v-html="tooltipContent">
|
||||
<div class="tooltip-content">
|
||||
{{ tooltipContent }}
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
@@ -41,7 +41,7 @@ const availableTasks = computed(() => {
|
||||
task =>
|
||||
task.type === 'task' &&
|
||||
task.id !== props.currentTaskId &&
|
||||
!selectedPredecessorIds.value.includes(task.id),
|
||||
!selectedPredecessorIds.value.includes(task.id)
|
||||
)
|
||||
})
|
||||
|
||||
@@ -69,13 +69,13 @@ watch(
|
||||
() => {
|
||||
selectedValue.value = ''
|
||||
},
|
||||
{ immediate: true },
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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 v-if="selectedPredecessors.length > 0" class="selected-tags">
|
||||
@@ -89,7 +89,12 @@ watch(
|
||||
|
||||
<!-- 下拉选择器 -->
|
||||
<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 v-for="task in availableTasks" :key="task.id" :value="task.id">
|
||||
{{ task.name }} (ID: {{ task.id }})
|
||||
|
||||
+69
-18
@@ -2,10 +2,7 @@
|
||||
import { ref, computed, onUnmounted, onMounted, nextTick, watch } from 'vue'
|
||||
import type { Task } from '../models/classes/Task'
|
||||
import { TimelineScale } from '../models/types/TimelineScale'
|
||||
|
||||
|
||||
import { useI18n } from '../composables/useI18n'
|
||||
|
||||
import TaskContextMenu from './TaskContextMenu.vue'
|
||||
|
||||
interface Props {
|
||||
task: Task
|
||||
@@ -44,9 +41,14 @@ const emit = defineEmits([
|
||||
'update:task',
|
||||
'bar-mounted',
|
||||
'dblclick',
|
||||
'drag-end', // 新增
|
||||
'resize-end', // 新增
|
||||
'scroll-to-position', // 新增:半圆点击定位事件
|
||||
'drag-end',
|
||||
'resize-end',
|
||||
'scroll-to-position',
|
||||
'start-timer',
|
||||
'stop-timer',
|
||||
'add-predecessor',
|
||||
'add-successor',
|
||||
'delete',
|
||||
])
|
||||
|
||||
// 日期工具函数 - 处理时区安全的日期创建和操作
|
||||
@@ -234,9 +236,7 @@ const taskStatus = computed(() => {
|
||||
})
|
||||
|
||||
// 判断是否已完成
|
||||
const isCompleted = computed(() => {
|
||||
return (props.task.progress || 0) >= 100
|
||||
})
|
||||
const isCompleted = computed(() => (props.task.progress || 0) >= 100)
|
||||
|
||||
// 计算完成部分的宽度
|
||||
const progressWidth = computed(() => {
|
||||
@@ -448,10 +448,14 @@ onMounted(() => {
|
||||
window.addEventListener('timeline-scale-updated', handleTimelineScaleUpdate)
|
||||
window.addEventListener('timeline-force-recalculate', handleForceRecalculate)
|
||||
|
||||
// 监听全局关闭菜单事件
|
||||
window.addEventListener('close-all-taskbar-menus', closeContextMenu)
|
||||
|
||||
// 清理函数
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('timeline-scale-updated', handleTimelineScaleUpdate)
|
||||
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 => {
|
||||
if (!dateStr) return t('dateNotSet')
|
||||
if (!dateStr) return '未设置'
|
||||
const date = createLocalDate(dateStr)
|
||||
if (!date) return t('dateNotSet')
|
||||
if (!date) return '未设置'
|
||||
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
@@ -975,7 +979,41 @@ const calculatePositionFromTimelineData = (
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -999,6 +1037,7 @@ const calculatePositionFromTimelineData = (
|
||||
'short-task-bar': isShortTaskBar,
|
||||
'overflow-effect': needsOverflowEffect,
|
||||
}"
|
||||
@contextmenu="handleContextMenu"
|
||||
@dblclick="handleTaskBarDoubleClick"
|
||||
>
|
||||
<!-- 父级任务的标签 -->
|
||||
@@ -1059,6 +1098,18 @@ const calculatePositionFromTimelineData = (
|
||||
@mousedown="handleBubbleMouseDown"
|
||||
@click="handleBubbleClick"
|
||||
></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>
|
||||
|
||||
<!-- Tooltip 弹窗 -->
|
||||
@@ -1075,23 +1126,23 @@ const calculatePositionFromTimelineData = (
|
||||
<div class="tooltip-title">{{ task.name }}</div>
|
||||
<div class="tooltip-content">
|
||||
<div class="tooltip-row">
|
||||
<span class="tooltip-label"> {{ t('startDate') }}:</span>
|
||||
<span class="tooltip-label">计划开始:</span>
|
||||
<span class="tooltip-value">{{ formatDisplayDate(task.startDate) }}</span>
|
||||
</div>
|
||||
<div class="tooltip-row">
|
||||
<span class="tooltip-label">{{ t('endDate') }}:</span>
|
||||
<span class="tooltip-label">计划结束:</span>
|
||||
<span class="tooltip-value">{{ formatDisplayDate(task.endDate) }}</span>
|
||||
</div>
|
||||
<div class="tooltip-row">
|
||||
<span class="tooltip-label">{{ t('estimatedHours') }}:</span>
|
||||
<span class="tooltip-label">计划工时:</span>
|
||||
<span class="tooltip-value">{{ workHourInfo.total }}h</span>
|
||||
</div>
|
||||
<div class="tooltip-row">
|
||||
<span class="tooltip-label"> {{ t('actualHours') }}:</span>
|
||||
<span class="tooltip-label">已用工时:</span>
|
||||
<span class="tooltip-value">{{ workHourInfo.used }}h</span>
|
||||
</div>
|
||||
<div class="tooltip-row">
|
||||
<span class="tooltip-label"> {{ t('progress') }}:</span>
|
||||
<span class="tooltip-label">完成率:</span>
|
||||
<span class="tooltip-value">{{ task.progress || 0 }}%</span>
|
||||
</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 GanttConfirmDialog from './GanttConfirmDialog.vue'
|
||||
import MultiSelectPredecessor from './MultiSelectPredecessor.vue'
|
||||
import ConfirmTimerDialog from './ConfirmTimerDialog.vue'
|
||||
import type { Task } from '../models/classes/Task'
|
||||
import '../styles/app.css'
|
||||
|
||||
@@ -27,6 +28,8 @@ const emit = defineEmits<{
|
||||
submit: [task: Task]
|
||||
close: []
|
||||
delete: [task: Task, deleteChildren?: boolean]
|
||||
'start-timer': [task: Task]
|
||||
'stop-timer': [task: Task]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -36,6 +39,78 @@ const submitting = ref(false)
|
||||
const isVisible = ref(props.visible)
|
||||
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类型
|
||||
const dialogType = computed(() => {
|
||||
return props.task?.type === 'story' ? 'yes-no-cancel' : 'confirm-cancel'
|
||||
@@ -63,6 +138,10 @@ const formData = reactive<Task>({
|
||||
progress: 0,
|
||||
description: '',
|
||||
parentId: undefined, // 上级任务ID
|
||||
isTimerRunning: false, // 是否正在计时
|
||||
timerStartTime: undefined, // 计时器开始时间
|
||||
timerEndTime: undefined, // 计时器结束时间
|
||||
timerElapsedTime: 0, // 计时器已用时间
|
||||
})
|
||||
|
||||
// 任务列表数据
|
||||
@@ -74,7 +153,7 @@ const availableParentTasks = computed(() => {
|
||||
.filter(
|
||||
task =>
|
||||
task.id !== props.task?.id && // 排除当前任务自己
|
||||
(task.type === 'story' || task.type === 'task'), // 只显示story和task类型
|
||||
(task.type === 'story' || task.type === 'task') // 只显示story和task类型
|
||||
)
|
||||
.map(task => ({
|
||||
...task,
|
||||
@@ -195,11 +274,16 @@ watch(
|
||||
if (props.task && props.isEdit) {
|
||||
// 编辑模式,填充表单数据
|
||||
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'))
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// 监听 isVisible 变化,同步到父组件
|
||||
@@ -207,6 +291,18 @@ watch(isVisible, 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 = () => {
|
||||
Object.assign(formData, {
|
||||
@@ -221,6 +317,10 @@ const resetForm = () => {
|
||||
progress: 0,
|
||||
description: '',
|
||||
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(),
|
||||
}
|
||||
emit('submit', taskData)
|
||||
showMessage(props.isEdit ? t.value.taskUpdateSuccess : t.value.taskCreateSuccess, 'success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
// 处理错误但不在控制台输出
|
||||
showMessage(t.value.operationFailed, 'error')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
@@ -298,6 +396,21 @@ const handleDelete = () => {
|
||||
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 = () => {
|
||||
showDeleteConfirm.value = false
|
||||
if (props.task && props.isEdit) {
|
||||
@@ -306,7 +419,7 @@ const confirmDelete = () => {
|
||||
emit('delete', props.task)
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
showMessage(t.value.taskDeleteFailed, 'error')
|
||||
handleError(error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
@@ -326,14 +439,13 @@ const handleDeleteYes = () => {
|
||||
emit('delete', props.task, true) // 传递true表示删除所有子任务
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
showMessage(t.value.taskDeleteFailed, 'error')
|
||||
handleError(error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Story删除:选择"否" - 仅删除story,保留子任务
|
||||
// Story删除:选择"否" - 仅删除story,保留子任务
|
||||
const handleDeleteNo = () => {
|
||||
showDeleteConfirm.value = false
|
||||
@@ -343,7 +455,7 @@ const handleDeleteNo = () => {
|
||||
emit('delete', props.task, false) // 传递false表示仅删除story
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
showMessage(t.value.taskDeleteFailed, 'error')
|
||||
handleError(error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
@@ -373,18 +485,193 @@ watch(
|
||||
newValue => {
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div v-if="isVisible" class="drawer-overlay" @click="handleOverlayClick">
|
||||
<div class="drawer-container" @click.stop>
|
||||
<!-- Drawer Header -->
|
||||
<div class="drawer-header">
|
||||
<h3 class="drawer-title">{{ isEdit ? t.editTask : t.newTask }}</h3>
|
||||
<div
|
||||
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">
|
||||
<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="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
@@ -426,15 +713,15 @@ watch(
|
||||
<span v-if="errors.type" class="error-text">{{ errors.type }}</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="task-assignee">{{ t.assignee }}</label>
|
||||
<select id="task-assignee" v-model="formData.assignee" class="form-select">
|
||||
<option value="">{{ t.selectAssignee }}</option>
|
||||
<!-- <option value="张三">张三</option>
|
||||
<option value="">{{ t.selectAssignee }}</option>
|
||||
<option value="张三">张三</option>
|
||||
<option value="李四">李四</option>
|
||||
<option value="王五">王五</option>
|
||||
<option value="赵六">赵六</option>
|
||||
<option value="钱七">钱七</option>-->
|
||||
<option value="钱七">钱七</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -606,6 +893,17 @@ watch(
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 计时器确认弹窗 -->
|
||||
<ConfirmTimerDialog
|
||||
v-if="showTimerConfirm"
|
||||
:visible="showTimerConfirm"
|
||||
:title="'确认开始计时'"
|
||||
:message="`即将为任务${props.task?.name}计时,若有特殊说明请完善下面的描述`"
|
||||
:default-desc="props.task?.name || ''"
|
||||
@confirm="confirmTimer"
|
||||
@cancel="cancelTimerConfirm"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -819,10 +1117,6 @@ watch(
|
||||
background: var(--gantt-primary, #409eff);
|
||||
}
|
||||
|
||||
.progress-slider::-moz-range-track {
|
||||
background: var(--gantt-border-light, #e4e7ed);
|
||||
}
|
||||
|
||||
/* 为更好的兼容性,添加一个伪元素来显示已完成部分 */
|
||||
.progress-slider::before {
|
||||
content: '';
|
||||
|
||||
@@ -14,7 +14,18 @@ interface Props {
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// 定义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()
|
||||
@@ -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 () => {
|
||||
window.addEventListener('task-updated', handleTaskUpdated as EventListener)
|
||||
window.addEventListener('task-added', handleTaskAdded as EventListener)
|
||||
@@ -354,6 +403,12 @@ onUnmounted(() => {
|
||||
:on-hover="handleTaskRowHover"
|
||||
@toggle="toggleCollapse"
|
||||
@dblclick="handleTaskRowDoubleClick"
|
||||
@contextmenu="handleTaskRowContextMenu"
|
||||
@start-timer="handleStartTimer"
|
||||
@stop-timer="handleStopTimer"
|
||||
@add-predecessor="handleAddPredecessor"
|
||||
@add-successor="handleAddSuccessor"
|
||||
@delete="handleTaskDelete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+179
-2
@@ -1,8 +1,9 @@
|
||||
<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 { formatPredecessorDisplay } from '../utils/predecessorUtils'
|
||||
import type { Task } from '../models/classes/Task'
|
||||
import TaskContextMenu from './TaskContextMenu.vue'
|
||||
|
||||
interface Props {
|
||||
task: Task
|
||||
@@ -13,7 +14,16 @@ interface Props {
|
||||
onHover?: (taskId: number | null) => void
|
||||
}
|
||||
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 overtimeText = computed(() => t.value?.overtime ?? '')
|
||||
const overdueText = computed(() => t.value?.overdue ?? '')
|
||||
@@ -129,15 +139,101 @@ const handleSplitterDragEnd = () => {
|
||||
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(() => {
|
||||
window.addEventListener('splitter-drag-start', handleSplitterDragStart)
|
||||
window.addEventListener('splitter-drag-end', handleSplitterDragEnd)
|
||||
window.addEventListener('close-all-taskbar-menus', closeContextMenu)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('splitter-drag-start', handleSplitterDragStart)
|
||||
window.removeEventListener('splitter-drag-end', handleSplitterDragEnd)
|
||||
window.removeEventListener('close-all-taskbar-menus', closeContextMenu)
|
||||
if (timerInterval.value) {
|
||||
clearInterval(timerInterval.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -160,6 +256,7 @@ onUnmounted(() => {
|
||||
@dblclick="handleTaskRowDoubleClick"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
@contextmenu="handleContextMenu"
|
||||
>
|
||||
<div class="col col-name" :style="{ paddingLeft: indent }">
|
||||
<span
|
||||
@@ -245,6 +342,15 @@ onUnmounted(() => {
|
||||
:title="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="overdueDays() > 0" class="status-badge overdue">
|
||||
{{ overdueText }}{{ overdueDays() > 0 ? overdueDays() + daysText : '' }}
|
||||
@@ -299,8 +405,25 @@ onUnmounted(() => {
|
||||
:on-hover="props.onHover"
|
||||
@toggle="emit('toggle', $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>
|
||||
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@@ -720,4 +843,58 @@ onUnmounted(() => {
|
||||
0 6px 16px rgba(246, 124, 124, 0.4),
|
||||
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>
|
||||
|
||||
+47
-79
@@ -2,7 +2,6 @@
|
||||
import { ref, onMounted, onUnmounted, computed, watch, nextTick } from 'vue'
|
||||
import TaskBar from './TaskBar.vue'
|
||||
import MilestonePoint from './MilestonePoint.vue'
|
||||
import TaskDrawer from './TaskDrawer.vue'
|
||||
import MilestoneDialog from './MilestoneDialog.vue'
|
||||
import { useI18n } from '../composables/useI18n'
|
||||
import { getPredecessorIds } from '../utils/predecessorUtils'
|
||||
@@ -47,6 +46,12 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
// 定义emits
|
||||
const emit = defineEmits<{
|
||||
'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 }
|
||||
}
|
||||
|
||||
// 抽屉状态管理
|
||||
const drawerVisible = ref(false)
|
||||
const currentTask = ref<Task | null>(null)
|
||||
const isEditMode = ref(false)
|
||||
|
||||
// 里程碑对话框状态管理
|
||||
const milestoneDialogVisible = ref(false)
|
||||
const currentMilestone = ref<Milestone | null>(null)
|
||||
@@ -861,44 +861,9 @@ const updateTask = (updatedTask: Task) => {
|
||||
)
|
||||
}
|
||||
|
||||
// 处理TaskBar双击事件 - 支持API和默认行为
|
||||
// 处理TaskBar双击事件 - 只emit事件
|
||||
const handleTaskBarDoubleClick = (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
|
||||
emit('edit-task', task)
|
||||
}
|
||||
|
||||
// 存储所有TaskBar的位置信息
|
||||
@@ -946,6 +911,16 @@ const handleTaskBarResizeEnd = (updatedTask: Task) => {
|
||||
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的滚动定位请求
|
||||
const handleScrollToPosition = (targetScrollLeft: number) => {
|
||||
if (timelineContainer.value) {
|
||||
@@ -1386,31 +1361,8 @@ onUnmounted(() => {
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
})
|
||||
|
||||
const handleTaskDelete = (taskId: number, deleteChildren?: boolean) => {
|
||||
// 调用父组件传入的删除处理器
|
||||
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)
|
||||
const handleTaskDelete = (task: Task, deleteChildren?: boolean) => {
|
||||
emit('delete', task, deleteChildren)
|
||||
}
|
||||
|
||||
// 月度视图中按年份分组的计算属性
|
||||
@@ -1442,6 +1394,16 @@ defineExpose({
|
||||
// 时间刻度更新
|
||||
updateTimeScale,
|
||||
})
|
||||
|
||||
// 处理开始计时事件
|
||||
const handleStartTimer = (task: Task) => {
|
||||
emit('start-timer', task)
|
||||
}
|
||||
// 处理停止计时事件
|
||||
const handleStopTimer = (task: Task) => {
|
||||
emit('stop-timer', task)
|
||||
}
|
||||
|
||||
// Task类型转换成Milestone类型, 需要返回一个Milestone对象
|
||||
const convertTaskToMilestone = (task: Task): Milestone => {
|
||||
// 保证 startDate 一定为 string,避免 undefined
|
||||
@@ -1627,6 +1589,16 @@ const generateMonthTimelineData = () => {
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 添加前置任务事件
|
||||
const handleAddPredecessor = (task: Task) => {
|
||||
emit('add-predecessor', task)
|
||||
}
|
||||
|
||||
// 添加后置任务事件
|
||||
const handleAddSuccessor = (task: Task) => {
|
||||
emit('add-successor', task)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -1648,7 +1620,7 @@ const generateMonthTimelineData = () => {
|
||||
class="timeline-year"
|
||||
:style="{ width: '719px' }"
|
||||
>
|
||||
<div class="year-label">{{ yearValue }}</div>
|
||||
<div class="year-label">{{ yearValue }}年</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1913,22 +1885,18 @@ const generateMonthTimelineData = () => {
|
||||
@drag-end="handleTaskBarDragEnd"
|
||||
@resize-end="handleTaskBarResizeEnd"
|
||||
@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>
|
||||
<!-- 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 里程碑对话框组件 -->
|
||||
<MilestoneDialog
|
||||
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 TaskDrawer } from './TaskDrawer.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 = {
|
||||
'zh-CN': {
|
||||
dateNotSet: '未设置',
|
||||
// TaskList Header
|
||||
taskName: '任务名称',
|
||||
predecessor: '前置任务',
|
||||
@@ -39,26 +38,8 @@ const messages = {
|
||||
// 月份格式
|
||||
monthFormat: (month: number) => `${month}月`,
|
||||
|
||||
// 月份名称
|
||||
monthNames: [
|
||||
'一月',
|
||||
'二月',
|
||||
'三月',
|
||||
'四月',
|
||||
'五月',
|
||||
'六月',
|
||||
'七月',
|
||||
'八月',
|
||||
'九月',
|
||||
'十月',
|
||||
'十一月',
|
||||
'十二月',
|
||||
],
|
||||
weekDays:['日', '一', '二', '三', '四', '五', '六'],
|
||||
|
||||
// 其他
|
||||
milestone: '里程碑',
|
||||
targetDate: '目标日期',
|
||||
today: '今天',
|
||||
// 里程碑对话框
|
||||
milestoneDetails: '里程碑详情',
|
||||
@@ -116,6 +97,13 @@ const messages = {
|
||||
newMilestone: '新建里程碑',
|
||||
editMilestone: '编辑里程碑',
|
||||
cancel: '取消',
|
||||
// 右键菜单
|
||||
startTimer: '开始计时',
|
||||
stopTimer: '停止计时',
|
||||
timerStarted: '任务计时已开始',
|
||||
timerStopped: '任务计时已停止',
|
||||
addPredecessor: '添加前置任务',
|
||||
addSuccessor: '添加后置任务',
|
||||
|
||||
// PDF导出相关
|
||||
pdfExportLoading: '正在生成PDF,请稍候...',
|
||||
@@ -175,9 +163,12 @@ const messages = {
|
||||
overtime: '超',
|
||||
overdue: '逾期',
|
||||
days: '天',
|
||||
// 计时确认弹窗
|
||||
timerConfirmPrefix: '即将为任务',
|
||||
timerConfirmSuffix: '计时,若有特殊说明请完善下面的描述',
|
||||
timerConfirmPlaceholder: '请输入计时说明',
|
||||
},
|
||||
'en-US': {
|
||||
dateNotSet:'Not set',
|
||||
// TaskList Header
|
||||
taskName: 'Task Name',
|
||||
predecessor: 'Predecessor',
|
||||
@@ -208,25 +199,8 @@ const messages = {
|
||||
// 月份格式
|
||||
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',
|
||||
targetDate: 'Target Date',
|
||||
today: 'Today',
|
||||
// 里程碑对话框
|
||||
milestoneDetails: 'Milestone Details',
|
||||
@@ -283,6 +257,13 @@ const messages = {
|
||||
newMilestone: 'New Milestone',
|
||||
editMilestone: 'Edit Milestone',
|
||||
cancel: 'Cancel',
|
||||
// Context menu
|
||||
timerStarted: 'Timer Started',
|
||||
timerStopped: 'Timer Stopped',
|
||||
startTimer: 'Start Timer',
|
||||
stopTimer: 'Stop Timer',
|
||||
addPredecessor: 'Add Predecessor',
|
||||
addSuccessor: 'Add Successor',
|
||||
|
||||
// PDF export
|
||||
pdfExportLoading: 'Generating PDF, please wait...',
|
||||
@@ -346,6 +327,10 @@ const messages = {
|
||||
overtime: 'Over',
|
||||
overdue: 'Overdue',
|
||||
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
|
||||
icon?: string
|
||||
level?: number
|
||||
// 计时相关字段
|
||||
isTimerRunning?: boolean
|
||||
timerStartTime?: number
|
||||
timerEndTime?: number // 结束计时时间
|
||||
timerStartDesc?: string // 计时开始时填写的描述
|
||||
timerElapsedTime?: number
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user