v1.8.0 - Add actual Taskbar component

This commit is contained in:
LINING-PC\lining
2026-01-17 18:03:19 +08:00
parent 9fc10b2851
commit 90f0f5bc60
23 changed files with 1445 additions and 127 deletions
+29
View File
@@ -59,6 +59,8 @@ const props = withDefaults(defineProps<Props>(), {
enableTaskListContextMenu: true,
enableTaskBarContextMenu: true,
enableLinkAnchor: true,
showActualTaskbar: false,
enableTaskBarTooltip: true,
fullscreen: false,
expandAll: true,
locale: 'zh-CN',
@@ -201,6 +203,21 @@ interface Props {
// 是否启用 LinkAnchor 连接触点功能(默认为 true)
// 当设置为 false 时,TaskBar 上不显示前置/后置任务的连接触点
enableLinkAnchor?: boolean
// 是否启用 TaskBar 气泡提示框(默认为 true)
// 当设置为 false 时,TaskBar 上不显示悬停气泡提示框
enableTaskBarTooltip?: boolean
// 是否显示实际任务条(默认为 false)
// 当设置为 true 且任务存在 actualStartDate 时,会在计划任务条下方显示实际任务条
showActualTaskbar?: boolean
// 自定义任务状态背景色(优先级高于默认配色,低于Task.barColor
// 待处理任务背景色:任务未开始且未逾期时使用
pendingTaskBackgroundColor?: string
// 逾期任务背景色:当前日期晚于任务结束日期时使用
delayTaskBackgroundColor?: string
// 已完成任务背景色:任务进度达到100%时使用
completeTaskBackgroundColor?: string
// 进行中任务背景色:任务已开始但未完成且未逾期时使用
ongoingTaskBackgroundColor?: string
// 全屏状态控制(响应式)
fullscreen?: boolean
// 展开/收起所有任务(响应式)
@@ -1201,6 +1218,8 @@ const milestonesForTimeline = computed((): Milestone[] => {
startDate: task.startDate, // 此时已确保非空
endDate: task.endDate,
assignee: task.assignee,
assigneeName: task.assigneeName,
avatar: task.avatar,
type: task.type || 'milestone',
icon: task.icon,
description: task.description,
@@ -2808,6 +2827,12 @@ defineExpose({
:working-hours="props.workingHours"
:task-bar-config="props.taskBarConfig"
:allow-drag-and-resize="props.allowDragAndResize"
:show-actual-taskbar="props.showActualTaskbar"
:enable-task-bar-tooltip="props.enableTaskBarTooltip"
:pending-task-background-color="props.pendingTaskBackgroundColor"
:delay-task-background-color="props.delayTaskBackgroundColor"
:complete-task-background-color="props.completeTaskBackgroundColor"
:ongoing-task-background-color="props.ongoingTaskBackgroundColor"
:use-default-drawer="props.useDefaultDrawer"
:use-default-milestone-dialog="props.useDefaultMilestoneDialog"
:on-milestone-save="handleMilestoneSave"
@@ -2861,6 +2886,10 @@ defineExpose({
:task="taskDrawerTask"
:is-edit="taskDrawerEditMode"
:assignee-options="props.assigneeOptions"
:pending-task-background-color="props.pendingTaskBackgroundColor"
:delay-task-background-color="props.delayTaskBackgroundColor"
:complete-task-background-color="props.completeTaskBackgroundColor"
:ongoing-task-background-color="props.ongoingTaskBackgroundColor"
@submit="handleTaskDrawerSubmit"
@close="taskDrawerVisible = false"
@start-timer="handleStartTimer"
+1 -1
View File
@@ -509,7 +509,7 @@ defineExpose({
top: `${offsetTop}px`,
width: `${width}px`,
height: `${height}px`,
zIndex: highlightedTaskId !== null ? 1001 : 25,
zIndex: highlightedTaskId !== null ? 2000 : 500,
pointerEvents: 'none',
}"
/>
File diff suppressed because it is too large Load Diff
+148 -7
View File
@@ -21,6 +21,10 @@ interface Props {
isEdit?: boolean
onDelete?: (task: Task) => void
assigneeOptions?: AssigneeOption[]
pendingTaskBackgroundColor?: string
delayTaskBackgroundColor?: string
completeTaskBackgroundColor?: string
ongoingTaskBackgroundColor?: string
}
const props = withDefaults(defineProps<Props>(), {
@@ -35,6 +39,10 @@ const props = withDefaults(defineProps<Props>(), {
{ value: 'zhaoliu', label: '赵六' },
{ value: 'qianqi', label: '钱七' },
],
pendingTaskBackgroundColor: '#c0c4cc',
delayTaskBackgroundColor: '#f56c6c',
completeTaskBackgroundColor: '#67c23a',
ongoingTaskBackgroundColor: '#409eff',
})
const emit = defineEmits<{
@@ -146,6 +154,8 @@ const formData = reactive<Task>({
assignee: '',
startDate: '',
endDate: '',
actualStartDate: '',
actualEndDate: '',
predecessor: [],
estimatedHours: 0,
actualHours: 0,
@@ -334,6 +344,8 @@ const errors = reactive({
type: '',
startDate: '',
endDate: '',
actualStartDate: '',
actualEndDate: '',
})
// visible
@@ -397,6 +409,8 @@ const resetForm = () => {
assignee: '',
startDate: '',
endDate: '',
actualStartDate: '',
actualEndDate: '',
predecessor: [],
estimatedHours: 0,
actualHours: 0,
@@ -408,6 +422,9 @@ const resetForm = () => {
timerEndTime: undefined,
timerElapsedTime: 0,
children: undefined,
bgColor: undefined,
avatar: undefined,
barColor: undefined,
})
//
@@ -434,14 +451,19 @@ const validateForm = (): boolean => {
isValid = false
}
if (!formData.startDate) {
errors.startDate = t.value.startDateRequired
errors.startDate = t.value.plannedStartDateRequired
isValid = false
}
if (!formData.endDate) {
errors.endDate = t.value.endDateRequired
errors.endDate = t.value.plannedEndDateRequired
isValid = false
} else if (formData.startDate && new Date(formData.endDate) < new Date(formData.startDate)) {
errors.endDate = t.value.endDateInvalid
errors.endDate = t.value.plannedEndDateInvalid
isValid = false
}
//
if (formData.actualStartDate && formData.actualEndDate && new Date(formData.actualEndDate) < new Date(formData.actualStartDate)) {
errors.actualEndDate = t.value.actualEndDateInvalid
isValid = false
}
return isValid
@@ -654,6 +676,71 @@ const handleAssigneeChanged = (event: Event) => {
formData.assigneeName = selected.label
}
}
//
const taskStatus = computed(() => {
if (!props.task) return { text: '', color: '', bgColor: '', borderColor: '' }
const task = props.task
const progress = task.progress || 0
const now = new Date()
const endDate = task.endDate ? new Date(task.endDate) : null
const startDate = task.startDate ? new Date(task.startDate) : null
const actualStartDate = task.actualStartDate
// 0
if (!actualStartDate && progress === 0) {
const color = props.pendingTaskBackgroundColor
return {
text: t.value.statusPending,
color: color,
bgColor: color + '20', // 20%
borderColor: color,
}
}
//
if (progress >= 100) {
const color = props.completeTaskBackgroundColor
return {
text: t.value.statusCompleted,
color: color,
bgColor: color + '20',
borderColor: color,
}
}
//
if (endDate && now > endDate && progress < 100) {
const color = props.delayTaskBackgroundColor
return {
text: t.value.statusDelayed,
color: color,
bgColor: color + '20',
borderColor: color,
}
}
//
if (startDate && now >= startDate && progress < 100) {
const color = props.ongoingTaskBackgroundColor
return {
text: t.value.statusOngoing,
color: color,
bgColor: color + '20',
borderColor: color,
}
}
//
const color = props.pendingTaskBackgroundColor
return {
text: t.value.statusPending,
color: color,
bgColor: color + '20',
borderColor: color,
}
})
</script>
<template>
@@ -763,6 +850,18 @@ const handleAssigneeChanged = (event: Event) => {
</span>
</div>
<div style="flex: 1"></div>
<!-- Status Badge -->
<div
v-if="isEdit && props.task"
class="status-badge"
:style="{
backgroundColor: taskStatus.bgColor,
color: taskStatus.color,
border: `1px solid ${taskStatus.borderColor}`,
}"
>
{{ taskStatus.text }}
</div>
<button class="drawer-close-btn" type="button" @click="handleClose">
<svg
class="close-icon"
@@ -844,14 +943,14 @@ const handleAssigneeChanged = (event: Event) => {
<div class="form-row">
<div class="form-group">
<label class="form-label" for="task-start-date">
{{ t.startDate }} <span class="required">*</span></label
{{ t.plannedStartDate }} <span class="required">*</span></label
>
<DatePicker
id="task-start-date"
v-model="formData.startDate"
:type="'datetime' as any"
value-format="YYYY-MM-DD HH:mm"
:placeholder="t.startDateRequired"
:placeholder="t.plannedStartDateRequired"
:class="{ error: errors.startDate }"
/>
<span v-if="errors.startDate" class="error-text">{{ errors.startDate }}</span>
@@ -859,20 +958,52 @@ const handleAssigneeChanged = (event: Event) => {
<div class="form-group">
<label class="form-label" for="task-end-date">
{{ t.endDate }} <span class="required">*</span></label
{{ t.plannedEndDate }} <span class="required">*</span></label
>
<DatePicker
id="task-end-date"
v-model="formData.endDate"
:type="'datetime' as any"
value-format="YYYY-MM-DD HH:mm"
:placeholder="t.endDateRequired"
:placeholder="t.plannedEndDateRequired"
:class="{ error: errors.endDate }"
/>
<span v-if="errors.endDate" class="error-text">{{ errors.endDate }}</span>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label" for="task-actual-start-date">
{{ t.actualStartDate }}</label
>
<DatePicker
id="task-actual-start-date"
v-model="formData.actualStartDate"
:type="'datetime' as any"
value-format="YYYY-MM-DD HH:mm"
:placeholder="t.actualStartDate"
:class="{ error: errors.actualStartDate }"
/>
<span v-if="errors.actualStartDate" class="error-text">{{ errors.actualStartDate }}</span>
</div>
<div class="form-group">
<label class="form-label" for="task-actual-end-date">
{{ t.actualEndDate }}</label
>
<DatePicker
id="task-actual-end-date"
v-model="formData.actualEndDate"
:type="'datetime' as any"
value-format="YYYY-MM-DD HH:mm"
:placeholder="t.actualEndDate"
:class="{ error: errors.actualEndDate }"
/>
<span v-if="errors.actualEndDate" class="error-text">{{ errors.actualEndDate }}</span>
</div>
</div>
<MultiSelectPredecessor
v-model="formData.predecessor"
:tasks="allTasks"
@@ -1068,6 +1199,15 @@ const handleAssigneeChanged = (event: Event) => {
color: var(--gantt-text-primary, #303133);
}
.status-badge {
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
margin-left: 12px;
}
.drawer-close-btn {
background: none;
border: none;
@@ -1075,6 +1215,7 @@ const handleAssigneeChanged = (event: Event) => {
padding: 4px;
color: var(--gantt-text-muted, #909399);
transition: color 0.2s;
margin-left: 12px;
}
.drawer-close-btn:hover {
+112 -4
View File
@@ -226,6 +226,59 @@ const slotPayload = computed(() => ({
daysText: daysText.value,
progressClass: progressClass.value,
}))
// - barColor
const leftBorderColor = computed(() => {
// barColor使
if (props.task.barColor) {
return props.task.barColor
}
// null使CSS
return null
})
//
const customBorderStyle = computed(() => {
if (leftBorderColor.value) {
return {
borderLeftColor: leftBorderColor.value
}
}
return {}
})
// assignee
const assigneeDisplayData = computed(() => {
const task = props.task
const avatar = task.avatar
const assignee = task.assignee
// avatar
let avatarList: string[] = []
if (avatar) {
avatarList = Array.isArray(avatar) ? avatar : [avatar]
}
// assignee
let assigneeList: string[] = []
if (assignee) {
assigneeList = Array.isArray(assignee) ? assignee : [assignee]
}
// avatarassignee
const displayAvatars = avatarList.length > 0
? avatarList.map(url => ({ type: 'image', url }))
: assigneeList.map(name => ({ type: 'text', name }))
//
const nameText = assigneeList.join('\n') || '-'
return {
avatars: displayAvatars,
nameText,
hasMultiple: displayAvatars.length > 1
}
})
</script>
<template>
@@ -243,7 +296,7 @@ const slotPayload = computed(() => ({
'task-type-milestone': isMilestoneTask,
[customRowClass]: customRowClass,
}"
:style="customRowStyle"
:style="[customRowStyle, customBorderStyle]"
@click="handleRowClick"
@dblclick="handleTaskRowDoubleClick"
@mousedown="handleMouseDown"
@@ -373,10 +426,25 @@ const slotPayload = computed(() => ({
<!-- 负责人列 -->
<template v-else-if="column.key === 'assignee'">
<div class="assignee-info">
<div class="avatar">
{{ props.task.assignee ? props.task.assignee.charAt(0) : '-' }}
<!-- 多头像容器 -->
<div class="assignee-avatars-container">
<div
v-for="(avatarItem, idx) in assigneeDisplayData.avatars"
:key="idx"
class="avatar"
:style="{
zIndex: idx + 1,
marginLeft: idx > 0 ? '-8px' : '0'
}"
>
<!-- 图片头像 -->
<img v-if="avatarItem.type === 'image'" :src="(avatarItem as any).url" :alt="`avatar-${idx}`" />
<!-- 文字头像 -->
<span v-else class="avatar-text">{{ (avatarItem as any).name.charAt(0).toUpperCase() }}</span>
</div>
</div>
<span class="assignee-name">{{ props.task.assignee || '-' }}</span>
<!-- 名称显示支持换行和超出显示... -->
<span class="assignee-name" :title="assigneeDisplayData.nameText">{{ assigneeDisplayData.nameText }}</span>
</div>
</template>
@@ -614,9 +682,29 @@ const slotPayload = computed(() => ({
gap: 8px;
}
/* 多头像容器 */
.assignee-avatars-container {
display: flex;
align-items: center;
position: relative;
}
.assignee-avatars-container .avatar {
position: relative;
transition: transform 0.2s ease;
cursor: pointer;
}
.assignee-avatars-container .avatar:hover {
transform: translateY(-2px) scale(1.1);
z-index: 999 !important;
}
.avatar {
min-width: 25px;
min-height: 25px;
width: 25px;
height: 25px;
border-radius: 50%;
background: var(--gantt-primary);
color: var(--gantt-text-white);
@@ -627,11 +715,31 @@ const slotPayload = computed(() => ({
font-weight: 500;
border: 2px solid var(--gantt-border-medium);
box-sizing: border-box;
flex-shrink: 0;
}
.avatar img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.avatar .avatar-text {
font-size: 11px;
font-weight: 600;
}
.assignee-name {
font-size: 14px;
color: var(--gantt-text-secondary);
white-space: pre-line;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.4;
}
.progress-value {
+21 -5
View File
@@ -37,6 +37,15 @@ interface Props {
taskBarConfig?: TaskBarConfig
// true
allowDragAndResize?: boolean
// false
showActualTaskbar?: boolean
// TaskBar true
enableTaskBarTooltip?: boolean
//
pendingTaskBackgroundColor?: string
delayTaskBackgroundColor?: string
completeTaskBackgroundColor?: string
ongoingTaskBackgroundColor?: string
}
const props = withDefaults(defineProps<Props>(), {
@@ -55,6 +64,12 @@ const props = withDefaults(defineProps<Props>(), {
}),
taskBarConfig: undefined,
allowDragAndResize: true,
showActualTaskbar: false,
enableTaskBarTooltip: true,
pendingTaskBackgroundColor: undefined,
delayTaskBackgroundColor: undefined,
completeTaskBackgroundColor: undefined,
ongoingTaskBackgroundColor: undefined,
})
// emits
@@ -1049,11 +1064,6 @@ const hasCircularDependency = (taskId: number, targetId: number): boolean => {
//
const createLink = (sourceTask: Task, targetTask: Task, mode: 'predecessor' | 'successor') => {
console.log('----> hello:', {
mode,
sourceTask,
targetTask,
})
if (mode === 'predecessor') {
// mode='predecessor' anchor
// anchor sourceTask targetTask
@@ -4478,6 +4488,12 @@ const handleAddSuccessor = (task: Task) => {
:current-time-scale="currentTimeScale"
:task-bar-config="props.taskBarConfig"
:allow-drag-and-resize="props.allowDragAndResize && !isInHighlightMode"
:show-actual-taskbar="props.showActualTaskbar"
:enable-task-bar-tooltip="props.enableTaskBarTooltip"
:pending-task-background-color="props.pendingTaskBackgroundColor"
:delay-task-background-color="props.delayTaskBackgroundColor"
:complete-task-background-color="props.completeTaskBackgroundColor"
:ongoing-task-background-color="props.ongoingTaskBackgroundColor"
:is-highlighted="highlightedTaskIds.has(task.id)"
:is-primary-highlight="highlightedTaskId === task.id"
:is-in-highlight-mode="isInHighlightMode"