Merge branch 'master' into my-feature-partial

This commit is contained in:
nelson.li
2025-08-12 21:01:02 +08:00
committed by GitHub
24 changed files with 2140 additions and 407 deletions
+22
View File
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.2.1] - 2025-07-22
### Fixed
- SonarQube代码质量检查问题修改
## [1.2.0] - 2025-07-21
### Added
- 增加TaskRow/TaskBar的右键菜单
- 增加Task的执行定时
- 增加添加前置任务功能
- 增加添加后置任务功能
- 增加TaskRow/TaskBar的删除功能
- GanttChart增加@predecessor-added(添加前置任务完成)事件
- GanttChart增加@successor-added(添加后置任务完成)事件
- GanttChart增加@task-deleted(删除任务完成)事件
- GanttChart增加@task-added(添加任务完成)事件
- GanttChart增加@task-updated(编辑任务完成)事件
### Changed
- 升级TaskDrawer统一在GanttChart中管理
## [1.1.0] - 2025-07-08
### Added
+87 -26
View File
@@ -55,12 +55,6 @@ pnpm add jordium-gantt-vue3
---
> 💡 **Badge Downloads**
> - npm version badgehttps://img.shields.io/npm/v/jordium-gantt-vue3.svg
> - MIT license badgehttps://img.shields.io/badge/license-MIT-blue.svg
> - Vue version badgehttps://img.shields.io/badge/vue-3.x-green.svg
> - TypeScript badgehttps://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)
}
```
+108 -44
View File
@@ -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 // 任务类型(如 taskstorymilestone 等)
description?: string // 任务描述
icon?: string // 图标
level?: number // 层级
// 计时相关字段
isTimerRunning?: boolean // 计时是否进行中
timerStartTime?: number // 计时开始时间(时间戳)
timerEndTime?: number // 计时结束时间(时间戳)
timerStartDesc?: string // 计时开始时的描述
timerElapsedTime?: number // 已累计计时时长(秒)
}
```
+50 -42
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, onMounted, nextTick } from 'vue'
import GanttChart from '../src/components/GanttChart.vue'
import TaskDrawer from '../src/components/TaskDrawer.vue'
// import TaskDrawer from '../src/components/TaskDrawer.vue' // 移除
import MilestoneDialog from '../src/components/MilestoneDialog.vue'
import demoData from './data.json'
import packageInfo from '../package.json'
@@ -19,11 +19,6 @@ const { t, formatTranslation } = useI18n()
const tasks = ref<Task[]>([])
const milestones = ref<Task[]>([])
// TaskDrawer状态管理
const showTaskDrawer = ref(false)
const currentTask = ref<Task | null>(null)
const isEditMode = ref(false)
// MilestoneDialog状态管理
const showMilestoneDialog = ref(false)
const currentMilestone = ref<Task | null>(null)
@@ -123,14 +118,14 @@ const handleMilestoneDelete = async (milestoneId: number) => {
window.dispatchEvent(
new CustomEvent('milestone-deleted', {
detail: { milestoneId },
}),
})
)
// 触发强制更新事件,确保Timeline重新渲染
window.dispatchEvent(
new CustomEvent('milestone-data-changed', {
detail: { milestones: milestones.value },
}),
})
)
}
@@ -140,6 +135,12 @@ const handleMilestoneDelete = async (milestoneId: number) => {
// 任务更新处理器
const handleTaskUpdate = (updatedTask: Task) => {
// 计时信息展示(无论来源于 TaskBar/TaskRow 还是 TaskDrawer header
if (updatedTask.timerStartTime) {
const msg = `任务【${updatedTask.name}】已更新`
showMessage(msg, 'success', { closable: true })
}
// 先找到原任务,检查parentId是否改变了
const findOriginalTask = (taskArray: Task[]): Task | null => {
for (const task of taskArray) {
@@ -218,7 +219,7 @@ const handleTaskUpdate = (updatedTask: Task) => {
showMessage(
formatTranslation('newParentTaskNotFound', { parentId: taskToAdd.parentId }),
'warning',
{ closable: true },
{ closable: true }
)
tasks.value.push(taskToAdd)
}
@@ -266,7 +267,7 @@ const handleTaskAdd = (newTask: Task) => {
const maxId = Math.max(
...tasks.value.map(t => t.id || 0),
...milestones.value.map(m => m.id || 0),
0,
0
)
newTask.id = maxId + 1
}
@@ -361,7 +362,7 @@ const handleStoryDeleteWithChildren = (storyToDelete: Task) => {
'success',
{
closable: false,
},
}
)
return true
}
@@ -423,7 +424,7 @@ const handleStoryDeleteOnly = (storyToDelete: Task) => {
'success',
{
closable: false,
},
}
)
return true
}
@@ -460,24 +461,6 @@ const handleMilestoneIconChange = (milestoneId: number, icon: string) => {
}
}
// TaskDrawer事件处理器
const handleTaskDrawerSubmit = (task: Task) => {
if (isEditMode.value) {
// 编辑模式:更新任务
handleTaskUpdate(task)
} else {
// 新建模式:添加任务
handleTaskAdd(task)
}
showTaskDrawer.value = false
}
const handleTaskDrawerClose = () => {
showTaskDrawer.value = false
currentTask.value = null
isEditMode.value = false
}
const handleTaskDrawerDelete = (task: Task, deleteChildren?: boolean) => {
if (task.type === 'story' && deleteChildren === true) {
// 删除story及其所有子任务
@@ -515,7 +498,7 @@ function handleTaskbarDragOrResizeEnd(newTask) {
`开始: ${oldTask.startDate}${newTask.startDate}\n` +
`结束: ${oldTask.endDate}${newTask.endDate}`,
'info',
{ closable: true },
{ closable: true }
)
}
function handleMilestoneDragEnd(newMilestone) {
@@ -525,7 +508,7 @@ function handleMilestoneDragEnd(newMilestone) {
`里程碑【${newMilestone.name}\n` +
`开始: ${oldMilestone.endDate}${newMilestone.startDate}`,
'info',
{ closable: true },
{ closable: true }
)
}
@@ -586,6 +569,26 @@ const collectAllTaskIds = (task: Task): number[] => {
}
return ids
}
// Timer事件处理
function onTimerStarted(task: Task) {
showMessage(
`Demo 任务【${task.name}\n开始计时:${new Date(task.timerStartTime).toLocaleString()}\n计时说明:${task.timerStartDesc ? task.timerStartDesc : ''}`,
'info',
{ closable: true }
)
}
function onTimerStopped(task: Task) {
let msg = `Demo 任务【${task.name}`
if (task.timerStartTime) {
msg += `\n开始计时:${new Date(task.timerStartTime).toLocaleString()}`
msg += `\n结束计时:${new Date().toLocaleString()}`
if (task.timerStartDesc) msg += `\n计时说明:${task.timerStartDesc}`
} else {
msg += `\n结束计时:${new Date().toLocaleString()}`
}
showMessage(msg, 'info', { closable: true })
}
</script>
<template>
@@ -639,22 +642,27 @@ const collectAllTaskIds = (task: Task): number[] => {
@taskbar-drag-end="handleTaskbarDragOrResizeEnd"
@taskbar-resize-end="handleTaskbarDragOrResizeEnd"
@milestone-drag-end="handleMilestoneDragEnd"
@edit-task="task => showMessage(`进入任务编辑:${task.name}`)"
@close="() => showMessage('已关闭任务编辑', 'info')"
@timer-started="onTimerStarted"
@timer-stopped="onTimerStopped"
@predecessor-added="
e =>
showMessage(`Demo 任务[${e.targetTask.name}] 添加前置任务 [${e.newTask.name}]`, 'info')
"
@successor-added="
e =>
showMessage(`Demo 任务[${e.targetTask.name}] 添加后置任务 [${e.newTask.name}]`, 'info')
"
@task-deleted="e => showMessage(`Demo 任务[${e.task.name}] 已删除`, 'info')"
@task-added="e => showMessage(`Demo 任务[${e.task.name}] 已创建`, 'info')"
@task-updated="e => showMessage(`Demo 任务[${e.task.name}] 已更新`, 'info')"
/>
</div>
<div class="license-info">
<p>MIT License @JORDIUM.COM</p>
</div>
<!-- TaskDrawer用于新建/编辑任务 -->
<TaskDrawer
v-model:visible="showTaskDrawer"
:task="currentTask"
:is-edit="isEditMode"
@submit="handleTaskDrawerSubmit"
@close="handleTaskDrawerClose"
@delete="handleTaskDrawerDelete"
/>
<!-- MilestoneDialog用于新建/编辑里程碑 -->
<MilestoneDialog
:visible="showMilestoneDialog"
+22
View File
@@ -140,5 +140,27 @@
"version": "1.1.0",
"date": "2025-07-08",
"notes": ["TaskBar拖拽触发Timeline水平滑动", "增加日|周|月视图切换", "问题修复"]
},
{
"version": "1.2.0",
"date": "2025-07-21",
"notes": [
"增加TaskRow/TaskBar的右键菜单",
"增加Task的执行定时",
"增加添加前置任务功能",
"增加添加后置任务功能",
"增加TaskRow/TaskBar的删除功能",
"GanttChart增加@predecessor-added(添加前置任务完成)事件",
"GanttChart增加@successor-added(添加后置任务完成)事件",
"GanttChart增加@task-deleted(删除任务完成)事件",
"GanttChart增加@task-added(添加任务完成)事件",
"GanttChart增加@task-updated(编辑任务完成)事件",
"升级TaskDrawer统一在GanttChart中管理"
]
},
{
"version": "1.2.1",
"date": "2025-07-22",
"notes": ["SonarQube代码质量检查问题修改"]
}
]
-61
View File
@@ -1,61 +0,0 @@
# GitHub Pages Deployment Guide
This project has been configured with automatic deployment of GitHub Pages, and users can experience the complete Gantt charting function online.
## 🌐 Online visit
**Demo Address**: https://nelson820125.github.io/jordium-gantt-vue3/
## 🔧 Deployment Configuration
### Automated deployment
Projects are automatically deployed to GitHub Pages using GitHub Actions:
- **Triggering condition**: Push to `main` or `master` branch
- **Build Command**: `npm run build:pages`
- **Deployment Directory**: `./dist`
- **Workflows**: `.github/workflows/deploy-github-pages.yml`
### Enable GitHub Pages manually
1. Enter the GitHub repository settings page
2. Find the "Pages" setting options
3. Select "Source" as "GitHub Actions"
4. Push code to the main branch and automatically trigger deployment
## 📁 Construct the product
- **Development and Construction**: `npm run build` → `dist/`
- **GitHub Pages**: `npm run build:pages` → `dist/` (contains the correct base path)
- **NPM package build**: `npm run build:lib` → `npm-package/dist/`
## 🛠️ Local Preview
```bash
# Installation dependencies
npm install
# Start the development server
npm run dev
# Build and preview the production version
npm run build:pages
npm run preview
```
## 🔄 Update deployment
GitHub Actions will automatically:
1. Check out the code
2. Install Node.js and dependencies
3. Build demo application
4. Deploy to GitHub Pages
## 📝 Notes
- Deployment usually takes 1-5 minutes to take effect
- Make sure GitHub Pages is enabled in repository settings
- Custom domain names need to be configured in the `demo/public/CNAME` file
- Static resource paths use relative paths to ensure normal loading in the Pages environment
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "jordium-gantt-vue3",
"version": "1.0.10",
"version": "1.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "jordium-gantt-vue3",
"version": "1.0.10",
"version": "1.1.0",
"license": "MIT",
"dependencies": {
"date-fns": "^4.1.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "jordium-gantt-vue3",
"version": "1.1.0",
"version": "1.2.1",
"type": "module",
"main": "dist/jordium-gantt-vue3.cjs.js",
"module": "dist/jordium-gantt-vue3.es.js",
+51 -42
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, onMounted, nextTick } from 'vue'
import GanttChart from '../src/components/GanttChart.vue'
import TaskDrawer from '../src/components/TaskDrawer.vue'
// import TaskDrawer from '../src/components/TaskDrawer.vue' // 移除
import MilestoneDialog from '../src/components/MilestoneDialog.vue'
import demoData from './data.json'
import packageInfo from '../package.json'
@@ -19,11 +19,6 @@ const { t, formatTranslation } = useI18n()
const tasks = ref<Task[]>([])
const milestones = ref<Task[]>([])
// TaskDrawer状态管理
const showTaskDrawer = ref(false)
const currentTask = ref<Task | null>(null)
const isEditMode = ref(false)
// MilestoneDialog状态管理
const showMilestoneDialog = ref(false)
const currentMilestone = ref<Task | null>(null)
@@ -41,6 +36,7 @@ const toolbarConfig = {
showLanguage: true,
showTheme: true,
showFullscreen: true,
showTimeScale: true, // 控制日|周|月时间刻度按钮组的可见性
}
// 自定义CSV导出处理器(可选)
@@ -122,14 +118,14 @@ const handleMilestoneDelete = async (milestoneId: number) => {
window.dispatchEvent(
new CustomEvent('milestone-deleted', {
detail: { milestoneId },
}),
})
)
// 触发强制更新事件,确保Timeline重新渲染
window.dispatchEvent(
new CustomEvent('milestone-data-changed', {
detail: { milestones: milestones.value },
}),
})
)
}
@@ -139,6 +135,12 @@ const handleMilestoneDelete = async (milestoneId: number) => {
// 任务更新处理器
const handleTaskUpdate = (updatedTask: Task) => {
// 计时信息展示(无论来源于 TaskBar/TaskRow 还是 TaskDrawer header
if (updatedTask.timerStartTime) {
const msg = `任务【${updatedTask.name}】已更新`
showMessage(msg, 'success', { closable: true })
}
// 先找到原任务,检查parentId是否改变了
const findOriginalTask = (taskArray: Task[]): Task | null => {
for (const task of taskArray) {
@@ -217,7 +219,7 @@ const handleTaskUpdate = (updatedTask: Task) => {
showMessage(
formatTranslation('newParentTaskNotFound', { parentId: taskToAdd.parentId }),
'warning',
{ closable: true },
{ closable: true }
)
tasks.value.push(taskToAdd)
}
@@ -265,7 +267,7 @@ const handleTaskAdd = (newTask: Task) => {
const maxId = Math.max(
...tasks.value.map(t => t.id || 0),
...milestones.value.map(m => m.id || 0),
0,
0
)
newTask.id = maxId + 1
}
@@ -360,7 +362,7 @@ const handleStoryDeleteWithChildren = (storyToDelete: Task) => {
'success',
{
closable: false,
},
}
)
return true
}
@@ -422,7 +424,7 @@ const handleStoryDeleteOnly = (storyToDelete: Task) => {
'success',
{
closable: false,
},
}
)
return true
}
@@ -459,24 +461,6 @@ const handleMilestoneIconChange = (milestoneId: number, icon: string) => {
}
}
// TaskDrawer事件处理器
const handleTaskDrawerSubmit = (task: Task) => {
if (isEditMode.value) {
// 编辑模式:更新任务
handleTaskUpdate(task)
} else {
// 新建模式:添加任务
handleTaskAdd(task)
}
showTaskDrawer.value = false
}
const handleTaskDrawerClose = () => {
showTaskDrawer.value = false
currentTask.value = null
isEditMode.value = false
}
const handleTaskDrawerDelete = (task: Task, deleteChildren?: boolean) => {
if (task.type === 'story' && deleteChildren === true) {
// 删除story及其所有子任务
@@ -514,7 +498,7 @@ function handleTaskbarDragOrResizeEnd(newTask) {
`开始: ${oldTask.startDate}${newTask.startDate}\n` +
`结束: ${oldTask.endDate}${newTask.endDate}`,
'info',
{ closable: true },
{ closable: true }
)
}
function handleMilestoneDragEnd(newMilestone) {
@@ -524,7 +508,7 @@ function handleMilestoneDragEnd(newMilestone) {
`里程碑【${newMilestone.name}\n` +
`开始: ${oldMilestone.endDate}${newMilestone.startDate}`,
'info',
{ closable: true },
{ closable: true }
)
}
@@ -585,6 +569,26 @@ const collectAllTaskIds = (task: Task): number[] => {
}
return ids
}
// Timer事件处理
function onTimerStarted(task: Task) {
showMessage(
`Demo 任务【${task.name}\n开始计时:${new Date(task.timerStartTime).toLocaleString()}\n计时说明:${task.timerStartDesc ? task.timerStartDesc : ''}`,
'info',
{ closable: true }
)
}
function onTimerStopped(task: Task) {
let msg = `Demo 任务【${task.name}`
if (task.timerStartTime) {
msg += `\n开始计时:${new Date(task.timerStartTime).toLocaleString()}`
msg += `\n结束计时:${new Date().toLocaleString()}`
if (task.timerStartDesc) msg += `\n计时说明:${task.timerStartDesc}`
} else {
msg += `\n结束计时:${new Date().toLocaleString()}`
}
showMessage(msg, 'info', { closable: true })
}
</script>
<template>
@@ -638,22 +642,27 @@ const collectAllTaskIds = (task: Task): number[] => {
@taskbar-drag-end="handleTaskbarDragOrResizeEnd"
@taskbar-resize-end="handleTaskbarDragOrResizeEnd"
@milestone-drag-end="handleMilestoneDragEnd"
@edit-task="task => showMessage(`进入任务编辑:${task.name}`)"
@close="() => showMessage('已关闭任务编辑', 'info')"
@timer-started="onTimerStarted"
@timer-stopped="onTimerStopped"
@predecessor-added="
e =>
showMessage(`Demo 任务[${e.targetTask.name}] 添加前置任务 [${e.newTask.name}]`, 'info')
"
@successor-added="
e =>
showMessage(`Demo 任务[${e.targetTask.name}] 添加后置任务 [${e.newTask.name}]`, 'info')
"
@task-deleted="e => showMessage(`Demo 任务[${e.task.name}] 已删除`, 'info')"
@task-added="e => showMessage(`Demo 任务[${e.task.name}] 已创建`, 'info')"
@task-updated="e => showMessage(`Demo 任务[${e.task.name}] 已更新`, 'info')"
/>
</div>
<div class="license-info">
<p>MIT License @JORDIUM.COM</p>
</div>
<!-- TaskDrawer用于新建/编辑任务 -->
<TaskDrawer
v-model:visible="showTaskDrawer"
:task="currentTask"
:is-edit="isEditMode"
@submit="handleTaskDrawerSubmit"
@close="handleTaskDrawerClose"
@delete="handleTaskDrawerDelete"
/>
<!-- MilestoneDialog用于新建/编辑里程碑 -->
<MilestoneDialog
:visible="showMilestoneDialog"
+27
View File
@@ -135,5 +135,32 @@
"Timeline中sub-task隐藏后,关系线也随之隐藏",
"增强TaskBar可以存在多个前置任务"
]
},
{
"version": "1.1.0",
"date": "2025-07-08",
"notes": ["TaskBar拖拽触发Timeline水平滑动", "增加日|周|月视图切换", "问题修复"]
},
{
"version": "1.2.0",
"date": "2025-07-21",
"notes": [
"增加TaskRow/TaskBar的右键菜单",
"增加Task的执行定时",
"增加添加前置任务功能",
"增加添加后置任务功能",
"增加TaskRow/TaskBar的删除功能",
"GanttChart增加@predecessor-added(添加前置任务完成)事件",
"GanttChart增加@successor-added(添加后置任务完成)事件",
"GanttChart增加@task-deleted(删除任务完成)事件",
"GanttChart增加@task-added(添加任务完成)事件",
"GanttChart增加@task-updated(编辑任务完成)事件",
"升级TaskDrawer统一在GanttChart中管理"
]
},
{
"version": "1.2.1",
"date": "2025-07-22",
"notes": ["SonarQube代码质量检查问题修改"]
}
]
+132
View 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>
+5 -11
View File
@@ -5,7 +5,6 @@ import { useI18n } from '../composables/useI18n'
const { t} = useI18n()
interface Props {
modelValue?: string | [string, string]
type?: 'date' | 'daterange'
@@ -72,9 +71,6 @@ const formatDisplayDate = (dateStr: string) => {
const date = new Date(dateStr)
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
}
//
const displayValue = computed(() => {
if (props.type === 'daterange') {
@@ -471,7 +467,6 @@ const yearList = computed(() => {
return years
})
/*
//
const monthNames = [
'一月',
@@ -488,7 +483,6 @@ const monthNames = [
'十二月',
]
const weekDays = ['日', '一', '二', '三', '四', '五', '六']
*/
//
onMounted(() => {
@@ -745,7 +739,7 @@ const panelStyle = computed(() => {
&lt;&lt;
</button>
<span class="el-month-picker__header-label" @click="showYearSelector($event)">
{{ currentYear }}
{{ currentYear }}
</span>
<button type="button" class="el-picker-panel__icon-btn" @click="nextYear">
&gt;&gt;
@@ -753,7 +747,7 @@ const panelStyle = computed(() => {
</div>
<div class="el-month-picker__content">
<div
v-for="(monthName, index) in t.monthNames"
v-for="(monthName, index) in monthNames"
:key="index"
class="el-month-picker__item"
:class="{ 'is-current': index === currentMonth }"
@@ -775,10 +769,10 @@ const panelStyle = computed(() => {
</button>
<span class="el-date-picker__header-label">
<span class="el-date-picker__header-year" @click="showYearSelector($event)">
{{ currentYear }}
{{ currentYear }}
</span>
<span class="el-date-picker__header-month" @click="showMonthSelector($event)">
{{ t.monthNames[currentMonth] }}
{{ monthNames[currentMonth] }}
</span>
</span>
<button type="button" class="el-picker-panel__icon-btn" @click="nextMonth">
@@ -792,7 +786,7 @@ const panelStyle = computed(() => {
<div class="el-date-picker__content">
<!-- 星期标题 -->
<div class="el-date-table__header">
<div v-for="day in t.weekDays" :key="day" class="el-date-table__header-cell">
<div v-for="day in weekDays" :key="day" class="el-date-table__header-cell">
{{ day }}
</div>
</div>
+331 -10
View File
@@ -3,6 +3,7 @@ import { ref, onUnmounted, onMounted, computed, watch, nextTick, defineEmits } f
import TaskList from './TaskList.vue'
import Timeline from './Timeline.vue'
import GanttToolbar from './GanttToolbar.vue'
import TaskDrawer from './TaskDrawer.vue'
import { useI18n, setCustomMessages } from '../composables/useI18n'
import { formatPredecessorDisplay } from '../utils/predecessorUtils'
import jsPDF from 'jspdf'
@@ -37,7 +38,18 @@ const props = withDefaults(defineProps<Props>(), {
localeMessages: undefined,
})
const emit = defineEmits(['taskbar-drag-end', 'taskbar-resize-end', 'milestone-drag-end'])
const emit = defineEmits([
'taskbar-drag-end',
'taskbar-resize-end',
'milestone-drag-end',
'timer-started',
'timer-stopped',
'predecessor-added',
'successor-added',
'task-deleted',
'task-added',
'task-updated',
])
const { showMessage } = useMessage()
@@ -237,7 +249,7 @@ const toggleTaskList = () => {
window.dispatchEvent(
new CustomEvent('timeline-container-resized', {
detail: { source: 'manual-task-list-toggle' },
}),
})
)
})
}, 400)
@@ -253,7 +265,7 @@ const handleToggleTaskList = (event: CustomEvent) => {
window.dispatchEvent(
new CustomEvent('timeline-container-resized', {
detail: { source: 'task-list-toggle' },
}),
})
)
})
}
@@ -268,6 +280,7 @@ function handleTaskBarResizeEnd(event: CustomEvent) {
function handleMilestoneDragEnd(event: CustomEvent) {
emit('milestone-drag-end', event.detail)
}
onMounted(() => {
window.addEventListener('taskbar-drag-end', handleTaskBarDragEnd as EventListener)
window.addEventListener('taskbar-resize-end', handleTaskBarResizeEnd as EventListener)
@@ -285,7 +298,7 @@ const handleTaskCollapseChange = (task: Task) => {
const updateTaskCollapsedState = (
tasks: Task[],
targetId: number,
collapsed: boolean,
collapsed: boolean
): boolean => {
for (const t of tasks) {
if (t.id === targetId) {
@@ -356,7 +369,7 @@ watch(
notifyTaskListUpdated()
})
},
{ deep: true, immediate: true },
{ deep: true, immediate: true }
)
onMounted(() => {
@@ -375,6 +388,8 @@ onMounted(() => {
window.addEventListener('request-task-list', handleRequestTaskList as EventListener)
//
window.addEventListener('resize', handleWindowResize)
// TaskBar
window.addEventListener('context-menu', handleTaskContextMenu as EventListener)
nextTick(() => {
if (timelineRef.value && typeof timelineRef.value.scrollToTodayCenter === 'function') {
@@ -392,12 +407,13 @@ onUnmounted(() => {
window.removeEventListener('task-added', handleTaskAdd as EventListener)
window.removeEventListener(
'milestone-icon-changed',
handleMilestoneIconChangeEvent as EventListener,
handleMilestoneIconChangeEvent as EventListener
)
window.removeEventListener('milestone-deleted', handleMilestoneDeleted as EventListener)
window.removeEventListener('milestone-data-changed', handleMilestoneDataChanged as EventListener)
window.removeEventListener('request-task-list', handleRequestTaskList as EventListener)
window.removeEventListener('resize', handleWindowResize)
window.removeEventListener('context-menu', handleTaskContextMenu as EventListener)
})
//
@@ -1167,8 +1183,289 @@ watch(
val => {
if (val) setCustomMessages(locale.value, val)
},
{ deep: true },
{ deep: true }
)
//
const contextMenuPosition = ref({ x: 0, y: 0 })
const contextMenuVisible = ref(false)
const contextMenuTask = ref<Task | null>(null)
// TaskDrawer
const taskDrawerVisible = ref(false)
const taskDrawerTask = ref<Task | null>(null)
const taskDrawerEditMode = ref(false)
//
const taskToAddPredecessorTo = ref<Task | null>(null) //
//
const taskToAddSuccessorTo = ref<Task | null>(null) //
//
const handleTaskContextMenu = (event: CustomEvent) => {
//
const { task, position } = event.detail
//
contextMenuTask.value = task
contextMenuPosition.value = position
contextMenuVisible.value = true
}
//
const closeContextMenu = () => {
contextMenuVisible.value = false
}
//
function handleToolbarAddTask() {
//
const newTask: Task = {
id: Date.now(), // id
name: '',
type: 'task',
assignee: '',
startDate: '',
endDate: '',
predecessor: [],
estimatedHours: 0,
actualHours: 0,
progress: 0,
description: '',
parentId: undefined,
children: [],
}
taskDrawerTask.value = newTask
taskDrawerEditMode.value = false
taskDrawerVisible.value = true
}
// TaskDrawerTaskListTimeline
const handleStartTimer = (task: Task) => {
//
if (props.tasks) {
const updateTask = (tasks: Task[]): boolean => {
for (let i = 0; i < tasks.length; i++) {
if (tasks[i].id === task.id) {
tasks[i].isTimerRunning = true
tasks[i].timerStartTime = task.timerStartTime || Date.now()
tasks[i].timerEndTime = undefined
tasks[i].timerElapsedTime = 0
return true
}
if (tasks[i].children?.length) {
if (updateTask(tasks[i].children as Task[])) return true
}
}
return false
}
updateTask(props.tasks)
}
closeContextMenu()
emit('timer-started', task)
}
const handleStopTimer = (task: Task) => {
//
if (props.tasks) {
const updateTask = (tasks: Task[]): boolean => {
for (let i = 0; i < tasks.length; i++) {
if (tasks[i].id === task.id) {
if (tasks[i].isTimerRunning && tasks[i].timerStartTime !== undefined) {
const elapsed = tasks[i].timerElapsedTime || 0
tasks[i].timerElapsedTime = elapsed + (Date.now() - tasks[i].timerStartTime!)
tasks[i].timerEndTime = Date.now()
}
tasks[i].isTimerRunning = false
if (
taskDrawerVisible.value &&
taskDrawerTask.value &&
taskDrawerTask.value.id === task.id
) {
taskDrawerTask.value.isTimerRunning = false
taskDrawerTask.value.timerEndTime = Date.now()
}
return true
}
if (tasks[i].children?.length) {
if (updateTask(tasks[i].children as Task[])) return true
}
}
return false
}
updateTask(props.tasks)
}
closeContextMenu()
emit('timer-stopped', task)
}
// Timeline
function handleTimelineEditTask(task: Task) {
taskDrawerTask.value = task
taskDrawerEditMode.value = true
taskDrawerVisible.value = true
}
//
function handleAddPredecessor(targetTask: Task) {
if (!targetTask) return
// 1.
taskToAddPredecessorTo.value = targetTask
// 2. TaskDrawer
// parentId
const newTask: Task = {
id: Date.now(), // id
name: '',
type: 'task',
assignee: '',
startDate: '',
endDate: '',
predecessor: [],
estimatedHours: 0,
actualHours: 0,
progress: 0,
description: '',
parentId: targetTask.parentId,
children: [],
isTimerRunning: false,
timerStartTime: undefined,
timerEndTime: undefined,
timerElapsedTime: 0,
}
taskDrawerTask.value = newTask
taskDrawerEditMode.value = false
taskDrawerVisible.value = true
}
//
function handleAddSuccessor(targetTask: Task) {
if (!targetTask) return
//
taskToAddSuccessorTo.value = targetTask
// parentId predecessor id
const newTask: Task = {
id: Date.now(), // id
name: '',
type: 'task',
assignee: '',
startDate: '',
endDate: '',
predecessor: [targetTask.id],
estimatedHours: 0,
actualHours: 0,
progress: 0,
description: '',
parentId: targetTask.parentId,
children: [],
isTimerRunning: false,
timerStartTime: undefined,
timerEndTime: undefined,
timerElapsedTime: 0,
}
taskDrawerTask.value = newTask
taskDrawerEditMode.value = false
taskDrawerVisible.value = true
}
// Task
// parentId TaskDrawer
const insertTask = (tasks: Task[], newTask: Task) => {
if (!newTask.parentId) {
tasks.push(newTask)
return true
}
for (const t of tasks) {
if (t.id === newTask.parentId) {
if (!t.children) t.children = []
t.children.push(newTask)
return true
}
if (t.children && t.children.length > 0) {
if (insertTask(t.children, newTask)) return true
}
}
return false
}
//
const updateTaskInTree = (tasks: Task[], updatedTask: Task): boolean => {
for (let i = 0; i < tasks.length; i++) {
if (tasks[i].id === updatedTask.id) {
tasks[i] = { ...tasks[i], ...updatedTask }
return true
}
if (tasks[i].children && (tasks[i].children as Task[]).length > 0) {
if (updateTaskInTree(tasks[i].children as Task[], updatedTask)) return true
}
}
return false
}
// handleTaskDrawerSubmit id predecessor
function handleTaskDrawerSubmit(task: Task) {
if (!taskDrawerEditMode.value) {
if (props.tasks) {
insertTask(props.tasks, task)
}
// emit
emit('task-added', { task })
if (taskToAddPredecessorTo.value) {
if (!taskToAddPredecessorTo.value.predecessor) {
taskToAddPredecessorTo.value.predecessor = []
}
taskToAddPredecessorTo.value.predecessor.push(task.id)
// emit
emit('predecessor-added', { targetTask: taskToAddPredecessorTo.value, newTask: task })
taskToAddPredecessorTo.value = null
}
if (taskToAddSuccessorTo.value) {
// emit
emit('successor-added', { targetTask: taskToAddSuccessorTo.value, newTask: task })
taskToAddSuccessorTo.value = null
}
} else {
if (props.tasks) {
updateTaskInTree(props.tasks, task)
}
updateTaskTrigger.value++
// emit
emit('task-updated', { task })
}
}
// deleteChildren
function removeTaskFromTree(tasks: Task[], taskId: number, deleteChildren?: boolean): boolean {
for (let i = 0; i < tasks.length; i++) {
if (tasks[i].id === taskId) {
if (deleteChildren) {
// splice
tasks.splice(i, 1)
} else {
// children
const children = tasks[i].children || []
tasks.splice(i, 1, ...children)
}
return true
}
if (tasks[i].children && (tasks[i].children as Task[]).length > 0) {
if (removeTaskFromTree(tasks[i].children as Task[], taskId, deleteChildren)) return true
}
}
return false
}
// Task
function handleTaskDelete(task: Task, deleteChildren?: boolean) {
if (props.tasks) {
removeTaskFromTree(props.tasks, task.id, deleteChildren)
}
taskDrawerVisible.value = false
taskDrawerTask.value = null
// emit
emit('task-deleted', { task })
}
</script>
<template>
@@ -1189,6 +1486,7 @@ watch(
:on-theme-change="props.onThemeChange"
:on-fullscreen-change="props.onFullscreenChange"
:on-time-scale-change="handleTimeScaleChange"
@add-task="handleToolbarAddTask"
/>
<!-- 甘特图主体 -->
@@ -1204,6 +1502,11 @@ watch(
:edit-component="props.editComponent"
:use-default-drawer="props.useDefaultDrawer"
@task-collapse-change="handleTaskCollapseChange"
@start-timer="handleStartTimer"
@stop-timer="handleStopTimer"
@add-predecessor="handleAddPredecessor"
@add-successor="handleAddSuccessor"
@delete="handleTaskDelete"
/>
</div>
<div class="gantt-splitter" @mousedown="onMouseDown">
@@ -1243,12 +1546,30 @@ watch(
:on-task-double-click="props.onTaskDoubleClick"
:edit-component="props.editComponent"
:use-default-drawer="props.useDefaultDrawer"
:on-task-delete="props.onTaskDelete"
:on-milestone-save="handleMilestoneSave"
@timeline-scale-changed="handleTimelineScaleChanged"
@edit-task="handleTimelineEditTask"
@start-timer="handleStartTimer"
@stop-timer="handleStopTimer"
@add-predecessor="handleAddPredecessor"
@add-successor="handleAddSuccessor"
@delete="handleTaskDelete"
/>
</div>
</div>
<!-- 任务抽屉组件 - 用于添加前置任务 -->
<TaskDrawer
v-if="props.useDefaultDrawer"
v-model:visible="taskDrawerVisible"
:task="taskDrawerTask"
:is-edit="taskDrawerEditMode"
@submit="handleTaskDrawerSubmit"
@close="taskDrawerVisible = false"
@start-timer="handleStartTimer"
@stop-timer="handleStopTimer"
@delete="handleTaskDelete"
/>
</div>
</template>
@@ -1535,8 +1856,8 @@ watch(
}
.gantt-root.splitter-dragging .gantt-panel-right {
/* 拖拽期间禁用Timeline区域的指针事件 */
pointer-events: none;
/* 拖拽时高亮右侧面板 */
background: rgba(255, 255, 255, 0.1);
}
.gantt-root.splitter-dragging * {
+8 -7
View File
@@ -485,26 +485,26 @@ const handleMilestoneMouseLeave = () => {
//
const formatDisplayDate = (dateStr: string): string => {
if (!dateStr) return t('dateNotSet') //Not Set
if (!dateStr) return '未设置'
try {
const date = new Date(dateStr)
if (isNaN(date.getTime())) return t('dateNotSet')
if (isNaN(date.getTime())) return '未设置'
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
} catch {
return t('dateNotSet')
return '未设置'
}
}
// Tooltip Target date
// Tooltip
const tooltipContent = computed(() => {
const milestoneName = props.name || props.milestone?.name || t('milestone')
const milestoneName = props.name || props.milestone?.name || '里程碑'
const targetDate = formatDisplayDate(props.date || props.milestone?.startDate || '')
return `${t('milestone')}${milestoneName} <br> ${t('targetDate')}${targetDate}`
return `里程碑:${milestoneName} - 目标日期${targetDate}`
})
//
@@ -688,7 +688,8 @@ const calculateMilestonePositionFromTimelineData = (
top: `${tooltipPosition.y}px`,
}"
>
<div class="tooltip-content" v-html="tooltipContent">
<div class="tooltip-content">
{{ tooltipContent }}
</div>
</div>
</Teleport>
+9 -4
View File
@@ -41,7 +41,7 @@ const availableTasks = computed(() => {
task =>
task.type === 'task' &&
task.id !== props.currentTaskId &&
!selectedPredecessorIds.value.includes(task.id),
!selectedPredecessorIds.value.includes(task.id)
)
})
@@ -69,13 +69,13 @@ watch(
() => {
selectedValue.value = ''
},
{ immediate: true },
{ immediate: true }
)
</script>
<template>
<div class="multi-select-predecessor">
<label class="form-label">{{ label }}</label>
<label class="form-label" for="predecessor-select">{{ label }}</label>
<div class="predecessor-selector">
<!-- 已选择的前置任务标签 -->
<div v-if="selectedPredecessors.length > 0" class="selected-tags">
@@ -89,7 +89,12 @@ watch(
<!-- 下拉选择器 -->
<div class="select-wrapper">
<select v-model="selectedValue" class="form-select" @change="addPredecessor">
<select
id="predecessor-select"
v-model="selectedValue"
class="form-select"
@change="addPredecessor"
>
<option value="">{{ placeholder }}</option>
<option v-for="task in availableTasks" :key="task.id" :value="task.id">
{{ task.name }} (ID: {{ task.id }})
+69 -18
View File
@@ -2,10 +2,7 @@
import { ref, computed, onUnmounted, onMounted, nextTick, watch } from 'vue'
import type { Task } from '../models/classes/Task'
import { TimelineScale } from '../models/types/TimelineScale'
import { useI18n } from '../composables/useI18n'
import TaskContextMenu from './TaskContextMenu.vue'
interface Props {
task: Task
@@ -44,9 +41,14 @@ const emit = defineEmits([
'update:task',
'bar-mounted',
'dblclick',
'drag-end', //
'resize-end', //
'scroll-to-position', //
'drag-end',
'resize-end',
'scroll-to-position',
'start-timer',
'stop-timer',
'add-predecessor',
'add-successor',
'delete',
])
// -
@@ -234,9 +236,7 @@ const taskStatus = computed(() => {
})
//
const isCompleted = computed(() => {
return (props.task.progress || 0) >= 100
})
const isCompleted = computed(() => (props.task.progress || 0) >= 100)
//
const progressWidth = computed(() => {
@@ -448,10 +448,14 @@ onMounted(() => {
window.addEventListener('timeline-scale-updated', handleTimelineScaleUpdate)
window.addEventListener('timeline-force-recalculate', handleForceRecalculate)
//
window.addEventListener('close-all-taskbar-menus', closeContextMenu)
//
onUnmounted(() => {
window.removeEventListener('timeline-scale-updated', handleTimelineScaleUpdate)
window.removeEventListener('timeline-force-recalculate', handleForceRecalculate)
window.removeEventListener('close-all-taskbar-menus', closeContextMenu)
})
})
@@ -835,9 +839,9 @@ const handleBubbleMouseDown = (event: MouseEvent) => {
//
const formatDisplayDate = (dateStr: string | undefined): string => {
if (!dateStr) return t('dateNotSet')
if (!dateStr) return '未设置'
const date = createLocalDate(dateStr)
if (!date) return t('dateNotSet')
if (!date) return '未设置'
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
@@ -975,7 +979,41 @@ const calculatePositionFromTimelineData = (
return cumulativePosition //
}
// ...existing code...
//
const contextMenuVisible = ref(false)
const contextMenuPosition = ref({ x: 0, y: 0 })
const contextMenuTask = computed(() => props.task)
function handleContextMenu(event: MouseEvent) {
// 广TaskBar
window.dispatchEvent(new CustomEvent('close-all-taskbar-menus'))
if (props.task.type !== 'task' && props.task.type !== 'story') {
//
event.preventDefault()
contextMenuVisible.value = false
return
}
event.preventDefault()
contextMenuVisible.value = true
contextMenuPosition.value = { x: event.clientX, y: event.clientY }
}
function closeContextMenu() {
contextMenuVisible.value = false
}
const handleTaskDelete = (task: Task, deleteChildren?: boolean) => {
//
emit('delete', task, deleteChildren)
closeContextMenu()
}
//
onMounted(() => {
window.addEventListener('close-all-taskbar-menus', closeContextMenu)
})
onUnmounted(() => {
window.removeEventListener('close-all-taskbar-menus', closeContextMenu)
})
</script>
<template>
@@ -999,6 +1037,7 @@ const calculatePositionFromTimelineData = (
'short-task-bar': isShortTaskBar,
'overflow-effect': needsOverflowEffect,
}"
@contextmenu="handleContextMenu"
@dblclick="handleTaskBarDoubleClick"
>
<!-- 父级任务的标签 -->
@@ -1059,6 +1098,18 @@ const calculatePositionFromTimelineData = (
@mousedown="handleBubbleMouseDown"
@click="handleBubbleClick"
></div>
<TaskContextMenu
:visible="contextMenuVisible"
:task="contextMenuTask"
:position="contextMenuPosition"
@close="closeContextMenu"
@start-timer="$emit('start-timer', props.task)"
@stop-timer="$emit('stop-timer', props.task)"
@add-predecessor="$emit('add-predecessor', props.task)"
@add-successor="$emit('add-successor', props.task)"
@delete="handleTaskDelete"
/>
</div>
<!-- Tooltip 弹窗 -->
@@ -1075,23 +1126,23 @@ const calculatePositionFromTimelineData = (
<div class="tooltip-title">{{ task.name }}</div>
<div class="tooltip-content">
<div class="tooltip-row">
<span class="tooltip-label"> {{ t('startDate') }}:</span>
<span class="tooltip-label">计划开始:</span>
<span class="tooltip-value">{{ formatDisplayDate(task.startDate) }}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">{{ t('endDate') }}:</span>
<span class="tooltip-label">计划结束:</span>
<span class="tooltip-value">{{ formatDisplayDate(task.endDate) }}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">{{ t('estimatedHours') }}:</span>
<span class="tooltip-label">计划工时:</span>
<span class="tooltip-value">{{ workHourInfo.total }}h</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label"> {{ t('actualHours') }}:</span>
<span class="tooltip-label">已用工时:</span>
<span class="tooltip-value">{{ workHourInfo.used }}h</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label"> {{ t('progress') }}:</span>
<span class="tooltip-label">完成率:</span>
<span class="tooltip-value">{{ task.progress || 0 }}%</span>
</div>
</div>
+591
View 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) {
// falsestory
emit('delete', props.task, false) // falsestory
closeMenu()
}
}
// story
const showTimerMenu = computed(() => props.task?.type !== 'story')
//
watch(
[() => props.visible, () => props.position],
([visible, newPosition]) => {
//
if (visible && newPosition) {
nextTick(() => {
if (menuRef.value) {
//
const viewportWidth = window.innerWidth
const viewportHeight = window.innerHeight
const menuWidth = menuRef.value.offsetWidth
const menuHeight = menuRef.value.offsetHeight
//
let adjustedX = newPosition.x - menuWidth / 2 //
let adjustedY = newPosition.y + 10 // 10px
let arrowLeft = '50%' //
let arrowTop = '-8px' //
let arrowTransform = 'rotate(0deg)' //
//
if (adjustedX + menuWidth > viewportWidth) {
const overflow = adjustedX + menuWidth - viewportWidth + 5
adjustedX -= overflow
//
arrowLeft = `calc(50% + ${overflow}px)`
}
//
if (adjustedX < 5) {
const overflow = 5 - adjustedX
adjustedX = 5
//
arrowLeft = `calc(50% - ${overflow}px)`
}
// -
if (adjustedY + menuHeight > viewportHeight) {
adjustedY = newPosition.y - menuHeight - 10
arrowTop = '100%' //
arrowTransform = 'rotate(180deg)' //
}
//
adjustedPosition.value = {
x: adjustedX,
y: adjustedY,
}
arrowStyle.value = {
display: 'block',
left: arrowLeft,
top: arrowTop,
transform: arrowTransform,
}
}
})
}
},
{ immediate: true, deep: true },
)
//
const closeMenu = () => {
emit('close')
}
//
const handleStartTimer = () => {
if (props.task?.isTimerRunning) {
emit('close')
// timerEndTime 便 formatTimerStopMessage
Object.assign(props.task, {
isTimerRunning: false,
timerEndTime: Date.now(),
timerElapsedTime:
(props.task.timerElapsedTime || 0) +
(Date.now() - (props.task.timerStartTime || Date.now())),
})
emit('stop-timer', props.task)
} else {
//
openTimerConfirm()
}
}
const handleAddPredecessor = () => {
if (props.task) {
//
emit('close')
nextTick(() => {
emit('add-predecessor', props.task)
})
}
}
const handleAddSuccessor = () => {
if (props.task) {
//
emit('close')
nextTick(() => {
emit('add-successor', props.task)
})
}
}
//
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.value && !menuRef.value.contains(event.target as Node)) {
closeMenu()
}
}
// ESC
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
closeMenu()
}
}
onMounted(() => {
//
document.addEventListener('mousedown', handleClickOutside)
document.addEventListener('keydown', handleKeyDown)
})
onUnmounted(() => {
//
document.removeEventListener('mousedown', handleClickOutside)
document.removeEventListener('keydown', handleKeyDown)
})
</script>
<template>
<teleport to="body">
<div
v-if="visible"
ref="menuRef"
class="task-context-menu"
:style="{
left: `${adjustedPosition.x}px`,
top: `${adjustedPosition.y}px`,
zIndex: 3000,
position: 'fixed',
}"
>
<!-- 气泡箭头 -->
<div
class="menu-arrow"
:style="{
display: arrowStyle.display,
left: arrowStyle.left,
top: arrowStyle.top,
transform: arrowStyle.transform,
}"
></div>
<div v-if="showTimerMenu" class="menu-item" @click="handleStartTimer">
<div class="icon-wrapper">
<i class="menu-icon" :class="props.task?.isTimerRunning ? 'stop-icon' : 'timer-icon'"></i>
</div>
{{ props.task?.isTimerRunning ? t.stopTimer : t.startTimer }}
</div>
<div class="menu-item" @click="handleAddPredecessor">
<div class="icon-wrapper">
<i class="menu-icon predecessor-icon"></i>
</div>
{{ t.addPredecessor }}
</div>
<div class="menu-item" @click="handleAddSuccessor">
<div class="icon-wrapper">
<i class="menu-icon successor-icon"></i>
</div>
{{ t.addSuccessor }}
</div>
<div class="menu-divider"></div>
<div class="menu-item menu-item-danger" @click="handleDeleteClick">
<div class="icon-wrapper">
<i class="menu-icon delete-icon"></i>
</div>
{{ t.delete }}
</div>
<!-- 删除确认弹窗 -->
<GanttConfirmDialog
:visible="showDeleteConfirm"
:title="t.delete"
:type="dialogType"
:message="dialogMessage"
:confirm-text="t.confirm"
:cancel-text="t.cancel"
:yes-text="t.storyDeleteYes"
:no-text="t.storyDeleteNo"
@confirm="confirmDelete"
@yes="handleDeleteYes"
@no="handleDeleteNo"
@cancel="cancelDelete"
/>
<!-- 确认计时对话框 -->
<ConfirmTimerDialog
v-if="showTimerConfirm"
:visible="showTimerConfirm"
:title="'确认开始计时'"
:message="`即将为任务${props.task?.name}计时,若有特殊说明请完善下面的描述`"
:default-desc="props.task?.name || ''"
@confirm="confirmTimer"
@cancel="cancelTimerConfirm"
/>
</div>
</teleport>
</template>
<style scoped>
.task-context-menu {
position: fixed;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
padding: 4px 0;
width: 180px; /* 调整固定宽度,确保文本不会被截断 */
z-index: 1000;
user-select: none;
animation: fadeIn 0.15s ease-out;
border: 1px solid #e4e7ed;
}
.menu-item {
padding: 6px 12px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: flex-start;
font-size: 14px;
transition: all 0.2s ease;
color: #333;
gap: 10px; /* 增加图标与文本之间的间距 */
height: 36px; /* 保持高度以适应32px的图标 */
}
.menu-item:hover {
background-color: #f5f7fa;
color: #409eff;
}
.icon-wrapper {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.menu-icon {
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
flex-shrink: 0; /* 防止图标被压缩 */
border-radius: 2px;
overflow: visible;
}
/* 计时图标保持32px,前置后置任务图标使用20px */
.timer-icon,
.stop-icon {
width: 32px;
height: 32px;
}
.predecessor-icon,
.successor-icon {
width: 20px;
height: 20px;
}
.timer-icon::before {
content: '';
position: absolute;
width: 20px;
height: 20px;
border: 2px solid currentColor;
border-radius: 50%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
box-sizing: border-box;
}
/* 右箭头样式 */
.timer-icon::after {
content: '';
position: absolute;
width: 0;
height: 0;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-left: 8px solid currentColor;
top: 50%;
left: 50%;
transform: translate(-30%, -50%);
}
.predecessor-icon::before {
content: '';
position: absolute;
width: 12px;
height: 2px;
background-color: currentColor;
top: 50%;
left: 50%;
transform: translate(-35%, -50%);
}
.predecessor-icon::after {
content: '';
position: absolute;
width: 5px;
height: 5px;
border-left: 2px solid currentColor;
border-bottom: 2px solid currentColor;
top: 50%;
left: 50%;
transform: translate(-120%, -50%) rotate(45deg);
}
.successor-icon::before {
content: '';
position: absolute;
width: 12px;
height: 2px;
background-color: currentColor;
top: 50%;
left: 50%;
transform: translate(-65%, -50%);
}
.successor-icon::after {
content: '';
position: absolute;
width: 5px;
height: 5px;
border-right: 2px solid currentColor;
border-top: 2px solid currentColor;
top: 50%;
left: 50%;
transform: translate(20%, -50%) rotate(45deg);
}
/* 停止计时图标 */
.stop-icon::before {
content: '';
position: absolute;
width: 20px;
height: 20px;
border: 2px solid currentColor;
border-radius: 50%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
box-sizing: border-box;
}
.stop-icon::after {
content: '';
position: absolute;
width: 8px;
height: 8px;
background-color: currentColor;
border-radius: 0;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* 菜单气泡箭头 */
.menu-arrow {
position: absolute;
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-bottom: 8px solid #fff; /* 匹配菜单背景色 */
transform-origin: center;
filter: drop-shadow(0 -1px 2px rgba(0, 0, 0, 0.1)); /* 为箭头添加阴影效果 */
z-index: 1001;
pointer-events: none; /* 确保箭头不会干扰鼠标事件 */
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* 暗色主题支持 */
:global(html[data-theme='dark']) .task-context-menu {
background-color: #2c2c2c;
border-color: #444444;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
}
:global(html[data-theme='dark']) .menu-item {
color: #e5e5e5;
}
:global(html[data-theme='dark']) .menu-item:hover {
background-color: #3a3a3a;
color: #409eff;
}
/* 暗色主题下箭头颜色 */
:global(html[data-theme='dark']) .menu-arrow {
border-bottom-color: #2c2c2c; /* 匹配暗色菜单背景色 */
filter: drop-shadow(0 -1px 2px rgba(0, 0, 0, 0.25));
}
.menu-item-danger {
color: #e74c3c;
}
.menu-item-danger:hover {
background-color: #faeaea;
color: #c0392b;
}
.menu-icon.delete-icon {
width: 20px;
height: 20px;
display: inline-block;
background: none;
position: relative;
}
.menu-icon.delete-icon::before {
content: '';
display: block;
width: 16px;
height: 16px;
margin: 2px auto;
background: url('data:image/svg+xml;utf8,<svg fill="%23e74c3c" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m2 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14z" stroke="%23e74c3c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>')
no-repeat center center;
background-size: contain;
}
.menu-divider {
height: 1px;
background: #ececec;
margin: 4px 0;
width: 92%;
margin-left: 4%;
}
</style>
+314 -20
View File
@@ -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 isTimerRunningtimerStartTimetimerElapsedTime header
watch(
() => [props.task?.isTimerRunning, props.task?.timerStartTime, props.task?.timerElapsedTime],
() => {
updateTimer()
}
)
// UI
const localTimerRunning = ref(false)
// props.task
watch(
() => props.task?.isTimerRunning,
val => {
localTimerRunning.value = !!val
if (!val && timerInterval.value) {
clearInterval(timerInterval.value)
timerInterval.value = null
}
},
{ immediate: true }
)
//
watch(
[localTimerRunning, timerStartTime, timerElapsedTime],
() => {
if (timerInterval.value) {
clearInterval(timerInterval.value)
timerInterval.value = null
}
if (localTimerRunning.value && timerStartTime.value) {
updateTimer()
timerInterval.value = window.setInterval(updateTimer, 1000)
} else {
updateTimer()
}
},
{ immediate: true }
)
onUnmounted(() => {
if (timerInterval.value) {
clearInterval(timerInterval.value)
}
})
// dialog
const dialogType = computed(() => {
return props.task?.type === 'story' ? 'yes-no-cancel' : 'confirm-cancel'
@@ -63,6 +138,10 @@ const formData = reactive<Task>({
progress: 0,
description: '',
parentId: undefined, // ID
isTimerRunning: false, //
timerStartTime: undefined, //
timerEndTime: undefined, //
timerElapsedTime: 0, //
})
//
@@ -74,7 +153,7 @@ const availableParentTasks = computed(() => {
.filter(
task =>
task.id !== props.task?.id && //
(task.type === 'story' || task.type === 'task'), // storytask
(task.type === 'story' || task.type === 'task') // storytask
)
.map(task => ({
...task,
@@ -195,11 +274,16 @@ watch(
if (props.task && props.isEdit) {
//
Object.assign(formData, props.task)
} else if (props.task && !props.isEdit) {
//
formData.parentId = props.task.parentId ?? undefined
//
formData.predecessor = props.task.predecessor ?? []
}
//
window.dispatchEvent(new CustomEvent('request-task-list'))
}
},
}
)
// isVisible
@@ -207,6 +291,18 @@ watch(isVisible, newVal => {
emit('update:visible', newVal)
})
// task parentId
watch(
() => props.task,
newTask => {
if (newTask && !props.isEdit) {
// parentId parentId
formData.parentId = newTask.parentId ?? undefined
}
},
{ immediate: true }
)
//
const resetForm = () => {
Object.assign(formData, {
@@ -221,6 +317,10 @@ const resetForm = () => {
progress: 0,
description: '',
parentId: undefined,
isTimerRunning: false,
timerStartTime: undefined,
timerEndTime: undefined,
timerElapsedTime: 0,
})
//
@@ -283,11 +383,9 @@ const handleSubmit = async () => {
id: props.isEdit && props.task ? props.task.id : Date.now(),
}
emit('submit', taskData)
showMessage(props.isEdit ? t.value.taskUpdateSuccess : t.value.taskCreateSuccess, 'success')
handleClose()
} catch (error) {
//
showMessage(t.value.operationFailed, 'error')
} finally {
submitting.value = false
}
@@ -298,6 +396,21 @@ const handleDelete = () => {
showDeleteConfirm.value = true
}
const handleError = (error: unknown) => {
let msg = ''
if (
error &&
typeof error === 'object' &&
'message' in error &&
typeof (error as { message?: unknown }).message === 'string'
) {
msg = (error as { message: string }).message
} else {
msg = String(error)
}
showMessage(msg, 'error', { closable: true })
}
const confirmDelete = () => {
showDeleteConfirm.value = false
if (props.task && props.isEdit) {
@@ -306,7 +419,7 @@ const confirmDelete = () => {
emit('delete', props.task)
handleClose()
} catch (error) {
showMessage(t.value.taskDeleteFailed, 'error')
handleError(error)
} finally {
submitting.value = false
}
@@ -326,14 +439,13 @@ const handleDeleteYes = () => {
emit('delete', props.task, true) // true
handleClose()
} catch (error) {
showMessage(t.value.taskDeleteFailed, 'error')
handleError(error)
} finally {
submitting.value = false
}
}
}
// Story"" - story
// Story"" - story
const handleDeleteNo = () => {
showDeleteConfirm.value = false
@@ -343,7 +455,7 @@ const handleDeleteNo = () => {
emit('delete', props.task, false) // falsestory
handleClose()
} catch (error) {
showMessage(t.value.taskDeleteFailed, 'error')
handleError(error)
} finally {
submitting.value = false
}
@@ -373,18 +485,193 @@ watch(
newValue => {
progressDisplayValue.value = (newValue || 0).toString()
},
{ immediate: true },
{ immediate: true }
)
// timerElapsed timerStartTime
watch(
() => props.visible,
visible => {
if (visible && props.task && props.task.type !== 'story') {
timerElapsed.value = props.task.timerElapsedTime || 0
// interval
if (!props.task.isTimerRunning) {
if (timerInterval.value) {
clearInterval(timerInterval.value)
timerInterval.value = null
}
}
}
},
{ immediate: true }
)
const handleStartTimer = (desc?: string) => {
const now = Date.now()
if (props.task && typeof props.task === 'object') {
Object.assign(props.task, {
timerStartTime: now,
isTimerRunning: true,
timerElapsedTime: 0, // 0
timerStartDesc: desc || '',
})
emit('start-timer', props.task)
}
timerElapsed.value = 0 //
localTimerRunning.value = true
updateTimer()
if (timerInterval.value) {
clearInterval(timerInterval.value)
timerInterval.value = null
}
window.dispatchEvent(new CustomEvent('start-timer', { detail: props.task }))
}
const handleStopTimer = () => {
if (props.task && typeof props.task === 'object') {
emit('stop-timer', props.task)
}
localTimerRunning.value = false
if (timerInterval.value) {
clearInterval(timerInterval.value)
timerInterval.value = null
}
updateTimer()
window.dispatchEvent(new CustomEvent('stop-timer', { detail: props.task }))
}
//
const showTimerConfirm = ref(false)
const timerDesc = ref('')
function openTimerConfirm() {
timerDesc.value = props.task?.name || ''
showTimerConfirm.value = true
}
function cancelTimerConfirm() {
showTimerConfirm.value = false
}
function confirmTimer(desc: string) {
showTimerConfirm.value = false
// desc
handleStartTimer(desc)
}
</script>
<template>
<div v-if="isVisible" class="drawer-overlay" @click="handleOverlayClick">
<div class="drawer-container" @click.stop>
<!-- Drawer Header -->
<div class="drawer-header">
<h3 class="drawer-title">{{ isEdit ? t.editTask : t.newTask }}</h3>
<div
class="drawer-header"
style="display: flex; align-items: center; justify-content: flex-start; gap: 8px"
>
<h3 class="drawer-title" style="margin: 0">{{ isEdit ? t.editTask : t.newTask }}</h3>
<div
v-if="props.task?.type !== 'story' && isEdit"
class="drawer-timer"
style="display: flex; align-items: center; gap: 6px; margin-left: 8px"
>
<button
v-if="!localTimerRunning"
class="timer-btn start minimal"
title="开始计时"
style="
width: 24px;
height: 24px;
background: #4caf50;
border: none;
padding: 0;
margin: 0;
box-shadow: none;
cursor: pointer;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
"
@click.stop="openTimerConfirm"
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
style="display: block; margin: 0 auto"
>
<circle cx="12" cy="12" r="11" stroke="#4caf50" stroke-width="2" fill="#4caf50" />
<polygon points="9,7 18,12 9,17" fill="#fff" />
</svg>
</button>
<button
v-else
class="timer-btn stop minimal"
title="停止计时"
style="
width: 24px;
height: 24px;
background: #f44336;
border: none;
padding: 0;
margin: 0;
box-shadow: none;
cursor: pointer;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
"
@click.stop="handleStopTimer"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="11" stroke="#f44336" stroke-width="2" fill="#f44336" />
<rect x="7" y="7" width="10" height="10" fill="#fff" rx="1.5" />
</svg>
</button>
<span
v-if="localTimerRunning"
class="timer-badge"
:class="{ 'timer-active': localTimerRunning }"
style="
margin-left: 8px;
font-size: 13px;
font-weight: 700;
padding: 2px 10px;
border-radius: 10px;
background: #fffbe6;
color: #e6a23c;
box-shadow: 0 0 0 1px #ffe58f;
display: inline-flex;
align-items: center;
min-width: 80px;
justify-content: center;
"
>
<span
v-if="localTimerRunning"
class="timer-dot"
style="
background: #67c23a;
width: 7px;
height: 7px;
border-radius: 50%;
margin-right: 5px;
animation: pulse 1s infinite;
"
></span>
{{ formattedTimer }}
</span>
</div>
<div style="flex: 1"></div>
<button class="drawer-close-btn" type="button" @click="handleClose">
<svg class="close-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<svg
class="close-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
style="vertical-align: middle"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
@@ -426,15 +713,15 @@ watch(
<span v-if="errors.type" class="error-text">{{ errors.type }}</span>
</div>
<div class="form-group">
<div class="form-group">
<label class="form-label" for="task-assignee">{{ t.assignee }}</label>
<select id="task-assignee" v-model="formData.assignee" class="form-select">
<option value="">{{ t.selectAssignee }}</option>
<!-- <option value="张三">张三</option>
<option value="">{{ t.selectAssignee }}</option>
<option value="张三">张三</option>
<option value="李四">李四</option>
<option value="王五">王五</option>
<option value="赵六">赵六</option>
<option value="钱七">钱七</option>-->
<option value="钱七">钱七</option>
</select>
</div>
@@ -606,6 +893,17 @@ watch(
</div>
</div>
</div>
<!-- 计时器确认弹窗 -->
<ConfirmTimerDialog
v-if="showTimerConfirm"
:visible="showTimerConfirm"
:title="'确认开始计时'"
:message="`即将为任务${props.task?.name}计时,若有特殊说明请完善下面的描述`"
:default-desc="props.task?.name || ''"
@confirm="confirmTimer"
@cancel="cancelTimerConfirm"
/>
</template>
<style scoped>
@@ -819,10 +1117,6 @@ watch(
background: var(--gantt-primary, #409eff);
}
.progress-slider::-moz-range-track {
background: var(--gantt-border-light, #e4e7ed);
}
/* 为更好的兼容性,添加一个伪元素来显示已完成部分 */
.progress-slider::before {
content: '';
+56 -1
View File
@@ -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 } }) => {
// TaskRowTaskList
// GanttChart
try {
window.dispatchEvent(
new CustomEvent('context-menu', {
detail: event,
}),
)
} catch (error) {
console.error('TaskList - Failed to dispatch context-menu event', error)
}
}
// TaskRow
const handleStartTimer = (task: Task) => {
emit('start-timer', task)
}
const handleStopTimer = (task: Task) => {
emit('stop-timer', task)
}
//
const handleAddPredecessor = (task: Task) => {
emit('add-predecessor', task)
}
//
const handleAddSuccessor = (task: Task) => {
emit('add-successor', task)
}
const handleTaskDelete = (task: Task, deleteChildren?: boolean) => {
//
emit('delete', task, deleteChildren)
}
onMounted(async () => {
window.addEventListener('task-updated', handleTaskUpdated as EventListener)
window.addEventListener('task-added', handleTaskAdded as EventListener)
@@ -354,6 +403,12 @@ onUnmounted(() => {
:on-hover="handleTaskRowHover"
@toggle="toggleCollapse"
@dblclick="handleTaskRowDoubleClick"
@contextmenu="handleTaskRowContextMenu"
@start-timer="handleStartTimer"
@stop-timer="handleStopTimer"
@add-predecessor="handleAddPredecessor"
@add-successor="handleAddSuccessor"
@delete="handleTaskDelete"
/>
</div>
</div>
+179 -2
View File
@@ -1,8 +1,9 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
import { useI18n } from '../composables/useI18n'
import { formatPredecessorDisplay } from '../utils/predecessorUtils'
import type { Task } from '../models/classes/Task'
import TaskContextMenu from './TaskContextMenu.vue'
interface Props {
task: Task
@@ -13,7 +14,16 @@ interface Props {
onHover?: (taskId: number | null) => void
}
const props = defineProps<Props>()
const emit = defineEmits(['toggle', 'dblclick'])
const emit = defineEmits([
'toggle',
'dblclick',
'contextmenu',
'start-timer',
'stop-timer',
'add-predecessor',
'add-successor',
'delete',
])
const { t } = useI18n()
const overtimeText = computed(() => t.value?.overtime ?? '')
const overdueText = computed(() => t.value?.overdue ?? '')
@@ -129,15 +139,101 @@ const handleSplitterDragEnd = () => {
isSplitterDragging.value = false
}
//
const timerElapsed = ref(0)
const timerInterval = ref<number | null>(null)
// HH:MM:SS
const formattedTimer = computed(() => {
const totalSeconds = Math.floor(timerElapsed.value / 1000)
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
})
//
const updateTimer = () => {
if (props.task.isTimerRunning && props.task.timerStartTime) {
// = - +
const previousElapsed = props.task.timerElapsedTime || 0
timerElapsed.value = Date.now() - props.task.timerStartTime + previousElapsed
} else if (props.task.timerElapsedTime) {
//
timerElapsed.value = props.task.timerElapsedTime
} else {
// 0
timerElapsed.value = 0
}
}
//
watch(
() => [props.task.isTimerRunning, props.task.timerStartTime, props.task.timerElapsedTime],
() => {
//
if (timerInterval.value) {
clearInterval(timerInterval.value)
timerInterval.value = null
}
//
if (props.task.isTimerRunning) {
updateTimer()
timerInterval.value = window.setInterval(updateTimer, 1000)
} else {
//
updateTimer()
}
},
{ immediate: true },
)
//
const contextMenuVisible = ref(false)
const contextMenuPosition = ref({ x: 0, y: 0 })
const contextMenuTask = computed(() => props.task)
//
function handleContextMenu(event: MouseEvent) {
// 广TaskRow
window.dispatchEvent(new CustomEvent('close-all-taskbar-menus'))
if (props.task.type !== 'task' && props.task.type !== 'story') {
//
event.preventDefault()
contextMenuVisible.value = false
return
}
event.preventDefault()
contextMenuVisible.value = true
contextMenuPosition.value = { x: event.clientX, y: event.clientY }
}
//
function closeContextMenu() {
contextMenuVisible.value = false
}
const handleTaskDelete = (task: Task, deleteChildren?: boolean) => {
//
emit('delete', task, deleteChildren)
closeContextMenu()
}
// -
onMounted(() => {
window.addEventListener('splitter-drag-start', handleSplitterDragStart)
window.addEventListener('splitter-drag-end', handleSplitterDragEnd)
window.addEventListener('close-all-taskbar-menus', closeContextMenu)
})
onUnmounted(() => {
window.removeEventListener('splitter-drag-start', handleSplitterDragStart)
window.removeEventListener('splitter-drag-end', handleSplitterDragEnd)
window.removeEventListener('close-all-taskbar-menus', closeContextMenu)
if (timerInterval.value) {
clearInterval(timerInterval.value)
}
})
</script>
@@ -160,6 +256,7 @@ onUnmounted(() => {
@dblclick="handleTaskRowDoubleClick"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
@contextmenu="handleContextMenu"
>
<div class="col col-name" :style="{ paddingLeft: indent }">
<span
@@ -245,6 +342,15 @@ onUnmounted(() => {
:title="props.task.name"
>
{{ props.task.name }}
<!-- 计时器显示 -->
<span
v-if="props.task.isTimerRunning || props.task.timerElapsedTime"
class="timer-badge"
:class="{ 'timer-active': props.task.isTimerRunning }"
>
<span v-if="props.task.isTimerRunning" class="timer-dot"></span>
{{ formattedTimer }}
</span>
<span v-if="isOvertime()" class="status-badge overtime">{{ overtimeText }}</span>
<span v-if="overdueDays() > 0" class="status-badge overdue">
{{ overdueText }}{{ overdueDays() > 0 ? overdueDays() + daysText : '' }}
@@ -299,8 +405,25 @@ onUnmounted(() => {
:on-hover="props.onHover"
@toggle="emit('toggle', $event)"
@dblclick="emit('dblclick', $event)"
@start-timer="emit('start-timer', $event)"
@stop-timer="emit('stop-timer', $event)"
@add-predecessor="emit('add-predecessor', $event)"
@add-successor="emit('add-successor', $event)"
@delete="handleTaskDelete"
/>
</template>
<TaskContextMenu
:visible="contextMenuVisible"
:task="contextMenuTask"
:position="contextMenuPosition"
@close="closeContextMenu"
@start-timer="$emit('start-timer', props.task)"
@stop-timer="$emit('stop-timer', props.task)"
@add-predecessor="$emit('add-predecessor', props.task)"
@add-successor="$emit('add-successor', props.task)"
@delete="handleTaskDelete"
/>
</div>
</template>
@@ -720,4 +843,58 @@ onUnmounted(() => {
0 6px 16px rgba(246, 124, 124, 0.4),
0 2px 8px rgba(255, 255, 255, 0.1);
}
/* 计时器样式 */
.timer-badge {
display: inline-flex;
align-items: center;
font-size: 12px;
font-weight: 700;
margin-left: 8px;
padding: 1px 6px;
border-radius: 10px;
background-color: rgba(0, 0, 0, 0.05);
color: var(--text-color-secondary);
}
.timer-badge.timer-active {
color: #e6a23c;
}
.timer-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: #67c23a; /* 绿色 */
margin-right: 4px;
animation: pulse 1s infinite;
}
@keyframes pulse {
0% {
transform: scale(0.8);
opacity: 0.8;
}
50% {
transform: scale(1.2);
opacity: 1;
}
100% {
transform: scale(0.8);
opacity: 0.8;
}
}
:global(html[data-theme='dark']) .timer-badge {
background-color: rgba(255, 255, 255, 0.1);
color: var(--text-color-secondary-dark);
}
:global(html[data-theme='dark']) .timer-badge.timer-active {
color: #e6c07b;
}
:global(html[data-theme='dark']) .timer-dot {
background-color: #85ce61; /* 暗色主题下的绿色 */
}
</style>
+47 -79
View File
@@ -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)
}
// TaskMilestone, Milestone
const convertTaskToMilestone = (task: Task): Milestone => {
// startDate string undefined
@@ -1627,6 +1589,16 @@ const generateMonthTimelineData = () => {
return result
}
//
const handleAddPredecessor = (task: Task) => {
emit('add-predecessor', task)
}
//
const handleAddSuccessor = (task: Task) => {
emit('add-successor', task)
}
</script>
<template>
@@ -1648,7 +1620,7 @@ const generateMonthTimelineData = () => {
class="timeline-year"
:style="{ width: '719px' }"
>
<div class="year-label">{{ yearValue }}</div>
<div class="year-label">{{ yearValue }}</div>
</div>
</div>
@@ -1913,22 +1885,18 @@ const generateMonthTimelineData = () => {
@drag-end="handleTaskBarDragEnd"
@resize-end="handleTaskBarResizeEnd"
@scroll-to-position="handleScrollToPosition"
@context-menu="handleTaskBarContextMenu"
@start-timer="handleStartTimer"
@stop-timer="handleStopTimer"
@add-predecessor="handleAddPredecessor"
@add-successor="handleAddSuccessor"
@delete="handleTaskDelete"
/>
</div>
</div>
</div>
</div>
</div>
<!-- Task Drawer 抽屉组件 - 仅在使用默认Drawer时显示 -->
<TaskDrawer
v-if="props.useDefaultDrawer"
v-model:visible="drawerVisible"
:task="currentTask"
:is-edit="isEditMode"
@submit="handleDrawerSubmit"
@close="handleDrawerClose"
@delete="handleDrawerTaskDelete"
/>
<!-- Milestone Dialog 里程碑对话框组件 -->
<MilestoneDialog
v-model:visible="milestoneDialogVisible"
+1
View File
@@ -8,3 +8,4 @@ export { default as MilestonePoint } from './MilestonePoint.vue'
export { default as MilestoneDialog } from './MilestoneDialog.vue'
export { default as TaskDrawer } from './TaskDrawer.vue'
export { default as DatePicker } from './DatePicker.vue'
export { default as TaskContextMenu } from './TaskContextMenu.vue'
+22 -37
View File
@@ -6,7 +6,6 @@ export type Locale = 'zh-CN' | 'en-US'
// 多语言配置
const messages = {
'zh-CN': {
dateNotSet: '未设置',
// TaskList Header
taskName: '任务名称',
predecessor: '前置任务',
@@ -39,26 +38,8 @@ const messages = {
// 月份格式
monthFormat: (month: number) => `${month}`,
// 月份名称
monthNames: [
'一月',
'二月',
'三月',
'四月',
'五月',
'六月',
'七月',
'八月',
'九月',
'十月',
'十一月',
'十二月',
],
weekDays:['日', '一', '二', '三', '四', '五', '六'],
// 其他
milestone: '里程碑',
targetDate: '目标日期',
today: '今天',
// 里程碑对话框
milestoneDetails: '里程碑详情',
@@ -116,6 +97,13 @@ const messages = {
newMilestone: '新建里程碑',
editMilestone: '编辑里程碑',
cancel: '取消',
// 右键菜单
startTimer: '开始计时',
stopTimer: '停止计时',
timerStarted: '任务计时已开始',
timerStopped: '任务计时已停止',
addPredecessor: '添加前置任务',
addSuccessor: '添加后置任务',
// PDF导出相关
pdfExportLoading: '正在生成PDF,请稍候...',
@@ -175,9 +163,12 @@ const messages = {
overtime: '超',
overdue: '逾期',
days: '天',
// 计时确认弹窗
timerConfirmPrefix: '即将为任务',
timerConfirmSuffix: '计时,若有特殊说明请完善下面的描述',
timerConfirmPlaceholder: '请输入计时说明',
},
'en-US': {
dateNotSet:'Not set',
// TaskList Header
taskName: 'Task Name',
predecessor: 'Predecessor',
@@ -208,25 +199,8 @@ const messages = {
// 月份格式
monthFormat: (month: number) => `M${String(month).padStart(2, '0')}`,
// Month name
monthNames: [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
],
weekDays:['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
// 其他
milestone: 'Milestone',
targetDate: 'Target Date',
today: 'Today',
// 里程碑对话框
milestoneDetails: 'Milestone Details',
@@ -283,6 +257,13 @@ const messages = {
newMilestone: 'New Milestone',
editMilestone: 'Edit Milestone',
cancel: 'Cancel',
// Context menu
timerStarted: 'Timer Started',
timerStopped: 'Timer Stopped',
startTimer: 'Start Timer',
stopTimer: 'Stop Timer',
addPredecessor: 'Add Predecessor',
addSuccessor: 'Add Successor',
// PDF export
pdfExportLoading: 'Generating PDF, please wait...',
@@ -346,6 +327,10 @@ const messages = {
overtime: 'Over',
overdue: 'Overdue',
days: ' days',
// 计时确认弹窗
timerConfirmPrefix: 'About to start timing for',
timerConfirmSuffix: '. If there are special notes, please complete the description below.',
timerConfirmPlaceholder: 'Please enter timer description',
},
}
+6
View File
@@ -17,4 +17,10 @@ export interface Task {
description?: string
icon?: string
level?: number
// 计时相关字段
isTimerRunning?: boolean
timerStartTime?: number
timerEndTime?: number // 结束计时时间
timerStartDesc?: string // 计时开始时填写的描述
timerElapsedTime?: number
}