v1.4.2 - enhancements

This commit is contained in:
LINING-PC\lining
2025-11-01 15:46:12 +08:00
parent 65059e1978
commit 4fff9d20f4
37 changed files with 10325 additions and 2635 deletions
+85 -25
View File
@@ -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"
File diff suppressed because it is too large Load Diff
+33 -2
View File
@@ -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
View File
@@ -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()
// 360px180px
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) // 11
} else {
// 7-12
startOfHalfYear = new Date(targetYear, 6, 1) // 71
}
dayOffset = Math.floor((targetDate.getTime() - startOfHalfYear.getTime()) / (1000 * 60 * 60 * 24))
// 181-184180px
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) // 11
endOfQuarter = new Date(targetYear, 2, 31) // 331
} else if (quarter === 2) {
startOfQuarter = new Date(targetYear, 3, 1) // 41
endOfQuarter = new Date(targetYear, 5, 30) // 630
} else if (quarter === 3) {
startOfQuarter = new Date(targetYear, 6, 1) // 71
endOfQuarter = new Date(targetYear, 8, 30) // 930
} else {
startOfQuarter = new Date(targetYear, 9, 1) // 101
endOfQuarter = new Date(targetYear, 11, 31) // 1231
}
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
}
// timelineDatasubDays
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) {
// yearsyearquarters
// 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) {
// yearsyearhalfYears
// 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
View File
File diff suppressed because it is too large Load Diff
+16 -6
View File
@@ -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 }"
/>
+8 -15
View File
@@ -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,
}),
)
}
// TimelineTimeline
// @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 {
+2 -9
View File
@@ -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)"
File diff suppressed because it is too large Load Diff
+71 -1
View File
@@ -199,6 +199,8 @@ const messages = {
timerConfirmPrefix: '即将为任务',
timerConfirmSuffix: '计时,若有特殊说明请完善下面的描述',
timerConfirmPlaceholder: '请输入计时说明',
// Demo配置面板
configDemo: '配置演示',
// TaskList配置
taskListConfig: {
title: 'TaskList 配置',
@@ -210,8 +212,34 @@ const messages = {
defaultWidth: '默认宽度',
minWidth: '最小宽度',
maxWidth: '最大宽度',
pixelsModel: '像素 (px)',
percentageModel: '百分比 (%)',
},
},
// TaskBar配置
taskBarConfig: {
title: 'TaskBar 配置',
display: {
title: '显示选项',
showAvatar: '显示头像 (Avatar)',
showTitle: '显示标题 (Title)',
showProgress: '显示进度 (Progress)',
},
mistouch: {
title: '防误触配置',
dragThreshold: '拖拽阈值 (px)',
dragThresholdHint: '移动超过此距离才触发拖拽',
resizeHandleWidth: '拉伸手柄宽度 (px)',
resizeHandleWidthHint: '调整手柄的可点击宽度 (5-15px)',
enableDragDelay: '启用拖拽延迟',
enableDragDelayHint: '按住一段时间后才能拖拽',
dragDelayTime: '延迟时间 (ms)',
dragDelayTimeHint: '延迟启动拖拽的时间',
allowDragOnClick: '允许拖拽和拉伸 TaskBar 和 Milestone',
allowDragOnClickHint: '控制是否允许拖拽 TaskBar 和 Milestone,以及拉伸 TaskBar 的长度',
},
},
disableTaskbarFocusMode: '关闭聚焦功能',
},
'en-US': {
dateNotSet: 'Not set',
@@ -409,6 +437,8 @@ const messages = {
timerConfirmPrefix: 'About to start timing for',
timerConfirmSuffix: '. If there are special notes, please complete the description below.',
timerConfirmPlaceholder: 'Please enter timer description',
// Demo配置面板
configDemo: 'Configuration Demo',
// TaskList配置
taskListConfig: {
title: 'TaskList Configuration',
@@ -420,15 +450,55 @@ const messages = {
defaultWidth: 'Default Width',
minWidth: 'Min Width',
maxWidth: 'Max Width',
pixelsModel: 'pixels (px)',
percentageModel: 'percentage (%)',
},
},
// TaskBar配置
taskBarConfig: {
title: 'TaskBar Configuration',
display: {
title: 'Display Options',
showAvatar: 'Show Avatar',
showTitle: 'Show Title',
showProgress: 'Show Progress',
},
mistouch: {
title: 'Mistouch Prevention',
dragThreshold: 'Drag Threshold (px)',
dragThresholdHint: 'Distance to trigger dragging',
resizeHandleWidth: 'Resize Handle Width (px)',
resizeHandleWidthHint: 'Clickable width of resize handle (5-15px)',
enableDragDelay: 'Enable Drag Delay',
enableDragDelayHint: 'Hold to drag after a delay',
dragDelayTime: 'Delay Time (ms)',
dragDelayTimeHint: 'Delay time before dragging starts',
allowDragOnClick: 'Allow dragging and resizing of TaskBars and Milestones',
allowDragOnClickHint: 'Controls whether to allow dragging of TaskBars and Milestones, as well as resizing the length of TaskBars',
},
},
disableTaskbarFocusMode: 'Disable Focus Mode',
},
}
// 允许外部合并自定义多语言
export function setCustomMessages(locale: Locale, custom: Partial<(typeof messages)['zh-CN']>) {
if (!messages[locale]) return
Object.assign(messages[locale], custom)
// 使用深度合并,触发响应式更新
messages[locale] = {
...messages[locale],
...custom,
} as (typeof messages)['zh-CN']
// 触发语言切换事件,强制刷新所有使用翻译的组件
if (currentLocale.value === locale) {
window.dispatchEvent(
new CustomEvent('locale-changed', {
detail: { locale },
}),
)
}
}
// LocalStorage key
+1
View File
@@ -12,6 +12,7 @@ export { useMessage } from './composables/useMessage.ts' // 导出useMessage组
// 导出配置类型
export type { TaskListConfig, TaskListColumnConfig, TaskListColumnType } from './models/configs/TaskListConfig'
export type { TaskBarConfig } from './models/configs/TaskBarConfig'
export type { ToolbarConfig } from './models/configs/ToolbarConfig'
// 导出样式文件
+3
View File
@@ -4,6 +4,7 @@ export interface Task {
name: string
predecessor?: number[] // 前置任务ID数组
assignee?: string
avatar?: string // 任务负责人头像URL
startDate?: string
endDate?: string
progress?: number
@@ -23,6 +24,8 @@ export interface Task {
timerEndTime?: number // 结束计时时间
timerStartDesc?: string // 计时开始时填写的描述
timerElapsedTime?: number
// 权限控制
isEditable?: boolean // 是否可编辑(可拖拽、拉伸),默认为true
// 支持自定义属性 - 使用 unknown 允许任意类型
[key: string]: unknown
}
+22
View File
@@ -0,0 +1,22 @@
// TaskBar 配置类型定义
export interface TaskBarConfig {
showAvatar?: boolean // 是否展示头像,默认 true
showTitle?: boolean // 是否展示标题文字,默认 true
showProgress?: boolean // 是否展示进度文字,默认 true
dragThreshold?: number // 拖拽触发阈值(像素),默认 5px
resizeHandleWidth?: number // 拉伸手柄宽度(像素),默认 5px,最大 15px
enableDragDelay?: boolean // 是否启用拖拽延迟(防止误触),默认 false
dragDelayTime?: number // 拖拽延迟时间(毫秒),默认 150ms
}
// 默认配置
export const DEFAULT_TASK_BAR_CONFIG: TaskBarConfig = {
showAvatar: true,
showTitle: true,
showProgress: true,
dragThreshold: 5,
resizeHandleWidth: 5,
enableDragDelay: false,
dragDelayTime: 150,
}
+43 -3
View File
@@ -22,9 +22,9 @@ export interface TaskListColumnConfig {
export interface TaskListConfig {
columns?: TaskListColumnConfig[]
showAllColumns?: boolean // 是否显示所有列,默认true
defaultWidth?: number // 默认展开宽度,单位像素,默认320px
minWidth?: number // 最小宽度,单位像素,默认280px,不能小于280px
maxWidth?: number // 最大宽度,单位像素,默认1160px
defaultWidth?: number | string // 默认展开宽度,支持像素数字(如 320)或百分比字符串(如 '30%',默认320px
minWidth?: number | string // 最小宽度,支持像素数字(如 280)或百分比字符串(如 '20%',默认280px,不能小于280px
maxWidth?: number | string // 最大宽度,支持像素数字(如 1160)或百分比字符串(如 '80%',默认1160px
}
// 默认宽度配置
@@ -32,6 +32,46 @@ export const DEFAULT_TASK_LIST_WIDTH = 320 // 默认展开宽度
export const DEFAULT_TASK_LIST_MIN_WIDTH = 280 // 最小宽度
export const DEFAULT_TASK_LIST_MAX_WIDTH = 1160 // 最大宽度
/**
*
* @param value '30%'
* @param containerWidth
* @param defaultValue
* @returns
*/
export function parseWidthValue(
value: number | string | undefined,
containerWidth: number,
defaultValue: number,
): number {
if (value === undefined || value === null) {
return defaultValue
}
// 如果是数字,直接返回
if (typeof value === 'number') {
return value
}
// 如果是字符串,检查是否是百分比
if (typeof value === 'string') {
const trimmed = value.trim()
if (trimmed.endsWith('%')) {
const percentage = parseFloat(trimmed)
if (!isNaN(percentage)) {
return Math.round((containerWidth * percentage) / 100)
}
}
// 尝试解析为数字
const parsed = parseFloat(trimmed)
if (!isNaN(parsed)) {
return parsed
}
}
return defaultValue
}
// 默认列配置
export const DEFAULT_TASK_LIST_COLUMNS: TaskListColumnConfig[] = [
{
+7 -1
View File
@@ -49,9 +49,15 @@ export const SCALE_CONFIGS = {
headerLevels: 2,
formatters: { primary: 'yyyy年', secondary: 'MM月' },
},
quarter: {
scale: TimelineScale.QUARTER,
cellWidth: 60, // 每个季度的宽度
headerLevels: 2,
formatters: { primary: 'yyyy年', secondary: 'Q季度' },
},
year: {
scale: TimelineScale.YEAR,
cellWidth: 360,
cellWidth: 180, // 每半年的宽度
headerLevels: 2,
formatters: { primary: 'yyyy年', secondary: '上半年|下半年' },
},