v1.4.0 - new features

This commit is contained in:
LINING-PC\lining
2025-10-11 18:57:38 +08:00
parent 811477852a
commit 0c317b1450
27 changed files with 159 additions and 5459 deletions

View File

@@ -34,6 +34,11 @@ Example:
- **Main Contributions**: added language support for task tooltip
- **Participation Period**: 2025-08
<img src="https://foruda.gitee.com/avatar/1676904209687527903/139467_qiuchengw_1578919251.jpg!avatar200" alt="" width="32" style="vertical-align:middle;margin-right:8px;" />[秋成](https://gitee.com/qiuchengw)
- **Contribution Type**: Code
- **Main Contributions**: Configurable TaskList; TaskContent slot; Collapse/expand all; Time-based sorting; Component optimizations
- **Participation Period**: 2025-09
## 📊 Contribution Statistics
### Code Contributions

View File

@@ -33,6 +33,11 @@
- **主要贡献**: 任务Tooltip语言增强
- **参与时间**: 2025-08
<img src="https://foruda.gitee.com/avatar/1676904209687527903/139467_qiuchengw_1578919251.jpg!avatar200" alt="" width="32" style="vertical-align:middle;margin-right:8px;" />[秋成](https://gitee.com/qiuchengw)
- **贡献类型**: 代码开发
- **主要贡献**: TaskList可配置TaskContent插槽全部收起/展开;按照时间排序;优化组件
- **参与时间**: 2025-09
## 📊 贡献统计

View File

@@ -39,20 +39,20 @@ const toolbarConfig = {
showTheme: true,
showFullscreen: true,
showTimeScale: true, // 控制日|周|月时间刻度按钮组的可见性
timeScaleDimensions: ['day', 'week', 'month', 'quarter', 'year'], // 设置时间刻度按钮的展示维度,包含所有时间维度
timeScaleDimensions: ['hour', 'day', 'week', 'month', 'quarter', 'year'], // 设置时间刻度按钮的展示维度,包含所有时间维度
defaultTimeScale: 'week',
showExpandCollapse: true, // 显示全部展开/折叠按钮
}
// TaskList列配置
const availableColumns = ref<TaskListColumnConfig[]>([
// { key: 'predecessor', label: '前置任务', visible: true },
// { key: 'assignee', label: '负责人', visible: true },
{ key: 'predecessor', label: '前置任务', visible: true },
{ key: 'assignee', label: '负责人', visible: true },
{ key: 'startDate', label: '开始日期', visible: true },
// { key: 'endDate', label: '结束日期', visible: true },
// { key: 'estimatedHours', label: '预估工时', visible: true },
// { key: 'actualHours', label: '实际工时', visible: true },
// { key: 'progress', label: '进度', visible: true },
{ key: 'endDate', label: '结束日期', visible: true },
{ key: 'estimatedHours', label: '预估工时', visible: true },
{ key: 'actualHours', label: '实际工时', visible: true },
{ key: 'progress', label: '进度', visible: true },
])
// TaskList宽度配置
@@ -172,14 +172,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 },
})
}),
)
}
@@ -273,7 +273,7 @@ const handleTaskUpdate = (updatedTask: Task) => {
showMessage(
formatTranslation('newParentTaskNotFound', { parentId: taskToAdd.parentId }),
'warning',
{ closable: true }
{ closable: true },
)
tasks.value.push(taskToAdd)
}
@@ -321,7 +321,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
}
@@ -416,7 +416,7 @@ const handleStoryDeleteWithChildren = (storyToDelete: Task) => {
'success',
{
closable: false,
}
},
)
return true
}
@@ -478,7 +478,7 @@ const handleStoryDeleteOnly = (storyToDelete: Task) => {
'success',
{
closable: false,
}
},
)
return true
}
@@ -552,7 +552,7 @@ function handleTaskbarDragOrResizeEnd(newTask) {
`开始: ${oldTask.startDate}${newTask.startDate}\n` +
`结束: ${oldTask.endDate}${newTask.endDate}`,
'info',
{ closable: true }
{ closable: true },
)
}
function handleMilestoneDragEnd(newMilestone) {
@@ -562,7 +562,7 @@ function handleMilestoneDragEnd(newMilestone) {
`里程碑【${newMilestone.name}\n` +
`开始: ${oldMilestone.endDate}${newMilestone.startDate}`,
'info',
{ closable: true }
{ closable: true },
)
}
@@ -629,7 +629,7 @@ function onTimerStarted(task: Task) {
showMessage(
`Demo 任务【${task.name}\n开始计时${new Date(task.timerStartTime).toLocaleString()}\n计时说明${task.timerStartDesc ? task.timerStartDesc : ''}`,
'info',
{ closable: true }
{ closable: true },
)
}
function onTimerStopped(task: Task) {
@@ -645,7 +645,7 @@ function onTimerStopped(task: Task) {
}
function taskDebug(item: any) {
console.log('Task Debug:', item)
//console.log('Task Debug:', item)
}
</script>
@@ -694,7 +694,7 @@ function taskDebug(item: any) {
>
<path d="M3 6h18v2H3V6zm0 5h18v2H3v-2zm0 5h18v2H3v-2z" fill="currentColor" />
</svg>
TaskList 配置
{{ t.taskListConfig.title }}
</h3>
<button class="collapse-button" :class="{ collapsed: isConfigPanelCollapsed }">
<svg
@@ -742,11 +742,11 @@ function taskDebug(item: any) {
fill="none"
/>
</svg>
宽度设置
{{ t.taskListConfig.width.title }}
</h4>
<div class="width-controls">
<div class="width-control">
<label class="width-label">默认宽度:</label>
<label class="width-label">{{ t.taskListConfig.width.defaultWidth }}:</label>
<input
v-model.number="taskListWidth.defaultWidth"
type="number"
@@ -758,7 +758,7 @@ function taskDebug(item: any) {
<span class="width-unit">px</span>
</div>
<div class="width-control">
<label class="width-label">最小宽度:</label>
<label class="width-label">{{ t.taskListConfig.width.minWidth }}:</label>
<input
v-model.number="taskListWidth.minWidth"
type="number"
@@ -770,7 +770,7 @@ function taskDebug(item: any) {
<span class="width-unit">px</span>
</div>
<div class="width-control">
<label class="width-label">最大宽度:</label>
<label class="width-label">{{ t.taskListConfig.width.maxWidth }}:</label>
<input
v-model.number="taskListWidth.maxWidth"
type="number"
@@ -795,7 +795,7 @@ function taskDebug(item: any) {
>
<path d="M3 6h18v2H3V6zm0 5h18v2H3v-2zm0 5h18v2H3v-2z" fill="currentColor" />
</svg>
列显示
{{ t.taskListConfig.columns.title }}
</h4>
<div class="column-controls">
<label v-for="column in availableColumns" :key="column.key" class="column-control">
@@ -804,7 +804,7 @@ function taskDebug(item: any) {
:checked="column.visible"
@change="toggleColumn(column.key, $event)"
/>
<span class="column-label">{{ column.label }}</span>
<span class="column-label">{{ (t as any)[column.key] || column.label }}</span>
</label>
</div>
</div>
@@ -850,7 +850,12 @@ function taskDebug(item: any) {
@task-updated="e => showMessage(`Demo 任务[${e.task.name}] 已更新`, 'info')"
>
<template #custom-task-content="item">
<HtmlContent :item="taskDebug(item)" :task="item.task" :type="item.type" />
<HtmlContent
:item="taskDebug(item)"
:task="item.task"
:type="item.type"
:style="item.dynamicStyles"
/>
</template>
</GanttChart>
</div>
@@ -1022,7 +1027,7 @@ function taskDebug(item: any) {
}
.width-label {
flex: 0 0 80px;
flex: 0 0 100px;
font-size: 13px;
color: var(--text-secondary);
}

View File

@@ -2,18 +2,17 @@
import type { Task } from '../src/models/Task'
interface Props {
task: Task,
type: string, // 'task-row' | 'task-bar'
task: Task
type: string // 'task-row' | 'task-bar'
}
const props = withDefaults(defineProps<Props>(), {
})
const props = withDefaults(defineProps<Props>(), {})
// console.error('props', props)
</script>
<template>
<div class="html-content-card">
<div v-if="type==='task-row'" class="task-row" v-html="task.name" />
<div v-else-if="type==='task-bar'" class="task-bar" v-html="task.name" />
<div v-if="props.type === 'task-row'" class="task-row" v-html="props.task.name" />
<div v-else-if="props.type === 'task-bar'" class="task-bar" v-html="props.task.name" />
</div>
</template>
@@ -31,7 +30,6 @@ const props = withDefaults(defineProps<Props>(), {
.task-bar {
flex: 1;
padding: 0 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

View File

@@ -46,7 +46,7 @@ onMounted(async () => {
</div>
<div class="version-timeline-content version-card">
<ul class="version-notes">
<li v-for="(note, nidx) in item.notes" :key="nidx">{{ note }}</li>
<li v-for="(note, nidx) in item.notes" :key="nidx" v-html="note"></li>
</ul>
</div>
</div>

View File

@@ -190,5 +190,31 @@
"SonarQube代码质量检查问题修改",
"SonarQube code quality inspection issue modification"
]
},
{
"version": "1.4.0",
"date": "2025-10-11",
"notes": [
"Timeline Scale客制化",
"Timeline Scale Customization",
"TaskList可配置",
"TaskList Configurable",
"TaskRow/TaskBar内容支持HTML插槽",
"TaskRow/TaskBar content supports HTML slots",
"TaskRow/TaskBar支持自定义样式",
"TaskRow/TaskBar supports custom styles",
"增加全部展开/收起按钮",
"按照时间排序TaskList",
"Sort TaskList by time",
"增加周视图的1号旗帜元素",
"Add 1st flag element for weekly view",
"Add Expand/Collapse All Button",
"SonarQube代码质量检查问题修改",
"SonarQube code quality inspection issue modification",
"修复缺陷",
"Bugfix",
"<span style=\"font-weight: bold; color: #f00;\">特别感谢贡献者 @qiuchengw</span>",
"<span style=\"font-weight: bold; color: #f00;\">Special thanks to contributor @qiuchengw</span>"
]
}
]

View File

@@ -1,6 +1,6 @@
{
"name": "jordium-gantt-vue3",
"version": "1.3.2",
"version": "1.4.0",
"type": "module",
"main": "dist/jordium-gantt-vue3.cjs.js",
"module": "dist/jordium-gantt-vue3.es.js",

View File

@@ -1,962 +0,0 @@
<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 MilestoneDialog from '../src/components/MilestoneDialog.vue'
import demoData from './data.json'
import packageInfo from '../package.json'
// 导入主题变量
import '../src/styles/theme-variables.css'
import VersionHistoryDrawer from './VersionHistoryDrawer.vue'
import { useMessage } from '../src/composables/useMessage'
import { useI18n } from '../src/composables/useI18n'
import { getPredecessorIds, predecessorIdsToString } from '../src/utils/predecessorUtils'
import type { Task } from '../src/models/Task'
const { showMessage } = useMessage()
const { t, formatTranslation } = useI18n()
const tasks = ref<Task[]>([])
const milestones = ref<Task[]>([])
// MilestoneDialog状态管理
const showMilestoneDialog = ref(false)
const currentMilestone = ref<Task | null>(null)
const isMilestoneEditMode = ref(false)
// 版本历史Drawer状态
const showVersionDrawer = ref(false)
const toolbarConfig = {
showAddTask: true,
showAddMilestone: true,
showTodayLocate: true,
showExportCsv: true,
showExportPdf: true,
showLanguage: true,
showTheme: true,
showFullscreen: true,
showTimeScale: true, // 控制日|周|月时间刻度按钮组的可见性
timeScaleDimensions: ['hour', 'day', 'week', 'month', 'quarter', 'year'], // 设置时间刻度按钮的展示维度,包含所有时间维度
}
// 工作时间配置示例
const workingHoursConfig = {
morning: { start: 8, end: 11 }, // 上午8:00-11:59为工作时间
afternoon: { start: 13, end: 17 }, // 下午13:00-17:00为工作时间
// 其他时间12:00-12:59, 18:00-07:59为休息时间显示为灰色背景
}
// 自定义CSV导出处理器可选
const handleCustomCsvExport = () => {
showMessage(t.value.customCsvExportCalled, 'info', { closable: true })
// 这里可以实现自定义的CSV导出逻辑
// 例如:添加额外的数据处理、格式化、或发送到服务器等
// 如果不实现自定义逻辑,组件会使用内置的默认导出功能
// return false // 返回false让组件使用默认实现
// 示例:这里直接使用默认实现
return false
}
// 其他工具栏事件处理器示例
const handleAddTask = () => {
// 打开TaskDrawer进行新建任务
currentTask.value = null
isEditMode.value = false
showTaskDrawer.value = true
}
const handleAddMilestone = () => {
// 打开MilestoneDialog进行新建里程碑
currentMilestone.value = null
isMilestoneEditMode.value = false
showMilestoneDialog.value = true
}
const handleLanguageChange = (lang: 'zh' | 'en') => {
const languageText = lang === 'zh' ? '中文' : 'English'
showMessage(formatTranslation('languageSwitchedTo', { language: languageText }), 'info', {
closable: true,
})
}
const handleThemeChange = (isDark: boolean) => {
const themeText = isDark ? t.value.darkModeText : t.value.lightModeText
showMessage(formatTranslation('themeSwitchedTo', { theme: themeText }), 'info', {
closable: true,
})
}
// 里程碑保存处理器示例
const handleMilestoneSave = (milestone: Task) => {
// 更新本地里程碑数据
const milestoneIndex = milestones.value.findIndex(m => m.id === milestone.id)
if (milestoneIndex !== -1) {
// 更新现有里程碑
milestones.value[milestoneIndex] = { ...milestone }
} else {
// 新增里程碑
const newMilestone = {
...milestone,
id: Date.now(), // 生成临时ID
type: 'milestone',
}
milestones.value.push(newMilestone)
}
// 关闭里程碑对话框
showMilestoneDialog.value = false
}
// 里程碑删除处理器
const handleMilestoneDelete = async (milestoneId: number) => {
// 从里程碑数据中删除
const milestoneIndex = milestones.value.findIndex(m => m.id === milestoneId)
if (milestoneIndex !== -1) {
milestones.value.splice(milestoneIndex, 1)
showMessage(t.value.milestoneDeleteSuccess, 'success', { closable: false })
// 等待DOM更新完成
await nextTick()
// 触发全局事件,通知其他组件里程碑已删除
window.dispatchEvent(
new CustomEvent('milestone-deleted', {
detail: { milestoneId },
}),
)
// 触发强制更新事件确保Timeline重新渲染
window.dispatchEvent(
new CustomEvent('milestone-data-changed', {
detail: { milestones: milestones.value },
}),
)
}
// 关闭里程碑对话框
showMilestoneDialog.value = false
}
// 任务更新处理器
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) {
if (task.id === updatedTask.id) {
return task
}
if (task.children && task.children.length > 0) {
const found = findOriginalTask(task.children)
if (found) return found
}
}
return null
}
const originalTask = findOriginalTask(tasks.value)
if (!originalTask) {
showMessage(formatTranslation('taskNotFound', { id: updatedTask.id }), 'warning', {
closable: true,
})
return
}
// 检查parentId是否改变了
const parentIdChanged = originalTask.parentId !== updatedTask.parentId
if (parentIdChanged) {
// parentId改变了需要移除任务并重新添加到新位置
const removeTaskFromArray = (taskArray: Task[]): Task | null => {
for (let i = 0; i < taskArray.length; i++) {
if (taskArray[i].id === updatedTask.id) {
const removedTask = taskArray.splice(i, 1)[0]
return removedTask
}
if (taskArray[i].children && taskArray[i].children.length > 0) {
const removedTask = removeTaskFromArray(taskArray[i].children!)
if (removedTask) {
if (taskArray[i].children!.length === 0) {
delete taskArray[i].children
taskArray[i].isParent = taskArray[i].type === 'story'
}
return removedTask
}
}
}
return null
}
const removedTask = removeTaskFromArray(tasks.value)
if (!removedTask) return
const taskToAdd = {
...updatedTask,
isParent:
updatedTask.type === 'story' || (updatedTask.children && updatedTask.children.length > 0),
}
// 重新添加到新位置
if (taskToAdd.parentId) {
const addToParentChildren = (taskArray: Task[]): boolean => {
for (const task of taskArray) {
if (task.id === taskToAdd.parentId) {
if (!task.children) task.children = []
task.children.push(taskToAdd)
task.isParent = true
return true
}
if (task.children && task.children.length > 0) {
if (addToParentChildren(task.children)) return true
}
}
return false
}
if (!addToParentChildren(tasks.value)) {
showMessage(
formatTranslation('newParentTaskNotFound', { parentId: taskToAdd.parentId }),
'warning',
{ closable: true },
)
tasks.value.push(taskToAdd)
}
} else {
tasks.value.push(taskToAdd)
}
} else {
// parentId没有改变只是就地更新任务数据
const updateTaskInPlace = (taskArray: Task[]): boolean => {
for (let i = 0; i < taskArray.length; i++) {
if (taskArray[i].id === updatedTask.id) {
// 保持原有的children和层级关系
taskArray[i] = {
...updatedTask,
children: taskArray[i].children, // 保持原有的children
isParent:
updatedTask.type === 'story' ||
(taskArray[i].children && taskArray[i].children.length > 0),
}
return true
}
if (taskArray[i].children && taskArray[i].children.length > 0) {
if (updateTaskInPlace(taskArray[i].children!)) {
return true
}
}
}
return false
}
if (!updateTaskInPlace(tasks.value)) {
showMessage(formatTranslation('inPlaceUpdateFailed', { id: updatedTask.id }), 'warning', {
closable: true,
})
}
}
}
// 任务添加处理器
const handleTaskAdd = (newTask: Task) => {
// 为新任务生成ID如果没有的话
if (!newTask.id) {
// 找到当前最大的ID然后+1
const maxId = Math.max(
...tasks.value.map(t => t.id || 0),
...milestones.value.map(m => m.id || 0),
0,
)
newTask.id = maxId + 1
}
// 设置isParent属性
newTask.isParent = newTask.type === 'story' || (newTask.children && newTask.children.length > 0)
// 处理父子关系
if (newTask.parentId) {
// 如果有上级任务需要将子任务添加到父任务的children中
const addToParentChildren = (taskArray: Task[]): boolean => {
for (const task of taskArray) {
if (task.id === newTask.parentId) {
// 找到父任务
if (!task.children) {
// 如果父任务没有children属性创建一个
task.children = []
}
// 将新任务添加到父任务的children中
task.children.push({ ...newTask })
return true
}
// 递归查找父任务(支持多层嵌套)
if (task.children && task.children.length > 0) {
if (addToParentChildren(task.children)) {
return true
}
}
}
return false
}
// 尝试添加到父任务的children中
if (!addToParentChildren(tasks.value)) {
// 如果没找到父任务,作为顶级任务添加
tasks.value.push({ ...newTask })
}
} else {
// 没有父任务,作为顶级任务添加
tasks.value.push({ ...newTask })
}
}
// 任务删除处理器
const handleTaskDelete = (taskToDelete: Task) => {
// 收集要删除的所有任务ID包括子任务
const deletedTaskIds = collectAllTaskIds(taskToDelete)
// 递归查找和删除任务(支持嵌套结构)
const deleteTaskFromArray = (taskArray: Task[]): boolean => {
for (let i = 0; i < taskArray.length; i++) {
if (taskArray[i].id === taskToDelete.id) {
// 找到任务,删除它
taskArray.splice(i, 1)
showMessage(t.value.taskDeletedSuccess, 'success', { closable: false })
return true
}
// 如果有子任务,递归查找
if (taskArray[i].children && taskArray[i].children.length > 0) {
if (deleteTaskFromArray(taskArray[i].children!)) {
return true
}
}
}
return false
}
if (deleteTaskFromArray(tasks.value)) {
// 删除成功后清理predecessor依赖关系
cleanupPredecessorReferences(deletedTaskIds)
} else {
showMessage(formatTranslation('taskToDeleteNotFound', { id: taskToDelete.id }), 'warning', {
closable: true,
})
}
}
// 删除story及其所有子任务
const handleStoryDeleteWithChildren = (storyToDelete: Task) => {
// 收集要删除的所有任务IDstory及其所有子任务
const deletedTaskIds = collectAllTaskIds(storyToDelete)
// 递归查找和删除story包含其所有子任务
const deleteStoryFromArray = (taskArray: Task[]): boolean => {
for (let i = 0; i < taskArray.length; i++) {
if (taskArray[i].id === storyToDelete.id) {
// 找到story直接删除它包含所有子任务
taskArray.splice(i, 1)
showMessage(
formatTranslation('storyDeleteAllSuccess', { name: storyToDelete.name }),
'success',
{
closable: false,
},
)
return true
}
// 如果有子任务,递归查找
if (taskArray[i].children && taskArray[i].children.length > 0) {
if (deleteStoryFromArray(taskArray[i].children!)) {
return true
}
}
}
return false
}
if (deleteStoryFromArray(tasks.value)) {
// 删除成功后清理predecessor依赖关系
cleanupPredecessorReferences(deletedTaskIds)
} else {
showMessage(formatTranslation('storyNotFound', { id: storyToDelete.id }), 'warning', {
closable: true,
})
}
}
// 仅删除story保留子任务
const handleStoryDeleteOnly = (storyToDelete: Task) => {
// 递归查找story并处理其子任务
const deleteStoryOnlyFromArray = (taskArray: Task[]): boolean => {
for (let i = 0; i < taskArray.length; i++) {
if (taskArray[i].id === storyToDelete.id) {
// 找到story
const childrenToPromote = taskArray[i].children || []
// 步骤a: 克隆并升级子任务
const upgradedChildren: Task[] = []
if (childrenToPromote.length > 0) {
childrenToPromote.forEach(child => {
// 深度克隆子任务
const clonedChild = JSON.parse(JSON.stringify(child))
// 升级移除parentId使其成为顶级任务
delete clonedChild.parentId
upgradedChildren.push(clonedChild)
})
}
// 步骤b: 删除story数据
taskArray.splice(i, 1)
// 将升级后的子任务push到tasks集合顶层
if (upgradedChildren.length > 0) {
tasks.value.push(...upgradedChildren)
}
showMessage(
formatTranslation('storyDeleteOnlySuccess', {
name: storyToDelete.name,
count: upgradedChildren.length,
}),
'success',
{
closable: false,
},
)
return true
}
// 如果有子任务,递归查找
if (taskArray[i].children && taskArray[i].children.length > 0) {
if (deleteStoryOnlyFromArray(taskArray[i].children!)) {
return true
}
}
}
return false
}
if (deleteStoryOnlyFromArray(tasks.value)) {
// 删除成功后清理story的predecessor依赖关系只清理story本身的ID不影响子任务
cleanupPredecessorReferences([storyToDelete.id])
} else {
showMessage(formatTranslation('storyNotFound', { id: storyToDelete.id }), 'warning', {
closable: true,
})
}
}
// 里程碑图标变更处理器
const handleMilestoneIconChange = (milestoneId: number, icon: string) => {
const milestoneIndex = milestones.value.findIndex(m => m.id === milestoneId)
if (milestoneIndex !== -1) {
milestones.value[milestoneIndex].icon = icon
} else {
showMessage(formatTranslation('milestoneIconUpdateNotFound', { id: milestoneId }), 'warning', {
closable: true,
})
}
}
const handleTaskDrawerDelete = (task: Task, deleteChildren?: boolean) => {
if (task.type === 'story' && deleteChildren === true) {
// 删除story及其所有子任务
handleStoryDeleteWithChildren(task)
} else if (task.type === 'story' && deleteChildren === false) {
// 仅删除story保留子任务
handleStoryDeleteOnly(task)
} else {
// 普通任务删除
handleTaskDelete(task)
}
showTaskDrawer.value = false
}
// GitHub 文档处理函数
const handleGithubDocsClick = (event: Event) => {
event.preventDefault()
// 打开GitHub仓库的README页面
window.open('https://github.com/nelson820125/jordium-gantt-vue3', '_blank')
}
// Gitee 文档处理函数
const handleGiteeDocsClick = (event: Event) => {
event.preventDefault()
// 打开Gitee仓库的README页面
window.open('https://gitee.com/jordium/jordium-gantt-vue3.git', '_blank')
}
// 任务拖拽/拉伸/里程碑拖拽监听
function handleTaskbarDragOrResizeEnd(newTask) {
const oldTask = findTaskDeep(tasks.value, newTask.id)
if (!oldTask) return
showMessage(
`任务【${newTask.name}\n` +
`开始: ${oldTask.startDate}${newTask.startDate}\n` +
`结束: ${oldTask.endDate}${newTask.endDate}`,
'info',
{ closable: true },
)
}
function handleMilestoneDragEnd(newMilestone) {
const oldMilestone = findTaskDeep(milestones.value, newMilestone.id)
if (!oldMilestone) return
showMessage(
`里程碑【${newMilestone.name}\n` +
`开始: ${oldMilestone.endDate}${newMilestone.startDate}`,
'info',
{ closable: true },
)
}
onMounted(() => {
tasks.value = demoData.tasks as Task[]
milestones.value = demoData.milestones as Task[]
})
// 递归查找任务/里程碑,因为原始结构一致
function findTaskDeep(taskArray: Task[], id: number): Task | null {
for (const task of taskArray) {
if (task.id === id) return task
if (task.children && task.children.length > 0) {
const found = findTaskDeep(task.children, id)
if (found) return found
}
}
return null
}
// 清理被删除任务的predecessor依赖关系
const cleanupPredecessorReferences = (deletedTaskIds: number[]) => {
// 递归清理所有任务包括嵌套的子任务的predecessor
const cleanupTaskArray = (taskArray: Task[]) => {
taskArray.forEach(task => {
if (task.predecessor) {
// 使用工具函数获取前置任务ID数组
const predecessorIds = getPredecessorIds(task.predecessor)
// 过滤掉被删除的任务ID
const validPredecessorIds = predecessorIds.filter(id => !deletedTaskIds.includes(id))
// 更新predecessor属性
if (validPredecessorIds.length === 0) {
delete task.predecessor
} else {
task.predecessor = predecessorIdsToString(validPredecessorIds)
}
}
// 递归处理子任务
if (task.children && task.children.length > 0) {
cleanupTaskArray(task.children)
}
})
}
cleanupTaskArray(tasks.value)
}
// 收集被删除任务及其所有子任务的ID
const collectAllTaskIds = (task: Task): number[] => {
const ids = [task.id]
if (task.children && task.children.length > 0) {
task.children.forEach(child => {
ids.push(...collectAllTaskIds(child))
})
}
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>
<div class="app-container">
<h1 class="page-title">
<div class="title-left">
<svg class="gantt-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="4" width="20" height="2" rx="1" fill="#409eff" />
<rect x="2" y="8" width="12" height="2" rx="1" fill="#67c23a" />
<rect x="2" y="12" width="16" height="2" rx="1" fill="#e6a23c" />
<rect x="2" y="16" width="8" height="2" rx="1" fill="#f56c6c" />
<rect x="2" y="20" width="14" height="2" rx="1" fill="#909399" />
<circle cx="22" cy="5" r="1" fill="#409eff" />
<circle cx="16" cy="9" r="1" fill="#67c23a" />
<circle cx="20" cy="13" r="1" fill="#e6a23c" />
<circle cx="12" cy="17" r="1" fill="#f56c6c" />
<circle cx="18" cy="21" r="1" fill="#909399" />
</svg>
Jordium Gantt Vue3 Demo
<span class="version-badge" style="cursor: pointer" @click="showVersionDrawer = true">{{
packageInfo.version
}}</span>
</div>
<div class="docs-links">
<a href="#github-docs" class="doc-link github-link" @click="handleGithubDocsClick">
<img class="doc-icon" src="./public/github.svg" alt="GitHub" />
</a>
<span class="docs-divider"></span>
<a href="#gitee-docs" class="doc-link gitee-link" @click="handleGiteeDocsClick">
<img class="doc-icon" src="./public/gitee.svg" alt="Gitee" />
</a>
</div>
</h1>
<VersionHistoryDrawer :visible="showVersionDrawer" @close="showVersionDrawer = false" />
<div class="gantt-wrapper">
<GanttChart
:tasks="tasks"
:milestones="milestones"
:toolbar-config="toolbarConfig"
:working-hours="workingHoursConfig"
:on-add-task="handleAddTask"
:on-add-milestone="handleAddMilestone"
:on-export-csv="handleCustomCsvExport"
:on-language-change="handleLanguageChange"
:on-theme-change="handleThemeChange"
:on-milestone-save="handleMilestoneSave"
:on-milestone-delete="handleMilestoneDelete"
:on-task-update="handleTaskUpdate"
:on-task-add="handleTaskAdd"
:on-task-delete="handleTaskDrawerDelete"
:on-milestone-icon-change="handleMilestoneIconChange"
@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>
<!-- MilestoneDialog用于新建/编辑里程碑 -->
<MilestoneDialog
:visible="showMilestoneDialog"
:milestone="currentMilestone"
@update:visible="showMilestoneDialog = $event"
@save="handleMilestoneSave"
@delete="handleMilestoneDelete"
@close="showMilestoneDialog = false"
/>
</div>
</template>
<style scoped>
.app-container {
width: 100%;
height: 100%;
padding: 20px;
box-sizing: border-box;
background: var(--gantt-bg-secondary, #f0f2f5);
display: flex;
flex-direction: column;
}
.page-title {
margin: 20px 0;
font-size: 1.8rem;
font-weight: 600;
color: var(--gantt-text-primary, #333);
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.title-left {
display: flex;
align-items: center;
gap: 12px;
}
.gantt-icon {
width: 32px;
height: 32px;
flex-shrink: 0;
transition: all 0.3s ease;
}
.gantt-icon:hover {
transform: scale(1.05);
}
.version-badge {
display: inline-block;
background: linear-gradient(135deg, #409eff 0%, #36d1dc 50%, #667eea 100%);
color: white;
font-size: 0.7rem;
font-weight: 700;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
padding: 6px 12px;
border-radius: 16px;
line-height: 1;
margin-left: 8px;
position: relative;
overflow: hidden;
text-transform: uppercase;
letter-spacing: 0.5px;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow:
0 0 20px rgba(64, 158, 255, 0.3),
0 4px 15px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.version-badge::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transition: left 0.6s ease;
}
.version-badge:hover {
transform: scale(1.05) translateY(-1px);
box-shadow:
0 0 30px rgba(64, 158, 255, 0.5),
0 8px 25px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
background: linear-gradient(135deg, #4dabf7 0%, #40c9ff 50%, #74c0fc 100%);
}
.version-badge:hover::before {
left: 100%;
}
/* 科技感呼吸动画 */
@keyframes glow-pulse {
0%,
100% {
box-shadow:
0 0 20px rgba(64, 158, 255, 0.3),
0 4px 15px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
50% {
box-shadow:
0 0 30px rgba(64, 158, 255, 0.5),
0 4px 15px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
}
.gantt-wrapper {
flex: 1 1 0%;
min-height: 0;
min-width: 0;
display: flex;
align-items: flex-start;
justify-content: flex-start;
margin-bottom: 24px;
}
.license-info {
text-align: center;
color: var(--gantt-text-muted, #c0c4cc);
font-size: 14px;
font-weight: 400;
letter-spacing: 0.5px;
}
/* 全局暗色主题支持 */
:global(html[data-theme='dark']) {
background: #1e1e1e !important;
}
:global(html[data-theme='dark']) body {
background: #1e1e1e !important;
color: #e5e5e5 !important;
}
/* 暗黑模式下的版本标签 */
:global(html[data-theme='dark']) .version-badge {
background: linear-gradient(135deg, #1a73e8 0%, #00bcd4 50%, #3f51b5 100%);
box-shadow:
0 0 25px rgba(102, 177, 255, 0.4),
0 4px 15px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
:global(html[data-theme='dark']) .version-badge:hover {
background: linear-gradient(135deg, #2196f3 0%, #00e5ff 50%, #5c6bc0 100%);
box-shadow:
0 0 35px rgba(102, 177, 255, 0.6),
0 8px 25px rgba(0, 0, 0, 0.5),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
border-color: rgba(102, 177, 255, 0.5);
}
/* 暗黑模式的呼吸动画 */
@keyframes glow-pulse-dark {
0%,
100% {
box-shadow:
0 0 25px rgba(102, 177, 255, 0.4),
0 4px 15px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
50% {
box-shadow:
0 0 35px rgba(102, 177, 255, 0.6),
0 4px 15px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
}
.docs-links {
display: flex;
align-items: center;
gap: 8px;
}
.docs-divider {
display: inline-block;
width: 1px;
height: 24px;
border-left: 1.5px dashed #bbb;
margin: 0 8px;
background: none;
}
.doc-link {
display: flex;
align-items: center;
gap: 6px;
color: var(--gantt-text-primary, #333333);
text-decoration: none;
font-size: 1rem;
font-weight: 500;
transition: color 0.2s ease;
padding: 4px 8px;
border-radius: 4px;
}
.doc-link:hover {
color: var(--gantt-text-primary, #333333);
background-color: rgba(0, 0, 0, 0.1);
}
.doc-link:nth-child(2) {
color: #c71d23;
}
.doc-link:nth-child(2):hover {
color: #a91b1b;
background-color: rgba(199, 29, 35, 0.1);
}
.doc-icon {
width: 24px;
height: 24px;
flex-shrink: 0;
transition: filter 0.2s ease;
}
/* GitHub 图标样式 - 黑色 */
.github-link .doc-icon {
filter: brightness(0) saturate(100%);
}
.github-link:hover .doc-icon {
filter: brightness(0) saturate(100%) invert(20%) sepia(15%) saturate(1500%) hue-rotate(200deg);
}
/* Gitee 图标样式 - 红色 */
.gitee-link .doc-icon {
filter: brightness(0) saturate(100%) invert(20%) sepia(100%) saturate(2000%) hue-rotate(350deg)
brightness(0.8);
}
.gitee-link:hover .doc-icon {
filter: brightness(0) saturate(100%) invert(15%) sepia(100%) saturate(2500%) hue-rotate(350deg)
brightness(0.7);
}
/* 移除旧的基于 SVG color 的样式,现在使用 filter */
/* 暗黑模式下覆盖所有链接样式 */
:global(html[data-theme='dark']) .doc-link {
color: #ffffff;
}
:global(html[data-theme='dark']) .doc-link:hover {
color: #ffffff;
background-color: rgba(255, 255, 255, 0.1);
}
:global(html[data-theme='dark']) .doc-link:nth-child(2) {
color: #ffffff !important;
}
:global(html[data-theme='dark']) .doc-link:nth-child(2):hover {
color: #ffffff !important;
background-color: rgba(199, 29, 35, 0.1);
}
/* 暗黑模式下图标样式 */
:global(html[data-theme='dark']) .github-link .doc-icon {
filter: brightness(0) saturate(100%) invert(100%);
}
:global(html[data-theme='dark']) .github-link:hover .doc-icon {
filter: brightness(0) saturate(100%) invert(70%) sepia(50%) saturate(2000%) hue-rotate(190deg)
brightness(1.2);
}
:global(html[data-theme='dark']) .gitee-link .doc-icon {
filter: brightness(0) saturate(100%) invert(45%) sepia(100%) saturate(1500%) hue-rotate(340deg)
brightness(1.1);
}
:global(html[data-theme='dark']) .gitee-link:hover .doc-icon {
filter: brightness(0) saturate(100%) invert(50%) sepia(100%) saturate(1800%) hue-rotate(340deg)
brightness(1.2);
}
</style>

View File

@@ -1,332 +0,0 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const props = defineProps<{ visible: boolean }>()
const versionList = ref<any[]>([])
onMounted(async () => {
const res = await fetch('./version-history.json')
const data = await res.json()
// 按日期和版本号倒序排列
versionList.value = data.sort((a, b) => {
if (a.date === b.date) {
// 版本号倒序
return b.version.localeCompare(a.version, undefined, { numeric: true })
}
return b.date.localeCompare(a.date)
})
})
</script>
<template>
<div>
<div v-if="props.visible" class="drawer-mask" @click="$emit('close')"></div>
<div class="version-history-drawer" :class="{ open: props.visible }">
<div class="drawer-header">
<h3 class="drawer-title">版本历史</h3>
<button class="drawer-close-btn" type="button" @click="$emit('close')">
<svg class="close-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="drawer-body">
<div class="version-timeline">
<div
v-for="(item, idx) in versionList"
:key="item.version"
class="version-timeline-group"
>
<div :class="['version-timeline-dot', idx === 0 ? 'latest' : '']">
<div class="dot-label">
<span class="dot-version">{{ item.version }}</span>
<span class="dot-date">{{ item.date }}</span>
</div>
</div>
<div class="version-timeline-content version-card">
<ul class="version-notes">
<li v-for="(note, nidx) in item.notes" :key="nidx">{{ note }}</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.version-history-drawer {
position: fixed;
top: 0;
left: 0;
width: 540px; /* 增加Drawer宽度适中大气 */
height: 100vh;
background: var(--gantt-bg-primary, #fff);
box-shadow: 2px 0 24px rgba(0, 0, 0, 0.1);
z-index: 2000;
transform: translateX(-100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
flex-direction: column;
/* 去除圆角 */
}
.version-history-drawer.open {
transform: translateX(0);
}
.drawer-mask {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5); /* 与TaskDrawer一致 */
z-index: 1999;
transition: background 0.2s;
}
.drawer-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
border-bottom: 1px solid var(--gantt-border-light, #ebeef5);
background: var(--gantt-bg-secondary, #f5f7fa);
}
.drawer-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--gantt-text-primary, #303133);
}
.drawer-close-btn {
background: none;
border: none;
cursor: pointer;
padding: 4px;
color: var(--gantt-text-muted, #909399);
transition: color 0.2s;
}
.drawer-close-btn:hover {
color: var(--gantt-text-secondary, #606266);
}
.close-icon {
width: 16px;
height: 16px;
stroke-width: 2;
}
.drawer-body {
flex: 1;
overflow-y: auto;
padding: 32px 32px 32px 60px;
background: var(--gantt-bg-primary, #fff); /* 统一用变量,便于主题切换 */
scrollbar-width: thin;
scrollbar-color: #b3c6e0 #f0f2f5;
/* 去除圆角 */
}
.drawer-body::-webkit-scrollbar {
width: 6px;
background: #f0f2f5;
}
.drawer-body::-webkit-scrollbar-thumb {
background: #b3c6e0;
border-radius: 4px;
}
.drawer-body::-webkit-scrollbar-track {
background: #f0f2f5;
}
.version-timeline {
position: relative;
margin-left: 140px;
border-left: 0;
}
.version-timeline::before {
content: '';
position: absolute;
left: -8px;
top: 0;
width: 2px;
height: 100%;
background: repeating-linear-gradient(
to bottom,
var(--gantt-timeline-line, #a0cfff) 0 8px,
transparent 8px 16px
);
border-radius: 1px;
z-index: 0;
}
.version-timeline-group {
position: relative;
margin-bottom: 40px;
padding-left: 18px;
min-height: 60px;
transition: box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.version-timeline-dot {
position: absolute;
left: -13px;
top: 18px;
width: 10px;
height: 10px;
background: var(--gantt-timeline-dot, #a0cfff); /* 更柔和主色 */
border-radius: 50%;
border: 2px solid #fff;
box-shadow: 0 0 0 2px var(--gantt-timeline-line, #b3d8ff);
z-index: 3;
display: flex;
align-items: flex-start;
justify-content: flex-end;
transition:
background 0.2s,
box-shadow 0.2s;
}
.version-timeline-group:hover .version-timeline-dot {
background: var(--gantt-timeline-dot-hover, #409eff); /* hover主色 */
}
.version-timeline-dot.latest {
background: var(--gantt-primary, #409eff);
}
.dot-label {
position: absolute;
left: -140px;
top: 50%;
transform: translateY(-50%);
display: flex;
flex-direction: column;
align-items: flex-end;
min-width: 90px;
max-width: 110px;
font-size: 13px;
pointer-events: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: left 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.version-timeline-group:hover .dot-label {
left: -110px; /* 悬停时向右移动,接近原点 */
}
.version-timeline-content.version-card {
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 12px rgba(64, 158, 255, 0.08); /* 主色阴影 */
padding: 18px 18px 12px 18px;
margin-left: 0;
margin-top: 0;
margin-bottom: 0;
min-width: 0;
border: none;
transition: box-shadow 0.2s;
position: relative;
z-index: 2;
}
.version-timeline-group:hover .version-timeline-content.version-card {
box-shadow: 0 8px 24px rgba(64, 158, 255, 0.16);
}
.version-timeline-content.version-card::before {
content: '';
position: absolute;
left: -16px; /* 让箭头更靠近原点但不覆盖 */
top: 18px;
width: 0;
height: 0;
filter: drop-shadow(-2px 0 2px var(--gantt-timeline-line, #a0cfff));
z-index: 1;
}
.version-timeline-group:hover .version-timeline-content.version-card::before {
filter: drop-shadow(-2px 0 2px var(--gantt-primary, #409eff));
}
.version-notes {
margin: 0;
padding-left: 18px;
color: #aaa;
font-size: 14px;
list-style: disc;
transition: color 0.2s;
}
.version-timeline-group:hover .version-notes {
color: #333;
}
.dot-version {
font-weight: 700;
font-size: 16px;
color: var(--gantt-primary, #409eff); /* 统一主色 */
letter-spacing: 0.5px;
line-height: 1.2;
text-shadow: 0 1px 4px rgba(64, 158, 255, 0.08);
}
.dot-date {
color: #b0b3bb;
font-size: 12px;
margin-top: 2px;
font-weight: 400;
letter-spacing: 0.2px;
line-height: 1.1;
}
/* 暗黑主题适配完全对齐TaskDrawer风格 */
:global(html[data-theme='dark']) .version-history-drawer {
background: var(--gantt-bg-primary, #6b6b6b) !important;
box-shadow: 2px 0 24px rgba(0, 0, 0, 0.4) !important;
}
:global(html[data-theme='dark']) .drawer-header {
background: var(--gantt-bg-secondary, #4b4b4b) !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08) !important;
}
:global(html[data-theme='dark']) .drawer-title {
color: var(--gantt-text-white, #fff) !important;
}
:global(html[data-theme='dark']) .drawer-close-btn:hover {
background: var(--gantt-bg-hover, rgba(255, 255, 255, 0.1)) !important;
border-radius: 4px;
}
:global(html[data-theme='dark']) .drawer-body {
background: var(--gantt-bg-primary, #6b6b6b) !important;
scrollbar-color: #888888 #4b4b4b !important;
}
:global(html[data-theme='dark']) .drawer-body::-webkit-scrollbar-thumb {
background: #888888 !important;
}
:global(html[data-theme='dark']) .drawer-body::-webkit-scrollbar-track {
background: #4b4b4b !important;
}
:global(html[data-theme='dark']) .version-timeline-content.version-card {
background: var(--gantt-bg-secondary, #4b4b4b) !important;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.32) !important;
color: var(--gantt-text-white, #fff) !important;
}
:global(html[data-theme='dark'])
.version-timeline-group:hover
.version-timeline-content.version-card {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.44) !important;
}
:global(html[data-theme='dark']) .version-timeline-content.version-card::before {
filter: drop-shadow(-2px 0 2px #222) !important;
}
:global(html[data-theme='dark']) .dot-version {
color: var(--gantt-primary, #3399ff) !important;
text-shadow: 0 1px 4px rgba(51, 153, 255, 0.12) !important;
}
:global(html[data-theme='dark']) .dot-date {
color: #e0e0e0 !important;
}
:global(html[data-theme='dark']) .version-notes {
color: #e0e0e0 !important;
}
:global(html[data-theme='dark']) .version-timeline-dot {
background: var(--gantt-timeline-dot, #3399ff) !important;
box-shadow: 0 0 0 2px #222 !important;
}
:global(html[data-theme='dark']) .version-timeline-dot.latest {
background: var(--gantt-primary, #3399ff) !important;
}
:global(html[data-theme='dark']) .version-timeline-group:hover .version-timeline-dot {
background: var(--gantt-primary, #3399ff) !important;
}
:global(html[data-theme='dark']) .version-timeline::before {
background: repeating-linear-gradient(to bottom, #3399ff 0 8px, transparent 8px 16px) !important;
}
</style>

View File

@@ -1,469 +0,0 @@
{
"tasks": [
{
"id": 1000,
"name": "新药ADX-2024临床试验项目",
"assignee": "首席研究员 Dr. Chen",
"startDate": "2025-01-01",
"endDate": "2027-12-31",
"progress": 15,
"estimatedHours": 26280.5,
"actualHours": 3942.75,
"type": "story",
"description": "ADX-2024新药完整临床试验计划从I期到III期临床试验",
"children": [
{
"id": 1100,
"name": "I期临床试验",
"assignee": "I期试验主任 Dr. Wang",
"startDate": "2025-01-01",
"endDate": "2025-08-31",
"progress": 65,
"estimatedHours": 5256,
"actualHours": 3417,
"type": "story",
"parentId": 1000,
"description": "评估ADX-2024在健康志愿者和患者中的安全性、耐受性和药代动力学特征",
"children": [
{
"id": 1101,
"name": "试验方案设计与伦理审查",
"assignee": "方案设计师 李明",
"startDate": "2025-01-01",
"endDate": "2025-02-28",
"progress": 100,
"estimatedHours": 464,
"actualHours": 480,
"type": "task",
"parentId": 1100,
"description": "完成I期试验方案设计、伦理委员会审批及监管部门申报"
},
{
"id": 1102,
"name": "受试者招募与筛选",
"assignee": "招募专员 张丽",
"startDate": "2025-03-01",
"endDate": "2025-04-30",
"progress": 90,
"estimatedHours": 1392.5,
"actualHours": 1250.25,
"type": "task",
"predecessor": [1101],
"parentId": 1100,
"description": "招募24名健康志愿者完成医学筛选和入组评估"
},
{
"id": 1103,
"name": "药物给药与安全性监测",
"assignee": "临床医生 Dr. Liu",
"startDate": "2025-05-01",
"endDate": "2025-08-31",
"progress": 40,
"estimatedHours": 3400,
"actualHours": 1687,
"type": "task",
"predecessor": [1102],
"parentId": 1100,
"description": "按照剂量递增方案给药,密切监测不良反应和药代动力学参数"
}
],
"collapsed": false
},
{
"id": 1200,
"name": "II期临床试验",
"assignee": "II期试验主任 Dr. Zhang",
"startDate": "2025-09-01",
"endDate": "2026-08-31",
"progress": 5,
"estimatedHours": 8760.75,
"actualHours": 438.25,
"type": "story",
"parentId": 1000,
"description": "在目标患者群体中评估ADX-2024的初步疗效和进一步的安全性",
"children": [
{
"id": 1201,
"name": "多中心试验启动",
"assignee": "项目经理 王芳",
"startDate": "2025-09-01",
"endDate": "2025-11-30",
"progress": 25,
"estimatedHours": 2160,
"actualHours": 324,
"type": "task",
"predecessor": [1103],
"parentId": 1200,
"description": "启动5个临床中心完成研究者培训和质量体系建立"
},
{
"id": 1202,
"name": "患者入组与随机化",
"assignee": "数据管理员 陈静",
"startDate": "2025-12-01",
"endDate": "2026-03-31",
"progress": 0,
"estimatedHours": 2880,
"actualHours": 0,
"type": "task",
"predecessor": [1201],
"parentId": 1200,
"description": "计划入组120名患者采用双盲随机对照设计"
},
{
"id": 1203,
"name": "疗效评估与数据收集",
"assignee": "统计师 赵磊",
"startDate": "2026-01-01",
"endDate": "2026-08-31",
"progress": 0,
"estimatedHours": 3720,
"actualHours": 0,
"type": "task",
"predecessor": [1202],
"parentId": 1200,
"description": "定期评估患者疗效指标,收集安全性数据,进行中期分析"
}
],
"collapsed": false
},
{
"id": 1300,
"name": "III期临床试验",
"assignee": "III期试验主任 Dr. Li",
"startDate": "2026-09-01",
"endDate": "2027-12-31",
"progress": 0,
"estimatedHours": 12264,
"actualHours": 0,
"type": "story",
"parentId": 1000,
"description": "大规模多中心随机对照试验确认ADX-2024的疗效和安全性",
"children": [
{
"id": 1301,
"name": "国际多中心试验准备",
"assignee": "国际事务专员 刘倩",
"startDate": "2026-09-01",
"endDate": "2026-12-31",
"progress": 0,
"estimatedHours": 2928,
"actualHours": 0,
"type": "task",
"predecessor": [1203],
"parentId": 1300,
"description": "启动15个国际临床中心获得各国监管审批"
},
{
"id": 1302,
"name": "大规模患者招募",
"assignee": "多中心协调员 孙伟",
"startDate": "2027-01-01",
"endDate": "2027-06-30",
"progress": 0,
"estimatedHours": 4380,
"actualHours": 0,
"type": "task",
"predecessor": [1301],
"parentId": 1300,
"description": "计划入组500名患者严格按照纳排标准执行"
},
{
"id": 1303,
"name": "最终疗效分析与报告",
"assignee": "首席统计师 马强",
"startDate": "2027-03-01",
"endDate": "2027-12-31",
"progress": 0,
"estimatedHours": 4956,
"actualHours": 0,
"type": "task",
"predecessor": [1302],
"parentId": 1300,
"description": "完成最终疗效分析,撰写临床研究报告,准备新药上市申请"
}
],
"collapsed": false
}
],
"collapsed": false
},
{
"id": 1001,
"name": "上午会议",
"assignee": "测试用户",
"startDate": "2025-09-10 09:00",
"endDate": "2025-09-10 10:30",
"progress": 50,
"estimatedHours": 1.5,
"actualHours": 0.75,
"type": "task"
},
{
"id": 1002,
"name": "午后开发",
"assignee": "开发者",
"startDate": "2025-09-10 14:00",
"endDate": "2025-09-10 16:15",
"progress": 30,
"estimatedHours": 2.25,
"actualHours": 1.33,
"type": "task"
},
{
"id": 1003,
"name": "测试任务",
"assignee": "测试员",
"startDate": "2025-09-10 16:30",
"endDate": "2025-09-10 17:45",
"progress": 0,
"estimatedHours": 1.25,
"actualHours": 0,
"type": "task"
},
{
"id": 1004,
"name": "当日整天任务",
"assignee": "项目经理",
"startDate": "2025-09-10",
"endDate": "2025-09-10",
"progress": 20,
"estimatedHours": 8.0,
"actualHours": 1.5,
"type": "task"
},
{
"id": 1005,
"name": "12点瞬间任务",
"assignee": "测试员",
"startDate": "2025-09-10 12:00",
"endDate": "2025-09-10 12:00",
"progress": 100,
"estimatedHours": 0,
"actualHours": 0,
"type": "task"
},
{
"id": 1,
"name": "项目启动",
"assignee": "张三",
"startDate": "2025-06-15",
"endDate": "2025-06-25",
"progress": 100,
"estimatedHours": 40,
"actualHours": 38,
"type": "story",
"children": [
{
"id": 2,
"name": "需求分析",
"assignee": "李四",
"startDate": "2025-04-16",
"endDate": "2025-06-20",
"progress": 100,
"estimatedHours": 16,
"actualHours": 15,
"type": "task",
"parentId": 1
},
{
"id": 3,
"name": "技术选型",
"assignee": "王五",
"startDate": "2025-06-21",
"endDate": "2025-06-28",
"progress": 80,
"estimatedHours": 24,
"actualHours": 28,
"type": "story",
"parentId": 1,
"children": [
{
"id": 4,
"name": "调研A",
"predecessor": [2],
"assignee": "赵六",
"startDate": "2025-06-21",
"endDate": "2025-06-23",
"progress": 80,
"estimatedHours": 8,
"actualHours": 10,
"type": "task",
"parentId": 3
},
{
"id": 5,
"name": "调研B",
"predecessor": [4],
"assignee": "钱七",
"startDate": "2025-06-24",
"endDate": "2025-06-28",
"progress": 0,
"estimatedHours": 16,
"actualHours": 0,
"type": "task",
"parentId": 3
}
],
"collapsed": false
}
],
"collapsed": false
},
{
"id": 7,
"name": "开发阶段",
"assignee": "开发团队",
"startDate": "2025-06-29",
"endDate": "2025-07-15",
"progress": 30,
"estimatedHours": 120,
"actualHours": 45,
"type": "story",
"children": [
{
"id": 8,
"name": "前端开发",
"assignee": "李四",
"startDate": "2025-06-29",
"endDate": "2025-07-10",
"progress": 40,
"estimatedHours": 80,
"actualHours": 35,
"type": "task",
"predecessor": [5],
"parentId": 7
},
{
"id": 11,
"name": "后端开发",
"assignee": "李四",
"startDate": "2025-07-07",
"endDate": "2025-07-15",
"progress": 0,
"estimatedHours": 80,
"actualHours": 0,
"type": "task",
"predecessor": [5],
"parentId": 7
},
{
"id": 12,
"name": "Job开发",
"assignee": "李四",
"startDate": "2025-07-07",
"endDate": "2025-07-18",
"progress": 0,
"estimatedHours": 88,
"actualHours": 0,
"type": "task",
"predecessor": [5],
"parentId": 7
}
],
"collapsed": false
},
{
"id": 13,
"name": "测试阶段",
"assignee": "测试团队",
"startDate": "2025-07-21",
"endDate": "2025-07-31",
"progress": 0,
"estimatedHours": 80,
"actualHours": 0,
"type": "story",
"children": [
{
"id": 14,
"name": "SIT",
"assignee": "李四",
"startDate": "2025-07-21",
"endDate": "2025-07-28",
"progress": 0,
"estimatedHours": 48,
"actualHours": 0,
"type": "task",
"predecessor": [8, 11, 12],
"parentId": 13
},
{
"id": 15,
"name": "UAT",
"assignee": "李四",
"startDate": "2025-07-29",
"endDate": "2025-07-31",
"progress": 0,
"estimatedHours": 24,
"actualHours": 0,
"type": "task",
"predecessor": [14],
"parentId": 13
}
],
"collapsed": false
}
],
"milestones": [
{
"id": 1150,
"name": "I期临床试验完成",
"assignee": "I期试验主任 Dr. Wang",
"startDate": "2025-08-31",
"endDate": "2025-08-31",
"type": "milestone",
"icon": "shield-check",
"description": "完成I期临床试验确认ADX-2024在人体的安全性和耐受性为II期试验奠定基础。"
},
{
"id": 1250,
"name": "II期临床中期分析",
"assignee": "统计师 赵磊",
"startDate": "2026-04-30",
"endDate": "2026-04-30",
"type": "milestone",
"icon": "bar-chart",
"description": "完成II期临床试验中期疗效分析评估是否继续进行III期试验。"
},
{
"id": 1350,
"name": "新药上市申请提交",
"assignee": "首席研究员 Dr. Chen",
"startDate": "2027-12-31",
"endDate": "2027-12-31",
"type": "milestone",
"icon": "trophy",
"description": "完成III期临床试验最终分析向监管部门提交新药上市申请(NDA)。"
},
{
"id": 6,
"name": "技术选型完成",
"assignee": "王五",
"startDate": "2025-06-25",
"endDate": "2025-06-25",
"type": "milestone",
"icon": "diamond",
"description": "完成技术选型调研,确定项目技术栈和架构方案。"
},
{
"id": 10,
"name": "开发完成",
"assignee": "开发团队",
"startDate": "2025-07-11",
"endDate": "2025-07-11",
"type": "milestone",
"icon": "rocket",
"description": "完成所有开发任务,准备进入测试阶段。"
},
{
"id": 9,
"name": "Alpha版本发布",
"assignee": "项目经理",
"startDate": "2025-07-15",
"endDate": "2025-07-15",
"type": "milestone",
"icon": "rocket",
"description": "发布Alpha测试版本提供给内部团队进行功能验证和测试。"
}
]
}

View File

@@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/public/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Jordium Gantt Vue3 Demo</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./main.ts"></script>
</body>
</html>

View File

@@ -1,5 +0,0 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +0,0 @@
{
"name": "packagedemo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "vite",
"dev:demo": "vite",
"build": "vue-tsc -b && vite build",
"build:demo": "vue-tsc -b && vite build",
"build:lib": "vite build --config vite.config.lib.ts"
},
"keywords": [
"jordium-gantt-vue3",
"vue3",
"gantt",
"chart",
"component"
],
"author": "Jordium.com",
"license": "MIT",
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-vue": "^5.2.3",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.7.0",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.25.0",
"prettier": "^3.2.5",
"typescript": "~5.8.3",
"vite": "^6.3.5",
"vue-tsc": "^2.2.8"
}
}

View File

@@ -1,12 +0,0 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="4" width="20" height="2" rx="1" fill="#409eff" />
<rect x="2" y="8" width="12" height="2" rx="1" fill="#67c23a" />
<rect x="2" y="12" width="16" height="2" rx="1" fill="#e6a23c" />
<rect x="2" y="16" width="8" height="2" rx="1" fill="#f56c6c" />
<rect x="2" y="20" width="14" height="2" rx="1" fill="#909399" />
<circle cx="22" cy="5" r="1" fill="#409eff" />
<circle cx="16" cy="9" r="1" fill="#67c23a" />
<circle cx="20" cy="13" r="1" fill="#e6a23c" />
<circle cx="12" cy="17" r="1" fill="#f56c6c" />
<circle cx="18" cy="21" r="1" fill="#909399" />
</svg>

Before

Width:  |  Height:  |  Size: 665 B

View File

@@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750864628544" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5058" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 1024C229.222 1024 0 794.778 0 512S229.222 0 512 0s512 229.222 512 512-229.222 512-512 512z m259.149-568.883h-290.74a25.293 25.293 0 0 0-25.292 25.293l-0.026 63.206c0 13.952 11.315 25.293 25.267 25.293h177.024c13.978 0 25.293 11.315 25.293 25.267v12.646a75.853 75.853 0 0 1-75.853 75.853h-240.23a25.293 25.293 0 0 1-25.267-25.293V417.203a75.853 75.853 0 0 1 75.827-75.853h353.946a25.293 25.293 0 0 0 25.267-25.292l0.077-63.207a25.293 25.293 0 0 0-25.268-25.293H417.152a189.62 189.62 0 0 0-189.62 189.645V771.15c0 13.977 11.316 25.293 25.294 25.293h372.94a170.65 170.65 0 0 0 170.65-170.65V480.384a25.293 25.293 0 0 0-25.293-25.267z" fill="#C71D23" p-id="5059"></path></svg>

Before

Width:  |  Height:  |  Size: 1010 B

View File

@@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750864675427" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6018" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 42.666667A464.64 464.64 0 0 0 42.666667 502.186667 460.373333 460.373333 0 0 0 363.52 938.666667c23.466667 4.266667 32-9.813333 32-22.186667v-78.08c-130.56 27.733333-158.293333-61.44-158.293333-61.44a122.026667 122.026667 0 0 0-52.053334-67.413333c-42.666667-28.16 3.413333-27.733333 3.413334-27.733334a98.56 98.56 0 0 1 71.68 47.36 101.12 101.12 0 0 0 136.533333 37.973334 99.413333 99.413333 0 0 1 29.866667-61.44c-104.106667-11.52-213.333333-50.773333-213.333334-226.986667a177.066667 177.066667 0 0 1 47.36-124.16 161.28 161.28 0 0 1 4.693334-121.173333s39.68-12.373333 128 46.933333a455.68 455.68 0 0 1 234.666666 0c89.6-59.306667 128-46.933333 128-46.933333a161.28 161.28 0 0 1 4.693334 121.173333A177.066667 177.066667 0 0 1 810.666667 477.866667c0 176.64-110.08 215.466667-213.333334 226.986666a106.666667 106.666667 0 0 1 32 85.333334v125.866666c0 14.933333 8.533333 26.88 32 22.186667A460.8 460.8 0 0 0 981.333333 502.186667 464.64 464.64 0 0 0 512 42.666667" fill="#231F20" p-id="6019"></path></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,113 +0,0 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
min-width: 320px;
height: 100vh;
padding: 30px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
width: 100%;
height: 100%;
max-width: 100vw;
max-height: 100vh;
min-width: 0;
min-height: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: center;
padding: 0;
text-align: left;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
/* 黑暗模式全局样式 */
html[data-theme='dark'] {
background: #1e1e1e !important;
color-scheme: dark !important;
}
html[data-theme='dark'] body {
background: #1e1e1e !important;
color: #e5e5e5 !important;
}
/* 明亮模式全局样式 */
html[data-theme='light'] {
background: #ffffff !important;
color-scheme: light !important;
}
html[data-theme='light'] body {
background: #ffffff !important;
color: #333333 !important;
}

View File

@@ -1,194 +0,0 @@
[
{
"version": "alpha 0.8.1",
"date": "2025-06-25",
"notes": ["升级优化DatePicker组件"]
},
{
"version": "alpha 0.8.2",
"date": "2025-06-25",
"notes": ["升级Demo", "移除导出按钮的初始化光晕效果"]
},
{
"version": "alpha 0.8.3",
"date": "2025-06-25",
"notes": ["升级里程碑Taskbar允许拖拽"]
},
{
"version": "alpha 0.8.4",
"date": "2025-06-25",
"notes": ["升级需求/任务TaskBar新建以及上下级关系变更", "添加Github和Gitee的文档入口"]
},
{
"version": "alpha 0.8.5",
"date": "2025-06-26",
"notes": ["升级Timeline今日定位和滑动问题"]
},
{
"version": "alpha 0.9.0",
"date": "2025-06-26",
"notes": ["添加新增里程碑和删除里程碑功能"]
},
{
"version": "alpha 0.9.1",
"date": "2025-06-27",
"notes": ["优化时间轴的延伸"]
},
{
"version": "alpha 0.9.2",
"date": "2025-06-27",
"notes": ["增加历史版本查看"]
},
{
"version": "alpha 0.9.3",
"date": "2025-06-27",
"notes": ["修复今日定位问题"]
},
{
"version": "alpha 0.9.4",
"date": "2025-06-27",
"notes": ["调整暗黑主题的亮度", "调整暗黑主题的布局", "修复暗黑主题下的版本历史的样式"]
},
{
"version": "alpha 0.9.5",
"date": "2025-06-27",
"notes": ["统一管理各个组件的全球化配置"]
},
{
"version": "alpha 0.9.6",
"date": "2025-06-27",
"notes": ["各个组件分离Task/Milestone/Language对象"]
},
{
"version": "alpha 0.9.7",
"date": "2025-06-28",
"notes": ["解决Milestone组件和类型的同名问题"]
},
{
"version": "alpha 0.9.8",
"date": "2025-06-28",
"notes": ["按钮样式统一管理"]
},
{
"version": "1.0.0",
"date": "2025-06-29",
"notes": [
"GanttChart接口增强",
"添加GanttChart的拖拽事件回调",
"添加GanttChart的里程碑拖拽事件回调",
"修复里程碑点的样式问题",
"更新README文档",
"重构项目目录结构"
]
},
{
"version": "1.0.1",
"date": "2025-07-01",
"notes": ["增加npm安装及组件集成示例"]
},
{
"version": "1.0.2",
"date": "2025-07-01",
"notes": ["升级README文档", "增加贡献说明", "增加贡献者列表"]
},
{
"version": "1.0.3",
"date": "2025-07-02",
"notes": ["增加Github Pages", "修改README, 添加GIthub在线Demo入口", "移除大体积静态资源"]
},
{
"version": "1.0.4",
"date": "2025-07-02",
"notes": ["修改NPM徽章CDN"]
},
{
"version": "1.0.5",
"date": "2025-07-02",
"notes": ["更新README中所有NPM徽章CDN地址"]
},
{
"version": "1.0.6",
"date": "2025-07-03",
"notes": [
"更新TaskDrawer组件的Progressbar样式",
"可通过拖拽进度条和文本修改的方式更改任务的进度",
"Story进度根据子任务集进度改编重新计算",
"同时保留Story进度单独的调整方式"
]
},
{
"version": "1.0.9",
"date": "2025-07-04",
"notes": [
"更新多语言标签",
"增加Story删除时选择是否删除子任务的选项",
"删除Story或Task时同时删除前后的关系线路"
]
},
{
"version": "1.0.10",
"date": "2025-07-06",
"notes": [
"升级用户体验",
"增加边框粘性toolBar表示隐藏的TaskBar和MilestoneBar",
"点击边框粘性toolBar快速定位滑动到对应的TaskBar和MilestoneBar",
"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代码质量检查问题修改"]
},
{
"version": "1.3.0",
"date": "2025-09-12",
"notes": [
"增加年度视图",
"Add annual timeline view",
"增加季度视图",
"Add quarterly timeline view",
"增加小时视图",
"Add hourly timeline view",
"升级TaskDrawer中DatePicker组件接受小时的设定",
"Upgrade DatePicker component in TaskDrawer to accept hour settings",
"升级TaskDrawer中预计时间和实际时间的控件接受小数后两位的录入",
"Upgrade the estimated time and actual time controls in TaskDrawer to accept two decimal places",
"SonarQube代码质量检查问题修改",
"SonarQube code quality inspection issue modification"
]
},
{
"version": "1.3.2",
"date": "2025-09-24",
"notes": [
"修复缺陷",
"Bugfix",
"SonarQube代码质量检查问题修改",
"SonarQube code quality inspection issue modification"
]
}
]

View File

@@ -1,7 +0,0 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
})

View File

@@ -126,7 +126,7 @@ const leftPanelWidth = ref(props.taskListConfig?.defaultWidth || 320)
// 监听taskListConfig变化更新相关配置
watch(
() => props.taskListConfig,
(newConfig) => {
newConfig => {
if (newConfig) {
// 更新默认宽度
if (newConfig.defaultWidth !== undefined) {
@@ -607,7 +607,8 @@ const tasksForTaskList = computed(() => {
// 不排序:直接使用原始任务数据
result.push(...props.tasks)
}
} return result
}
return result
})
// 为Timeline提供正确的扁平化数据

View File

@@ -42,6 +42,8 @@ interface TaskBarSlotProps {
currentTimeScale?: TimelineScale
rowHeight: number
dayWidth: number
// 新增:动态样式对象
dynamicStyles: Record<string, string>
}
const props = defineProps<Props>()
@@ -165,6 +167,9 @@ const quarterDragOverride = ref<{
const barRef = ref<HTMLElement | null>(null)
const taskBarNameRef = ref<HTMLElement | null>(null)
const nameTextWidth = ref(0)
// 计算任务条位置和宽度
const taskBarStyle = computed(() => {
// 季度视图拖拽时使用位置覆盖
@@ -390,6 +395,7 @@ const slotPayload = computed(() => ({
currentTimeScale: props.currentTimeScale,
rowHeight: props.rowHeight,
dayWidth: props.dayWidth,
dynamicStyles: getNameStyles(),
}))
// 判断是否已完成
@@ -835,6 +841,9 @@ const handleMouseUp = () => {
onMounted(() => {
nextTick(() => {
reportBarPosition()
if (taskBarNameRef.value) {
nameTextWidth.value = taskBarNameRef.value.getBoundingClientRect().width
}
})
// 监听时间刻度变化事件,重新计算位置
@@ -936,8 +945,7 @@ const stickyStyles = computed(() => {
let progressTop = ''
// 估算文字内容的实际位置
const nameText = props.task.name || ''
const nameWidth = Math.max(nameText.length * 7, 40)
const nameWidth = Math.max(nameTextWidth.value, 40) // 最小40px
const progressWidth = 35
// 计算名称和进度在默认居中状态下的位置
@@ -959,12 +967,12 @@ const stickyStyles = computed(() => {
const offset = leftBoundary - taskLeft
nameLeft = `${offset + 8}px`
namePosition = 'absolute'
nameTop = '6px'
nameTop = '2px'
} else if (nameNeedsRightSticky) {
const offset = rightBoundary - taskLeft - nameWidth
nameLeft = `${offset - 8}px`
namePosition = 'absolute'
nameTop = '6px'
nameTop = '2px'
}
// 进度左侧边界粘性逻辑
@@ -980,12 +988,12 @@ const stickyStyles = computed(() => {
const offset = leftBoundary - taskLeft
progressLeft = `${offset + 8}px`
progressPosition = 'absolute'
progressTop = '24px'
progressTop = '18px'
} else if (progressNeedsRightSticky) {
const offset = rightBoundary - taskLeft - progressWidth
progressLeft = `${offset - 8}px`
progressPosition = 'absolute'
progressTop = '24px'
progressTop = '18px'
}
return {
@@ -1529,14 +1537,8 @@ onUnmounted(() => {
>
<!-- 父级任务的标签 -->
<div v-if="isParent" class="parent-label">
<slot
v-if="hasContentSlot"
name="custom-task-content"
v-bind="slotPayload"
/>
<template v-else>
{{ task.name }} ({{ task.progress || 0 }}%)
</template>
<slot v-if="hasContentSlot" name="custom-task-content" v-bind="slotPayload" />
<template v-else> {{ task.name }} ({{ task.progress || 0 }}%) </template>
</div>
<!-- 完成进度条非父级任务 -->
@@ -1564,14 +1566,17 @@ onUnmounted(() => {
@mousedown="e => (isInteractionDisabled ? null : handleMouseDown(e, 'drag'))"
>
<!-- 任务名称 -->
<div ref="taskBarNameRef">
<slot
v-if="hasContentSlot"
name="custom-task-content"
v-bind="slotPayload"
:style="getNameStyles()"
/>
<div v-else class="task-name" :style="getNameStyles()">
{{ task.name }}
</div>
</div>
<!-- 进度百分比 -->
<div v-if="shouldShowProgress" class="task-progress" :style="getProgressStyles()">

View File

@@ -313,7 +313,7 @@ const handleTaskListScroll = (event: Event) => {
detail: { scrollTop },
}),
)
}// 处理Timeline垂直滚动同步
} // 处理Timeline垂直滚动同步
const handleTimelineVerticalScroll = (event: CustomEvent) => {
const { scrollTop } = event.detail
const taskListBodyElement = document.querySelector('.task-list-body') as HTMLElement
@@ -406,7 +406,7 @@ onUnmounted(() => {
:class="column.cssClass || `col-${column.key}`"
:style="column.width ? { width: column.width + 'px' } : undefined"
>
{{ column.label || (t as any)[column.key] }}
{{ (t as any)[column.key] || column.label }}
</div>
</div>
<div class="task-list-body" @scroll="handleTaskListScroll">

View File

@@ -67,7 +67,7 @@ const isStoryTask = computed(() => props.task.type === 'story')
const isMilestoneGroup = computed(() => props.task.type === 'milestone-group')
const isMilestoneTask = computed(() => props.task.type === 'milestone')
const isParentTask = computed(
() => isStoryTask.value || hasChildren.value || isMilestoneGroup.value,
() => isStoryTask.value || hasChildren.value || isMilestoneGroup.value
)
function handleToggle() {
emit('toggle', props.task)
@@ -75,10 +75,7 @@ function handleToggle() {
function handleRowClick() {
// 如果是普通父级任务story类型或有子任务的任务非里程碑分组点击行也可以展开/收起
if (
(isStoryTask.value || hasChildren.value) &&
!isMilestoneGroup.value
) {
if ((isStoryTask.value || hasChildren.value) && !isMilestoneGroup.value) {
emit('toggle', props.task)
}
}
@@ -224,7 +221,7 @@ watch(
updateTimer()
}
},
{ immediate: true },
{ immediate: true }
)
// 右键菜单相关状态
@@ -308,7 +305,7 @@ onUnmounted(() => {
'milestone-group-row': isMilestoneGroup,
'task-type-story': isStoryTask,
'task-type-task': props.task.type === 'task',
'task-type-milestone': isMilestoneTask
'task-type-milestone': isMilestoneTask,
}"
@click="handleRowClick"
@dblclick="handleTaskRowDoubleClick"
@@ -470,9 +467,7 @@ onUnmounted(() => {
</template>
</div>
</div>
<template
v-if="hasChildren && !props.task.collapsed && !isMilestoneGroup"
>
<template v-if="hasChildren && !props.task.collapsed && !isMilestoneGroup">
<TaskRow
v-for="child in props.task.children"
:key="child.id"
@@ -492,7 +487,7 @@ onUnmounted(() => {
@delete="handleTaskDelete"
>
<template v-if="hasContentSlot" #custom-task-content="slotProps">
<slot name="custom-task-content" v-bind="(slotProps as TaskRowSlotProps)" />
<slot name="custom-task-content" v-bind="slotProps as TaskRowSlotProps" />
</template>
</TaskRow>
</template>
@@ -696,8 +691,8 @@ onUnmounted(() => {
}
.avatar {
width: 24px;
height: 24px;
min-width: 25px;
min-height: 25px;
border-radius: 50%;
background: var(--gantt-primary);
color: var(--gantt-text-white);

View File

@@ -2419,8 +2419,10 @@ const handleAddSuccessor = (task: Task) => {
v-if="subDay.date && subDay.date.getDate() === 1"
class="month-first-flag"
:style="{
left: `${getGlobalWeekPosition(monthIndex, weekIndex) + dayIndex * (60/7)}px`,
transform: 'translateX(-50%)' // 使旗帜中心(杆子)对齐日期位置
left: `${
getGlobalWeekPosition(monthIndex, weekIndex) + dayIndex * (60 / 7)
}px`,
transform: 'translateX(-50%)', // 使旗帜中心(杆子)对齐日期位置
}"
>
<div class="flag-pole"></div>
@@ -2551,8 +2553,10 @@ const handleAddSuccessor = (task: Task) => {
v-if="subDay.date && subDay.date.getDate() === 1"
class="month-first-vertical-line"
:style="{
left: `${getGlobalWeekPosition(monthIndex, weekIndex) + dayIndex * (60/7)}px`,
height: `${contentHeight}px`
left: `${
getGlobalWeekPosition(monthIndex, weekIndex) + dayIndex * (60 / 7)
}px`,
height: `${contentHeight}px`,
}"
></div>
</template>
@@ -2691,7 +2695,7 @@ const handleAddSuccessor = (task: Task) => {
class="sub-day-column"
:class="{
weekend: subDay.dayOfWeek === 0 || subDay.dayOfWeek === 6,
today: isToday(subDay.date)
today: isToday(subDay.date),
}"
:style="{ height: `${contentHeight}px`, width: '8.57px' }"
></div>

View File

@@ -199,6 +199,19 @@ const messages = {
timerConfirmPrefix: '即将为任务',
timerConfirmSuffix: '计时,若有特殊说明请完善下面的描述',
timerConfirmPlaceholder: '请输入计时说明',
// TaskList配置
taskListConfig: {
title: 'TaskList 配置',
columns: {
title: '列显示',
},
width: {
title: '宽度设置',
defaultWidth: '默认宽度',
minWidth: '最小宽度',
maxWidth: '最大宽度',
},
},
},
'en-US': {
dateNotSet: 'Not set',
@@ -396,6 +409,19 @@ const messages = {
timerConfirmPrefix: 'About to start timing for',
timerConfirmSuffix: '. If there are special notes, please complete the description below.',
timerConfirmPlaceholder: 'Please enter timer description',
// TaskList配置
taskListConfig: {
title: 'TaskList Configuration',
columns: {
title: 'Columns',
},
width: {
title: 'Width Settings',
defaultWidth: 'Default Width',
minWidth: 'Min Width',
maxWidth: 'Max Width',
},
},
},
}