增强周视图的显示:支持每月1号的展示

This commit is contained in:
qiuchengw
2025-09-25 18:50:08 +08:00
parent 1ccf978bc0
commit bf125d1b50
4 changed files with 693 additions and 9 deletions
+238
View File
@@ -157,10 +157,26 @@ const tempTaskData = ref<{
endDate?: string
} | null>(null)
// 季度视图拖拽时的位置覆盖(直接使用像素位置)
const quarterDragOverride = ref<{
left?: number
width?: number
} | null>(null)
const barRef = ref<HTMLElement | null>(null)
// 计算任务条位置和宽度
const taskBarStyle = computed(() => {
// 季度视图拖拽时使用位置覆盖
if (quarterDragOverride.value && props.currentTimeScale === TimelineScale.QUARTER) {
return {
left: `${quarterDragOverride.value.left ?? 0}px`,
width: `${quarterDragOverride.value.width ?? 100}px`,
height: `${props.rowHeight - 10}px`,
top: '4px',
}
}
const currentStartDate = tempTaskData.value?.startDate || props.task.startDate
const currentEndDate = tempTaskData.value?.endDate || props.task.endDate
@@ -469,6 +485,11 @@ function reportBarPosition() {
}
}
// 拖拽时的实时日期提示框状态
const dragTooltipVisible = ref(false)
const dragTooltipPosition = ref({ x: 0, y: 0 })
const dragTooltipContent = ref({ startDate: '', endDate: '' })
const handleMouseMove = (e: MouseEvent) => {
// 发送边界检测事件给Timeline
window.dispatchEvent(
@@ -480,6 +501,15 @@ const handleMouseMove = (e: MouseEvent) => {
}),
)
// 更新拖拽提示框位置
if (isDragging.value || isResizingLeft.value || isResizingRight.value) {
dragTooltipVisible.value = true
dragTooltipPosition.value = {
x: e.clientX + 15, // 鼠标右侧偏移
y: e.clientY - 60, // 鼠标上方偏移
}
}
if (isDragging.value) {
const deltaX = e.clientX - dragStartX.value
@@ -522,6 +552,52 @@ const handleMouseMove = (e: MouseEvent) => {
startDate: formatDateToLocalString(newStartDate),
endDate: formatDateToLocalString(newEndDate),
}
// 更新拖拽提示框内容
dragTooltipContent.value = {
startDate: formatDateToLocalString(newStartDate),
endDate: formatDateToLocalString(newEndDate),
}
} else if (props.currentTimeScale === TimelineScale.QUARTER) {
// 季度视图:直接使用像素位置,保持拖拽跟随鼠标
const newLeft = Math.max(0, dragStartLeft.value + deltaX)
// 更新位置覆盖
quarterDragOverride.value = {
left: newLeft,
width: dragStartWidth.value,
}
// 同时计算日期用于提示框(使用与Timeline相同的季度视图计算)
const quarterWidth = 60 // 与Timeline.vue保持一致
const daysInQuarter = 90 // 季度平均天数
const pixelsPerDay = quarterWidth / daysInQuarter // 约0.67px/天
const dayOffset = Math.round(deltaX / pixelsPerDay)
// 基于原始任务日期计算新日期
const originalStartDate = createLocalDate(props.task.startDate) || props.startDate
const originalEndDate = createLocalDate(props.task.endDate) || props.startDate
const newStartDate = new Date(originalStartDate)
newStartDate.setDate(newStartDate.getDate() + dayOffset)
// 计算任务持续天数
const durationMs = originalEndDate.getTime() - originalStartDate.getTime()
const duration = Math.ceil(durationMs / (1000 * 60 * 60 * 24))
const newEndDate = new Date(newStartDate)
newEndDate.setDate(newEndDate.getDate() + Math.max(0, duration))
// 只更新临时数据,不触发事件
tempTaskData.value = {
startDate: formatDateToLocalString(newStartDate),
endDate: formatDateToLocalString(newEndDate),
}
// 更新拖拽提示框内容
dragTooltipContent.value = {
startDate: formatDateToLocalString(newStartDate),
endDate: formatDateToLocalString(newEndDate),
}
} else {
// 其他视图:保持原有逻辑
const newLeft = Math.max(0, dragStartLeft.value + deltaX)
@@ -534,6 +610,12 @@ const handleMouseMove = (e: MouseEvent) => {
startDate: formatDateToLocalString(newStartDate),
endDate: formatDateToLocalString(newEndDate),
}
// 更新拖拽提示框内容
dragTooltipContent.value = {
startDate: formatDateToLocalString(newStartDate),
endDate: formatDateToLocalString(newEndDate),
}
}
} else if (isResizingLeft.value) {
const deltaX = e.clientX - resizeStartX.value
@@ -559,6 +641,44 @@ const handleMouseMove = (e: MouseEvent) => {
startDate: formatDateToLocalString(newStartDate),
endDate: props.task.endDate, // 保持原来的结束日期
}
// 更新拖拽提示框内容
dragTooltipContent.value = {
startDate: formatDateToLocalString(newStartDate),
endDate: props.task.endDate || '',
}
} else if (props.currentTimeScale === TimelineScale.QUARTER) {
// 季度视图:左侧resize直接使用像素位置
const newLeft = Math.max(0, resizeStartLeft.value + deltaX)
const newWidth = Math.max(10, resizeStartWidth.value - deltaX) // 保持最小宽度
// 更新位置覆盖
quarterDragOverride.value = {
left: newLeft,
width: newWidth,
}
// 计算日期用于提示框(使用与Timeline相同的季度视图计算)
const quarterWidth = 60 // 与Timeline.vue保持一致
const daysInQuarter = 90 // 季度平均天数
const pixelsPerDay = quarterWidth / daysInQuarter // 约0.67px/天
const dayOffset = Math.round(deltaX / pixelsPerDay)
const originalStartDate = createLocalDate(props.task.startDate) || props.startDate
const newStartDate = new Date(originalStartDate)
newStartDate.setDate(newStartDate.getDate() + dayOffset)
// 只更新临时数据,不触发事件
tempTaskData.value = {
startDate: formatDateToLocalString(newStartDate),
endDate: props.task.endDate, // 保持原来的结束日期
}
// 更新拖拽提示框内容
dragTooltipContent.value = {
startDate: formatDateToLocalString(newStartDate),
endDate: props.task.endDate || '',
}
} else {
// 其他视图:保持原有逻辑
const newLeft = Math.max(0, resizeStartLeft.value + deltaX)
@@ -569,6 +689,12 @@ const handleMouseMove = (e: MouseEvent) => {
startDate: formatDateToLocalString(newStartDate),
endDate: props.task.endDate, // 保持原来的结束日期
}
// 更新拖拽提示框内容
dragTooltipContent.value = {
startDate: formatDateToLocalString(newStartDate),
endDate: props.task.endDate || '',
}
}
} else if (isResizingRight.value) {
const deltaX = e.clientX - resizeStartX.value
@@ -594,6 +720,43 @@ const handleMouseMove = (e: MouseEvent) => {
startDate: props.task.startDate, // 保持原来的开始日期
endDate: formatDateToLocalString(newEndDate),
}
// 更新拖拽提示框内容
dragTooltipContent.value = {
startDate: props.task.startDate || '',
endDate: formatDateToLocalString(newEndDate),
}
} else if (props.currentTimeScale === TimelineScale.QUARTER) {
// 季度视图:右侧resize直接使用像素位置
const newWidth = Math.max(10, resizeStartWidth.value + deltaX) // 保持最小宽度
// 更新位置覆盖
quarterDragOverride.value = {
left: resizeStartLeft.value,
width: newWidth,
}
// 计算日期用于提示框(使用与Timeline相同的季度视图计算)
const quarterWidth = 60 // 与Timeline.vue保持一致
const daysInQuarter = 90 // 季度平均天数
const pixelsPerDay = quarterWidth / daysInQuarter // 约0.67px/天
const dayOffset = Math.round(deltaX / pixelsPerDay)
const originalEndDate = createLocalDate(props.task.endDate) || props.startDate
const newEndDate = new Date(originalEndDate)
newEndDate.setDate(newEndDate.getDate() + dayOffset)
// 只更新临时数据,不触发事件
tempTaskData.value = {
startDate: props.task.startDate, // 保持原来的开始日期
endDate: formatDateToLocalString(newEndDate),
}
// 更新拖拽提示框内容
dragTooltipContent.value = {
startDate: props.task.startDate || '',
endDate: formatDateToLocalString(newEndDate),
}
} else {
// 其他视图:保持原有逻辑
const newWidth = Math.max(props.dayWidth, resizeStartWidth.value + deltaX)
@@ -608,11 +771,23 @@ const handleMouseMove = (e: MouseEvent) => {
startDate: props.task.startDate, // 保持原来的开始日期
endDate: formatDateToLocalString(newEndDate),
}
// 更新拖拽提示框内容
dragTooltipContent.value = {
startDate: props.task.startDate || '',
endDate: formatDateToLocalString(newEndDate),
}
}
}
}
const handleMouseUp = () => {
// 隐藏拖拽提示框
dragTooltipVisible.value = false
// 清除季度视图位置覆盖
quarterDragOverride.value = null
// 停止边界检测
window.dispatchEvent(
new CustomEvent('drag-boundary-check', {
@@ -1479,6 +1654,29 @@ onUnmounted(() => {
</div>
</div>
</Teleport>
<!-- 拖拽实时反馈提示框 -->
<Teleport to="body">
<div
v-if="dragTooltipVisible"
class="drag-tooltip"
:style="{
left: `${dragTooltipPosition.x}px`,
top: `${dragTooltipPosition.y}px`,
}"
>
<div class="drag-tooltip-content">
<div class="tooltip-row">
<span class="tooltip-label">{{ t('startDate') }}:</span>
<span class="tooltip-value">{{ formatDisplayDate(dragTooltipContent.startDate) }}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">{{ t('endDate') }}:</span>
<span class="tooltip-value">{{ formatDisplayDate(dragTooltipContent.endDate) }}</span>
</div>
</div>
</div>
</Teleport>
</template>
<style scoped>
@@ -1918,6 +2116,46 @@ onUnmounted(() => {
color: #ffffff;
}
/* === 拖拽实时反馈提示框样式 === */
.drag-tooltip {
position: fixed;
background: rgba(0, 123, 255, 0.95);
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
z-index: 10001;
box-shadow: 0 2px 12px rgba(0, 123, 255, 0.4);
pointer-events: none;
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(2px);
}
.drag-tooltip .tooltip-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2px;
}
.drag-tooltip .tooltip-row:last-child {
margin-bottom: 0;
}
.drag-tooltip .tooltip-label {
opacity: 0.9;
min-width: 55px;
font-size: 11px;
}
.drag-tooltip .tooltip-value {
font-weight: 600;
text-align: right;
font-size: 11px;
margin-left: 8px;
}
.sticky-text {
position: absolute;
white-space: nowrap;
+140 -1
View File
@@ -913,6 +913,26 @@ const isWeekContainsToday = (weekStart: Date, weekEnd: Date) => {
return today >= weekStart && today <= weekEnd
}
// 计算周在全局时间轴中的位置(用于旗帜定位)
const getGlobalWeekPosition = (monthIndex: number, weekIndex: number) => {
let position = 0
// 累加前面月份的宽度
for (let i = 0; i < monthIndex; i++) {
const month = timelineData.value[i]
if (month && month.isWeekView && month.weeks) {
position += month.weeks.length * 60
} else if (month && month.days) {
position += month.days.length * 30
}
}
// 加上当前月份内的周位置
position += weekIndex * 60
return position
}
// 更新时间刻度方法 - 供外部调用
const updateTimeScale = (scale: TimelineScale) => {
currentTimeScale.value = scale
@@ -2380,6 +2400,36 @@ const handleAddSuccessor = (task: Task) => {
>
<div class="year-month-label">{{ month.yearMonthLabel }}</div>
</div>
<!-- 月份1号标记旗帜 - 统一放在外层容器 -->
<template
v-for="(month, monthIndex) in timelineData"
:key="`flags-${month.year}-${month.month}`"
>
<template v-if="month.isWeekView && month.weeks">
<template
v-for="(week, weekIndex) in month.weeks"
:key="`flag-${month.year}-${month.month}-${weekIndex}`"
>
<template
v-for="(subDay, dayIndex) in week.subDays || []"
:key="`flagday-${monthIndex}-${weekIndex}-${dayIndex}`"
>
<div
v-if="subDay.date && subDay.date.getDate() === 1"
class="month-first-flag"
:style="{
left: `${getGlobalWeekPosition(monthIndex, weekIndex) + dayIndex * (60/7)}px`,
transform: 'translateX(-50%)' // 使旗帜中心(杆子)对齐日期位置
}"
>
<div class="flag-pole"></div>
<div class="flag-content">{{ subDay.date.getDate() }}</div>
</div>
</template>
</template>
</template>
</template>
</div>
<!-- 第二行:周/日期 -->
@@ -2481,6 +2531,36 @@ const handleAddSuccessor = (task: Task) => {
height: `${contentHeight}px`,
}"
></div>
<!-- 月份1号竖直线(周视图) -->
<template v-if="currentTimeScale === TimelineScale.WEEK">
<template
v-for="(month, monthIndex) in timelineData"
:key="`vlines-${month.year}-${month.month}`"
>
<template v-if="month.isWeekView && month.weeks">
<template
v-for="(week, weekIndex) in month.weeks"
:key="`vline-${month.year}-${month.month}-${weekIndex}`"
>
<template
v-for="(subDay, dayIndex) in week.subDays || []"
:key="`vlineday-${monthIndex}-${weekIndex}-${dayIndex}`"
>
<div
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`
}"
></div>
</template>
</template>
</template>
</template>
</template>
<!-- 背景列 -->
<div class="day-columns" :style="{ height: `${contentHeight}px` }">
<!-- 小时视图背景列 -->
@@ -2611,7 +2691,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>
@@ -2838,6 +2918,7 @@ const handleAddSuccessor = (task: Task) => {
.year-month-row {
align-items: center;
position: relative; /* 为旗帜提供定位上下文 */
}
.days-row {
@@ -2982,6 +3063,64 @@ const handleAddSuccessor = (task: Task) => {
/* 不显示边框,仅用于定位计算 */
}
/* 月份1号标记旗帜样式 */
.month-first-flag {
position: absolute;
bottom: -40px;
z-index: 1;
pointer-events: none;
display: flex;
flex-direction: column;
align-items: center;
}
.flag-content {
background-color: var(--gantt-primary, #409eff);
color: white;
font-size: 10px;
font-weight: 600;
padding: 1px 4px;
border-radius: 2px;
text-align: center;
min-width: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
order: 1; /* 旗帜内容在上 */
}
.flag-pole {
width: 1px;
height: 50px;
background-color: var(--gantt-primary, #409eff);
order: 2; /* 旗杆在下 */
}
/* 暗色主题下的旗帜样式 */
:global(html[data-theme='dark']) .flag-pole {
background-color: var(--gantt-primary-light, #66b1ff);
}
:global(html[data-theme='dark']) .flag-content {
background-color: var(--gantt-primary-light, #66b1ff);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
/* 月份1号竖直线样式 */
.month-first-vertical-line {
position: absolute;
top: 0;
width: 1px;
background-color: var(--gantt-primary, #409eff);
opacity: 0.6;
z-index: 5;
pointer-events: none;
}
/* 暗色主题下的竖直线 */
:global(html[data-theme='dark']) .month-first-vertical-line {
background-color: var(--gantt-primary-light, #66b1ff);
opacity: 0.7;
}
/* 周视图背景列样式 */
.month-week-columns {
display: flex;