v1.4.2 - enhancements
This commit is contained in:
@@ -1,6 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useI18n } from '../composables/useI18n'
|
||||
|
||||
interface Props {
|
||||
modelValue?: string | [string, string]
|
||||
type?: 'date' | 'daterange' | 'datetime'
|
||||
placeholder?: string
|
||||
startPlaceholder?: string
|
||||
endPlaceholder?: string
|
||||
disabled?: boolean
|
||||
clearable?: boolean
|
||||
size?: 'small' | 'default' | 'large'
|
||||
format?: string
|
||||
valueFormat?: string
|
||||
rangeSeparator?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: '',
|
||||
type: 'date',
|
||||
@@ -24,20 +39,6 @@ const emit = defineEmits<{
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Props {
|
||||
modelValue?: string | [string, string]
|
||||
type?: 'date' | 'daterange'
|
||||
placeholder?: string
|
||||
startPlaceholder?: string
|
||||
endPlaceholder?: string
|
||||
disabled?: boolean
|
||||
clearable?: boolean
|
||||
size?: 'small' | 'default' | 'large'
|
||||
format?: string
|
||||
valueFormat?: string
|
||||
rangeSeparator?: string
|
||||
}
|
||||
|
||||
// 内部状态
|
||||
const isFocused = ref(false)
|
||||
const isHovered = ref(false)
|
||||
@@ -54,6 +55,12 @@ const minuteListRef = ref<HTMLElement>()
|
||||
const blurTimer = ref<number | null>(null)
|
||||
const positionUpdateKey = ref(0) // 用于强制重新计算位置
|
||||
|
||||
// 保存打开选择器时的原始值(用于取消时恢复)
|
||||
const originalSingleValue = ref('')
|
||||
const originalSelectedTime = ref('12:00')
|
||||
const originalStartValue = ref('')
|
||||
const originalEndValue = ref('')
|
||||
|
||||
// 当前显示的年月
|
||||
const currentYear = ref(new Date().getFullYear())
|
||||
const currentMonth = ref(new Date().getMonth())
|
||||
@@ -80,10 +87,12 @@ const formatDisplayDateTime = (dateStr: string, timeStr: string) => {
|
||||
const date = new Date(dateStr)
|
||||
if (isNaN(date.getTime())) return ''
|
||||
|
||||
const dateFormat = `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`
|
||||
// 使用 format 属性来决定日期格式
|
||||
const separator = props.format.includes('-') ? '-' : '/'
|
||||
const dateFormat = `${date.getFullYear()}${separator}${String(date.getMonth() + 1).padStart(2, '0')}${separator}${String(date.getDate()).padStart(2, '0')}`
|
||||
|
||||
// 总是显示时间部分
|
||||
if (timeStr) {
|
||||
// 仅在 datetime 模式下显示时间部分
|
||||
if (props.type === 'datetime' && timeStr) {
|
||||
return `${dateFormat} ${timeStr}`
|
||||
}
|
||||
|
||||
@@ -102,6 +111,7 @@ const displayValue = computed(() => {
|
||||
}
|
||||
return start || end || ''
|
||||
}
|
||||
// 对于单日期/日期时间,使用内部状态来显示(即使未确认也显示)
|
||||
return singleValue.value ? formatDisplayDateTime(singleValue.value, selectedTime.value) : ''
|
||||
})
|
||||
|
||||
@@ -217,6 +227,15 @@ const togglePicker = () => {
|
||||
if (props.disabled) return
|
||||
showPicker.value = !showPicker.value
|
||||
if (showPicker.value) {
|
||||
// 保存打开时的原始值
|
||||
if (props.type === 'daterange') {
|
||||
originalStartValue.value = startValue.value
|
||||
originalEndValue.value = endValue.value
|
||||
} else {
|
||||
originalSingleValue.value = singleValue.value
|
||||
originalSelectedTime.value = selectedTime.value
|
||||
}
|
||||
|
||||
// 记录当前滚动位置
|
||||
lastScrollPosition.x = window.scrollX
|
||||
lastScrollPosition.y = window.scrollY
|
||||
@@ -246,8 +265,17 @@ const togglePicker = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭日期选择器
|
||||
// 关闭日期选择器(未确认时恢复原始值)
|
||||
const closePicker = () => {
|
||||
// 如果是 datetime 模式,恢复原始值(因为可能没有点击确认按钮)
|
||||
if (props.type === 'datetime' && showPicker.value) {
|
||||
singleValue.value = originalSingleValue.value
|
||||
selectedTime.value = originalSelectedTime.value
|
||||
} else if (props.type === 'daterange' && showPicker.value) {
|
||||
startValue.value = originalStartValue.value
|
||||
endValue.value = originalEndValue.value
|
||||
}
|
||||
|
||||
showPicker.value = false
|
||||
showYearPicker.value = false
|
||||
showMonthPicker.value = false
|
||||
@@ -354,7 +382,9 @@ const confirmDate = () => {
|
||||
// 格式化日期和时间
|
||||
const formatDateTime = (dateStr: string) => {
|
||||
const date = new Date(dateStr)
|
||||
const dateFormat = `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`
|
||||
// 使用 valueFormat 来格式化日期
|
||||
const separator = props.valueFormat.includes('-') ? '-' : '/'
|
||||
const dateFormat = `${date.getFullYear()}${separator}${String(date.getMonth() + 1).padStart(2, '0')}${separator}${String(date.getDate()).padStart(2, '0')}`
|
||||
return selectedTime.value ? `${dateFormat} ${selectedTime.value}` : dateFormat
|
||||
}
|
||||
|
||||
@@ -364,17 +394,27 @@ const confirmDate = () => {
|
||||
|
||||
emit('update:modelValue', newValue)
|
||||
emit('change', newValue)
|
||||
|
||||
// 更新原始值,这样关闭时不会恢复
|
||||
originalStartValue.value = startValue.value
|
||||
originalEndValue.value = endValue.value
|
||||
}
|
||||
} else if (singleValue.value) {
|
||||
// 格式化日期和时间
|
||||
const date = new Date(singleValue.value)
|
||||
const dateFormat = `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`
|
||||
// 使用 valueFormat 来格式化日期
|
||||
const separator = props.valueFormat.includes('-') ? '-' : '/'
|
||||
const dateFormat = `${date.getFullYear()}${separator}${String(date.getMonth() + 1).padStart(2, '0')}${separator}${String(date.getDate()).padStart(2, '0')}`
|
||||
const formattedDateTime = selectedTime.value
|
||||
? `${dateFormat} ${selectedTime.value}`
|
||||
: dateFormat
|
||||
|
||||
emit('update:modelValue', formattedDateTime)
|
||||
emit('change', formattedDateTime)
|
||||
|
||||
// 更新原始值,这样关闭时不会恢复
|
||||
originalSingleValue.value = singleValue.value
|
||||
originalSelectedTime.value = selectedTime.value
|
||||
}
|
||||
|
||||
// 确认后关闭面板
|
||||
@@ -494,7 +534,27 @@ const selectDate = (dateStr: string) => {
|
||||
}
|
||||
} else {
|
||||
singleValue.value = dateStr
|
||||
// 不再自动提交,等待用户点击确认按钮
|
||||
|
||||
// 如果 type='date'(纯日期模式),立即提交并关闭
|
||||
if (props.type === 'date') {
|
||||
// 格式化日期
|
||||
const date = new Date(dateStr)
|
||||
const separator = props.valueFormat.includes('-') ? '-' : '/'
|
||||
const formattedDate = `${date.getFullYear()}${separator}${String(date.getMonth() + 1).padStart(2, '0')}${separator}${String(date.getDate()).padStart(2, '0')}`
|
||||
|
||||
// 发送事件
|
||||
emit('update:modelValue', formattedDate)
|
||||
emit('change', formattedDate)
|
||||
|
||||
// 更新原始值,这样关闭时不会恢复
|
||||
originalSingleValue.value = singleValue.value
|
||||
|
||||
// 延迟关闭,让用户看到选中效果
|
||||
setTimeout(() => {
|
||||
closePicker()
|
||||
}, 150)
|
||||
}
|
||||
// 如果 type='datetime',等待用户选择时间并点击确认按钮
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1145,8 +1205,8 @@ const timePickerStyle = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 时间选择器输入框 -->
|
||||
<div class="el-time-picker-input">
|
||||
<!-- 时间选择器输入框(仅在 datetime 模式下显示) -->
|
||||
<div v-if="type === 'datetime'" class="el-time-picker-input">
|
||||
<label class="el-time-picker-label" for="time-input">{{ t.time }}:</label>
|
||||
<input
|
||||
id="time-input"
|
||||
@@ -1161,8 +1221,8 @@ const timePickerStyle = computed(() => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 日期选择器确认按钮 -->
|
||||
<div class="el-date-picker-footer">
|
||||
<!-- 日期选择器确认按钮(仅在 datetime 模式下显示) -->
|
||||
<div v-if="type === 'datetime'" class="el-date-picker-footer">
|
||||
<button
|
||||
class="el-date-picker-btn el-date-picker-btn--confirm"
|
||||
@click.stop="confirmDate"
|
||||
|
||||
+782
-227
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import { useI18n } from '../composables/useI18n'
|
||||
import DatePicker from './DatePicker.vue'
|
||||
import GanttConfirmDialog from './GanttConfirmDialog.vue'
|
||||
import type { Milestone } from '../models/classes/Milestone'
|
||||
import type { Task } from '../models/classes/Task'
|
||||
import '../styles/app.css'
|
||||
|
||||
interface Props {
|
||||
@@ -16,12 +17,13 @@ const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [visible: boolean]
|
||||
close: []
|
||||
save: [milestone: Milestone]
|
||||
save: [milestone: Task]
|
||||
delete: [milestoneId: number]
|
||||
}>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive<Milestone>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
startDate: '',
|
||||
assignee: '',
|
||||
@@ -87,6 +89,28 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 监听对话框打开,确保新建时重置表单
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVisible) => {
|
||||
if (newVisible && !props.milestone) {
|
||||
// 打开对话框且没有传入里程碑(新建模式),重置表单
|
||||
Object.assign(formData, {
|
||||
id: undefined,
|
||||
name: '',
|
||||
startDate: '',
|
||||
assignee: '',
|
||||
type: 'milestone',
|
||||
icon: 'diamond',
|
||||
description: '',
|
||||
})
|
||||
// 清空错误
|
||||
errors.name = ''
|
||||
errors.startDate = ''
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 表单验证
|
||||
const validateForm = () => {
|
||||
errors.name = ''
|
||||
@@ -121,7 +145,13 @@ const selectIcon = (icon: string) => {
|
||||
// 保存处理
|
||||
const handleSave = () => {
|
||||
if (validateForm()) {
|
||||
emit('save', { ...formData })
|
||||
// 里程碑的 endDate 必须与 startDate 相同
|
||||
const milestoneData = {
|
||||
...formData,
|
||||
endDate: formData.startDate,
|
||||
id: formData.id!, // 确保id不为undefined
|
||||
} as Task
|
||||
emit('save', milestoneData)
|
||||
closeDialog()
|
||||
}
|
||||
}
|
||||
@@ -245,6 +275,7 @@ const t = (key: string) => {
|
||||
id="milestone-date"
|
||||
v-model="formData.startDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择里程碑日期"
|
||||
:class="{ error: errors.startDate }"
|
||||
/>
|
||||
|
||||
+262
-147
@@ -3,6 +3,7 @@
|
||||
import { computed, ref, onUnmounted } from 'vue'
|
||||
import type { Milestone } from '../models/classes/Milestone'
|
||||
import { TimelineScale } from '../models/types/TimelineScale'
|
||||
import type { TimelineMonth, TimelineYear, TimelineDay } from '../models/types/TimelineDataTypes'
|
||||
import { useI18n } from '../composables/useI18n'
|
||||
import { createLocalDate } from '../utils/predecessorUtils'
|
||||
const props = defineProps<Props>()
|
||||
@@ -21,29 +22,30 @@ const t = (key: string): string => {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
date: string // 里程碑日期
|
||||
date: string
|
||||
milestone: Milestone
|
||||
name?: string
|
||||
rowHeight: number
|
||||
dayWidth: number
|
||||
startDate: Date
|
||||
name?: string
|
||||
milestone?: Milestone // 完整的里程碑数据
|
||||
// 新增:用于边界粘性显示的滚动位置信息
|
||||
timelineStart: Date
|
||||
timelineEnd: Date
|
||||
scrollLeft?: number
|
||||
containerWidth?: number
|
||||
// 新增:里程碑推挤效果所需的信息
|
||||
milestoneId?: string | number // 唯一标识符
|
||||
milestoneId?: number
|
||||
otherMilestones?: Array<{
|
||||
id: string | number
|
||||
left: number
|
||||
originalLeft: number // 原始位置(不考虑停靠)
|
||||
id: number
|
||||
isSticky: boolean
|
||||
stickyPosition: 'left' | 'right' | 'none'
|
||||
priority: number // 推挤优先级
|
||||
}> // 其他里程碑的位置信息
|
||||
// 新增:时间线数据,用于精确计算subDays定位
|
||||
timelineData?: unknown[]
|
||||
// 新增:当前时间刻度
|
||||
currentTimeScale?: TimelineScale
|
||||
stickyPosition: string
|
||||
left: number
|
||||
originalLeft: number
|
||||
priority: number
|
||||
}>
|
||||
currentTimeScale?: TimelineScale | null
|
||||
timelineData?: TimelineMonth[] | TimelineYear[] | TimelineDay[]
|
||||
periodWidth: number
|
||||
isInHighlightMode?: boolean
|
||||
allowDragAndResize: boolean
|
||||
}
|
||||
|
||||
// 拖拽相关状态
|
||||
@@ -102,8 +104,112 @@ const formatDateToLocalString = (date: Date): string => {
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
// 根据像素位置计算日期(支持月度、季度视图)
|
||||
const calculateDateFromPosition = (
|
||||
pixelPosition: number,
|
||||
timelineData: Array<{
|
||||
year: number
|
||||
month: number
|
||||
startDate: Date
|
||||
endDate: Date
|
||||
days?: Array<{ date: Date; day: number }>
|
||||
monthData?: { dayCount: number }
|
||||
}>,
|
||||
timeScale: TimelineScale,
|
||||
): Date | null => {
|
||||
if (!timelineData) {
|
||||
return null
|
||||
}
|
||||
|
||||
let cumulativePosition = 0
|
||||
|
||||
if (timeScale === TimelineScale.DAY) {
|
||||
// 日视图:基于 days 数组
|
||||
for (const periodData of timelineData) {
|
||||
const days = periodData.days || []
|
||||
const periodWidth = days.length * 30 // 日视图每天30px
|
||||
|
||||
if (pixelPosition >= cumulativePosition && pixelPosition < cumulativePosition + periodWidth) {
|
||||
const relativePosition = pixelPosition - cumulativePosition
|
||||
const dayIndex = Math.floor(relativePosition / 30)
|
||||
|
||||
if (dayIndex >= 0 && dayIndex < days.length) {
|
||||
return new Date(days[dayIndex].date)
|
||||
}
|
||||
}
|
||||
|
||||
cumulativePosition += periodWidth
|
||||
}
|
||||
} else if (timeScale === TimelineScale.MONTH) {
|
||||
// 月视图:每个月60px
|
||||
for (const periodData of timelineData) {
|
||||
const monthWidth = 60
|
||||
|
||||
if (pixelPosition >= cumulativePosition && pixelPosition < cumulativePosition + monthWidth) {
|
||||
const relativePosition = pixelPosition - cumulativePosition
|
||||
const daysInMonth = periodData.monthData?.dayCount || 30
|
||||
const dayWidth = monthWidth / daysInMonth
|
||||
|
||||
const dayIndex = Math.floor(relativePosition / dayWidth)
|
||||
const day = Math.min(dayIndex + 1, daysInMonth)
|
||||
|
||||
return new Date(periodData.year, periodData.month - 1, day)
|
||||
}
|
||||
|
||||
cumulativePosition += monthWidth
|
||||
}
|
||||
} else if (timeScale === TimelineScale.QUARTER) {
|
||||
// 季度视图:每个季度60px
|
||||
for (const periodData of timelineData) {
|
||||
const quarters = (periodData as Record<string, unknown>).quarters as Array<{
|
||||
quarter: number
|
||||
startDate: Date
|
||||
endDate: Date
|
||||
}> || []
|
||||
|
||||
for (const quarter of quarters) {
|
||||
const quarterStart = new Date(quarter.startDate)
|
||||
const quarterEnd = new Date(quarter.endDate)
|
||||
const quarterWidth = 60
|
||||
|
||||
if (
|
||||
pixelPosition >= cumulativePosition &&
|
||||
pixelPosition < cumulativePosition + quarterWidth
|
||||
) {
|
||||
const relativePosition = pixelPosition - cumulativePosition
|
||||
const daysInQuarter = Math.ceil(
|
||||
(quarterEnd.getTime() - quarterStart.getTime()) / (1000 * 60 * 60 * 24),
|
||||
)
|
||||
const dayWidth = quarterWidth / daysInQuarter
|
||||
|
||||
const dayIndex = Math.floor(relativePosition / dayWidth)
|
||||
|
||||
const resultDate = new Date(quarterStart)
|
||||
resultDate.setDate(resultDate.getDate() + dayIndex)
|
||||
|
||||
return resultDate
|
||||
}
|
||||
|
||||
cumulativePosition += quarterWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// 拖拽事件处理 - 使用相对位置拖拽方案
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
// 如果禁用了拖拽和拉伸,直接返回
|
||||
if (props.allowDragAndResize === false) {
|
||||
return
|
||||
}
|
||||
|
||||
// 年度视图禁止拖拽(与TaskBar保持一致)
|
||||
if (props.currentTimeScale === TimelineScale.YEAR) {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是停靠状态或被推出边界,禁止拖拽
|
||||
if (
|
||||
milestoneVisibility.value.isSticky ||
|
||||
@@ -162,7 +268,42 @@ const handleMouseMove = (e: MouseEvent) => {
|
||||
isDragging.value = true
|
||||
|
||||
const newLeft = Math.max(0, dragStartLeft.value + deltaX)
|
||||
const newStartDate = addDaysToLocalDate(props.startDate, newLeft / props.dayWidth)
|
||||
let newStartDate: Date
|
||||
|
||||
// 根据当前时间刻度使用不同的日期计算方法
|
||||
if (
|
||||
props.currentTimeScale === TimelineScale.MONTH ||
|
||||
props.currentTimeScale === TimelineScale.QUARTER ||
|
||||
props.currentTimeScale === TimelineScale.DAY
|
||||
) {
|
||||
// 月度、季度、日视图:使用 timelineData 精确计算
|
||||
if (props.timelineData && props.currentTimeScale) {
|
||||
const calculatedDate = calculateDateFromPosition(
|
||||
newLeft,
|
||||
props.timelineData as Array<{
|
||||
year: number
|
||||
month: number
|
||||
startDate: Date
|
||||
endDate: Date
|
||||
days?: Array<{ date: Date; day: number }>
|
||||
monthData?: { dayCount: number }
|
||||
}>,
|
||||
props.currentTimeScale,
|
||||
)
|
||||
if (calculatedDate) {
|
||||
newStartDate = calculatedDate
|
||||
} else {
|
||||
// 如果计算失败,回退到简单计算
|
||||
newStartDate = addDaysToLocalDate(props.startDate, newLeft / props.dayWidth)
|
||||
}
|
||||
} else {
|
||||
// 如果没有 timelineData,回退到简单计算
|
||||
newStartDate = addDaysToLocalDate(props.startDate, newLeft / props.dayWidth)
|
||||
}
|
||||
} else {
|
||||
// 其他视图(周视图、小时视图):使用原有的简单计算
|
||||
newStartDate = addDaysToLocalDate(props.startDate, newLeft / props.dayWidth)
|
||||
}
|
||||
|
||||
// 只更新临时数据,不触发事件
|
||||
tempMilestoneData.value = {
|
||||
@@ -246,6 +387,11 @@ const handleMilestoneClick = (e: MouseEvent) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否应该显示为暗淡(处于高亮模式)
|
||||
const isDimmed = computed(() => {
|
||||
return props.isInHighlightMode === true
|
||||
})
|
||||
|
||||
// 计算菱形位置 - 考虑拖拽临时数据
|
||||
const milestoneStyle = computed(() => {
|
||||
const currentMilestoneDate = tempMilestoneData.value?.startDate || props.date
|
||||
@@ -282,15 +428,8 @@ const milestoneStyle = computed(() => {
|
||||
size = Math.min(props.rowHeight, props.dayWidth * 1.2, 24)
|
||||
}
|
||||
|
||||
// 年度视图:使用专门的年度位置计算
|
||||
if (props.currentTimeScale === TimelineScale.YEAR) {
|
||||
const centerPosition = calculateYearViewMilestonePosition(milestoneDate, props.startDate)
|
||||
left = centerPosition - size / 2 // 从中心位置偏移到图标左上角
|
||||
} else if (props.currentTimeScale === TimelineScale.QUARTER) {
|
||||
// 季度视图:使用类似年度视图的简单计算方法
|
||||
const centerPosition = calculateQuarterViewMilestonePosition(milestoneDate, props.startDate)
|
||||
left = centerPosition - size / 2 // 从中心位置偏移到图标左上角
|
||||
} else if (props.currentTimeScale === TimelineScale.HOUR) {
|
||||
// 小时视图:使用专门的小时位置计算
|
||||
if (props.currentTimeScale === TimelineScale.HOUR) {
|
||||
// 小时视图:精确到小时和分钟的定位
|
||||
const centerPosition = calculateHourViewMilestonePosition(milestoneDate, props.startDate)
|
||||
left = centerPosition - size / 2 // 从中心位置偏移到图标左上角
|
||||
@@ -298,9 +437,12 @@ const milestoneStyle = computed(() => {
|
||||
props.timelineData &&
|
||||
props.currentTimeScale &&
|
||||
(props.currentTimeScale === TimelineScale.WEEK ||
|
||||
props.currentTimeScale === TimelineScale.MONTH)
|
||||
props.currentTimeScale === TimelineScale.MONTH ||
|
||||
props.currentTimeScale === TimelineScale.DAY ||
|
||||
props.currentTimeScale === TimelineScale.QUARTER ||
|
||||
props.currentTimeScale === TimelineScale.YEAR)
|
||||
) {
|
||||
// 优先使用基于timelineData的精确定位(适用于周视图和月视图)
|
||||
// 优先使用基于timelineData的精确定位(适用于周视图、月视图、日视图、季度视图和年度视图)
|
||||
const centerPosition = calculateMilestonePositionFromTimelineData(
|
||||
milestoneDate,
|
||||
props.timelineData,
|
||||
@@ -309,7 +451,7 @@ const milestoneStyle = computed(() => {
|
||||
|
||||
left = centerPosition - size / 2 // 从中心位置偏移到图标左上角
|
||||
} else {
|
||||
// 日视图:保持原有逻辑
|
||||
// 其他情况(没有 timelineData):保持原有逻辑
|
||||
const startDiff = Math.floor(
|
||||
(milestoneDate.getTime() - props.startDate.getTime()) / (1000 * 60 * 60 * 24),
|
||||
)
|
||||
@@ -543,58 +685,6 @@ onUnmounted(() => {
|
||||
})
|
||||
|
||||
// 年度视图里程碑位置计算函数
|
||||
const calculateYearViewMilestonePosition = (targetDate: Date, baseStartDate: Date): number => {
|
||||
const targetYear = targetDate.getFullYear()
|
||||
const baseYear = baseStartDate.getFullYear()
|
||||
|
||||
// 每年的宽度是360px,每半年180px
|
||||
const yearWidth = 360
|
||||
const halfYearWidth = 180
|
||||
|
||||
// 计算目标年份相对于基准年份的偏移
|
||||
const yearOffset = targetYear - baseYear
|
||||
let position = yearOffset * yearWidth
|
||||
|
||||
// 判断是上半年还是下半年
|
||||
const month = targetDate.getMonth() + 1 // getMonth()返回0-11,需要+1
|
||||
|
||||
if (month > 6) {
|
||||
// 下半年,添加半年偏移
|
||||
position += halfYearWidth
|
||||
}
|
||||
|
||||
// 在半年内的具体位置计算
|
||||
let dayOffset = 0
|
||||
let startOfHalfYear: Date
|
||||
|
||||
if (month <= 6) {
|
||||
// 上半年:1-6月
|
||||
startOfHalfYear = new Date(targetYear, 0, 1) // 1月1日
|
||||
} else {
|
||||
// 下半年:7-12月
|
||||
startOfHalfYear = new Date(targetYear, 6, 1) // 7月1日
|
||||
}
|
||||
|
||||
dayOffset = Math.floor((targetDate.getTime() - startOfHalfYear.getTime()) / (1000 * 60 * 60 * 24))
|
||||
|
||||
// 半年大约181-184天,将天数映射到180px的宽度
|
||||
const daysInHalfYear =
|
||||
month <= 6
|
||||
? Math.floor(
|
||||
(new Date(targetYear, 6, 1).getTime() - new Date(targetYear, 0, 1).getTime()) /
|
||||
(1000 * 60 * 60 * 24),
|
||||
)
|
||||
: Math.floor(
|
||||
(new Date(targetYear + 1, 0, 1).getTime() - new Date(targetYear, 6, 1).getTime()) /
|
||||
(1000 * 60 * 60 * 24),
|
||||
)
|
||||
|
||||
const dayPositionInHalfYear = (dayOffset / daysInHalfYear) * halfYearWidth
|
||||
position += dayPositionInHalfYear
|
||||
|
||||
return position
|
||||
}
|
||||
|
||||
// 小时视图里程碑位置计算 - 精确到小时和分钟
|
||||
const calculateHourViewMilestonePosition = (targetDate: Date, baseStartDate: Date): number => {
|
||||
// 计算基础天数差
|
||||
@@ -628,81 +718,94 @@ const calculateHourViewMilestonePosition = (targetDate: Date, baseStartDate: Dat
|
||||
return totalPosition
|
||||
}
|
||||
|
||||
// 季度视图里程碑位置计算 - 参考年度视图的简单算法
|
||||
const calculateQuarterViewMilestonePosition = (targetDate: Date, baseStartDate: Date): number => {
|
||||
const targetYear = targetDate.getFullYear()
|
||||
const baseYear = baseStartDate.getFullYear()
|
||||
|
||||
// 每年的宽度是240px (4季度 * 60px),每季度60px
|
||||
const yearWidth = 240
|
||||
const quarterWidth = 60
|
||||
|
||||
// 计算目标年份相对于基准年份的偏移
|
||||
const yearOffset = targetYear - baseYear
|
||||
let position = yearOffset * yearWidth
|
||||
|
||||
// 判断是哪个季度
|
||||
const month = targetDate.getMonth() + 1 // getMonth()返回0-11,需要+1
|
||||
let quarter = 1
|
||||
if (month >= 1 && month <= 3) {
|
||||
// Q1: 1-3月
|
||||
} else if (month >= 4 && month <= 6) {
|
||||
quarter = 2 // Q2: 4-6月
|
||||
} else if (month >= 7 && month <= 9) {
|
||||
quarter = 3 // Q3: 7-9月
|
||||
} else {
|
||||
quarter = 4 // Q4: 10-12月
|
||||
}
|
||||
|
||||
// 添加季度偏移 (Q1=0px, Q2=60px, Q3=120px, Q4=180px)
|
||||
position += (quarter - 1) * quarterWidth
|
||||
|
||||
// 在季度内的具体位置计算
|
||||
let dayOffset = 0
|
||||
let startOfQuarter: Date
|
||||
let endOfQuarter: Date
|
||||
|
||||
if (quarter === 1) {
|
||||
startOfQuarter = new Date(targetYear, 0, 1) // 1月1日
|
||||
endOfQuarter = new Date(targetYear, 2, 31) // 3月31日
|
||||
} else if (quarter === 2) {
|
||||
startOfQuarter = new Date(targetYear, 3, 1) // 4月1日
|
||||
endOfQuarter = new Date(targetYear, 5, 30) // 6月30日
|
||||
} else if (quarter === 3) {
|
||||
startOfQuarter = new Date(targetYear, 6, 1) // 7月1日
|
||||
endOfQuarter = new Date(targetYear, 8, 30) // 9月30日
|
||||
} else {
|
||||
startOfQuarter = new Date(targetYear, 9, 1) // 10月1日
|
||||
endOfQuarter = new Date(targetYear, 11, 31) // 12月31日
|
||||
}
|
||||
|
||||
dayOffset = Math.floor((targetDate.getTime() - startOfQuarter.getTime()) / (1000 * 60 * 60 * 24))
|
||||
|
||||
// 计算季度内的天数,将天数映射到60px的宽度
|
||||
const endTime = endOfQuarter.getTime()
|
||||
const startTime = startOfQuarter.getTime()
|
||||
const daysInQuarter = Math.floor((endTime - startTime) / (1000 * 60 * 60 * 24)) + 1 // 包含结束日期
|
||||
|
||||
const dayPositionInQuarter = (dayOffset / daysInQuarter) * quarterWidth
|
||||
|
||||
position += dayPositionInQuarter
|
||||
|
||||
return position
|
||||
}
|
||||
|
||||
// 基于timelineData和subDays精确计算里程碑位置的函数
|
||||
const calculateMilestonePositionFromTimelineData = (
|
||||
targetDate: Date,
|
||||
timelineData: any,
|
||||
timelineData: TimelineMonth[] | TimelineYear[] | TimelineDay[],
|
||||
timeScale: TimelineScale,
|
||||
) => {
|
||||
// 回退到原来的逻辑用于其他时间刻度
|
||||
let cumulativePosition = 0
|
||||
|
||||
for (const periodData of timelineData) {
|
||||
if (timeScale === TimelineScale.WEEK) {
|
||||
if (timeScale === TimelineScale.DAY) {
|
||||
// 日视图:处理days数组,返回中心位置
|
||||
// 类型保护:确保 periodData 是 TimelineMonth 并有 days 属性
|
||||
const days = ('days' in periodData && periodData.days) ? periodData.days : []
|
||||
|
||||
for (let i = 0; i < days.length; i++) {
|
||||
const dayData = days[i]
|
||||
const dayDate = new Date(dayData.date)
|
||||
|
||||
// 比较日期(忽略时分秒)
|
||||
if (
|
||||
dayDate.getFullYear() === targetDate.getFullYear() &&
|
||||
dayDate.getMonth() === targetDate.getMonth() &&
|
||||
dayDate.getDate() === targetDate.getDate()
|
||||
) {
|
||||
// 找到目标日期,返回累计位置 + 当前天数索引 * 日宽度 + 半个日宽度(中心)
|
||||
return cumulativePosition + i * 30 + 15 // 日视图每天30px,中心+15px
|
||||
}
|
||||
}
|
||||
|
||||
// 累加当前月份所有天数的宽度
|
||||
cumulativePosition += days.length * 30
|
||||
} else if (timeScale === TimelineScale.QUARTER) {
|
||||
// 季度视图:处理years数组,每个year包含quarters
|
||||
// 类型保护:确保 periodData 是 TimelineYear 并有 quarters 属性
|
||||
const quarters = ('quarters' in periodData && periodData.quarters) ? periodData.quarters : []
|
||||
|
||||
for (const quarter of quarters) {
|
||||
const quarterStart = new Date(quarter.startDate)
|
||||
const quarterEnd = new Date(quarter.endDate)
|
||||
|
||||
if (targetDate >= quarterStart && targetDate <= quarterEnd) {
|
||||
// 找到目标日期所在的季度
|
||||
const quarterWidth = 60
|
||||
const daysInQuarter = Math.ceil(
|
||||
(quarterEnd.getTime() - quarterStart.getTime()) / (1000 * 60 * 60 * 24),
|
||||
)
|
||||
const dayWidth = quarterWidth / daysInQuarter
|
||||
const dayInQuarter = Math.ceil(
|
||||
(targetDate.getTime() - quarterStart.getTime()) / (1000 * 60 * 60 * 24),
|
||||
)
|
||||
const finalPosition = cumulativePosition + dayInQuarter * dayWidth + dayWidth / 2
|
||||
|
||||
return finalPosition
|
||||
}
|
||||
|
||||
// 累加每季度的宽度
|
||||
cumulativePosition += 60
|
||||
}
|
||||
} else if (timeScale === TimelineScale.YEAR) {
|
||||
// 年度视图:处理years数组,每个year包含halfYears
|
||||
// 类型保护:确保 periodData 是 TimelineYear 并有 halfYears 属性
|
||||
const halfYears = ('halfYears' in periodData && periodData.halfYears) ? periodData.halfYears : []
|
||||
|
||||
for (const halfYear of halfYears) {
|
||||
const halfYearStart = new Date(halfYear.startDate)
|
||||
const halfYearEnd = new Date(halfYear.endDate)
|
||||
|
||||
if (targetDate >= halfYearStart && targetDate <= halfYearEnd) {
|
||||
// 找到目标日期所在的半年
|
||||
const halfYearWidth = 180 // 年度视图每半年180px
|
||||
const daysInHalfYear = Math.ceil(
|
||||
(halfYearEnd.getTime() - halfYearStart.getTime()) / (1000 * 60 * 60 * 24),
|
||||
)
|
||||
const dayWidth = halfYearWidth / daysInHalfYear
|
||||
const dayInHalfYear = Math.ceil(
|
||||
(targetDate.getTime() - halfYearStart.getTime()) / (1000 * 60 * 60 * 24),
|
||||
)
|
||||
return cumulativePosition + dayInHalfYear * dayWidth + dayWidth / 2
|
||||
}
|
||||
|
||||
// 累加每半年的宽度
|
||||
cumulativePosition += 180
|
||||
}
|
||||
} else if (timeScale === TimelineScale.WEEK) {
|
||||
// 周视图:处理嵌套的weeks结构,返回中心位置
|
||||
const weeks = periodData.weeks || []
|
||||
// 类型保护:确保 periodData 是 TimelineMonth 并有 weeks 属性
|
||||
const weeks = ('weeks' in periodData && periodData.weeks) ? periodData.weeks : []
|
||||
|
||||
for (const week of weeks) {
|
||||
const weekStart = new Date(week.weekStart)
|
||||
@@ -741,13 +844,17 @@ const calculateMilestonePositionFromTimelineData = (
|
||||
}
|
||||
} else if (timeScale === TimelineScale.MONTH) {
|
||||
// 月视图:处理扁平化的subDays结构,返回中心位置
|
||||
// 类型保护:确保 periodData 是 TimelineMonth
|
||||
if (!('startDate' in periodData) || !('endDate' in periodData)) {
|
||||
continue
|
||||
}
|
||||
const periodStart = new Date(periodData.startDate)
|
||||
const periodEnd = new Date(periodData.endDate)
|
||||
|
||||
if (targetDate >= periodStart && targetDate <= periodEnd) {
|
||||
// 找到目标日期所在的时间段
|
||||
const monthWidth = 60
|
||||
const daysInMonth = periodData.monthData?.dayCount || 30
|
||||
const daysInMonth = ('monthData' in periodData && periodData.monthData?.dayCount) || 30
|
||||
const dayWidth = monthWidth / daysInMonth
|
||||
const dayInMonth = targetDate.getDate()
|
||||
const finalPosition = cumulativePosition + (dayInMonth - 1) * dayWidth + dayWidth / 2
|
||||
@@ -777,6 +884,7 @@ const calculateMilestonePositionFromTimelineData = (
|
||||
'milestone-sticky-left': milestoneVisibility.stickyPosition === 'left',
|
||||
'milestone-sticky-right': milestoneVisibility.stickyPosition === 'right',
|
||||
'milestone-pushed-out': milestoneVisibility.isPushedOut,
|
||||
dimmed: isDimmed,
|
||||
}"
|
||||
@click.stop="handleMilestoneClick"
|
||||
>
|
||||
@@ -875,6 +983,13 @@ const calculateMilestonePositionFromTimelineData = (
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* 高亮模式下,非高亮的Milestone变暗淡 */
|
||||
.milestone.dimmed {
|
||||
opacity: 0.35 !important;
|
||||
filter: grayscale(0.3) !important;
|
||||
transition: all 0.3s ease !important;
|
||||
}
|
||||
|
||||
/* 里程碑SVG发光效果 */
|
||||
.milestone svg {
|
||||
filter: drop-shadow(0 0 8px var(--gantt-danger, #f56c6c));
|
||||
|
||||
+1010
-160
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, reactive, watch, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useI18n } from '../composables/useI18n'
|
||||
import { useMessage } from '../composables/useMessage'
|
||||
import DatePicker from './DatePicker.vue'
|
||||
@@ -327,21 +327,29 @@ watch(
|
||||
newVal => {
|
||||
isVisible.value = newVal
|
||||
if (newVal) {
|
||||
resetForm()
|
||||
if (props.task && props.isEdit) {
|
||||
// 编辑模式,填充表单数据
|
||||
// 编辑模式,先重置表单再填充表单数据
|
||||
resetForm()
|
||||
const taskData = { ...props.task }
|
||||
|
||||
// 处理日期格式:如果是只有日期部分的数据,在小时视图编辑时需要特殊处理
|
||||
taskData.startDate = processDateForHourView(taskData.startDate, 'start')
|
||||
taskData.endDate = processDateForHourView(taskData.endDate, 'end')
|
||||
|
||||
Object.assign(formData, taskData)
|
||||
// 使用 nextTick 确保 resetForm 的响应式更新完成后再赋值
|
||||
nextTick(() => {
|
||||
Object.assign(formData, taskData)
|
||||
})
|
||||
} else if (props.task && !props.isEdit) {
|
||||
// 新建模式,重置表单
|
||||
resetForm()
|
||||
// 新建模式,自动绑定上级任务
|
||||
formData.parentId = props.task.parentId ?? undefined
|
||||
// 新建模式,自动绑定前置任务
|
||||
formData.predecessor = props.task.predecessor ?? []
|
||||
} else {
|
||||
// 没有 task,也重置表单
|
||||
resetForm()
|
||||
}
|
||||
// 抽屉显示时重新请求任务数据,确保前置任务列表是最新的
|
||||
window.dispatchEvent(new CustomEvent('request-task-list'))
|
||||
@@ -811,7 +819,8 @@ function confirmTimer(desc: string) {
|
||||
<DatePicker
|
||||
id="task-start-date"
|
||||
v-model="formData.startDate"
|
||||
type="date"
|
||||
:type="'datetime' as any"
|
||||
value-format="YYYY-MM-DD HH:mm"
|
||||
:placeholder="t.startDateRequired"
|
||||
:class="{ error: errors.startDate }"
|
||||
/>
|
||||
@@ -825,7 +834,8 @@ function confirmTimer(desc: string) {
|
||||
<DatePicker
|
||||
id="task-end-date"
|
||||
v-model="formData.endDate"
|
||||
type="date"
|
||||
:type="'datetime' as any"
|
||||
value-format="YYYY-MM-DD HH:mm"
|
||||
:placeholder="t.endDateRequired"
|
||||
:class="{ error: errors.endDate }"
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch, useSlots, computed } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
import TaskRow from './TaskRow.vue'
|
||||
import { useI18n } from '../composables/useI18n'
|
||||
import type { Task } from '../models/classes/Task'
|
||||
@@ -9,8 +8,6 @@ import { DEFAULT_TASK_LIST_COLUMNS } from '../models/configs/TaskListConfig'
|
||||
|
||||
interface Props {
|
||||
tasks?: Task[]
|
||||
onTaskDoubleClick?: (task: Task) => void
|
||||
editComponent?: Component
|
||||
useDefaultDrawer?: boolean
|
||||
taskListConfig?: TaskListConfig
|
||||
}
|
||||
@@ -91,17 +88,13 @@ const handleTaskRowDoubleClick = (task: Task) => {
|
||||
return
|
||||
}
|
||||
|
||||
// 优先调用外部传入的双击处理器
|
||||
if (props.onTaskDoubleClick && typeof props.onTaskDoubleClick === 'function') {
|
||||
props.onTaskDoubleClick(task)
|
||||
} else if (props.useDefaultDrawer) {
|
||||
// 默认行为:发送到Timeline处理(通过全局事件)
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('task-row-double-click', {
|
||||
detail: task,
|
||||
}),
|
||||
)
|
||||
}
|
||||
// 始终发送到Timeline处理(通过全局事件),让Timeline决定是否打开编辑器
|
||||
// 这样外部监听的 @task-double-click 事件也会被触发
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('task-row-double-click', {
|
||||
detail: task,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// 计算父级任务的进度和日期范围
|
||||
@@ -417,7 +410,6 @@ onUnmounted(() => {
|
||||
:level="0"
|
||||
:is-hovered="hoveredTaskId === task.id"
|
||||
:hovered-task-id="hoveredTaskId"
|
||||
:on-double-click="props.onTaskDoubleClick"
|
||||
:on-hover="handleTaskRowHover"
|
||||
:columns="visibleColumns"
|
||||
@toggle="toggleCollapse"
|
||||
@@ -478,6 +470,7 @@ onUnmounted(() => {
|
||||
background: var(--gantt-bg-secondary);
|
||||
color: var(--gantt-text-header);
|
||||
border-right-color: var(--gantt-border-medium);
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.task-list-body {
|
||||
|
||||
@@ -31,7 +31,6 @@ interface TaskRowSlotProps {
|
||||
interface Props {
|
||||
task: Task
|
||||
level: number
|
||||
onDoubleClick?: (task: Task) => void
|
||||
isHovered?: boolean
|
||||
hoveredTaskId?: number | null
|
||||
onHover?: (taskId: number | null) => void
|
||||
@@ -85,13 +84,8 @@ const handleTaskRowDoubleClick = (e: MouseEvent) => {
|
||||
// 阻止事件冒泡
|
||||
e.stopPropagation()
|
||||
|
||||
// 优先调用外部传入的双击处理器
|
||||
if (props.onDoubleClick && typeof props.onDoubleClick === 'function') {
|
||||
props.onDoubleClick(props.task)
|
||||
} else {
|
||||
// 默认行为:发出双击事件给父组件
|
||||
emit('dblclick', props.task)
|
||||
}
|
||||
// 发出双击事件给父组件
|
||||
emit('dblclick', props.task)
|
||||
}
|
||||
|
||||
// 处理悬停事件
|
||||
@@ -480,7 +474,6 @@ onUnmounted(() => {
|
||||
:level="props.level + 1"
|
||||
:is-hovered="props.hoveredTaskId === child.id"
|
||||
:hovered-task-id="props.hoveredTaskId"
|
||||
:on-double-click="props.onDoubleClick"
|
||||
:on-hover="props.onHover"
|
||||
:columns="props.columns"
|
||||
@toggle="emit('toggle', $event)"
|
||||
|
||||
+906
-194
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user