v1.7.1 - Add Props & Exposes

This commit is contained in:
LINING-PC\lining
2026-01-11 17:22:32 +08:00
parent 898406802d
commit 189913d6b8
13 changed files with 3272 additions and 50 deletions
+383 -14
View File
@@ -58,6 +58,11 @@ const props = withDefaults(defineProps<Props>(), {
taskListRowStyle: undefined,
enableTaskListContextMenu: true,
enableTaskBarContextMenu: true,
fullscreen: false,
expandAll: true,
locale: 'zh-CN',
timeScale: 'week',
theme: 'light',
})
const emit = defineEmits([
@@ -189,6 +194,16 @@ interface Props {
// 当设置为 true 且声明了 TaskBarContextMenu 组件时,使用自定义菜单
// 当设置为 false 时,无论是否声明组件,TaskBar 右键菜单都失效
enableTaskBarContextMenu?: boolean
// 全屏状态控制(响应式)
fullscreen?: boolean
// 展开/收起所有任务(响应式)
expandAll?: boolean
// 语言设置(响应式)
locale?: 'zh-CN' | 'en-US'
// 时间刻度(响应式)
timeScale?: TimelineScale
// 主题模式(响应式)
theme?: 'light' | 'dark'
}
// TaskList的固定总长度(所有列的最小宽度之和 + 边框等额外空间)
@@ -694,7 +709,7 @@ const handleTaskRowMoved = (payload: {
// 通知 TaskList 更新父级任务数据
nextTick(() => {
window.dispatchEvent(new CustomEvent('task-updated', {
detail: result.movedTask
detail: result.movedTask,
}))
})
@@ -731,6 +746,90 @@ const handleCollapseAll = () => {
}
}
// === 展开/收起相关方法 ===
/**
* 展开所有任务
*/
const expandAllTasks = () => {
handleExpandAll()
}
/**
* 收起所有任务
*/
const collapseAllTasks = () => {
handleCollapseAll()
}
/**
* 切换展开/收起所有任务
*/
const toggleExpandAllTasks = () => {
// 检查是否所有有子任务的任务都已展开
const checkAllExpanded = (tasks: Task[]): boolean => {
for (const task of tasks) {
if (task.children && task.children.length > 0) {
if (task.collapsed) {
return false
}
if (!checkAllExpanded(task.children)) {
return false
}
}
}
return true
}
if (props.tasks) {
const allExpanded = checkAllExpanded(props.tasks)
if (allExpanded) {
collapseAllTasks()
} else {
expandAllTasks()
}
}
}
/**
* 获取当前是否所有任务都已展开
*/
const getIsExpandAll = (): boolean => {
if (!props.tasks || props.tasks.length === 0) {
return true
}
const checkAllExpanded = (tasks: Task[]): boolean => {
for (const task of tasks) {
if (task.children && task.children.length > 0) {
if (task.collapsed) {
return false
}
if (!checkAllExpanded(task.children)) {
return false
}
}
}
return true
}
return checkAllExpanded(props.tasks)
}
// 监听 Props expandAll 变化
watch(
() => props.expandAll,
(newValue) => {
if (newValue !== undefined) {
if (newValue) {
expandAllTasks()
} else {
collapseAllTasks()
}
}
},
{ immediate: true },
)
// 处理TaskDrawer请求任务列表
const handleRequestTaskList = () => {
// 创建扁平化的任务列表,包含所有任务和里程碑
@@ -817,11 +916,42 @@ onUnmounted(() => {
window.removeEventListener('context-menu', handleTaskContextMenu as EventListener)
})
// 主题状态管理
const currentThemeMode = ref<'light' | 'dark'>('light')
/**
* 设置主题模式
* @param mode 主题模式,默认为 'dark'
*/
const setTheme = (mode?: 'light' | 'dark') => {
const targetMode = mode || 'dark'
currentThemeMode.value = targetMode
document.documentElement.setAttribute('data-theme', targetMode)
}
/**
* 获取当前主题模式
*/
const currentTheme = (): string => {
return currentThemeMode.value
}
// 监听 Props theme 变化
watch(
() => props.theme,
(newTheme) => {
if (newTheme && newTheme !== currentThemeMode.value) {
setTheme(newTheme)
}
},
{ immediate: true },
)
// 全屏状态管理
const isFullscreen = ref(false)
// 多语言支持
const { t, locale } = useI18n()
const { t, locale: i18nLocale } = useI18n()
const collapseTaskListText = computed(() => t.value.collapseTaskList)
const expandTaskListText = computed(() => t.value.expandTaskList)
@@ -1443,6 +1573,63 @@ const handleTimelineScaleChanged = (scale: TimelineScale) => {
})
}
// === 时间维度相关方法 ===
// 时间刻度顺序定义
const TIME_SCALE_ORDER: TimelineScale[] = ['hour', 'day', 'week', 'month', 'quarter', 'year']
/**
* 设置时间刻度
* @param scale 时间刻度,默认为 'week'
*/
const setTimeScale = (scale?: TimelineScale) => {
const targetScale = scale || 'week'
if (TIME_SCALE_ORDER.includes(targetScale)) {
handleTimeScaleChange(targetScale)
}
}
/**
* 放大时间刻度(显示更细粒度)
* year -> quarter -> month -> week -> day -> hour
*/
const zoomIn = () => {
const currentIndex = TIME_SCALE_ORDER.indexOf(currentTimeScale.value)
if (currentIndex > 0) {
const newScale = TIME_SCALE_ORDER[currentIndex - 1]
handleTimeScaleChange(newScale)
}
}
/**
* 缩小时间刻度(显示更粗粒度)
* hour -> day -> week -> month -> quarter -> year
*/
const zoomOut = () => {
const currentIndex = TIME_SCALE_ORDER.indexOf(currentTimeScale.value)
if (currentIndex < TIME_SCALE_ORDER.length - 1) {
const newScale = TIME_SCALE_ORDER[currentIndex + 1]
handleTimeScaleChange(newScale)
}
}
/**
* 获取当前时间刻度
*/
const currentScale = (): string => {
return currentTimeScale.value
}
// 监听 Props timeScale 变化
watch(
() => props.timeScale,
(newScale) => {
if (newScale && newScale !== currentTimeScale.value) {
handleTimeScaleChange(newScale)
}
},
{ immediate: true },
)
// 处理关闭高亮
const handleClearHighlight = () => {
if (timelineRef.value?.clearHighlight) {
@@ -1672,7 +1859,7 @@ const pdfExportHandler = async () => {
// 添加日期
pdf.setFontSize(10)
const currentDate = new Date().toLocaleDateString(locale.value)
const currentDate = new Date().toLocaleDateString(i18nLocale.value)
pdf.text(`${dateLabel}: ${currentDate}`, pdfWidth - 10, 10, { align: 'right' })
// 添加甘特图图片
@@ -1730,6 +1917,78 @@ const handleFullscreenToggle = (event: CustomEvent) => {
}, 500) // 比动画时间稍长一点,确保完全完成
}
// === 全屏相关方法 ===
/**
* 进入全屏模式
*/
const enterFullscreen = () => {
if (!isFullscreen.value) {
isFullscreen.value = true
if (props.onFullscreenChange && typeof props.onFullscreenChange === 'function') {
props.onFullscreenChange(true)
}
setTimeout(() => {
window.dispatchEvent(
new CustomEvent('timeline-container-resized', {
detail: { source: 'fullscreen-toggle' },
}),
)
}, 500)
}
}
/**
* 退出全屏模式
*/
const exitFullscreen = () => {
if (isFullscreen.value) {
isFullscreen.value = false
if (props.onFullscreenChange && typeof props.onFullscreenChange === 'function') {
props.onFullscreenChange(false)
}
setTimeout(() => {
window.dispatchEvent(
new CustomEvent('timeline-container-resized', {
detail: { source: 'fullscreen-toggle' },
}),
)
}, 500)
}
}
/**
* 切换全屏模式
*/
const toggleFullscreen = () => {
if (isFullscreen.value) {
exitFullscreen()
} else {
enterFullscreen()
}
}
/**
* 获取当前是否全屏
*/
const getIsFullscreen = (): boolean => {
return isFullscreen.value
}
// 监听 Props fullscreen 变化
watch(
() => props.fullscreen,
(newValue) => {
if (newValue !== undefined && newValue !== isFullscreen.value) {
if (newValue) {
enterFullscreen()
} else {
exitFullscreen()
}
}
},
{ immediate: true },
)
// 更新或添加里程碑到列表中
const updateOrAddMilestone = (milestones: Task[], milestone: Task): boolean => {
const existingIndex = milestones.findIndex(m => m.id === milestone.id)
@@ -1898,22 +2157,94 @@ const defaultTodayLocate = () => {
const offset = timelinePanelW ? timelinePanelW / 2 : 200 // 偏移量让今天居中显示
const scrollPosition = (totalDays - 1) * 30 - offset
// 滚动到指定位置
const timeline = document.querySelector('.timeline') as HTMLElement
if (timeline) {
timeline.scrollLeft = Math.max(0, scrollPosition)
if (timelinePanel) {
timelinePanel.scrollLeft = Math.max(0, scrollPosition)
}
}
// 添加今日高亮效果到对应的日期列
const todayColumn = timeline.querySelector('.day-column.today') as HTMLElement
if (todayColumn) {
todayColumn.classList.add('today-highlight')
setTimeout(() => {
todayColumn.classList.remove('today-highlight')
}, 2000)
// === 今日定位相关方法 ===
/**
* 滚动到今日位置
*/
const scrollToToday = () => {
if (timelineRef.value && typeof timelineRef.value.scrollToTodayCenter === 'function') {
timelineRef.value.scrollToTodayCenter()
} else {
defaultTodayLocate()
}
}
/**
* 滚动到指定任务
* @param taskId 任务ID
*/
const scrollToTask = (taskId: string | number) => {
// 查找任务并滚动到对应位置
const findTaskById = (tasks: Task[], id: string | number): Task | null => {
for (const task of tasks) {
if (task.id === id || String(task.id) === String(id)) {
return task
}
if (task.children && task.children.length > 0) {
const found = findTaskById(task.children, id)
if (found) return found
}
}
return null
}
if (props.tasks) {
const task = findTaskById(props.tasks, taskId)
if (task && task.startDate) {
// 使用Timeline的scrollToDate方法
if (timelineRef.value && typeof timelineRef.value.scrollToDate === 'function') {
timelineRef.value.scrollToDate(task.startDate)
}
}
}
}
/**
* 滚动到指定日期
* @param date 日期(Date对象或日期字符串)
*/
const scrollToDate = (date: Date | string) => {
// 使用Timeline的scrollToDate方法
if (timelineRef.value && typeof timelineRef.value.scrollToDate === 'function') {
timelineRef.value.scrollToDate(date)
}
}
// === 语言切换相关方法 ===
/**
* 获取当前语言
*/
const currentLocale = (): string => {
return i18nLocale.value
}
/**
* 设置语言
* @param locale 语言代码
*/
const setLocale = (locale: 'zh-CN' | 'en-US') => {
const { setLocale: setI18nLocale } = useI18n()
setI18nLocale(locale)
}
// 监听 Props locale 变化
watch(
() => props.locale,
(newLocale) => {
if (newLocale && newLocale !== i18nLocale.value) {
// 导入 setLocale 方法
const { setLocale } = useI18n()
setLocale(newLocale)
}
},
{ immediate: true },
)
// 窗口大小变化处理函数
const handleWindowResize = () => {
// 直接检查当前宽度是否仍然有效,如果超出限制则调整
@@ -2335,6 +2666,40 @@ function handleMilestoneDialogDelete(milestoneId: number) {
// 4. 关闭对话框
handleMilestoneDialogClose()
}
// 暴露方法供外部调用
defineExpose({
// 全屏相关
enterFullscreen,
exitFullscreen,
toggleFullscreen,
isFullscreen: getIsFullscreen,
// 展开/收起相关
expandAll: expandAllTasks,
collapseAll: collapseAllTasks,
toggleExpandAll: toggleExpandAllTasks,
isExpandAll: getIsExpandAll,
// 今日定位相关
scrollToToday,
scrollToTask,
scrollToDate,
// 语言切换相关
setLocale,
currentLocale,
// 时间维度相关
setTimeScale,
zoomIn,
zoomOut,
currentScale,
// 主题相关
setTheme,
currentTheme,
})
</script>
<template>
@@ -2347,6 +2712,10 @@ function handleMilestoneDialogDelete(milestoneId: number) {
<GanttToolbar
v-if="props.showToolbar"
:config="props.toolbarConfig"
:time-scale="currentTimeScale"
:theme="currentThemeMode"
:fullscreen="isFullscreen"
:expand-all="getIsExpandAll()"
:on-today-locate="todayLocateHandler"
:on-export-csv="csvExportHandler"
:on-export-pdf="pdfExportHandler"
+47
View File
@@ -10,6 +10,10 @@ type Language = 'zh' | 'en'
const props = withDefaults(defineProps<Props>(), {
config: () => ({}),
timeScale: undefined,
theme: undefined,
fullscreen: undefined,
expandAll: undefined,
onAddTask: undefined,
onAddMilestone: undefined,
onTodayLocate: undefined,
@@ -50,6 +54,10 @@ const localeMap: Record<Language, 'zh-CN' | 'en-US'> = {
interface Props {
config?: ToolbarConfig
timeScale?: TimelineScale
theme?: 'light' | 'dark'
fullscreen?: boolean
expandAll?: boolean
// 自定义事件处理器
onAddTask?: () => void
onAddMilestone?: () => void
@@ -90,6 +98,45 @@ const isFullscreen = ref(false)
const showLanguageDropdown = ref(false)
const currentTimeScale = ref<TimelineScale>(TimelineScale.DAY)
// 如果外部通过 Prop 传入 timeScale,则同步到本地状态
watch(
() => props.timeScale,
(newScale) => {
if (newScale && newScale !== currentTimeScale.value) {
currentTimeScale.value = newScale
}
},
{ immediate: true },
)
// 监听 theme prop
watch(
() => props.theme,
(newTheme) => {
if (newTheme) {
const newMode = newTheme === 'dark'
if (isDarkMode.value !== newMode) {
isDarkMode.value = newMode
document.documentElement.setAttribute('data-theme', newTheme)
}
}
},
{ immediate: true },
)
// 监听 fullscreen prop
watch(
() => props.fullscreen,
(newFullscreen) => {
if (newFullscreen !== undefined && isFullscreen.value !== newFullscreen) {
isFullscreen.value = newFullscreen
}
},
{ immediate: true },
)
// 监听 expandAll prop (注:这里不需要内部状态,只是用于 UI 显示)
// 翻译函数 - 使用 useI18n 提供的 getTranslation 函数
const t = (key: string): string => {
return getTranslation(key)
+149
View File
@@ -2504,6 +2504,154 @@ const scrollToToday = () => {
}, 500) // 等待滚动完成后再添加高亮
}
/**
* 滚动到指定日期(居中显示)
* @param date 日期(Date对象或日期字符串)
*/
const scrollToDate = (date: Date | string) => {
const targetDate = typeof date === 'string' ? new Date(date) : date
const timelineStart = timelineConfig.value.startDate
// 确保日期计算的精确性 - 使用年月日,忽略时分秒
const targetNormalized = new Date(
targetDate.getFullYear(),
targetDate.getMonth(),
targetDate.getDate(),
)
// 根据不同的时间刻度使用不同的起始日期
let startNormalized: Date
if (
currentTimeScale.value === TimelineScale.YEAR ||
currentTimeScale.value === TimelineScale.QUARTER
) {
const yearRange = getYearTimelineRange()
startNormalized = new Date(
yearRange.startDate.getFullYear(),
yearRange.startDate.getMonth(),
yearRange.startDate.getDate(),
)
} else if (currentTimeScale.value === TimelineScale.MONTH) {
const monthRange = getMonthTimelineRange()
startNormalized = new Date(
monthRange.startDate.getFullYear(),
monthRange.startDate.getMonth(),
monthRange.startDate.getDate(),
)
} else {
startNormalized = new Date(
timelineStart.getFullYear(),
timelineStart.getMonth(),
timelineStart.getDate(),
)
}
// 计算目标日期距离时间线开始日期的天数
const timeDiff = targetNormalized.getTime() - startNormalized.getTime()
const daysDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24))
// 计算目标日期在时间线中的像素位置(根据当前时间刻度)
let datePosition: number
if (currentTimeScale.value === TimelineScale.HOUR) {
// 小时视图:精确到小时的定位
const targetHour = targetDate.getHours()
const targetMinute = targetDate.getMinutes()
// 基础天数偏移(到目标日0点的位置)
const baseDayPosition = daysDiff * dayWidth.value
// 小时偏移:每小时40px
const hourOffset = targetHour * 40
// 分钟偏移:在当前小时内的精确位置
const minuteOffset = (targetMinute / 60) * 40
datePosition = baseDayPosition + hourOffset + minuteOffset
} else if (currentTimeScale.value === TimelineScale.QUARTER) {
// 季度视图:计算季度偏移
const targetYear = targetNormalized.getFullYear()
const baseYear = startNormalized.getFullYear()
const yearWidth = 240 // 每年4季度 * 60px
const quarterWidth = 60
// 计算年份偏移
const yearOffset = targetYear - baseYear
datePosition = yearOffset * yearWidth
// 计算季度内的偏移
const targetQuarter = Math.floor(targetNormalized.getMonth() / 3)
datePosition += targetQuarter * quarterWidth
// 计算季度内的天数偏移(季度内的细微定位)
const quarterStartMonth = targetQuarter * 3
const quarterStartDate = new Date(targetYear, quarterStartMonth, 1)
const daysIntoQuarter = Math.floor(
(targetNormalized.getTime() - quarterStartDate.getTime()) / (1000 * 60 * 60 * 24),
)
const avgDaysInQuarter = 91 // 平均每季度91天
datePosition += (daysIntoQuarter / avgDaysInQuarter) * quarterWidth
} else if (currentTimeScale.value === TimelineScale.YEAR) {
// 年视图:计算年内偏移
const targetYear = targetNormalized.getFullYear()
const baseYear = startNormalized.getFullYear()
const yearWidth = 360 // 每年360px
// 计算年份偏移
const yearOffset = targetYear - baseYear
datePosition = yearOffset * yearWidth
// 计算年内的天数偏移
const yearStartDate = new Date(targetYear, 0, 1)
const daysIntoYear = Math.floor(
(targetNormalized.getTime() - yearStartDate.getTime()) / (1000 * 60 * 60 * 24),
)
const daysInYear = 365 // 不考虑闰年的简化处理
datePosition += (daysIntoYear / daysInYear) * yearWidth
} else if (currentTimeScale.value === TimelineScale.MONTH) {
// 月视图:需要累计每个月的实际宽度
const targetYear = targetNormalized.getFullYear()
const targetMonth = targetNormalized.getMonth()
const baseYear = startNormalized.getFullYear()
const baseMonth = startNormalized.getMonth()
const monthWidth = 60 // 每月60px
// 计算跨越的月数
const monthsDiff = (targetYear - baseYear) * 12 + (targetMonth - baseMonth)
datePosition = monthsDiff * monthWidth
// 计算月内的天数偏移
const targetDay = targetNormalized.getDate()
const daysInMonth = new Date(targetYear, targetMonth + 1, 0).getDate()
datePosition += (targetDay / daysInMonth) * monthWidth
} else if (currentTimeScale.value === TimelineScale.WEEK) {
// 周视图:每周60px
const weekWidth = 60
datePosition = (daysDiff / 7) * weekWidth
} else {
// 日视图:每天30px
datePosition = daysDiff * dayWidth.value
}
// 使用缓存的容器元素
const timeline = timelineContainerElement.value
if (!timeline) return
const containerWidth = timeline.clientWidth
// 计算居中滚动位置
const centeredScrollPosition = datePosition - containerWidth / 2
// 滚动到指定位置,确保目标日期在中间
timeline.scrollTo({
left: Math.max(0, centeredScrollPosition),
behavior: 'smooth',
})
}
// 更新任务
const updateTask = (updatedTask: Task) => {
// 不直接修改props数据,而是通过事件通知父组件
@@ -3366,6 +3514,7 @@ defineExpose({
scrollToTasks,
scrollToToday,
scrollToTodayCenter,
scrollToDate,
// 时间线配置
timelineConfig,
// 时间刻度更新