v1.2.0 - Add TaskContextMenu & new features; TaskDrawer is uniformly managed by GanttChart.
This commit is contained in:
17
CHANGELOG.md
17
CHANGELOG.md
@@ -5,6 +5,23 @@ 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.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
|
||||
|
||||
113
README-EN.md
113
README-EN.md
@@ -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)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
152
README.md
152
README.md
@@ -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 // 已累计计时时长(秒)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
76
demo/App.vue
76
demo/App.vue
@@ -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)
|
||||
@@ -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) {
|
||||
@@ -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及其所有子任务
|
||||
@@ -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()}${task.timerStartDesc ? `\n计时说明:${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,22 @@
|
||||
"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中管理"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "jordium-gantt-vue3",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"type": "module",
|
||||
"main": "dist/jordium-gantt-vue3.cjs.js",
|
||||
"module": "dist/jordium-gantt-vue3.es.js",
|
||||
|
||||
@@ -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导出处理器(可选)
|
||||
@@ -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) {
|
||||
@@ -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及其所有子任务
|
||||
@@ -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()}${task.timerStartDesc ? `\n计时说明:${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,27 @@
|
||||
"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中管理"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
132
src/components/ConfirmTimerDialog.vue
Normal file
132
src/components/ConfirmTimerDialog.vue
Normal file
@@ -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>
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
@@ -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') {
|
||||
@@ -398,6 +413,7 @@ onUnmounted(() => {
|
||||
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)
|
||||
})
|
||||
|
||||
// 全屏状态管理
|
||||
@@ -1169,6 +1185,279 @@ watch(
|
||||
},
|
||||
{ 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: [],
|
||||
}
|
||||
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: [],
|
||||
}
|
||||
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 +1478,7 @@ watch(
|
||||
:on-theme-change="props.onThemeChange"
|
||||
:on-fullscreen-change="props.onFullscreenChange"
|
||||
:on-time-scale-change="handleTimeScaleChange"
|
||||
@add-task="handleToolbarAddTask"
|
||||
/>
|
||||
|
||||
<!-- 甘特图主体 -->
|
||||
@@ -1204,6 +1494,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 +1538,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 +1848,8 @@ watch(
|
||||
}
|
||||
|
||||
.gantt-root.splitter-dragging .gantt-panel-right {
|
||||
/* 拖拽期间禁用Timeline区域的指针事件 */
|
||||
pointer-events: none;
|
||||
/* 拖拽时高亮右侧面板 */
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.gantt-root.splitter-dragging * {
|
||||
|
||||
@@ -2,6 +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 TaskContextMenu from './TaskContextMenu.vue'
|
||||
|
||||
interface Props {
|
||||
task: Task
|
||||
@@ -34,9 +35,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',
|
||||
])
|
||||
|
||||
// 日期工具函数 - 处理时区安全的日期创建和操作
|
||||
@@ -224,9 +230,7 @@ const taskStatus = computed(() => {
|
||||
})
|
||||
|
||||
// 判断是否已完成
|
||||
const isCompleted = computed(() => {
|
||||
return (props.task.progress || 0) >= 100
|
||||
})
|
||||
const isCompleted = computed(() => (props.task.progress || 0) >= 100)
|
||||
|
||||
// 计算完成部分的宽度
|
||||
const progressWidth = computed(() => {
|
||||
@@ -438,10 +442,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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -965,7 +973,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>
|
||||
@@ -989,6 +1031,7 @@ const calculatePositionFromTimelineData = (
|
||||
'short-task-bar': isShortTaskBar,
|
||||
'overflow-effect': needsOverflowEffect,
|
||||
}"
|
||||
@contextmenu="handleContextMenu"
|
||||
@dblclick="handleTaskBarDoubleClick"
|
||||
>
|
||||
<!-- 父级任务的标签 -->
|
||||
@@ -1049,6 +1092,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 弹窗 -->
|
||||
|
||||
591
src/components/TaskContextMenu.vue
Normal file
591
src/components/TaskContextMenu.vue
Normal file
@@ -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>
|
||||
@@ -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'
|
||||
@@ -195,6 +270,11 @@ 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'))
|
||||
@@ -207,6 +287,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, {
|
||||
@@ -283,11 +375,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 +388,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 +411,7 @@ const confirmDelete = () => {
|
||||
emit('delete', props.task)
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
showMessage(t.value.taskDeleteFailed, 'error')
|
||||
handleError(error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
@@ -326,14 +431,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 +447,7 @@ const handleDeleteNo = () => {
|
||||
emit('delete', props.task, false) // 传递false表示仅删除story
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
showMessage(t.value.taskDeleteFailed, 'error')
|
||||
handleError(error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
@@ -375,16 +479,225 @@ watch(
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 计时器本地状态,保证点击后UI立即切换
|
||||
// !!! 只保留一处 localTimerRunning 定义,彻底移除重复声明 !!!
|
||||
|
||||
// 只要 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 },
|
||||
)
|
||||
|
||||
// 修正计时器首次启动不跳动问题:每次打开抽屉时重置 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>
|
||||
@@ -606,6 +919,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
|
||||
@@ -97,6 +97,13 @@ const messages = {
|
||||
newMilestone: '新建里程碑',
|
||||
editMilestone: '编辑里程碑',
|
||||
cancel: '取消',
|
||||
// 右键菜单
|
||||
startTimer: '开始计时',
|
||||
stopTimer: '停止计时',
|
||||
timerStarted: '任务计时已开始',
|
||||
timerStopped: '任务计时已停止',
|
||||
addPredecessor: '添加前置任务',
|
||||
addSuccessor: '添加后置任务',
|
||||
|
||||
// PDF导出相关
|
||||
pdfExportLoading: '正在生成PDF,请稍候...',
|
||||
@@ -156,6 +163,10 @@ const messages = {
|
||||
overtime: '超',
|
||||
overdue: '逾期',
|
||||
days: '天',
|
||||
// 计时确认弹窗
|
||||
timerConfirmPrefix: '即将为任务',
|
||||
timerConfirmSuffix: '计时,若有特殊说明请完善下面的描述',
|
||||
timerConfirmPlaceholder: '请输入计时说明',
|
||||
},
|
||||
'en-US': {
|
||||
// TaskList Header
|
||||
@@ -246,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...',
|
||||
@@ -309,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