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"
+34
View File
@@ -13,6 +13,10 @@ const messages = {
assignee: '分配人',
startDate: '开始日期',
endDate: '结束日期',
plannedStartDate: '预计开始日期',
plannedEndDate: '预计结束日期',
actualStartDate: '实际开始日期',
actualEndDate: '实际结束日期',
estimatedHours: '预计工时(hr)',
actualHours: '实际工时(hr)',
progress: '进度',
@@ -26,6 +30,10 @@ const messages = {
assignee: '分配人',
startDate: '开始日期',
endDate: '结束日期',
plannedStartDate: '预计开始日期',
plannedEndDate: '预计结束日期',
actualStartDate: '实际开始日期',
actualEndDate: '实际结束日期',
estimatedHours: '预计工时(hr)',
actualHours: '实际工时(hr)',
progress: '进度(%)',
@@ -119,6 +127,15 @@ const messages = {
startDateRequired: '开始日期不能为空',
endDateRequired: '结束日期不能为空',
endDateInvalid: '结束日期不能早于开始日期',
plannedStartDateRequired: '预计开始日期不能为空',
plannedEndDateRequired: '预计结束日期不能为空',
plannedEndDateInvalid: '预计结束日期不能早于预计开始日期',
actualEndDateInvalid: '实际结束日期不能早于实际开始日期',
// 任务状态
statusPending: '待处理',
statusOngoing: '进行中',
statusDelayed: '已逾期',
statusCompleted: '已完成',
// 新建里程碑对话框
newMilestone: '新建里程碑',
editMilestone: '编辑里程碑',
@@ -279,6 +296,10 @@ const messages = {
assignee: 'Assignee',
startDate: 'Start Date',
endDate: 'End Date',
plannedStartDate: 'Planned Start Date',
plannedEndDate: 'Planned End Date',
actualStartDate: 'Actual Start Date',
actualEndDate: 'Actual End Date',
estimatedHours: 'Est. Hours',
actualHours: 'Act. Hours',
progress: 'Progress',
@@ -291,6 +312,10 @@ const messages = {
assignee: 'Assignee',
startDate: 'Start Date',
endDate: 'End Date',
plannedStartDate: 'Planned Start Date',
plannedEndDate: 'Planned End Date',
actualStartDate: 'Actual Start Date',
actualEndDate: 'Actual End Date',
estimatedHours: 'Est. Hours',
actualHours: 'Act. Hours',
progress: 'Progress (%)',
@@ -382,6 +407,15 @@ const messages = {
startDateRequired: 'Start date is required',
endDateRequired: 'End date is required',
endDateInvalid: 'End date cannot be earlier than start date',
plannedStartDateRequired: 'Planned start date is required',
plannedEndDateRequired: 'Planned end date is required',
plannedEndDateInvalid: 'Planned end date cannot be earlier than planned start date',
actualEndDateInvalid: 'Actual end date cannot be earlier than actual start date',
// Task status
statusPending: 'Pending',
statusOngoing: 'Ongoing',
statusDelayed: 'Delayed',
statusCompleted: 'Completed',
// 新建里程碑对话框
newMilestone: 'New Milestone',
editMilestone: 'Edit Milestone',
+3 -1
View File
@@ -4,7 +4,9 @@ export interface Milestone {
name: string
startDate: string
endDate?: string
assignee?: string
assignee?: string | string[] // 支持单个或多个负责人
assigneeName?: string | string[] // 支持单个或多个负责人姓名
avatar?: string | string[] // 支持单个或多个头像
type: string
icon?: string
description?: string
+6 -2
View File
@@ -3,11 +3,13 @@ export interface Task {
id: number
name: string
predecessor?: number[] // 前置任务ID数组
assignee?: string // 记录唯一键值,如用户ID或用户名
assignee?: string | string[] // 记录唯一键值,如用户ID或用户名,支持单个或多个
assigneeName?: string // 任务负责人名称
avatar?: string // 任务负责人头像URL
avatar?: string | string[] // 任务负责人头像URL,支持单个或多个头像数组
startDate?: string
endDate?: string
actualStartDate?: string // 实际开始日期
actualEndDate?: string // 实际结束日期
progress?: number
estimatedHours?: number
actualHours?: number
@@ -27,6 +29,8 @@ export interface Task {
timerElapsedTime?: number
// 权限控制
isEditable?: boolean // 是否可编辑(可拖拽、拉伸),默认为true
// 自定义样式
barColor?: string // 自定义TaskBar颜色,如 '#ff5733',若不设置则使用默认颜色方案
// 支持自定义属性 - 使用 unknown 允许任意类型
[key: string]: unknown
}