v1.6.0-preview2 - 增加ask-list-row-class-name和task-list-row-style属性
This commit is contained in:
+117
@@ -357,7 +357,63 @@ const handleAddMilestone = () => {
|
|||||||
showMilestoneDialog.value = true
|
showMilestoneDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据任务进度返回行样式类名
|
||||||
|
// task-list-row-class-name 使用示例:
|
||||||
|
// 1. 父级节点(包含子任务) → task-row-parent (保留默认样式)
|
||||||
|
// 2. progress = 100 → task-row-success (淡绿色,已完成)
|
||||||
|
// 3. 0 < progress < 100 → task-row-warning (淡黄色,进行中)
|
||||||
|
// 4. progress <= 0 → task-row-info (灰色,未开始)
|
||||||
|
const getTaskRowClassName = (row: Task, rowIndex: number) => {
|
||||||
|
// 如果是父级节点(包含子任务),使用默认父级样式
|
||||||
|
if (row.children && row.children.length > 0) {
|
||||||
|
return 'parent-task'
|
||||||
|
}
|
||||||
|
|
||||||
|
const progress = row.progress || 0
|
||||||
|
|
||||||
|
// 进度 = 100:已完成,淡绿色
|
||||||
|
if (progress === 100) {
|
||||||
|
return 'task-row-success'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进度 > 0 且 < 100:进行中,淡黄色
|
||||||
|
if (progress > 0 && progress < 100) {
|
||||||
|
return 'task-row-warning'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进度 <= 0:未开始,灰色
|
||||||
|
return 'task-row-info'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据任务进度返回行内联样式
|
||||||
|
// task-list-row-style 使用示例(优先级高于 task-list-row-class-name):
|
||||||
|
// 可以返回对象形式的样式,支持更灵活的样式控制
|
||||||
|
const getTaskRowStyle = (row: Task, rowIndex: number) => {
|
||||||
|
// 父级节点保持默认样式
|
||||||
|
if (row.children && row.children.length > 0) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const progress = row.progress || 0
|
||||||
|
|
||||||
|
// 这里可以返回内联样式对象
|
||||||
|
// 示例:根据进度值设置不同的边框颜色
|
||||||
|
if (progress === 100) {
|
||||||
|
return {
|
||||||
|
borderLeft: '3px solid #67c23a',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress > 0 && progress < 100) {
|
||||||
|
return {
|
||||||
|
borderLeft: '3px solid #e6a23c',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
borderLeft: '3px solid #909399',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleThemeChange = (isDark: boolean) => {
|
const handleThemeChange = (isDark: boolean) => {
|
||||||
const themeText = isDark ? t.value.darkModeText : t.value.lightModeText
|
const themeText = isDark ? t.value.darkModeText : t.value.lightModeText
|
||||||
@@ -1123,6 +1179,8 @@ const handleTaskRowMoved = async (payload: {
|
|||||||
:allow-drag-and-resize="allowDragAndResize"
|
:allow-drag-and-resize="allowDragAndResize"
|
||||||
:enable-task-row-move="enableTaskRowMove"
|
:enable-task-row-move="enableTaskRowMove"
|
||||||
:assignee-options="assigneeOptions"
|
:assignee-options="assigneeOptions"
|
||||||
|
:task-list-row-class-name="getTaskRowClassName"
|
||||||
|
:task-list-row-style="getTaskRowStyle"
|
||||||
:on-export-csv="handleCustomCsvExport"
|
:on-export-csv="handleCustomCsvExport"
|
||||||
:on-language-change="handleLanguageChange"
|
:on-language-change="handleLanguageChange"
|
||||||
:on-theme-change="handleThemeChange"
|
:on-theme-change="handleThemeChange"
|
||||||
@@ -2483,4 +2541,63 @@ const handleTaskRowMoved = async (payload: {
|
|||||||
:global(html[data-theme='dark']) .property-value {
|
:global(html[data-theme='dark']) .property-value {
|
||||||
color: var(--gantt-text-primary, #e0e0e0);
|
color: var(--gantt-text-primary, #e0e0e0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 任务行根据进度值的自定义样式 */
|
||||||
|
/* 父级节点 (task-row-parent) 保留默认样式,不做特殊处理 */
|
||||||
|
|
||||||
|
/* 已完成 (progress = 100) - 淡绿色,参考 Element Plus success */
|
||||||
|
:deep(.task-row-success) {
|
||||||
|
background-color: #f0f9ff !important;
|
||||||
|
background: linear-gradient(90deg, #f0f9ff 0%, #e8f5e9 100%) !important;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.task-row-success:hover) {
|
||||||
|
background: linear-gradient(90deg, #e1f5fe 0%, #c8e6c9 100%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 进行中 (0 < progress < 100) - 淡黄色,参考 Element Plus warning */
|
||||||
|
:deep(.task-row-warning) {
|
||||||
|
background-color: #fdf6ec !important;
|
||||||
|
background: linear-gradient(90deg, #fdf6ec 0%, #fff3e0 100%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.task-row-warning:hover) {
|
||||||
|
background: linear-gradient(90deg, #faecd8 0%, #ffe0b2 100%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 未开始 (progress <= 0) - 灰色,参考 Element Plus info */
|
||||||
|
:deep(.task-row-info) {
|
||||||
|
background-color: #f4f4f5 !important;
|
||||||
|
background: linear-gradient(90deg, #f4f4f5 0%, #f5f5f5 100%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.task-row-info:hover) {
|
||||||
|
background: linear-gradient(90deg, #e9e9eb 0%, #eeeeee 100%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗黑模式下的任务行样式 */
|
||||||
|
:global(html[data-theme='dark']) :deep(.task-row-success) {
|
||||||
|
background: linear-gradient(90deg, #0a3a2a 0%, #1b4d3e 100%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) :deep(.task-row-success:hover) {
|
||||||
|
background: linear-gradient(90deg, #0f4d35 0%, #276749 100%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) :deep(.task-row-warning) {
|
||||||
|
background: linear-gradient(90deg, #3d2f1f 0%, #4d3b2a 100%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) :deep(.task-row-warning:hover) {
|
||||||
|
background: linear-gradient(90deg, #4d3b26 0%, #5d4a35 100%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) :deep(.task-row-info) {
|
||||||
|
background: linear-gradient(90deg, #2a2a2a 0%, #333333 100%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) :deep(.task-row-info:hover) {
|
||||||
|
background: linear-gradient(90deg, #353535 0%, #3d3d3d 100%) !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onUnmounted, onMounted, computed, watch, nextTick, useSlots, provide } from 'vue'
|
import { ref, onUnmounted, onMounted, computed, watch, nextTick, useSlots, provide } from 'vue'
|
||||||
|
import type { StyleValue } from 'vue'
|
||||||
import TaskList from './TaskList.vue'
|
import TaskList from './TaskList.vue'
|
||||||
import Timeline from './Timeline.vue'
|
import Timeline from './Timeline.vue'
|
||||||
import GanttToolbar from './GanttToolbar.vue'
|
import GanttToolbar from './GanttToolbar.vue'
|
||||||
@@ -50,6 +51,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
allowDragAndResize: true,
|
allowDragAndResize: true,
|
||||||
enableTaskRowMove: false,
|
enableTaskRowMove: false,
|
||||||
assigneeOptions: () => [],
|
assigneeOptions: () => [],
|
||||||
|
taskListRowClassName: undefined,
|
||||||
|
taskListRowStyle: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
@@ -145,6 +148,12 @@ interface Props {
|
|||||||
// 格式:{ key?: string | number, value: string | number, label: string }
|
// 格式:{ key?: string | number, value: string | number, label: string }
|
||||||
// key 为可选项,若不存在则使用 value 作为选项的唯一标识
|
// key 为可选项,若不存在则使用 value 作为选项的唯一标识
|
||||||
assigneeOptions?: Array<{ key?: string | number; value: string | number; label: string }>
|
assigneeOptions?: Array<{ key?: string | number; value: string | number; label: string }>
|
||||||
|
// 任务行自定义样式类名,支持字符串或函数
|
||||||
|
// 函数形式:(row: Task, rowIndex: number) => string
|
||||||
|
taskListRowClassName?: string | ((row: Task, rowIndex: number) => string)
|
||||||
|
// 任务行自定义样式,支持对象或函数,优先级高于 taskListRowClassName
|
||||||
|
// 函数形式:(row: Task, rowIndex: number) => StyleValue
|
||||||
|
taskListRowStyle?: StyleValue | ((row: Task, rowIndex: number) => StyleValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TaskList的固定总长度(所有列的最小宽度之和 + 边框等额外空间)
|
// TaskList的固定总长度(所有列的最小宽度之和 + 边框等额外空间)
|
||||||
@@ -2307,6 +2316,8 @@ function handleMilestoneDialogDelete(milestoneId: number) {
|
|||||||
:task-list-config="props.taskListConfig"
|
:task-list-config="props.taskListConfig"
|
||||||
:task-list-column-render-mode="props.taskListColumnRenderMode"
|
:task-list-column-render-mode="props.taskListColumnRenderMode"
|
||||||
:enable-task-row-move="props.enableTaskRowMove"
|
:enable-task-row-move="props.enableTaskRowMove"
|
||||||
|
:task-list-row-class-name="props.taskListRowClassName"
|
||||||
|
:task-list-row-style="props.taskListRowStyle"
|
||||||
@task-collapse-change="handleTaskCollapseChange"
|
@task-collapse-change="handleTaskCollapseChange"
|
||||||
@start-timer="handleStartTimer"
|
@start-timer="handleStartTimer"
|
||||||
@stop-timer="handleStopTimer"
|
@stop-timer="handleStopTimer"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, useSlots, computed, nextTick, inject } from 'vue'
|
import { ref, onMounted, onUnmounted, useSlots, computed, nextTick, inject } from 'vue'
|
||||||
|
import type { StyleValue, Slots } from 'vue'
|
||||||
import TaskRow from './TaskRow.vue'
|
import TaskRow from './TaskRow.vue'
|
||||||
import { useI18n } from '../composables/useI18n'
|
import { useI18n } from '../composables/useI18n'
|
||||||
import type { Task } from '../models/classes/Task'
|
import type { Task } from '../models/classes/Task'
|
||||||
@@ -7,7 +8,6 @@ import type { TaskListConfig } from '../models/configs/TaskListConfig'
|
|||||||
import { DEFAULT_TASK_LIST_COLUMNS } from '../models/configs/TaskListConfig'
|
import { DEFAULT_TASK_LIST_COLUMNS } from '../models/configs/TaskListConfig'
|
||||||
import { useTaskRowDrag } from '../composables/useTaskRowDrag'
|
import { useTaskRowDrag } from '../composables/useTaskRowDrag'
|
||||||
import { moveTask } from '../utils/taskTreeUtils'
|
import { moveTask } from '../utils/taskTreeUtils'
|
||||||
import type { Slots } from 'vue'
|
|
||||||
import { useTaskListColumns } from '../composables/useTaskListColumns'
|
import { useTaskListColumns } from '../composables/useTaskListColumns'
|
||||||
import type { DeclarativeColumnConfig } from '../composables/useTaskListColumns'
|
import type { DeclarativeColumnConfig } from '../composables/useTaskListColumns'
|
||||||
|
|
||||||
@@ -17,6 +17,8 @@ interface Props {
|
|||||||
taskListConfig?: TaskListConfig
|
taskListConfig?: TaskListConfig
|
||||||
taskListColumnRenderMode?: 'default' | 'declarative'
|
taskListColumnRenderMode?: 'default' | 'declarative'
|
||||||
enableTaskRowMove?: boolean
|
enableTaskRowMove?: boolean
|
||||||
|
taskListRowClassName?: string | ((row: Task, rowIndex: number) => string)
|
||||||
|
taskListRowStyle?: StyleValue | ((row: Task, rowIndex: number) => StyleValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -330,7 +332,12 @@ const visibleTaskRange = computed(() => {
|
|||||||
// 虚拟列表中需要渲染的任务
|
// 虚拟列表中需要渲染的任务
|
||||||
const visibleTasks = computed(() => {
|
const visibleTasks = computed(() => {
|
||||||
const { startIndex, endIndex } = visibleTaskRange.value
|
const { startIndex, endIndex } = visibleTaskRange.value
|
||||||
return flattenedTasks.value.slice(startIndex, endIndex)
|
const slicedTasks = flattenedTasks.value.slice(startIndex, endIndex)
|
||||||
|
// 添加 rowIndex(基于在扁平化列表中的实际索引)
|
||||||
|
return slicedTasks.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
rowIndex: startIndex + index,
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
// Spacer 高度用于撑起滚动区域
|
// Spacer 高度用于撑起滚动区域
|
||||||
@@ -677,10 +684,11 @@ onUnmounted(() => {
|
|||||||
<div class="task-list-body-spacer" :style="{ height: `${startSpacerHeight}px` }"></div>
|
<div class="task-list-body-spacer" :style="{ height: `${startSpacerHeight}px` }"></div>
|
||||||
|
|
||||||
<TaskRow
|
<TaskRow
|
||||||
v-for="{ task, level } in visibleTasks"
|
v-for="{ task, level, rowIndex } in visibleTasks"
|
||||||
:key="task.id"
|
:key="task.id"
|
||||||
:task="task"
|
:task="task"
|
||||||
:level="level"
|
:level="level"
|
||||||
|
:row-index="rowIndex"
|
||||||
:is-hovered="hoveredTaskId === task.id"
|
:is-hovered="hoveredTaskId === task.id"
|
||||||
:hovered-task-id="hoveredTaskId"
|
:hovered-task-id="hoveredTaskId"
|
||||||
:on-hover="handleTaskRowHover"
|
:on-hover="handleTaskRowHover"
|
||||||
@@ -693,6 +701,8 @@ onUnmounted(() => {
|
|||||||
:enable-drag="props.enableTaskRowMove"
|
:enable-drag="props.enableTaskRowMove"
|
||||||
:drag-start="startDrag"
|
:drag-start="startDrag"
|
||||||
:drag-over="handleDragOver"
|
:drag-over="handleDragOver"
|
||||||
|
:task-list-row-class-name="props.taskListRowClassName"
|
||||||
|
:task-list-row-style="props.taskListRowStyle"
|
||||||
@toggle="toggleCollapse"
|
@toggle="toggleCollapse"
|
||||||
@dblclick="handleTaskRowDoubleClick"
|
@dblclick="handleTaskRowDoubleClick"
|
||||||
@contextmenu="handleTaskRowContextMenu"
|
@contextmenu="handleTaskRowContextMenu"
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ interface TaskRowSlotProps {
|
|||||||
interface Props {
|
interface Props {
|
||||||
task: Task
|
task: Task
|
||||||
level: number
|
level: number
|
||||||
|
rowIndex?: number
|
||||||
isHovered?: boolean
|
isHovered?: boolean
|
||||||
hoveredTaskId?: number | null
|
hoveredTaskId?: number | null
|
||||||
onHover?: (taskId: number | null) => void
|
onHover?: (taskId: number | null) => void
|
||||||
@@ -45,6 +46,8 @@ interface Props {
|
|||||||
enableDrag?: boolean
|
enableDrag?: boolean
|
||||||
dragStart?: (task: Task, element: HTMLElement, event: MouseEvent) => void
|
dragStart?: (task: Task, element: HTMLElement, event: MouseEvent) => void
|
||||||
dragOver?: (task: Task, element: HTMLElement, event: MouseEvent) => void
|
dragOver?: (task: Task, element: HTMLElement, event: MouseEvent) => void
|
||||||
|
taskListRowClassName?: string | ((row: Task, rowIndex: number) => string)
|
||||||
|
taskListRowStyle?: StyleValue | ((row: Task, rowIndex: number) => StyleValue)
|
||||||
}
|
}
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
renderMode: 'default',
|
renderMode: 'default',
|
||||||
@@ -235,6 +238,29 @@ const isMilestoneTask = computed(() => props.task.type === 'milestone')
|
|||||||
const isParentTask = computed(
|
const isParentTask = computed(
|
||||||
() => isStoryTask.value || hasChildren.value || isMilestoneGroup.value,
|
() => isStoryTask.value || hasChildren.value || isMilestoneGroup.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 计算自定义行类名
|
||||||
|
const customRowClass = computed(() => {
|
||||||
|
if (!props.taskListRowClassName) return ''
|
||||||
|
|
||||||
|
if (typeof props.taskListRowClassName === 'function') {
|
||||||
|
return props.taskListRowClassName(props.task, props.rowIndex ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.taskListRowClassName
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算自定义行样式(优先级高于类名)
|
||||||
|
const customRowStyle = computed(() => {
|
||||||
|
if (!props.taskListRowStyle) return {}
|
||||||
|
|
||||||
|
if (typeof props.taskListRowStyle === 'function') {
|
||||||
|
return props.taskListRowStyle(props.task, props.rowIndex ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.taskListRowStyle
|
||||||
|
})
|
||||||
|
|
||||||
function handleToggle() {
|
function handleToggle() {
|
||||||
emit('toggle', props.task)
|
emit('toggle', props.task)
|
||||||
}
|
}
|
||||||
@@ -506,7 +532,9 @@ onUnmounted(() => {
|
|||||||
'task-type-story': isStoryTask,
|
'task-type-story': isStoryTask,
|
||||||
'task-type-task': props.task.type === 'task',
|
'task-type-task': props.task.type === 'task',
|
||||||
'task-type-milestone': isMilestoneTask,
|
'task-type-milestone': isMilestoneTask,
|
||||||
|
[customRowClass]: customRowClass,
|
||||||
}"
|
}"
|
||||||
|
:style="customRowStyle"
|
||||||
@click="handleRowClick"
|
@click="handleRowClick"
|
||||||
@dblclick="handleTaskRowDoubleClick"
|
@dblclick="handleTaskRowDoubleClick"
|
||||||
@mousedown="handleMouseDown"
|
@mousedown="handleMouseDown"
|
||||||
|
|||||||
Reference in New Issue
Block a user