v1.5.0 - 增加Gantt Task Table的Column Slot
This commit is contained in:
+110
-3
@@ -145,13 +145,36 @@ const toolbarConfig = {
|
|||||||
// TaskList列配置
|
// TaskList列配置
|
||||||
const availableColumns = ref<TaskListColumnConfig[]>([
|
const availableColumns = ref<TaskListColumnConfig[]>([
|
||||||
{ key: 'predecessor', label: '前置任务', visible: true },
|
{ key: 'predecessor', label: '前置任务', visible: true },
|
||||||
{ key: 'assignee', label: '负责人', visible: true, width: 250 },
|
{
|
||||||
{ key: 'startDate', label: '开始日期', visible: true },
|
key: 'assignee',
|
||||||
|
label: '负责人',
|
||||||
|
visible: true,
|
||||||
|
width: 250,
|
||||||
|
// 示例:使用 formatter 格式化显示(可选,如果不使用列级slot)
|
||||||
|
// formatter: (task) => task.assignee ? `👤 ${task.assignee}` : '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'startDate',
|
||||||
|
label: '开始日期',
|
||||||
|
visible: true,
|
||||||
|
// 示例:日期格式化
|
||||||
|
formatter: (task) => {
|
||||||
|
if (!task.startDate) return '-'
|
||||||
|
const date = new Date(task.startDate)
|
||||||
|
return `${date.getMonth() + 1}/${date.getDate()}`
|
||||||
|
}
|
||||||
|
},
|
||||||
{ key: 'endDate', label: '结束日期', visible: true },
|
{ key: 'endDate', label: '结束日期', visible: true },
|
||||||
{ key: 'estimatedHours', label: '预估工时', visible: true },
|
{ key: 'estimatedHours', label: '预估工时', visible: true },
|
||||||
{ key: 'actualHours', label: '实际工时', visible: true },
|
{ key: 'actualHours', label: '实际工时', visible: true },
|
||||||
{ key: 'progress', label: '进度', visible: true },
|
{ key: 'progress', label: '进度', visible: true },
|
||||||
{ key: 'custom', label: '自定义列', visible: true, width: '30%' }, // 添加默认宽度120px
|
{
|
||||||
|
key: 'custom',
|
||||||
|
label: '自定义列',
|
||||||
|
visible: true,
|
||||||
|
width: '30%',
|
||||||
|
// 自定义列将使用 #column-custom slot 渲染
|
||||||
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
// TaskList宽度配置
|
// TaskList宽度配置
|
||||||
@@ -1128,6 +1151,7 @@ const handleTaskRowMoved = async (payload: {
|
|||||||
@task-updated="handleTaskUpdateEvent"
|
@task-updated="handleTaskUpdateEvent"
|
||||||
@task-row-moved="handleTaskRowMoved"
|
@task-row-moved="handleTaskRowMoved"
|
||||||
>
|
>
|
||||||
|
<!-- 自定义任务名称内容 (TaskRow 和 TaskBar) -->
|
||||||
<template #custom-task-content="item">
|
<template #custom-task-content="item">
|
||||||
<HtmlContent
|
<HtmlContent
|
||||||
:item="taskDebug(item)"
|
:item="taskDebug(item)"
|
||||||
@@ -1136,6 +1160,89 @@ const handleTaskRowMoved = async (payload: {
|
|||||||
:style="item.dynamicStyles"
|
:style="item.dynamicStyles"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- 列级 Slot 示例:'name'列的渲染 -->
|
||||||
|
<template #column-name="{ task, column, value }">
|
||||||
|
<div style="display: flex; align-items: center; gap: 6px;">
|
||||||
|
<img src="https://i.pravatar.cc/50?img=1" width="20" height="20" style="border-radius: 50%;" />
|
||||||
|
<!-- 如果 value 包含 HTML,使用 v-html 渲染 -->
|
||||||
|
<span v-html="value"></span>
|
||||||
|
<!-- 示例标签 -->
|
||||||
|
<span
|
||||||
|
v-if="task.priority"
|
||||||
|
style="
|
||||||
|
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
P-{{ task.priority }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 列级 Slot 示例:自定义 'custom' 列的渲染 -->
|
||||||
|
<template #column-custom="{ task, column, value }">
|
||||||
|
<div style="display: flex; align-items: center; gap: 4px;">
|
||||||
|
<span
|
||||||
|
v-if="typeof value === 'number'"
|
||||||
|
style="
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
💰 {{ value.toLocaleString() }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="typeof value === 'string'"
|
||||||
|
style="
|
||||||
|
background: #e8f5e9;
|
||||||
|
color: #2e7d32;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 1px solid #81c784;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
📝 {{ value }}
|
||||||
|
</span>
|
||||||
|
<span v-else style="color: #999;">-</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 列级 Slot 示例:自定义 'assignee' 列的渲染(覆盖内置渲染) -->
|
||||||
|
<!-- 取消下面的注释可以看到效果 -->
|
||||||
|
<!--
|
||||||
|
<template #column-assignee="{ task }">
|
||||||
|
<div style="display: flex; align-items: center; gap: 6px;">
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ task.assignee ? task.assignee.charAt(0) : '?' }}
|
||||||
|
</div>
|
||||||
|
<span style="font-size: 13px;">{{ task.assignee || '未分配' }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
-->
|
||||||
</GanttChart>
|
</GanttChart>
|
||||||
</div>
|
</div>
|
||||||
<div class="license-info">
|
<div class="license-info">
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"parentId": 1000,
|
"parentId": 1000,
|
||||||
"description": "评估ADX-2024在健康志愿者和患者中的安全性、耐受性和药代动力学特征",
|
"description": "评估ADX-2024在健康志愿者和患者中的安全性、耐受性和药代动力学特征",
|
||||||
"custom": "test11111",
|
"custom": "test11111",
|
||||||
|
"priority": "高",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": 1101,
|
"id": 1101,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onUnmounted, onMounted, computed, watch, nextTick } from 'vue'
|
import { ref, onUnmounted, onMounted, computed, watch, nextTick, useSlots, provide } 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'
|
||||||
@@ -82,6 +82,15 @@ const emit = defineEmits([
|
|||||||
])
|
])
|
||||||
|
|
||||||
const { showMessage } = useMessage()
|
const { showMessage } = useMessage()
|
||||||
|
const slots = useSlots()
|
||||||
|
|
||||||
|
// 获取所有列级 slot 名称并通过 provide 传递给子组件
|
||||||
|
const columnSlotNames = computed(() => {
|
||||||
|
return Object.keys(slots).filter(name => name.startsWith('column-'))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 提供 slots 给子组件(TaskList 和 TaskRow)
|
||||||
|
provide('gantt-column-slots', slots)
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
// 任务数据
|
// 任务数据
|
||||||
@@ -2307,6 +2316,7 @@ function handleMilestoneDialogDelete(milestoneId: number) {
|
|||||||
@delete="handleTaskDelete"
|
@delete="handleTaskDelete"
|
||||||
@task-row-moved="handleTaskRowMoved"
|
@task-row-moved="handleTaskRowMoved"
|
||||||
>
|
>
|
||||||
|
<!-- 传递 custom-task-content slot -->
|
||||||
<template v-if="$slots['custom-task-content']" #custom-task-content="rowScope">
|
<template v-if="$slots['custom-task-content']" #custom-task-content="rowScope">
|
||||||
<slot name="custom-task-content" v-bind="rowScope" />
|
<slot name="custom-task-content" v-bind="rowScope" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, useSlots, computed, nextTick } from 'vue'
|
import { ref, onMounted, onUnmounted, useSlots, computed, nextTick, inject } 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,6 +7,7 @@ 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'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
tasks?: Task[]
|
tasks?: Task[]
|
||||||
@@ -42,6 +43,9 @@ const emit = defineEmits<{
|
|||||||
const slots = useSlots()
|
const slots = useSlots()
|
||||||
const hasRowSlot = computed(() => Boolean(slots['custom-task-content']))
|
const hasRowSlot = computed(() => Boolean(slots['custom-task-content']))
|
||||||
|
|
||||||
|
// 从 GanttChart 注入列级 slots
|
||||||
|
const columnSlots = inject<Slots>('gantt-column-slots', {})
|
||||||
|
|
||||||
// 多语言支持
|
// 多语言支持
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
@@ -634,6 +638,7 @@ onUnmounted(() => {
|
|||||||
@add-successor="handleAddSuccessor"
|
@add-successor="handleAddSuccessor"
|
||||||
@delete="handleTaskDelete"
|
@delete="handleTaskDelete"
|
||||||
>
|
>
|
||||||
|
<!-- 传递 custom-task-content slot -->
|
||||||
<template v-if="hasRowSlot" #custom-task-content="rowScope">
|
<template v-if="hasRowSlot" #custom-task-content="rowScope">
|
||||||
<slot name="custom-task-content" v-bind="rowScope" />
|
<slot name="custom-task-content" v-bind="rowScope" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+196
-72
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, computed, watch, useSlots } from 'vue'
|
import { ref, onMounted, onUnmounted, computed, watch, useSlots, inject, h } from 'vue'
|
||||||
import type { StyleValue } from 'vue'
|
import type { StyleValue, Slots } from 'vue'
|
||||||
import { useI18n } from '../composables/useI18n'
|
import { useI18n } from '../composables/useI18n'
|
||||||
import { formatPredecessorDisplay } from '../utils/predecessorUtils'
|
import { formatPredecessorDisplay } from '../utils/predecessorUtils'
|
||||||
import type { Task } from '../models/classes/Task'
|
import type { Task } from '../models/classes/Task'
|
||||||
@@ -66,6 +66,25 @@ const daysText = computed(() => t.value?.days ?? '')
|
|||||||
const slots = useSlots()
|
const slots = useSlots()
|
||||||
const hasContentSlot = computed(() => Boolean(slots['custom-task-content']))
|
const hasContentSlot = computed(() => Boolean(slots['custom-task-content']))
|
||||||
|
|
||||||
|
// 从 GanttChart 注入列级 slots
|
||||||
|
const columnSlots = inject<Slots>('gantt-column-slots', {})
|
||||||
|
|
||||||
|
// 检查指定列是否有自定义 slot(从注入的 slots 中查找)
|
||||||
|
const hasColumnSlot = (columnKey: string) => {
|
||||||
|
const slotName = `column-${columnKey}`
|
||||||
|
return Boolean(columnSlots[slotName])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染列级 slot
|
||||||
|
const renderColumnSlot = (columnKey: string, slotProps: any) => {
|
||||||
|
const slotName = `column-${columnKey}`
|
||||||
|
const slotFn = columnSlots[slotName]
|
||||||
|
if (slotFn) {
|
||||||
|
return slotFn(slotProps)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const baseIndent = 10
|
const baseIndent = 10
|
||||||
const indent = computed(() => `${baseIndent + props.level * 20}px`)
|
const indent = computed(() => `${baseIndent + props.level * 20}px`)
|
||||||
const hasChildren = computed(() => !!(props.task.children && props.task.children.length > 0))
|
const hasChildren = computed(() => !!(props.task.children && props.task.children.length > 0))
|
||||||
@@ -382,79 +401,169 @@ onUnmounted(() => {
|
|||||||
class="leaf-spacer"
|
class="leaf-spacer"
|
||||||
></span>
|
></span>
|
||||||
|
|
||||||
<!-- 任务图标 -->
|
<!-- 优先级1: 列级自定义 Slot (#column-name) - 覆盖整列内容(图标+文本+徽章) -->
|
||||||
<span v-if="showTaskIcon !== false" class="task-icon">
|
<component
|
||||||
<!-- 里程碑分组图标 - 使用菱形图标 -->
|
:is="() => renderColumnSlot('name', {
|
||||||
<svg
|
task: props.task,
|
||||||
v-if="isMilestoneGroup"
|
column: { key: 'name' },
|
||||||
width="16"
|
value: props.task.name,
|
||||||
height="16"
|
isParentTask,
|
||||||
viewBox="0 0 24 24"
|
hasChildren,
|
||||||
fill="none"
|
isStoryTask: isStoryTask.value,
|
||||||
stroke="currentColor"
|
isMilestoneGroup: isMilestoneGroup.value,
|
||||||
stroke-width="2"
|
isMilestoneTask: isMilestoneTask.value,
|
||||||
class="milestone-group-icon"
|
showTaskIcon: props.showTaskIcon,
|
||||||
>
|
formattedTimer: formattedTimer.value,
|
||||||
<polygon points="12,2 22,12 12,22 2,12" />
|
isOvertime: isOvertime(),
|
||||||
</svg>
|
overdueDays: overdueDays(),
|
||||||
<!-- 父级任务图标 -->
|
overtimeText: overtimeText.value,
|
||||||
<svg
|
overdueText: overdueText.value,
|
||||||
v-else-if="isStoryTask || hasChildren"
|
daysText: daysText.value,
|
||||||
width="16"
|
})"
|
||||||
height="16"
|
v-if="hasColumnSlot('name')"
|
||||||
viewBox="0 0 24 24"
|
/>
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
<!-- 优先级2: custom-task-content Slot (向后兼容) - 仅替换文本部分 -->
|
||||||
stroke-width="2"
|
<template v-else-if="hasContentSlot">
|
||||||
>
|
<!-- 任务图标 -->
|
||||||
<path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-5l-2-2H5a2 2 0 00-2 2z" />
|
<span v-if="showTaskIcon !== false" class="task-icon">
|
||||||
</svg>
|
<!-- 里程碑分组图标 - 使用菱形图标 -->
|
||||||
<!-- 普通任务图标 -->
|
<svg
|
||||||
<svg
|
v-if="isMilestoneGroup"
|
||||||
v-else
|
width="16"
|
||||||
width="16"
|
height="16"
|
||||||
height="16"
|
viewBox="0 0 24 24"
|
||||||
viewBox="0 0 24 24"
|
fill="none"
|
||||||
fill="none"
|
stroke="currentColor"
|
||||||
stroke="currentColor"
|
stroke-width="2"
|
||||||
stroke-width="2"
|
class="milestone-group-icon"
|
||||||
>
|
>
|
||||||
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
|
<polygon points="12,2 22,12 12,22 2,12" />
|
||||||
<polyline points="14,2 14,8 20,8" />
|
</svg>
|
||||||
<line x1="16" y1="13" x2="8" y2="13" />
|
<!-- 父级任务图标 -->
|
||||||
<line x1="16" y1="17" x2="8" y2="17" />
|
<svg
|
||||||
<polyline points="10,9 9,9 8,9" />
|
v-else-if="isStoryTask || hasChildren"
|
||||||
</svg>
|
width="16"
|
||||||
</span>
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-5l-2-2H5a2 2 0 00-2 2z" />
|
||||||
|
</svg>
|
||||||
|
<!-- 普通任务图标 -->
|
||||||
|
<svg
|
||||||
|
v-else
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
|
||||||
|
<polyline points="14,2 14,8 20,8" />
|
||||||
|
<line x1="16" y1="13" x2="8" y2="13" />
|
||||||
|
<line x1="16" y1="17" x2="8" y2="17" />
|
||||||
|
<polyline points="10,9 9,9 8,9" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
|
||||||
<span
|
|
||||||
class="task-name-text"
|
|
||||||
:class="{ 'parent-task': isParentTask }"
|
|
||||||
:title="props.task.name"
|
|
||||||
>
|
|
||||||
<slot
|
|
||||||
v-if="hasContentSlot"
|
|
||||||
name="custom-task-content"
|
|
||||||
v-bind="slotPayload"
|
|
||||||
type="task-row"
|
|
||||||
/>
|
|
||||||
<template v-else>
|
|
||||||
{{ props.task.name }}
|
|
||||||
</template>
|
|
||||||
<!-- 计时器显示 -->
|
|
||||||
<span
|
<span
|
||||||
v-if="props.task.isTimerRunning || props.task.timerElapsedTime"
|
class="task-name-text"
|
||||||
class="timer-badge"
|
:class="{ 'parent-task': isParentTask }"
|
||||||
:class="{ 'timer-active': props.task.isTimerRunning }"
|
:title="props.task.name"
|
||||||
>
|
>
|
||||||
<span v-if="props.task.isTimerRunning" class="timer-dot"></span>
|
<slot
|
||||||
{{ formattedTimer }}
|
name="custom-task-content"
|
||||||
|
v-bind="slotPayload"
|
||||||
|
type="task-row"
|
||||||
|
/>
|
||||||
|
<!-- 计时器显示 -->
|
||||||
|
<span
|
||||||
|
v-if="props.task.isTimerRunning || props.task.timerElapsedTime"
|
||||||
|
class="timer-badge"
|
||||||
|
:class="{ 'timer-active': props.task.isTimerRunning }"
|
||||||
|
>
|
||||||
|
<span v-if="props.task.isTimerRunning" class="timer-dot"></span>
|
||||||
|
{{ formattedTimer }}
|
||||||
|
</span>
|
||||||
|
<span v-if="isOvertime()" class="status-badge overtime">{{ overtimeText }}</span>
|
||||||
|
<span v-if="overdueDays() > 0" class="status-badge overdue">
|
||||||
|
{{ overdueText }}{{ overdueDays() > 0 ? overdueDays() + daysText : '' }}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="isOvertime()" class="status-badge overtime">{{ overtimeText }}</span>
|
</template>
|
||||||
<span v-if="overdueDays() > 0" class="status-badge overdue">
|
|
||||||
{{ overdueText }}{{ overdueDays() > 0 ? overdueDays() + daysText : '' }}
|
<!-- 优先级3: 默认渲染 -->
|
||||||
|
<template v-else>
|
||||||
|
<!-- 任务图标 -->
|
||||||
|
<span v-if="showTaskIcon !== false" class="task-icon">
|
||||||
|
<!-- 里程碑分组图标 - 使用菱形图标 -->
|
||||||
|
<svg
|
||||||
|
v-if="isMilestoneGroup"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
class="milestone-group-icon"
|
||||||
|
>
|
||||||
|
<polygon points="12,2 22,12 12,22 2,12" />
|
||||||
|
</svg>
|
||||||
|
<!-- 父级任务图标 -->
|
||||||
|
<svg
|
||||||
|
v-else-if="isStoryTask || hasChildren"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-5l-2-2H5a2 2 0 00-2 2z" />
|
||||||
|
</svg>
|
||||||
|
<!-- 普通任务图标 -->
|
||||||
|
<svg
|
||||||
|
v-else
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
|
||||||
|
<polyline points="14,2 14,8 20,8" />
|
||||||
|
<line x1="16" y1="13" x2="8" y2="13" />
|
||||||
|
<line x1="16" y1="17" x2="8" y2="17" />
|
||||||
|
<polyline points="10,9 9,9 8,9" />
|
||||||
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
|
||||||
|
<span
|
||||||
|
class="task-name-text"
|
||||||
|
:class="{ 'parent-task': isParentTask }"
|
||||||
|
:title="props.task.name"
|
||||||
|
>
|
||||||
|
{{ props.task.name }}
|
||||||
|
<!-- 计时器显示 -->
|
||||||
|
<span
|
||||||
|
v-if="props.task.isTimerRunning || props.task.timerElapsedTime"
|
||||||
|
class="timer-badge"
|
||||||
|
:class="{ 'timer-active': props.task.isTimerRunning }"
|
||||||
|
>
|
||||||
|
<span v-if="props.task.isTimerRunning" class="timer-dot"></span>
|
||||||
|
{{ formattedTimer }}
|
||||||
|
</span>
|
||||||
|
<span v-if="isOvertime()" class="status-badge overtime">{{ overtimeText }}</span>
|
||||||
|
<span v-if="overdueDays() > 0" class="status-badge overdue">
|
||||||
|
{{ overdueText }}{{ overdueDays() > 0 ? overdueDays() + daysText : '' }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 动态渲染列 -->
|
<!-- 动态渲染列 -->
|
||||||
@@ -471,8 +580,22 @@ onUnmounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
<!-- 普通任务显示具体内容 -->
|
<!-- 普通任务显示具体内容 -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
<!-- 优先级1: 列级自定义 Slot (约定: #column-{key}),通过 renderColumnSlot 动态渲染 -->
|
||||||
|
<component
|
||||||
|
:is="() =>
|
||||||
|
renderColumnSlot(column.key,
|
||||||
|
{ task: props.task, column, value: (props.task as any)[column.key] })"
|
||||||
|
v-if="hasColumnSlot(column.key)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 优先级2: Formatter 函数 -->
|
||||||
|
<template v-else-if="column.formatter">
|
||||||
|
{{ column.formatter(props.task, column) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 优先级3: 内置列类型渲染 -->
|
||||||
<!-- 前置任务列 -->
|
<!-- 前置任务列 -->
|
||||||
<template v-if="column.key === 'predecessor'">
|
<template v-else-if="column.key === 'predecessor'">
|
||||||
{{ formatPredecessorDisplay(props.task.predecessor) }}
|
{{ formatPredecessorDisplay(props.task.predecessor) }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -513,7 +636,7 @@ onUnmounted(() => {
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 自定义列 - 通过task对象的key动态获取值 -->
|
<!-- 优先级4: 默认渲染 - 自定义列通过task对象的key动态获取值 -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ (props.task as any)[column.key] || '-' }}
|
{{ (props.task as any)[column.key] || '-' }}
|
||||||
</template>
|
</template>
|
||||||
@@ -548,6 +671,7 @@ onUnmounted(() => {
|
|||||||
@add-successor="emit('add-successor', $event)"
|
@add-successor="emit('add-successor', $event)"
|
||||||
@delete="handleTaskDelete"
|
@delete="handleTaskDelete"
|
||||||
>
|
>
|
||||||
|
<!-- 传递 custom-task-content slot -->
|
||||||
<template v-if="hasContentSlot" #custom-task-content="slotProps">
|
<template v-if="hasContentSlot" #custom-task-content="slotProps">
|
||||||
<slot name="custom-task-content" v-bind="slotProps as TaskRowSlotProps" />
|
<slot name="custom-task-content" v-bind="slotProps as TaskRowSlotProps" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+6
-1
@@ -11,7 +11,12 @@ export type { Task } from './models/classes/Task.ts' // 导出Task类型
|
|||||||
export { useMessage } from './composables/useMessage.ts' // 导出useMessage组合式函数
|
export { useMessage } from './composables/useMessage.ts' // 导出useMessage组合式函数
|
||||||
|
|
||||||
// 导出配置类型
|
// 导出配置类型
|
||||||
export type { TaskListConfig, TaskListColumnConfig, TaskListColumnType } from './models/configs/TaskListConfig'
|
export type {
|
||||||
|
TaskListConfig,
|
||||||
|
TaskListColumnConfig,
|
||||||
|
TaskListColumnType,
|
||||||
|
ColumnFormatter
|
||||||
|
} from './models/configs/TaskListConfig'
|
||||||
export type { TaskBarConfig } from './models/configs/TaskBarConfig'
|
export type { TaskBarConfig } from './models/configs/TaskBarConfig'
|
||||||
export type { ToolbarConfig } from './models/configs/ToolbarConfig'
|
export type { ToolbarConfig } from './models/configs/ToolbarConfig'
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// TaskList 列配置类型定义
|
// TaskList 列配置类型定义
|
||||||
|
import type { Task } from '../classes/Task'
|
||||||
|
|
||||||
export type TaskListColumnType =
|
export type TaskListColumnType =
|
||||||
| 'name'
|
| 'name'
|
||||||
@@ -10,6 +11,14 @@ export type TaskListColumnType =
|
|||||||
| 'actualHours'
|
| 'actualHours'
|
||||||
| 'progress'
|
| 'progress'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列格式化函数类型
|
||||||
|
* @param task 当前任务对象
|
||||||
|
* @param column 当前列配置
|
||||||
|
* @returns 格式化后的字符串
|
||||||
|
*/
|
||||||
|
export type ColumnFormatter = (task: Task, column: TaskListColumnConfig) => string
|
||||||
|
|
||||||
export interface TaskListColumnConfig {
|
export interface TaskListColumnConfig {
|
||||||
type?: TaskListColumnType
|
type?: TaskListColumnType
|
||||||
key: string // 用于国际化的key,也可以作为识别符
|
key: string // 用于国际化的key,也可以作为识别符
|
||||||
@@ -17,6 +26,7 @@ export interface TaskListColumnConfig {
|
|||||||
cssClass?: string // CSS类名
|
cssClass?: string // CSS类名
|
||||||
width?: number | string // 列宽度,支持像素(120)或百分比('15%')
|
width?: number | string // 列宽度,支持像素(120)或百分比('15%')
|
||||||
visible?: boolean // 是否显示,默认true
|
visible?: boolean // 是否显示,默认true
|
||||||
|
formatter?: ColumnFormatter // 自定义格式化函数(优先级低于 slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskListConfig {
|
export interface TaskListConfig {
|
||||||
|
|||||||
Reference in New Issue
Block a user