v1.9.2-rc.1 - tooltip slots / taskList collapse enable etc.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -38,3 +38,4 @@ Desktop.ini
|
|||||||
|
|
||||||
yarn.lock
|
yarn.lock
|
||||||
.ai/
|
.ai/
|
||||||
|
demo/data-custome.json
|
||||||
|
|||||||
342
demo/App.vue
342
demo/App.vue
@@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, nextTick, watch } from 'vue'
|
import { ref, computed, onMounted, nextTick, watch, reactive } from 'vue'
|
||||||
// GanttChart 和 TaskListColumn 已经通过 app.use(JordiumGantt) 全局注册,无需导入
|
// GanttChart 和 TaskListColumn 已经通过 app.use(JordiumGantt) 全局注册,无需导入
|
||||||
// import TaskDrawer from '../src/components/TaskDrawer.vue' // 移除
|
// import TaskDrawer from '../src/components/TaskDrawer.vue' // 移除
|
||||||
import MilestoneDialog from '../src/components/MilestoneDialog.vue'
|
import MilestoneDialog from '../src/components/MilestoneDialog.vue'
|
||||||
import normalData from './data.json'
|
import normalData from './data.json'
|
||||||
|
import mediumData from './data-100.json'
|
||||||
import largeData from './data-large-1m.json'
|
import largeData from './data-large-1m.json'
|
||||||
import resourcesData from './data-resources.json'
|
import resourcesData from './data-resources.json'
|
||||||
import largeResourcesData from './data-resources-large.json'
|
import largeResourcesData from './data-resources-large.json'
|
||||||
@@ -48,6 +49,11 @@ const rawDataSources = [
|
|||||||
fileName: 'data.json',
|
fileName: 'data.json',
|
||||||
payload: normalData,
|
payload: normalData,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'medium',
|
||||||
|
fileName: 'data-100.json',
|
||||||
|
payload: mediumData,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'large',
|
key: 'large',
|
||||||
fileName: 'data-large-1m.json',
|
fileName: 'data-large-1m.json',
|
||||||
@@ -71,7 +77,7 @@ const dataSourceOptions = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentDataSource = ref<DataSourceKey>('normal')
|
const currentDataSource = ref<DataSourceKey>('medium')
|
||||||
const dataLoading = ref(false)
|
const dataLoading = ref(false)
|
||||||
|
|
||||||
const cloneData = <T>(data: T): T => {
|
const cloneData = <T>(data: T): T => {
|
||||||
@@ -212,7 +218,7 @@ const clickedResource = ref<Resource | null>(null)
|
|||||||
// 改为在handleTaskDoubleClick中根据对象类型判断行为
|
// 改为在handleTaskDoubleClick中根据对象类型判断行为
|
||||||
// useDefaultDrawer保持为true,确保新建任务和TaskBar双击能正常工作
|
// useDefaultDrawer保持为true,确保新建任务和TaskBar双击能正常工作
|
||||||
|
|
||||||
const toolbarConfig = {
|
const toolbarConfig = reactive({
|
||||||
showAddTask: true,
|
showAddTask: true,
|
||||||
showAddMilestone: true,
|
showAddMilestone: true,
|
||||||
showTodayLocate: true,
|
showTodayLocate: true,
|
||||||
@@ -225,7 +231,8 @@ const toolbarConfig = {
|
|||||||
timeScaleDimensions: ['hour', 'day', 'week', 'month', 'quarter', 'year'], // 设置时间刻度按钮的展示维度,包含所有时间维度
|
timeScaleDimensions: ['hour', 'day', 'week', 'month', 'quarter', 'year'], // 设置时间刻度按钮的展示维度,包含所有时间维度
|
||||||
defaultTimeScale: 'week',
|
defaultTimeScale: 'week',
|
||||||
showExpandCollapse: true, // 显示全部展开/折叠按钮
|
showExpandCollapse: true, // 显示全部展开/折叠按钮
|
||||||
}
|
showViewMode: true, // 显示 Task/Resource 视图切换按钮组
|
||||||
|
})
|
||||||
|
|
||||||
// TaskList列渲染模式配置
|
// TaskList列渲染模式配置
|
||||||
const taskListColumnRenderMode = ref<'default' | 'declarative'>('default')
|
const taskListColumnRenderMode = ref<'default' | 'declarative'>('default')
|
||||||
@@ -292,6 +299,10 @@ const taskListConfig = computed<TaskListConfig>(() => ({
|
|||||||
widthUnit.value === '%' ? `${widthPercentage.value.maxWidth}%` : taskListWidth.value.maxWidth,
|
widthUnit.value === '%' ? `${widthPercentage.value.maxWidth}%` : taskListWidth.value.maxWidth,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// TaskList 展开/收起配置
|
||||||
|
const enableTaskListCollapsible = ref(true)
|
||||||
|
const taskListVisible = ref(true)
|
||||||
|
|
||||||
// 资源列表配置
|
// 资源列表配置
|
||||||
const resourceListConfig = computed<ResourceListConfig>(() => ({
|
const resourceListConfig = computed<ResourceListConfig>(() => ({
|
||||||
columns: [
|
columns: [
|
||||||
@@ -392,6 +403,8 @@ const taskBarOptions = ref({
|
|||||||
|
|
||||||
// 自定义任务状态背景色
|
// 自定义任务状态背景色
|
||||||
const showActualTaskBar = ref(true)
|
const showActualTaskBar = ref(true)
|
||||||
|
// Tooltip 自定义 Slot 演示
|
||||||
|
const useCustomTooltip = ref(true)
|
||||||
const pendingTaskBackgroundColor = ref('#409eff')
|
const pendingTaskBackgroundColor = ref('#409eff')
|
||||||
const delayTaskBackgroundColor = ref('#f56c6c')
|
const delayTaskBackgroundColor = ref('#f56c6c')
|
||||||
const completeTaskBackgroundColor = ref('#909399')
|
const completeTaskBackgroundColor = ref('#909399')
|
||||||
@@ -467,6 +480,14 @@ watch(propsExpandAll, (newExpandAll) => {
|
|||||||
expandStatus.value = newExpandAll
|
expandStatus.value = newExpandAll
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TaskList 显隐 expose 方法
|
||||||
|
const handleSetTaskListVisible = (visible: boolean) => {
|
||||||
|
gantt.value?.setTaskListVisible(visible)
|
||||||
|
}
|
||||||
|
const handleToggleTaskList = () => {
|
||||||
|
gantt.value?.toggleTaskList()
|
||||||
|
}
|
||||||
|
|
||||||
// 切换配置面板折叠状态
|
// 切换配置面板折叠状态
|
||||||
const toggleConfigPanel = () => {
|
const toggleConfigPanel = () => {
|
||||||
isConfigPanelCollapsed.value = !isConfigPanelCollapsed.value
|
isConfigPanelCollapsed.value = !isConfigPanelCollapsed.value
|
||||||
@@ -1596,6 +1617,40 @@ const handleCustomMenuAction = (action: string, task: Task) => {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 展开/收起控制 -->
|
||||||
|
<div class="subsection">
|
||||||
|
<h5 class="subsection-title">{{ (t.taskListConfig as any).collapsible.title }}</h5>
|
||||||
|
<!-- enableTaskListCollapsible 开关 -->
|
||||||
|
<div class="column-control" style="margin-bottom: 10px;">
|
||||||
|
<label class="taskbar-control">
|
||||||
|
<input v-model="enableTaskListCollapsible" type="checkbox" />
|
||||||
|
<span class="taskbar-label">{{ (t.taskListConfig as any).collapsible.enableCollapsible }}</span>
|
||||||
|
</label>
|
||||||
|
<div class="config-hint" style="margin-top: 4px; margin-left: 20px;">
|
||||||
|
{{ (t.taskListConfig as any).collapsible.enableCollapsibleHint }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- taskListVisible 切换(仅当 enableTaskListCollapsible=true 时展示) -->
|
||||||
|
<transition name="section-content">
|
||||||
|
<div v-show="enableTaskListCollapsible">
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
|
||||||
|
<span class="width-label" style="flex-shrink: 0;">{{ (t.taskListConfig as any).collapsible.visible }}:</span>
|
||||||
|
<label class="taskbar-control">
|
||||||
|
<input v-model="taskListVisible" type="radio" :value="true" />
|
||||||
|
<span class="taskbar-label">{{ (t.taskListConfig as any).collapsible.visibleExpanded }}</span>
|
||||||
|
</label>
|
||||||
|
<label class="taskbar-control">
|
||||||
|
<input v-model="taskListVisible" type="radio" :value="false" />
|
||||||
|
<span class="taskbar-label">{{ (t.taskListConfig as any).collapsible.visibleCollapsed }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="config-hint" style="margin-top: 4px;">
|
||||||
|
{{ (t.taskListConfig as any).collapsible.visibleHint }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
@@ -1660,6 +1715,10 @@ const handleCustomMenuAction = (action: string, task: Task) => {
|
|||||||
<input v-model="showActualTaskBar" type="checkbox" />
|
<input v-model="showActualTaskBar" type="checkbox" />
|
||||||
<span class="taskbar-label">{{ taskBarConfigMessages.display?.showActualTaskBar }}</span>
|
<span class="taskbar-label">{{ taskBarConfigMessages.display?.showActualTaskBar }}</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="taskbar-control">
|
||||||
|
<input v-model="useCustomTooltip" type="checkbox" />
|
||||||
|
<span class="taskbar-label">🎨 自定义 Tooltip(Slot 演示)</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 自定义任务状态背景色 -->
|
<!-- 自定义任务状态背景色 -->
|
||||||
@@ -1907,6 +1966,20 @@ const handleCustomMenuAction = (action: string, task: Task) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 工具栏配置 - showViewMode -->
|
||||||
|
<div class="subsection">
|
||||||
|
<h5 class="subsection-title">🔀 {{ (ts as any).showViewMode?.title || 'Task/Resource 视图切换按钮组' }}</h5>
|
||||||
|
<div class="column-control">
|
||||||
|
<label class="taskbar-control">
|
||||||
|
<input v-model="toolbarConfig.showViewMode" type="checkbox" />
|
||||||
|
<span class="taskbar-label">{{ (ts as any).showViewMode?.label || '显示视图切换按钮组' }}</span>
|
||||||
|
</label>
|
||||||
|
<div class="config-hint" style="margin-top: 4px; margin-left: 20px;">
|
||||||
|
{{ (ts as any).showViewMode?.hint || '关闭后工具栏不展示 Task/Resource 切换控件' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Expose Methods 控制区域 -->
|
<!-- Expose Methods 控制区域 -->
|
||||||
<div v-show="controlMode === 'expose'" class="subsection">
|
<div v-show="controlMode === 'expose'" class="subsection">
|
||||||
<h4 class="section-subtitle">⚡ {{ ts.exposeMethods?.sectionTitle || 'Expose 方法控制' }}</h4>
|
<h4 class="section-subtitle">⚡ {{ ts.exposeMethods?.sectionTitle || 'Expose 方法控制' }}</h4>
|
||||||
@@ -2041,6 +2114,24 @@ const handleCustomMenuAction = (action: string, task: Task) => {
|
|||||||
<button class="tool-button" @click="handleScrollToDate">{{ ts.exposeMethods?.navigation?.go || 'Go' }}</button>
|
<button class="tool-button" @click="handleScrollToDate">{{ ts.exposeMethods?.navigation?.go || 'Go' }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="tool-divider"></div>
|
||||||
|
|
||||||
|
<!-- TaskList 显隐 Expose 控制 -->
|
||||||
|
<div class="tool-control-group">
|
||||||
|
<h5 class="subsection-title">📋 {{ (ts as any).taskListControl?.sectionTitle || 'TaskList 显隐' }}</h5>
|
||||||
|
<div class="tool-button-group">
|
||||||
|
<button class="tool-button" @click="handleSetTaskListVisible(true)">
|
||||||
|
✅ {{ (ts as any).taskListControl?.show || '展开' }}
|
||||||
|
</button>
|
||||||
|
<button class="tool-button" @click="handleSetTaskListVisible(false)">
|
||||||
|
❌ {{ (ts as any).taskListControl?.hide || '收起' }}
|
||||||
|
</button>
|
||||||
|
<button class="tool-button primary" @click="handleToggleTaskList">
|
||||||
|
🔄 {{ (ts as any).taskListControl?.toggle || '切换(动画)' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -2193,6 +2284,57 @@ const handleCustomMenuAction = (action: string, task: Task) => {
|
|||||||
</div>
|
</div>
|
||||||
<p class="prop-info">:expand-all="{{ propsExpandAll }}"</p>
|
<p class="prop-info">:expand-all="{{ propsExpandAll }}"</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="tool-divider"></div>
|
||||||
|
|
||||||
|
<!-- TaskList 控制 Props -->
|
||||||
|
<div class="tool-control-group">
|
||||||
|
<h5 class="subsection-title">📋 {{ (ts as any).taskListControl?.sectionTitle || 'TaskList 可折叠控制' }}</h5>
|
||||||
|
<!-- enableTaskListCollapsible -->
|
||||||
|
<div class="tool-button-group">
|
||||||
|
<button
|
||||||
|
class="tool-button"
|
||||||
|
:class="{ primary: enableTaskListCollapsible === true }"
|
||||||
|
@click="enableTaskListCollapsible = true"
|
||||||
|
>
|
||||||
|
✅ {{ (ts as any).taskListControl?.true || '开启' }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="tool-button"
|
||||||
|
:class="{ primary: enableTaskListCollapsible === false }"
|
||||||
|
@click="enableTaskListCollapsible = false"
|
||||||
|
>
|
||||||
|
❌ {{ (ts as any).taskListControl?.false || '关闭' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="prop-info">:enable-task-list-collapsible="{{ enableTaskListCollapsible }}"</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tool-divider"></div>
|
||||||
|
|
||||||
|
<div class="tool-control-group">
|
||||||
|
<h5 class="subsection-title">📋 {{ (ts as any).taskListControl?.sectionTitle1 || 'TaskList 显隐控制' }}</h5>
|
||||||
|
<!-- taskListVisible -->
|
||||||
|
<div class="tool-button-group">
|
||||||
|
<button
|
||||||
|
class="tool-button"
|
||||||
|
:class="{ primary: taskListVisible === true }"
|
||||||
|
:disabled="!enableTaskListCollapsible"
|
||||||
|
@click="taskListVisible = true"
|
||||||
|
>
|
||||||
|
✅ {{ (ts as any).taskListControl?.show || '展开' }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="tool-button"
|
||||||
|
:class="{ primary: taskListVisible === false }"
|
||||||
|
:disabled="!enableTaskListCollapsible"
|
||||||
|
@click="taskListVisible = false"
|
||||||
|
>
|
||||||
|
❌ {{ (ts as any).taskListControl?.hide || '收起' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="prop-info">:task-list-visible="{{ taskListVisible }}"</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2216,6 +2358,8 @@ const handleCustomMenuAction = (action: string, task: Task) => {
|
|||||||
:expand-all="controlMode === 'props' ? propsExpandAll : undefined"
|
:expand-all="controlMode === 'props' ? propsExpandAll : undefined"
|
||||||
:toolbar-config="toolbarConfig"
|
:toolbar-config="toolbarConfig"
|
||||||
:task-list-config="taskListConfig"
|
:task-list-config="taskListConfig"
|
||||||
|
:enable-task-list-collapsible="enableTaskListCollapsible"
|
||||||
|
:task-list-visible="enableTaskListCollapsible ? taskListVisible : undefined"
|
||||||
:task-bar-config="taskBarConfig"
|
:task-bar-config="taskBarConfig"
|
||||||
:working-hours="workingHoursConfig"
|
:working-hours="workingHoursConfig"
|
||||||
:use-default-milestone-dialog="true"
|
:use-default-milestone-dialog="true"
|
||||||
@@ -2404,6 +2548,196 @@ const handleCustomMenuAction = (action: string, task: Task) => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</TaskBarContextMenu> -->
|
</TaskBarContextMenu> -->
|
||||||
|
|
||||||
|
<!-- ✨ #taskbar-tooltip Slot 示例:自定义悬浮 Tooltip -->
|
||||||
|
<!-- 此 slot 完全替换内置 Tooltip,通过 useCustomTooltip 开关切换两种样式 -->
|
||||||
|
<template #taskbar-tooltip="{ task, taskStatus, resourcePercent }">
|
||||||
|
<!-- 默认复刻样式(slot 接管,保持内置风格) -->
|
||||||
|
<template v-if="!useCustomTooltip">
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
position: 'absolute',
|
||||||
|
left: '50%',
|
||||||
|
bottom: '-5px',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
width: '0',
|
||||||
|
height: '0',
|
||||||
|
borderLeft: '6px solid transparent',
|
||||||
|
borderRight: '6px solid transparent',
|
||||||
|
borderTop: `6px solid ${taskStatus.color}`,
|
||||||
|
borderBottom: '0',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 6px;">
|
||||||
|
<div style="font-weight: 600; font-size: 13px; margin-bottom: 4px; border-bottom: 1px solid rgba(255,255,255,0.3); padding-bottom: 4px;">
|
||||||
|
{{ task.name }}
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: space-between; gap: 12px; font-size: 11px;">
|
||||||
|
<span style="opacity: 0.9;">📅 开始:</span>
|
||||||
|
<span style="font-weight: 500;">{{ task.startDate?.slice(0, 10) ?? '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: space-between; gap: 12px; font-size: 11px;">
|
||||||
|
<span style="opacity: 0.9;">🏁 结束:</span>
|
||||||
|
<span style="font-weight: 500;">{{ task.endDate?.slice(0, 10) ?? '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: space-between; gap: 12px; font-size: 11px;">
|
||||||
|
<span style="opacity: 0.9;">📊 进度:</span>
|
||||||
|
<span style="font-weight: 500;">{{ task.progress ?? 0 }}%</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; align-items: center; gap: 4px; margin-top: 2px;">
|
||||||
|
<span
|
||||||
|
style="
|
||||||
|
background: rgba(255,255,255,0.25);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
border: 1px solid rgba(255,255,255,0.4);
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ taskStatus.label }}
|
||||||
|
</span>
|
||||||
|
<span style="opacity: 0.7; font-size: 10px; margin-left: 4px;">● Slot 接管(默认风格)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 🎨 自定义富文本样式(完全重设计) -->
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
min-width: 230px;
|
||||||
|
margin: -10px -14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
color: #333;
|
||||||
|
box-shadow: 0 8px 24px rgba(0,0,0,0.18);
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<!-- 彩色顶栏 -->
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
background: `linear-gradient(135deg, ${taskStatus.color} 0%, ${taskStatus.color}cc 100%)`,
|
||||||
|
padding: '12px 14px 10px',
|
||||||
|
color: 'white',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
|
<img
|
||||||
|
v-if="task.avatar && typeof task.avatar === 'string'"
|
||||||
|
:src="task.avatar"
|
||||||
|
width="28"
|
||||||
|
height="28"
|
||||||
|
style="border-radius: 50%; border: 2px solid rgba(255,255,255,0.6); flex-shrink: 0;"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
:style="{
|
||||||
|
width: '28px', height: '28px', borderRadius: '50%',
|
||||||
|
background: 'rgba(255,255,255,0.3)',
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
fontSize: '14px', flexShrink: '0',
|
||||||
|
}"
|
||||||
|
>📋</div>
|
||||||
|
<div style="flex: 1; min-width: 0;">
|
||||||
|
<div style="font-weight: 700; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{{ task.name }}</div>
|
||||||
|
<div style="font-size: 10px; opacity: 0.85; margin-top: 2px;">{{ task.assigneeName ?? '未分配' }}</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
style="
|
||||||
|
background: rgba(255,255,255,0.25);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 1px solid rgba(255,255,255,0.5);
|
||||||
|
"
|
||||||
|
>{{ taskStatus.label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主体内容 -->
|
||||||
|
<div style="padding: 10px 14px; display: flex; flex-direction: column; gap: 8px;">
|
||||||
|
<!-- 进度条 -->
|
||||||
|
<div>
|
||||||
|
<div style="display: flex; justify-content: space-between; font-size: 11px; color: #555; margin-bottom: 4px;">
|
||||||
|
<span>完成进度</span>
|
||||||
|
<span style="font-weight: 600;" :style="{ color: taskStatus.color }">{{ task.progress ?? 0 }}%</span>
|
||||||
|
</div>
|
||||||
|
<div style="height: 6px; background: #eee; border-radius: 3px; overflow: hidden;">
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
height: '100%',
|
||||||
|
width: `${task.progress ?? 0}%`,
|
||||||
|
background: `linear-gradient(90deg, ${taskStatus.color} 0%, ${taskStatus.color}99 100%)`,
|
||||||
|
borderRadius: '3px',
|
||||||
|
transition: 'width 0.3s',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 资源利用率(资源视图时) -->
|
||||||
|
<div v-if="resourcePercent !== 100">
|
||||||
|
<div style="display: flex; justify-content: space-between; font-size: 11px; color: #555; margin-bottom: 4px;">
|
||||||
|
<span>资源占用</span>
|
||||||
|
<span
|
||||||
|
style="font-weight: 600;"
|
||||||
|
:style="{ color: resourcePercent > 100 ? '#f56c6c' : '#67c23a' }"
|
||||||
|
>{{ resourcePercent }}%</span>
|
||||||
|
</div>
|
||||||
|
<div style="height: 6px; background: #eee; border-radius: 3px; overflow: hidden;">
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
height: '100%',
|
||||||
|
width: `${Math.min(resourcePercent, 100)}%`,
|
||||||
|
background: resourcePercent > 100 ? '#f56c6c' : '#67c23a',
|
||||||
|
borderRadius: '3px',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 日期信息 -->
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 4px; font-size: 11px;">
|
||||||
|
<div style="display: flex; justify-content: space-between; color: #666;">
|
||||||
|
<span>📅 计划开始</span>
|
||||||
|
<span style="color: #333; font-weight: 500;">{{ task.startDate?.slice(0, 10) ?? '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: space-between; color: #666;">
|
||||||
|
<span>🏁 计划结束</span>
|
||||||
|
<span style="color: #333; font-weight: 500;">{{ task.endDate?.slice(0, 10) ?? '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<template v-if="task.actualStartDate || task.actualEndDate">
|
||||||
|
<div style="border-top: 1px solid #f0f0f0; margin: 2px 0;"/>
|
||||||
|
<div v-if="task.actualStartDate" style="display: flex; justify-content: space-between; color: #888;">
|
||||||
|
<span>✅ 实际开始</span>
|
||||||
|
<span style="color: #555; font-weight: 500;">{{ task.actualStartDate?.slice(0, 10) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="task.actualEndDate" style="display: flex; justify-content: space-between; color: #888;">
|
||||||
|
<span>✅ 实际结束</span>
|
||||||
|
<span style="color: #555; font-weight: 500;">{{ task.actualEndDate?.slice(0, 10) }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部标签 -->
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
margin-top: 2px;
|
||||||
|
padding-top: 6px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #999;
|
||||||
|
text-align: right;
|
||||||
|
"
|
||||||
|
>✨ 自定义 Slot Tooltip</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
</GanttChart>
|
</GanttChart>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
1756
demo/data-100.json
Normal file
1756
demo/data-100.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -115,6 +115,22 @@
|
|||||||
"true": "True",
|
"true": "True",
|
||||||
"false": "False"
|
"false": "False"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"showViewMode": {
|
||||||
|
"title": "Task/Resource View Toggle",
|
||||||
|
"label": "Show view mode switcher",
|
||||||
|
"hint": "Hides the Task/Resource toggle control from the toolbar when off"
|
||||||
|
},
|
||||||
|
"taskListControl": {
|
||||||
|
"sectionTitle": "TaskList Collapsible Control",
|
||||||
|
"sectionTitle1": "TaskList Show/Hide Control",
|
||||||
|
"show": "Expand",
|
||||||
|
"hide": "Collapse",
|
||||||
|
"toggle": "Toggle (animated)",
|
||||||
|
"enableCollapsible": "enableTaskListCollapsible",
|
||||||
|
"visibleProp": "taskListVisible",
|
||||||
|
"true": "Enabled",
|
||||||
|
"false": "Disabled"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"taskBarConfig": {
|
"taskBarConfig": {
|
||||||
|
|||||||
@@ -115,6 +115,22 @@
|
|||||||
"true": "真",
|
"true": "真",
|
||||||
"false": "假"
|
"false": "假"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"showViewMode": {
|
||||||
|
"title": "Task/Resource 视图切换按钮组",
|
||||||
|
"label": "显示视图切换按钮组",
|
||||||
|
"hint": "关闭后工具栏不展示 Task/Resource 切换控件"
|
||||||
|
},
|
||||||
|
"taskListControl": {
|
||||||
|
"sectionTitle": "TaskList 可折叠控制",
|
||||||
|
"sectionTitle1": "TaskList 显隐控制",
|
||||||
|
"show": "展开",
|
||||||
|
"hide": "收起",
|
||||||
|
"toggle": "切换(动画)",
|
||||||
|
"enableCollapsible": "enableTaskListCollapsible",
|
||||||
|
"visibleProp": "taskListVisible",
|
||||||
|
"true": "允许",
|
||||||
|
"false": "禁用"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"taskBarConfig": {
|
"taskBarConfig": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jordium-gantt-vue3",
|
"name": "jordium-gantt-vue3",
|
||||||
"version": "1.9.1",
|
"version": "1.9.2-rc.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "npm-package/dist/jordium-gantt-vue3.cjs.js",
|
"main": "npm-package/dist/jordium-gantt-vue3.cjs.js",
|
||||||
"module": "npm-package/dist/jordium-gantt-vue3.es.js",
|
"module": "npm-package/dist/jordium-gantt-vue3.es.js",
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
locale: 'zh-CN',
|
locale: 'zh-CN',
|
||||||
timeScale: 'week',
|
timeScale: 'week',
|
||||||
theme: undefined, // 不设置默认值,允许自动检测系统主题
|
theme: undefined, // 不设置默认值,允许自动检测系统主题
|
||||||
|
enableTaskListCollapsible: true,
|
||||||
|
taskListVisible: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
@@ -545,6 +547,18 @@ interface Props {
|
|||||||
timeScale?: TimelineScale
|
timeScale?: TimelineScale
|
||||||
// 主题模式(响应式)
|
// 主题模式(响应式)
|
||||||
theme?: 'light' | 'dark'
|
theme?: 'light' | 'dark'
|
||||||
|
/**
|
||||||
|
* 是否允许 TaskList 展开/收起功能(默认为 true)
|
||||||
|
* 当设置为 false 时,TaskList 面板强制隐藏,SplitterBar 及折叠按钮同步隐藏,仅展示 Timeline 区域
|
||||||
|
* taskListVisible 在此情况下失效
|
||||||
|
*/
|
||||||
|
enableTaskListCollapsible?: boolean
|
||||||
|
/**
|
||||||
|
* TaskList 可见状态(响应式),默认为 true
|
||||||
|
* 仅当 enableTaskListCollapsible=true 时有效
|
||||||
|
* 可通过 prop 传入或通过 defineExpose 暴露的方法控制
|
||||||
|
*/
|
||||||
|
taskListVisible?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// TaskList的固定总长度(所有列的最小宽度之和 + 边框等额外空间)
|
// TaskList的固定总长度(所有列的最小宽度之和 + 边框等额外空间)
|
||||||
@@ -733,7 +747,6 @@ watch(
|
|||||||
// props变化时清空增量追踪,执行全量更新
|
// props变化时清空增量追踪,执行全量更新
|
||||||
triggerFullUpdate()
|
triggerFullUpdate()
|
||||||
},
|
},
|
||||||
{ deep: true },
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// v1.9.9 监听props.resources变化,自动触发Timeline更新
|
// v1.9.9 监听props.resources变化,自动触发Timeline更新
|
||||||
@@ -744,7 +757,6 @@ watch(
|
|||||||
// props变化时清空增量追踪,执行全量更新
|
// props变化时清空增量追踪,执行全量更新
|
||||||
triggerFullUpdate()
|
triggerFullUpdate()
|
||||||
},
|
},
|
||||||
{ deep: true },
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 时间刻度状态
|
// 时间刻度状态
|
||||||
@@ -934,7 +946,32 @@ function onMouseDown(e: MouseEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TaskList显示/隐藏状态管理
|
// TaskList显示/隐藏状态管理
|
||||||
const isTaskListVisible = ref(true)
|
// 初始值:enableTaskListCollapsible=false 时强制隐藏;否则使用 taskListVisible prop 的值
|
||||||
|
const isTaskListVisible = ref(props.enableTaskListCollapsible ? (props.taskListVisible ?? true) : false)
|
||||||
|
|
||||||
|
// 响应 taskListVisible prop 变化(仅在 enableTaskListCollapsible=true 时生效)
|
||||||
|
watch(
|
||||||
|
() => props.taskListVisible,
|
||||||
|
(newVal) => {
|
||||||
|
if (props.enableTaskListCollapsible) {
|
||||||
|
isTaskListVisible.value = newVal ?? true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// 响应 enableTaskListCollapsible prop 变化
|
||||||
|
watch(
|
||||||
|
() => props.enableTaskListCollapsible,
|
||||||
|
(newVal) => {
|
||||||
|
if (!newVal) {
|
||||||
|
// 禁用折叠功能时,强制隐藏 TaskList
|
||||||
|
isTaskListVisible.value = false
|
||||||
|
} else {
|
||||||
|
// 恢复折叠功能时,还原为 taskListVisible prop 的值
|
||||||
|
isTaskListVisible.value = props.taskListVisible ?? true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// 动画状态管理
|
// 动画状态管理
|
||||||
const isAnimating = ref(false)
|
const isAnimating = ref(false)
|
||||||
@@ -942,6 +979,8 @@ const animationClass = ref('')
|
|||||||
|
|
||||||
// 切换TaskList显示状态
|
// 切换TaskList显示状态
|
||||||
const toggleTaskList = () => {
|
const toggleTaskList = () => {
|
||||||
|
// enableTaskListCollapsible=false 时,禁止切换
|
||||||
|
if (!props.enableTaskListCollapsible) return
|
||||||
// 如果正在动画中,忽略点击
|
// 如果正在动画中,忽略点击
|
||||||
if (isAnimating.value) return
|
if (isAnimating.value) return
|
||||||
|
|
||||||
@@ -1299,7 +1338,7 @@ watch(
|
|||||||
notifyTaskListUpdated()
|
notifyTaskListUpdated()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{ deep: true, immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -3303,6 +3342,21 @@ defineExpose({
|
|||||||
// 主题相关
|
// 主题相关
|
||||||
setTheme,
|
setTheme,
|
||||||
currentTheme,
|
currentTheme,
|
||||||
|
|
||||||
|
// TaskList 显隐相关
|
||||||
|
/** 获取 TaskList 当前可见状态 */
|
||||||
|
getTaskListVisible: () => isTaskListVisible.value,
|
||||||
|
/**
|
||||||
|
* 设置 TaskList 可见状态(仅在 enableTaskListCollapsible=true 时生效)
|
||||||
|
* @param visible true=展开,false=收起
|
||||||
|
*/
|
||||||
|
setTaskListVisible: (visible: boolean) => {
|
||||||
|
if (props.enableTaskListCollapsible) {
|
||||||
|
isTaskListVisible.value = visible
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** 切换 TaskList 展开/收起(仅在 enableTaskListCollapsible=true 时生效,带动画) */
|
||||||
|
toggleTaskList,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -3371,7 +3425,7 @@ defineExpose({
|
|||||||
</template>
|
</template>
|
||||||
</TaskList>
|
</TaskList>
|
||||||
</div>
|
</div>
|
||||||
<div class="gantt-splitter" @mousedown="onMouseDown">
|
<div v-if="props.enableTaskListCollapsible" class="gantt-splitter" @mousedown="onMouseDown">
|
||||||
<!-- TaskList切换按钮 - 贴合splitter右侧 -->
|
<!-- TaskList切换按钮 - 贴合splitter右侧 -->
|
||||||
<div
|
<div
|
||||||
class="task-list-toggle"
|
class="task-list-toggle"
|
||||||
@@ -3434,6 +3488,10 @@ defineExpose({
|
|||||||
<template v-if="$slots['custom-task-content']" #custom-task-content="barScope">
|
<template v-if="$slots['custom-task-content']" #custom-task-content="barScope">
|
||||||
<slot name="custom-task-content" v-bind="barScope" />
|
<slot name="custom-task-content" v-bind="barScope" />
|
||||||
</template>
|
</template>
|
||||||
|
<!-- 向 Timeline 转发 #taskbar-tooltip scoped slot(仅此一层,不穿透至 TaskBar) -->
|
||||||
|
<template v-if="$slots['taskbar-tooltip']" #taskbar-tooltip="tooltipScope">
|
||||||
|
<slot name="taskbar-tooltip" v-bind="tooltipScope" />
|
||||||
|
</template>
|
||||||
</Timeline>
|
</Timeline>
|
||||||
|
|
||||||
<!-- 关闭聚焦按钮 - 固定在gantt-panel-right底部居中 -->
|
<!-- 关闭聚焦按钮 - 固定在gantt-panel-right底部居中 -->
|
||||||
|
|||||||
@@ -551,7 +551,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- v1.9.0 视图模式切换按钮组 - 使用 Segmented Control 样式 -->
|
<!-- v1.9.0 视图模式切换按钮组 - 使用 Segmented Control 样式 -->
|
||||||
<div class="gantt-view-mode-control">
|
<div v-if="config.showViewMode !== false" class="gantt-view-mode-control">
|
||||||
<div class="view-mode-track">
|
<div class="view-mode-track">
|
||||||
<div
|
<div
|
||||||
class="view-mode-thumb"
|
class="view-mode-thumb"
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ const emit = defineEmits([
|
|||||||
'link-drag-start',
|
'link-drag-start',
|
||||||
'link-drag-move',
|
'link-drag-move',
|
||||||
'link-drag-end',
|
'link-drag-end',
|
||||||
|
// Singleton Tooltip:TaskBar 只负责触发,Timeline 统一渲染
|
||||||
|
'tooltip-show',
|
||||||
|
'tooltip-hide',
|
||||||
])
|
])
|
||||||
|
|
||||||
defineSlots<{
|
defineSlots<{
|
||||||
@@ -197,9 +200,6 @@ const resourceTaskLayouts = inject<ComputedRef<Map<string, { taskRowMap: Map<str
|
|||||||
// v1.9.6 Phase1 - 注入位置计算缓存实例(由Timeline提供)
|
// v1.9.6 Phase1 - 注入位置计算缓存实例(由Timeline提供)
|
||||||
const positionCache = inject<PositionCache | null>('positionCache', null)
|
const positionCache = inject<PositionCache | null>('positionCache', null)
|
||||||
|
|
||||||
// 注入 TaskList 宽度(用于 tooltip 定位边界检测)
|
|
||||||
const taskListWidth = inject<Ref<number>>('gantt-task-list-width', ref(0))
|
|
||||||
|
|
||||||
// 注入右键菜单配置
|
// 注入右键菜单配置
|
||||||
const enableTaskBarContextMenu = inject<ComputedRef<boolean>>('enable-task-bar-context-menu', computed(() => true))
|
const enableTaskBarContextMenu = inject<ComputedRef<boolean>>('enable-task-bar-context-menu', computed(() => true))
|
||||||
const hasTaskBarContextMenuSlot = inject<ComputedRef<boolean>>('task-bar-context-menu-slot', computed(() => false))
|
const hasTaskBarContextMenuSlot = inject<ComputedRef<boolean>>('task-bar-context-menu-slot', computed(() => false))
|
||||||
@@ -2396,10 +2396,7 @@ const bubbleIndicator = computed(() => {
|
|||||||
const showTooltip = ref(false)
|
const showTooltip = ref(false)
|
||||||
const tooltipPosition = ref({ x: 0, y: 0 })
|
const tooltipPosition = ref({ x: 0, y: 0 })
|
||||||
|
|
||||||
// TaskBar 悬停 tooltip 状态
|
// TaskBar 悬停 tooltip 延迟定时器(状态已上移至 Timeline)
|
||||||
const showHoverTooltip = ref(false)
|
|
||||||
const hoverTooltipPosition = ref({ x: 0, y: 0 })
|
|
||||||
const isTooltipBelow = ref(false) // v1.9.0 标记tooltip是否显示在TaskBar下方
|
|
||||||
let hoverTooltipTimer: number | null = null
|
let hoverTooltipTimer: number | null = null
|
||||||
|
|
||||||
// 跟踪滚动状态,避免非滚动时的动画
|
// 跟踪滚动状态,避免非滚动时的动画
|
||||||
@@ -2568,7 +2565,7 @@ const handleBubbleMouseEnter = (event: MouseEvent) => {
|
|||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
// v1.9.1 隐藏TaskBar的hover tooltip,只显示bubble tooltip
|
// v1.9.1 隐藏TaskBar的hover tooltip,只显示bubble tooltip
|
||||||
showHoverTooltip.value = false
|
emit('tooltip-hide')
|
||||||
// 清除可能正在等待的tooltip定时器
|
// 清除可能正在等待的tooltip定时器
|
||||||
if (hoverTooltipTimer) {
|
if (hoverTooltipTimer) {
|
||||||
window.clearTimeout(hoverTooltipTimer)
|
window.clearTimeout(hoverTooltipTimer)
|
||||||
@@ -2668,67 +2665,16 @@ const handleTaskBarMouseEnter = (event: MouseEvent) => {
|
|||||||
const mouseY = event.clientY
|
const mouseY = event.clientY
|
||||||
|
|
||||||
// 延迟显示tooltip,避免快速滑过时显示
|
// 延迟显示tooltip,避免快速滑过时显示
|
||||||
|
// Singleton Tooltip:Timer后 emit tooltip-show,由 Timeline 统一计算位置和渲染
|
||||||
hoverTooltipTimer = window.setTimeout(() => {
|
hoverTooltipTimer = window.setTimeout(() => {
|
||||||
showHoverTooltip.value = true
|
|
||||||
const rect = targetElement.getBoundingClientRect()
|
const rect = targetElement.getBoundingClientRect()
|
||||||
|
emit('tooltip-show', {
|
||||||
// 计算tooltip的预估宽高(根据实际CSS设置)
|
task: props.task,
|
||||||
const tooltipWidth = 250 // 预估宽度
|
taskStatus: taskStatus.value,
|
||||||
const margin = 10 // 边距
|
resourcePercent: resourcePercent.value,
|
||||||
|
hasResourceConflict: props.hasResourceConflict ?? false,
|
||||||
// 视口尺寸
|
targetRect: rect,
|
||||||
const viewportWidth = window.innerWidth
|
})
|
||||||
const viewportHeight = window.innerHeight
|
|
||||||
|
|
||||||
// v1.9.0 改为基于TaskBar边界定位
|
|
||||||
// 水平位置:TaskBar中心对齐
|
|
||||||
let x = rect.left + rect.width / 2
|
|
||||||
// 默认显示在TaskBar上方边缘外(CSS transform: translateY(-100%)会向上偏移tooltip高度)
|
|
||||||
let y = rect.top - 10
|
|
||||||
|
|
||||||
// 获取TaskList的右边界(用于防止tooltip进入TaskList区域)
|
|
||||||
const taskListRightBoundary = taskListWidth.value + margin
|
|
||||||
|
|
||||||
// 水平边界检测:左侧超出或进入TaskList区域
|
|
||||||
if (x - tooltipWidth / 2 < taskListRightBoundary) {
|
|
||||||
x = taskListRightBoundary + tooltipWidth / 2
|
|
||||||
}
|
|
||||||
// 水平边界检测:右侧超出
|
|
||||||
if (x + tooltipWidth / 2 > viewportWidth - margin) {
|
|
||||||
x = viewportWidth - margin - tooltipWidth / 2
|
|
||||||
}
|
|
||||||
|
|
||||||
// 垂直边界检测:计算上下方可用空间,选择空间更大的方向
|
|
||||||
const spaceAbove = rect.top - margin
|
|
||||||
const spaceBelow = viewportHeight - rect.bottom - margin
|
|
||||||
|
|
||||||
// 动态计算tooltip高度(基础高度 + 每行内容)
|
|
||||||
const baseHeight = 80 // 基础高度(标题 + padding)
|
|
||||||
const rowHeight = 24 // 每行内容高度
|
|
||||||
let contentRows = 4 // 默认4行(开始日期、结束日期、预估工时、实际工时、进度)
|
|
||||||
|
|
||||||
// 资源视图且利用率<100%时,多1行
|
|
||||||
if (viewMode.value === 'resource' && resourcePercent.value < 100) {
|
|
||||||
contentRows += 1
|
|
||||||
}
|
|
||||||
// 有资源冲突警告时,多1行
|
|
||||||
if (props.hasResourceConflict) {
|
|
||||||
contentRows += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
const estimatedTooltipHeight = baseHeight + (contentRows * rowHeight)
|
|
||||||
|
|
||||||
// 如果上方空间不足,显示在下方
|
|
||||||
if (spaceAbove < estimatedTooltipHeight && spaceBelow > spaceAbove) {
|
|
||||||
// 显示在TaskBar下方
|
|
||||||
y = rect.bottom + 10
|
|
||||||
isTooltipBelow.value = true
|
|
||||||
} else {
|
|
||||||
// 显示在TaskBar上方
|
|
||||||
isTooltipBelow.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
hoverTooltipPosition.value = { x, y }
|
|
||||||
}, 300) // 300ms延迟
|
}, 300) // 300ms延迟
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2736,19 +2682,19 @@ const handleTaskBarMouseEnter = (event: MouseEvent) => {
|
|||||||
const handleTaskBarMouseLeave = () => {
|
const handleTaskBarMouseLeave = () => {
|
||||||
isTaskBarHovered.value = false
|
isTaskBarHovered.value = false
|
||||||
|
|
||||||
// 清除定时器并隐藏tooltip
|
// 清除定时器并通知 Timeline 隐藏 tooltip
|
||||||
if (hoverTooltipTimer) {
|
if (hoverTooltipTimer) {
|
||||||
clearTimeout(hoverTooltipTimer)
|
clearTimeout(hoverTooltipTimer)
|
||||||
hoverTooltipTimer = null
|
hoverTooltipTimer = null
|
||||||
}
|
}
|
||||||
showHoverTooltip.value = false
|
emit('tooltip-hide')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听拖拽/拉伸状态,如果开始拖拽/拉伸,立即隐藏tooltip
|
// 监听拖拽/拉伸状态,如果开始拖拽/拉伸,立即隐藏tooltip
|
||||||
// v1.9.7 使用 flush: 'sync' 确保状态变化时立即同步执行,避免在资源视图拖拽时因组件更新导致watch延迟执行
|
// v1.9.7 使用 flush: 'sync' 确保状态变化时立即同步执行,避免在资源视图拖拽时因组件更新导致watch延迟执行
|
||||||
watch([isDragging, isResizingLeft, isResizingRight], ([dragging, resizingL, resizingR]) => {
|
watch([isDragging, isResizingLeft, isResizingRight], ([dragging, resizingL, resizingR]) => {
|
||||||
if (dragging || resizingL || resizingR) {
|
if (dragging || resizingL || resizingR) {
|
||||||
showHoverTooltip.value = false
|
emit('tooltip-hide')
|
||||||
if (hoverTooltipTimer) {
|
if (hoverTooltipTimer) {
|
||||||
clearTimeout(hoverTooltipTimer)
|
clearTimeout(hoverTooltipTimer)
|
||||||
hoverTooltipTimer = null
|
hoverTooltipTimer = null
|
||||||
@@ -2767,18 +2713,14 @@ watch([isDragging, isResizingLeft, isResizingRight], ([dragging, resizingL, resi
|
|||||||
// v1.9.2 监听 Tab 悬停状态,当 Tab 悬停时立即隐藏 TaskBar 的 tooltip
|
// v1.9.2 监听 Tab 悬停状态,当 Tab 悬停时立即隐藏 TaskBar 的 tooltip
|
||||||
watch(isTabHovered, (tabHovered) => {
|
watch(isTabHovered, (tabHovered) => {
|
||||||
if (tabHovered) {
|
if (tabHovered) {
|
||||||
// Tab 悬停:隐藏 TaskBar 的 tooltip
|
// Tab 悬停:隐藏 TaskBar 的 tooltip(通知 Timeline)
|
||||||
showHoverTooltip.value = false
|
emit('tooltip-hide')
|
||||||
if (hoverTooltipTimer) {
|
if (hoverTooltipTimer) {
|
||||||
clearTimeout(hoverTooltipTimer)
|
clearTimeout(hoverTooltipTimer)
|
||||||
hoverTooltipTimer = null
|
hoverTooltipTimer = null
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Tab 离开:如果鼠标还在 TaskBar 上,重新显示 tooltip
|
|
||||||
if (isTaskBarHovered.value && props.enableTaskBarTooltip !== false) {
|
|
||||||
showHoverTooltip.value = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// Tab 离开后不主动重显 tooltip,等待用户下次 mouseenter 触发
|
||||||
})
|
})
|
||||||
|
|
||||||
// 格式化日期显示
|
// 格式化日期显示
|
||||||
@@ -3726,44 +3668,7 @@ const handleAnchorDragEnd = (anchorEvent: { taskId: number; type: 'predecessor'
|
|||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
|
||||||
<!-- TaskBar悬停提示框 -->
|
<!-- TaskBar悬停 Tooltip 已上移至 Timeline 单例渲染(Singleton Tooltip)-->
|
||||||
<Teleport to="body">
|
|
||||||
<div
|
|
||||||
v-if="showHoverTooltip"
|
|
||||||
class="task-hover-tooltip"
|
|
||||||
:class="{ 'tooltip-below': isTooltipBelow }"
|
|
||||||
:style="{
|
|
||||||
left: `${hoverTooltipPosition.x}px`,
|
|
||||||
top: `${hoverTooltipPosition.y}px`,
|
|
||||||
backgroundColor: taskStatus.color,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="hover-tooltip-arrow" :style="{
|
|
||||||
borderTopColor: isTooltipBelow ? 'transparent' : taskStatus.color,
|
|
||||||
borderBottomColor: isTooltipBelow ? taskStatus.color : 'transparent'
|
|
||||||
}"></div>
|
|
||||||
<div class="hover-tooltip-content">
|
|
||||||
<div class="hover-tooltip-title">{{ task.name }}</div>
|
|
||||||
<div class="hover-tooltip-row">
|
|
||||||
<span class="hover-tooltip-label">{{ t('plannedStartDate') }}:</span>
|
|
||||||
<span class="hover-tooltip-value">{{ formatDisplayDate(task.startDate) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="hover-tooltip-row">
|
|
||||||
<span class="hover-tooltip-label">{{ t('plannedEndDate') }}:</span>
|
|
||||||
<span class="hover-tooltip-value">{{ formatDisplayDate(task.endDate) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="hover-tooltip-row">
|
|
||||||
<span class="hover-tooltip-label">{{ t('actualStartDate') }}:</span>
|
|
||||||
<span class="hover-tooltip-value">{{ task.actualStartDate ? formatDisplayDate(task.actualStartDate) : '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="hover-tooltip-row">
|
|
||||||
<span class="hover-tooltip-label">{{ t('actualEndDate') }}:</span>
|
|
||||||
<span class="hover-tooltip-value">{{ task.actualEndDate ? formatDisplayDate(task.actualEndDate) : '-' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, computed, watch, nextTick, shallowRef, inject, provide } from 'vue'
|
import { ref, onMounted, onUnmounted, computed, watch, nextTick, shallowRef, inject, provide, reactive } from 'vue'
|
||||||
import type { Ref, ComputedRef } from 'vue'
|
import type { Ref, ComputedRef } from 'vue'
|
||||||
import TaskBar from './TaskBar.vue'
|
import TaskBar from './TaskBar.vue'
|
||||||
import MilestonePoint from './MilestonePoint.vue'
|
import MilestonePoint from './MilestonePoint.vue'
|
||||||
@@ -18,6 +18,7 @@ import type { Resource } from '../models/classes/Resource'
|
|||||||
import type { Milestone } from '../models/classes/Milestone'
|
import type { Milestone } from '../models/classes/Milestone'
|
||||||
import type { TimelineConfig } from '../models/configs/TimelineConfig'
|
import type { TimelineConfig } from '../models/configs/TimelineConfig'
|
||||||
import { TimelineScale } from '../models/types/TimelineScale'
|
import { TimelineScale } from '../models/types/TimelineScale'
|
||||||
|
import type { TooltipShowPayload } from '../models/types/TimelineDataTypes'
|
||||||
import { positionCache } from '../utils/positionCache' // v1.9.6 Phase1 位置计算缓存
|
import { positionCache } from '../utils/positionCache' // v1.9.6 Phase1 位置计算缓存
|
||||||
|
|
||||||
// 定义Props接口
|
// 定义Props接口
|
||||||
@@ -181,6 +182,77 @@ const timelineBodyHeight = ref(0) // 容器高度状态管理
|
|||||||
const resourceTaskLayouts = inject<ComputedRef<Map<string | number, any>>>('resourceTaskLayouts', computed(() => new Map()))
|
const resourceTaskLayouts = inject<ComputedRef<Map<string | number, any>>>('resourceTaskLayouts', computed(() => new Map()))
|
||||||
const resourceRowPositions = inject<ComputedRef<Map<string | number, number>>>('resourceRowPositions', computed(() => new Map()))
|
const resourceRowPositions = inject<ComputedRef<Map<string | number, number>>>('resourceRowPositions', computed(() => new Map()))
|
||||||
|
|
||||||
|
// ─── Singleton Tooltip 状态(方案 B:Timeline 统一渲染)─────────────────────────────────
|
||||||
|
// 注入 TaskList 宽度,用于 Tooltip 定位边界保护
|
||||||
|
const ganttTaskListWidth = inject<Ref<number>>('gantt-task-list-width', ref(0))
|
||||||
|
const tooltipState = reactive({
|
||||||
|
visible: false,
|
||||||
|
task: null as any,
|
||||||
|
taskStatus: { color: '#409eff', label: '' } as { color: string; label: string; type?: string },
|
||||||
|
resourcePercent: 100,
|
||||||
|
hasResourceConflict: false,
|
||||||
|
isBelow: false,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 格式化日期显示 (YYYY-MM-DD) */
|
||||||
|
const formatTooltipDate = (dateStr: string | undefined): string => {
|
||||||
|
if (!dateStr) return t('dateNotSet')
|
||||||
|
return String(dateStr).substring(0, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** TaskBar 触发, Timeline 接受并计算定位 */
|
||||||
|
const handleTooltipShow = (payload: TooltipShowPayload) => {
|
||||||
|
if (props.enableTaskBarTooltip === false) return
|
||||||
|
|
||||||
|
const { task, taskStatus, resourcePercent, hasResourceConflict, targetRect } = payload
|
||||||
|
tooltipState.task = task
|
||||||
|
tooltipState.taskStatus = taskStatus
|
||||||
|
tooltipState.resourcePercent = resourcePercent
|
||||||
|
tooltipState.hasResourceConflict = hasResourceConflict
|
||||||
|
|
||||||
|
// 定位计算(迁移自 TaskBar,使用 Timeline 已有容器 ref)
|
||||||
|
const tooltipWidth = 250
|
||||||
|
const margin = 10
|
||||||
|
const viewportWidth = window.innerWidth
|
||||||
|
const viewportHeight = window.innerHeight
|
||||||
|
|
||||||
|
let x = targetRect.left + targetRect.width / 2
|
||||||
|
let y = targetRect.top - 10
|
||||||
|
|
||||||
|
const taskListRightBoundary = ganttTaskListWidth.value + margin
|
||||||
|
if (x - tooltipWidth / 2 < taskListRightBoundary) {
|
||||||
|
x = taskListRightBoundary + tooltipWidth / 2
|
||||||
|
}
|
||||||
|
if (x + tooltipWidth / 2 > viewportWidth - margin) {
|
||||||
|
x = viewportWidth - margin - tooltipWidth / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
const spaceAbove = targetRect.top - margin
|
||||||
|
const spaceBelow = viewportHeight - targetRect.bottom - margin
|
||||||
|
const baseHeight = 80
|
||||||
|
const rowHeight = 24
|
||||||
|
let contentRows = 4
|
||||||
|
if (viewMode.value === 'resource' && resourcePercent < 100) contentRows += 1
|
||||||
|
if (hasResourceConflict) contentRows += 1
|
||||||
|
const estimatedTooltipHeight = baseHeight + contentRows * rowHeight
|
||||||
|
|
||||||
|
if (spaceAbove < estimatedTooltipHeight && spaceBelow > spaceAbove) {
|
||||||
|
y = targetRect.bottom + 10
|
||||||
|
tooltipState.isBelow = true
|
||||||
|
} else {
|
||||||
|
tooltipState.isBelow = false
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltipState.position = { x, y }
|
||||||
|
tooltipState.visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** TaskBar 鼠标离开 / 拖拽 / Tab悬停时触发 */
|
||||||
|
const handleTooltipHide = () => {
|
||||||
|
tooltipState.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
// 获取以今天为中心的时间线范围(缓存结果,避免每次计算创建新对象)
|
// 获取以今天为中心的时间线范围(缓存结果,避免每次计算创建新对象)
|
||||||
const cachedTodayCenteredRange = (() => {
|
const cachedTodayCenteredRange = (() => {
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
@@ -3777,10 +3849,25 @@ const handleTimelineBodyScroll = (event: Event) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听任务数量变化,更新SVG尺寸
|
// 合并优化:整合三处 tasks.value.length watch,消除重复的 invalidateTaskDateRangeCache 调用
|
||||||
|
// 洵盖:缓存失效 / 里程碑位置更新 / 首次加载时间轴重算 / SVG 尺寸更新
|
||||||
watch(
|
watch(
|
||||||
() => tasks.value.length,
|
() => tasks.value?.length,
|
||||||
() => {
|
(newLength, oldLength) => {
|
||||||
|
// 原 ~4562:缓存失效 + 里程碑位置(无条件执行)
|
||||||
|
invalidateTaskDateRangeCache()
|
||||||
|
computeAllMilestonesPositions()
|
||||||
|
|
||||||
|
// 原 ~4616:首次加载时重算时间轴
|
||||||
|
// bugfix: 重置 hasInitialAutoScroll,确保首次加载真实数据后能重新定位今日
|
||||||
|
// 场景:数据异步加载时,初始 updateTimelineRange() 以空任务计算范围并完成滚动,
|
||||||
|
// 真实数据到来后范围扩展,若不重置则 scrollToTodayCenter 不会再次触发
|
||||||
|
if (oldLength === 0 && newLength > 0) {
|
||||||
|
hasInitialAutoScroll = false
|
||||||
|
debouncedUpdateTimelineRange(50)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 原 ~3854:更新 SVG 尺寸
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
updateSvgSize()
|
updateSvgSize()
|
||||||
})
|
})
|
||||||
@@ -4485,16 +4572,6 @@ const convertTaskToMilestone = (task: Task): Milestone => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听tasks变化,重新计算里程碑位置(使用 shallow watch 避免深度监听)
|
|
||||||
watch(
|
|
||||||
() => tasks.value.length,
|
|
||||||
() => {
|
|
||||||
invalidateTaskDateRangeCache()
|
|
||||||
computeAllMilestonesPositions()
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
)
|
|
||||||
|
|
||||||
// 优化:统一的防抖函数,避免重复的 setTimeout 造成性能浪费
|
// 优化:统一的防抖函数,避免重复的 setTimeout 造成性能浪费
|
||||||
let timelineUpdateTimer: number | null = null
|
let timelineUpdateTimer: number | null = null
|
||||||
|
|
||||||
@@ -4542,19 +4619,6 @@ const updateTimelineRange = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
|
||||||
() => tasks.value?.length,
|
|
||||||
(newLength, oldLength) => {
|
|
||||||
if (newLength !== oldLength) {
|
|
||||||
invalidateTaskDateRangeCache()
|
|
||||||
}
|
|
||||||
// 当任务从无到有时,重新计算时间范围
|
|
||||||
if (oldLength === 0 && newLength > 0) {
|
|
||||||
debouncedUpdateTimelineRange(50)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
timelineContainerWidth,
|
timelineContainerWidth,
|
||||||
(newWidth, oldWidth) => {
|
(newWidth, oldWidth) => {
|
||||||
@@ -4597,11 +4661,16 @@ watch([timelineData, timelineContainerWidth], () => {
|
|||||||
// 监听viewMode和dataSource变化,刷新缓存和时间线
|
// 监听viewMode和dataSource变化,刷新缓存和时间线
|
||||||
watch(
|
watch(
|
||||||
[viewMode, dataSource],
|
[viewMode, dataSource],
|
||||||
() => {
|
([newViewMode]) => {
|
||||||
invalidateTaskDateRangeCache()
|
invalidateTaskDateRangeCache()
|
||||||
|
// bugfix: 切换回任务视图时重置 hasInitialAutoScroll,确保 updateTimelineRange 完成后能重新定位今日
|
||||||
|
// 场景:资源视图切换回任务视图时,updateTimelineRange 重新计算任务范围导致像素偏移,
|
||||||
|
// 若不重置则 scrollToTodayCenter 不会被触发,今日标记将出现在视口之外
|
||||||
|
if (newViewMode === 'task') {
|
||||||
|
hasInitialAutoScroll = false
|
||||||
|
}
|
||||||
debouncedUpdateTimelineRange()
|
debouncedUpdateTimelineRange()
|
||||||
},
|
},
|
||||||
{ deep: true },
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 监听tasks变化,清理不再存在的任务的位置信息
|
// 监听tasks变化,清理不再存在的任务的位置信息
|
||||||
@@ -4626,7 +4695,6 @@ watch(
|
|||||||
}
|
}
|
||||||
taskBarPositions.value = newPositions
|
taskBarPositions.value = newPositions
|
||||||
},
|
},
|
||||||
{ deep: true },
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 处理里程碑点击定位事件
|
// 处理里程碑点击定位事件
|
||||||
@@ -5473,6 +5541,8 @@ const handleAddSuccessor = (task: Task) => {
|
|||||||
@link-drag-start="handleLinkDragStart"
|
@link-drag-start="handleLinkDragStart"
|
||||||
@link-drag-move="handleLinkDragMove"
|
@link-drag-move="handleLinkDragMove"
|
||||||
@link-drag-end="handleLinkDragEnd"
|
@link-drag-end="handleLinkDragEnd"
|
||||||
|
@tooltip-show="handleTooltipShow"
|
||||||
|
@tooltip-hide="handleTooltipHide"
|
||||||
>
|
>
|
||||||
<template v-if="$slots['custom-task-content']" #custom-task-content="barScope">
|
<template v-if="$slots['custom-task-content']" #custom-task-content="barScope">
|
||||||
<slot name="custom-task-content" v-bind="barScope" />
|
<slot name="custom-task-content" v-bind="barScope" />
|
||||||
@@ -5567,6 +5637,8 @@ const handleAddSuccessor = (task: Task) => {
|
|||||||
@link-drag-start="handleLinkDragStart"
|
@link-drag-start="handleLinkDragStart"
|
||||||
@link-drag-move="handleLinkDragMove"
|
@link-drag-move="handleLinkDragMove"
|
||||||
@link-drag-end="handleLinkDragEnd"
|
@link-drag-end="handleLinkDragEnd"
|
||||||
|
@tooltip-show="handleTooltipShow"
|
||||||
|
@tooltip-hide="handleTooltipHide"
|
||||||
>
|
>
|
||||||
<template v-if="$slots['custom-task-content']" #custom-task-content="barScope">
|
<template v-if="$slots['custom-task-content']" #custom-task-content="barScope">
|
||||||
<slot name="custom-task-content" v-bind="barScope" />
|
<slot name="custom-task-content" v-bind="barScope" />
|
||||||
@@ -5616,11 +5688,138 @@ const handleAddSuccessor = (task: Task) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Singleton Tooltip:单一 Teleport、所有 TaskBar 共享、应支持 #taskbar-tooltip slot -->
|
||||||
|
<Teleport v-if="props.enableTaskBarTooltip !== false" to="body">
|
||||||
|
<div
|
||||||
|
v-if="tooltipState.visible"
|
||||||
|
class="task-hover-tooltip"
|
||||||
|
:class="{ 'tooltip-below': tooltipState.isBelow }"
|
||||||
|
:style="{
|
||||||
|
left: `${tooltipState.position.x}px`,
|
||||||
|
top: `${tooltipState.position.y}px`,
|
||||||
|
backgroundColor: tooltipState.taskStatus.color,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- 有自定义 slot:消费方内容 -->
|
||||||
|
<template v-if="$slots['taskbar-tooltip']">
|
||||||
|
<slot
|
||||||
|
name="taskbar-tooltip"
|
||||||
|
:task="tooltipState.task"
|
||||||
|
:task-status="tooltipState.taskStatus"
|
||||||
|
:resource-percent="tooltipState.resourcePercent"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<!-- 默认内容:向后兼容 -->
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
class="hover-tooltip-arrow"
|
||||||
|
:style="{
|
||||||
|
borderTopColor: tooltipState.isBelow ? 'transparent' : tooltipState.taskStatus.color,
|
||||||
|
borderBottomColor: tooltipState.isBelow ? tooltipState.taskStatus.color : 'transparent',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<div class="hover-tooltip-content">
|
||||||
|
<div class="hover-tooltip-title">{{ tooltipState.task?.name }}</div>
|
||||||
|
<div class="hover-tooltip-row">
|
||||||
|
<span class="hover-tooltip-label">{{ t('plannedStartDate') }}:</span>
|
||||||
|
<span class="hover-tooltip-value">{{ formatTooltipDate(tooltipState.task?.startDate) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="hover-tooltip-row">
|
||||||
|
<span class="hover-tooltip-label">{{ t('plannedEndDate') }}:</span>
|
||||||
|
<span class="hover-tooltip-value">{{ formatTooltipDate(tooltipState.task?.endDate) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="hover-tooltip-row">
|
||||||
|
<span class="hover-tooltip-label">{{ t('actualStartDate') }}:</span>
|
||||||
|
<span class="hover-tooltip-value">{{ tooltipState.task?.actualStartDate ? formatTooltipDate(tooltipState.task.actualStartDate) : '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="hover-tooltip-row">
|
||||||
|
<span class="hover-tooltip-label">{{ t('actualEndDate') }}:</span>
|
||||||
|
<span class="hover-tooltip-value">{{ tooltipState.task?.actualEndDate ? formatTooltipDate(tooltipState.task.actualEndDate) : '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@import '../styles/theme-variables.css';
|
@import '../styles/theme-variables.css';
|
||||||
|
|
||||||
|
/* ─── Singleton Tooltip CSS(从 TaskBar.vue 迁移至此) ─────────────────────── */
|
||||||
|
.task-hover-tooltip {
|
||||||
|
position: fixed;
|
||||||
|
background-color: rgba(0, 0, 0, 0.85);
|
||||||
|
color: white;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
z-index: 999999999;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translate(-50%, -100%);
|
||||||
|
margin-top: -8px;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-hover-tooltip.tooltip-below {
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-tooltip-arrow {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: -5px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
|
border-top: 6px solid rgba(0, 0, 0, 0.85);
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-below .hover-tooltip-arrow {
|
||||||
|
bottom: auto;
|
||||||
|
top: -5px;
|
||||||
|
border-top: 0;
|
||||||
|
border-bottom: 6px solid rgba(0, 0, 0, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-tooltip-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-tooltip-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-tooltip-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-tooltip-label {
|
||||||
|
opacity: 0.9;
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-tooltip-value {
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
.timeline {
|
.timeline {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -247,8 +247,15 @@ const messages = {
|
|||||||
maxWidth: '最大宽度',
|
maxWidth: '最大宽度',
|
||||||
pixelsModel: '像素 (px)',
|
pixelsModel: '像素 (px)',
|
||||||
percentageModel: '百分比 (%)',
|
percentageModel: '百分比 (%)',
|
||||||
},
|
}, collapsible: {
|
||||||
},
|
title: '展开/收起控制',
|
||||||
|
enableCollapsible: '启用任务列可折叠',
|
||||||
|
enableCollapsibleHint: '启用后任务列可展开/收起',
|
||||||
|
visible: '任务列显示状态',
|
||||||
|
visibleExpanded: '展开',
|
||||||
|
visibleCollapsed: '收起',
|
||||||
|
visibleHint: '通过 prop 控制任务列初始/当前显示状态',
|
||||||
|
}, },
|
||||||
// TaskBar配置
|
// TaskBar配置
|
||||||
taskBarConfig: {
|
taskBarConfig: {
|
||||||
title: 'TaskBar 配置',
|
title: 'TaskBar 配置',
|
||||||
@@ -289,6 +296,11 @@ const messages = {
|
|||||||
description: 'data.json · 含完整前/后置依赖,适合功能演示',
|
description: 'data.json · 含完整前/后置依赖,适合功能演示',
|
||||||
badge: 'data.json',
|
badge: 'data.json',
|
||||||
},
|
},
|
||||||
|
medium: {
|
||||||
|
label: '中等数据源',
|
||||||
|
description: 'data-100.json · 含完整前/后置依赖,适合功能演示',
|
||||||
|
badge: 'data-100.json',
|
||||||
|
},
|
||||||
large: {
|
large: {
|
||||||
label: '超大数据源',
|
label: '超大数据源',
|
||||||
description: 'data-large-1m.json · 百万级任务,验证虚拟渲染性能',
|
description: 'data-large-1m.json · 百万级任务,验证虚拟渲染性能',
|
||||||
@@ -551,8 +563,15 @@ const messages = {
|
|||||||
maxWidth: 'Max Width',
|
maxWidth: 'Max Width',
|
||||||
pixelsModel: 'pixels (px)',
|
pixelsModel: 'pixels (px)',
|
||||||
percentageModel: 'percentage (%)',
|
percentageModel: 'percentage (%)',
|
||||||
},
|
}, collapsible: {
|
||||||
},
|
title: 'Collapsible Control',
|
||||||
|
enableCollapsible: 'Enable collapsible task list',
|
||||||
|
enableCollapsibleHint: 'Allow task list to expand/collapse when enabled',
|
||||||
|
visible: 'Task list visibility',
|
||||||
|
visibleExpanded: 'Expanded',
|
||||||
|
visibleCollapsed: 'Collapsed',
|
||||||
|
visibleHint: 'Controls initial/current visibility state of the task list',
|
||||||
|
}, },
|
||||||
// TaskBar配置
|
// TaskBar配置
|
||||||
taskBarConfig: {
|
taskBarConfig: {
|
||||||
title: 'TaskBar Configuration',
|
title: 'TaskBar Configuration',
|
||||||
@@ -593,6 +612,11 @@ const messages = {
|
|||||||
description: 'data.json · Full predecessor graph for feature demos',
|
description: 'data.json · Full predecessor graph for feature demos',
|
||||||
badge: 'data.json',
|
badge: 'data.json',
|
||||||
},
|
},
|
||||||
|
medium: {
|
||||||
|
label: 'Medium Dataset',
|
||||||
|
description: 'data-100.json · Full predecessor graph for feature demos',
|
||||||
|
badge: 'data-100.json',
|
||||||
|
},
|
||||||
large: {
|
large: {
|
||||||
label: 'Massive Dataset',
|
label: 'Massive Dataset',
|
||||||
description: 'data-large-1m.json · Million-level tasks to stress virtual rendering',
|
description: 'data-large-1m.json · Million-level tasks to stress virtual rendering',
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ export interface ToolbarConfig {
|
|||||||
timeScaleDimensions?: TimelineScale[] // 设置时间刻度按钮的展示维度
|
timeScaleDimensions?: TimelineScale[] // 设置时间刻度按钮的展示维度
|
||||||
defaultTimeScale?: TimelineScale // 默认选中的时间刻度
|
defaultTimeScale?: TimelineScale // 默认选中的时间刻度
|
||||||
showExpandCollapse?: boolean // 显示全部展开/折叠按钮
|
showExpandCollapse?: boolean // 显示全部展开/折叠按钮
|
||||||
|
showViewMode?: boolean // 显示 Task/Resource 视图切换按钮组
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* 时间轴数据类型定义
|
* 时间轴数据类型定义
|
||||||
* 统一管理所有时间轴相关的数据结构
|
* 统一管理所有时间轴相关的数据结构
|
||||||
*/
|
*/
|
||||||
|
import type { Task } from '../classes/Task'
|
||||||
|
|
||||||
// 基础时间单位接口
|
// 基础时间单位接口
|
||||||
export interface TimelineHour {
|
export interface TimelineHour {
|
||||||
@@ -129,3 +130,26 @@ export interface TimelineCacheKey {
|
|||||||
scale: string
|
scale: string
|
||||||
workingHours: string
|
workingHours: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Tooltip 相关类型(Singleton Tooltip 方案) ───────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TaskBar 向 Timeline emit tooltip-show 时携带的数据
|
||||||
|
*/
|
||||||
|
export interface TooltipShowPayload {
|
||||||
|
task: Task
|
||||||
|
taskStatus: { color: string; label: string; type?: string }
|
||||||
|
resourcePercent: number
|
||||||
|
hasResourceConflict: boolean
|
||||||
|
/** TaskBar 元素的 DOMRect,用于 Timeline 计算定位 */
|
||||||
|
targetRect: DOMRect
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* #taskbar-tooltip scoped slot 向消费方暴露的数据
|
||||||
|
*/
|
||||||
|
export interface TaskbarTooltipSlotScope {
|
||||||
|
task: Task
|
||||||
|
taskStatus: { color: string; label: string }
|
||||||
|
resourcePercent: number
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user