i#ICZHVS 增加task内容支持自定义渲染

This commit is contained in:
qiuchengw
2025-09-24 16:52:49 +08:00
parent f77bd31198
commit 86aab1be0f
10 changed files with 190 additions and 47 deletions
+6 -1
View File
@@ -8,6 +8,7 @@ import packageInfo from '../package.json'
// 导入主题变量 // 导入主题变量
import '../src/styles/theme-variables.css' import '../src/styles/theme-variables.css'
import VersionHistoryDrawer from './VersionHistoryDrawer.vue' import VersionHistoryDrawer from './VersionHistoryDrawer.vue'
import HtmlContent from './HtmlContent.vue'
import { useMessage } from '../src/composables/useMessage' import { useMessage } from '../src/composables/useMessage'
import { useI18n } from '../src/composables/useI18n' import { useI18n } from '../src/composables/useI18n'
import { getPredecessorIds, predecessorIdsToString } from '../src/utils/predecessorUtils' import { getPredecessorIds, predecessorIdsToString } from '../src/utils/predecessorUtils'
@@ -666,7 +667,11 @@ function onTimerStopped(task: Task) {
@task-deleted="e => showMessage(`Demo 任务[${e.task.name}] 已删除`, 'info')" @task-deleted="e => showMessage(`Demo 任务[${e.task.name}] 已删除`, 'info')"
@task-added="e => showMessage(`Demo 任务[${e.task.name}] 已创建`, 'info')" @task-added="e => showMessage(`Demo 任务[${e.task.name}] 已创建`, 'info')"
@task-updated="e => showMessage(`Demo 任务[${e.task.name}] 已更新`, 'info')" @task-updated="e => showMessage(`Demo 任务[${e.task.name}] 已更新`, 'info')"
/> >
<template #custom-task-content="{ task, type }">
<HtmlContent :task="task" :type="type" />
</template>
</GanttChart>
</div> </div>
<div class="license-info"> <div class="license-info">
<p>MIT License @JORDIUM.COM</p> <p>MIT License @JORDIUM.COM</p>
+51
View File
@@ -0,0 +1,51 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { Task } from '../src/models/Task'
interface Props {
task: Task,
type: 'task-row' | 'task-bar'
}
const props = withDefaults(defineProps<Props>(), {
color: '#409eff',
progress: 0,
})
console.error("props", props)
function handleButtonClick() {
alert('hello')
}
</script>
<template>
<div class="html-content-card" :style="{ borderColor: color }">
<div v-if="type==='task-row'" class="task-row" v-html="task.name" />
<div v-else-if="type==='task-bar'" class="task-bar" v-html="task.name" />
</div>
</template>
<style scoped>
.html-content-card {
display: inline-block;
}
.task-row {
flex: 1;
padding: 0 4px;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-bar {
flex: 1;
padding: 0 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.btn {
height: 20px;
margin-left: 20px;
}
</style>
+2 -2
View File
@@ -27,7 +27,7 @@
"children": [ "children": [
{ {
"id": 1101, "id": 1101,
"name": "试验方案设计与伦理审查", "name": "试验方案设计与<span style='font-weight: bold;color:red;'>伦理审查</span>",
"assignee": "方案设计师 李明", "assignee": "方案设计师 李明",
"startDate": "2025-01-01", "startDate": "2025-01-01",
"endDate": "2025-02-28", "endDate": "2025-02-28",
@@ -84,7 +84,7 @@
"children": [ "children": [
{ {
"id": 1201, "id": 1201,
"name": "多中心试验启动", "name": "多中心试验<span style='font-weight: bold; color: blue;'>启动</span>",
"assignee": "项目经理 王芳", "assignee": "项目经理 王芳",
"startDate": "2025-09-01", "startDate": "2025-09-01",
"endDate": "2025-11-30", "endDate": "2025-11-30",
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "jordium-gantt-vue3", "name": "jordium-gantt-vue3",
"version": "1.1.0", "version": "1.3.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "jordium-gantt-vue3", "name": "jordium-gantt-vue3",
"version": "1.1.0", "version": "1.3.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
+10 -2
View File
@@ -1536,7 +1536,11 @@ function handleTaskDelete(task: Task, deleteChildren?: boolean) {
@add-predecessor="handleAddPredecessor" @add-predecessor="handleAddPredecessor"
@add-successor="handleAddSuccessor" @add-successor="handleAddSuccessor"
@delete="handleTaskDelete" @delete="handleTaskDelete"
/> >
<template v-if="$slots['custom-task-content']" #custom-task-content="rowScope">
<slot name="custom-task-content" v-bind="rowScope" />
</template>
</TaskList>
</div> </div>
<div class="gantt-splitter" @mousedown="onMouseDown"> <div class="gantt-splitter" @mousedown="onMouseDown">
<!-- TaskList切换按钮 - 贴合splitter右侧 --> <!-- TaskList切换按钮 - 贴合splitter右侧 -->
@@ -1584,7 +1588,11 @@ function handleTaskDelete(task: Task, deleteChildren?: boolean) {
@add-predecessor="handleAddPredecessor" @add-predecessor="handleAddPredecessor"
@add-successor="handleAddSuccessor" @add-successor="handleAddSuccessor"
@delete="handleTaskDelete" @delete="handleTaskDelete"
/> >
<template v-if="$slots['custom-task-content']" #custom-task-content="barScope">
<slot name="custom-task-content" v-bind="barScope" />
</template>
</Timeline>
</div> </div>
</div> </div>
+37 -3
View File
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onUnmounted, onMounted, nextTick, watch } from 'vue' import { ref, computed, onUnmounted, onMounted, nextTick, watch, useSlots } from 'vue'
import type { Task } from '../models/classes/Task' import type { Task } from '../models/classes/Task'
import { TimelineScale } from '../models/types/TimelineScale' import { TimelineScale } from '../models/types/TimelineScale'
import TaskContextMenu from './TaskContextMenu.vue' import TaskContextMenu from './TaskContextMenu.vue'
@@ -41,11 +41,15 @@ const emit = defineEmits([
'delete', 'delete',
'contextmenu', // 添加原生contextmenu事件声明 'contextmenu', // 添加原生contextmenu事件声明
]) ])
const slots = useSlots()
const { getTranslation } = useI18n() const { getTranslation } = useI18n()
const t = (key: string): string => { const t = (key: string): string => {
return getTranslation(key) return getTranslation(key)
} }
const hasContentSlot = computed(() => Boolean(slots.content))
// 日期工具函数 - 处理时区安全的日期创建和操作 // 日期工具函数 - 处理时区安全的日期创建和操作
const createLocalDate = (dateString: string | Date | undefined | null): Date | null => { const createLocalDate = (dateString: string | Date | undefined | null): Date | null => {
if (!dateString) return null if (!dateString) return null
@@ -1313,7 +1317,24 @@ onUnmounted(() => {
@dblclick="handleTaskBarDoubleClick" @dblclick="handleTaskBarDoubleClick"
> >
<!-- 父级任务的标签 --> <!-- 父级任务的标签 -->
<div v-if="isParent" class="parent-label">{{ task.name }} ({{ task.progress || 0 }}%)</div> <div v-if="isParent" class="parent-label">
<slot
v-if="hasContentSlot"
name="content"
type="task-bar"
:task="task"
:status="taskStatus"
:status-type="taskStatus.type"
:is-parent="isParent"
:progress="task.progress || 0"
:current-time-scale="currentTimeScale"
:row-height="rowHeight"
:day-width="dayWidth"
/>
<block v-else>
{{ task.name }} ({{ task.progress || 0 }}%)
</block>
</div>
<!-- 完成进度条非父级任务 --> <!-- 完成进度条非父级任务 -->
<div <div
@@ -1340,7 +1361,20 @@ onUnmounted(() => {
@mousedown="e => (isInteractionDisabled ? null : handleMouseDown(e, 'drag'))" @mousedown="e => (isInteractionDisabled ? null : handleMouseDown(e, 'drag'))"
> >
<!-- 任务名称 --> <!-- 任务名称 -->
<div class="task-name" :style="getNameStyles()"> <slot
v-if="hasContentSlot"
name="content"
type="task-bar"
:task="task"
:status="taskStatus"
:status-type="taskStatus.type"
:is-parent="isParent"
:progress="task.progress || 0"
:current-time-scale="currentTimeScale"
:row-height="rowHeight"
:day-width="dayWidth"
/>
<div v-else class="task-name" :style="getNameStyles()">
{{ task.name }} {{ task.name }}
</div> </div>
+10 -3
View File
@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue' import { ref, onMounted, onUnmounted, watch, useSlots, computed } from 'vue'
import type { Component } 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,7 @@ import type { Task } from '../models/classes/Task'
interface Props { interface Props {
tasks?: Task[] tasks?: Task[]
onTaskDoubleClick?: (task: Task) => void onTaskDoubleClick?: (task: Task) => void
editComponent?: any editComponent?: Component
useDefaultDrawer?: boolean useDefaultDrawer?: boolean
} }
@@ -26,6 +27,8 @@ const emit = defineEmits<{
'add-successor': [task: Task] // 新增:添加后置任务事件 'add-successor': [task: Task] // 新增:添加后置任务事件
delete: [task: Task, deleteChildren?: boolean] delete: [task: Task, deleteChildren?: boolean]
}>() }>()
const slots = useSlots()
const hasRowSlot = computed(() => Boolean(slots['custom-task-content']))
// 多语言支持 // 多语言支持
const { t } = useI18n() const { t } = useI18n()
@@ -409,7 +412,11 @@ onUnmounted(() => {
@add-predecessor="handleAddPredecessor" @add-predecessor="handleAddPredecessor"
@add-successor="handleAddSuccessor" @add-successor="handleAddSuccessor"
@delete="handleTaskDelete" @delete="handleTaskDelete"
/> >
<template v-if="hasRowSlot" #custom-task-content="rowScope">
<slot name="custom-task-content" v-bind="rowScope" />
</template>
</TaskRow>
</div> </div>
</div> </div>
</template> </template>
+62 -31
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted, computed, watch } from 'vue' import { ref, onMounted, onUnmounted, computed, watch, useSlots } 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'
@@ -29,8 +29,18 @@ const overtimeText = computed(() => t.value?.overtime ?? '')
const overdueText = computed(() => t.value?.overdue ?? '') const overdueText = computed(() => t.value?.overdue ?? '')
const daysText = computed(() => t.value?.days ?? '') const daysText = computed(() => t.value?.days ?? '')
const slots = useSlots()
const hasContentSlot = computed(() => Boolean(slots['custom-task-content']))
const baseIndent = 10 const baseIndent = 10
const indent = `${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 isStoryTask = computed(() => props.task.type === 'story')
const isMilestoneGroup = computed(() => props.task.type === 'milestone-group')
const isMilestoneTask = computed(() => props.task.type === 'milestone')
const isParentTask = computed(
() => isStoryTask.value || hasChildren.value || isMilestoneGroup.value,
)
function handleToggle() { function handleToggle() {
emit('toggle', props.task) emit('toggle', props.task)
} }
@@ -38,8 +48,8 @@ function handleToggle() {
function handleRowClick() { function handleRowClick() {
// 如果是普通父级任务(story类型或有子任务的任务,非里程碑分组),点击行也可以展开/收起 // 如果是普通父级任务(story类型或有子任务的任务,非里程碑分组),点击行也可以展开/收起
if ( if (
(props.task.type === 'story' || (props.task.children && props.task.children.length > 0)) && (isStoryTask.value || hasChildren.value) &&
props.task.type !== 'milestone-group' !isMilestoneGroup.value
) { ) {
emit('toggle', props.task) emit('toggle', props.task)
} }
@@ -220,6 +230,29 @@ const handleTaskDelete = (task: Task, deleteChildren?: boolean) => {
closeContextMenu() closeContextMenu()
} }
const progressClass = computed(() => getProgressClass())
const slotPayload = computed(() => ({
isRowContent: true,
task: props.task,
level: props.level,
indent: indent.value,
isHovered: props.isHovered ?? false,
hoveredTaskId: props.hoveredTaskId ?? null,
isParent: isParentTask.value,
hasChildren: hasChildren.value,
collapsed: !!props.task.collapsed,
formattedTimer: formattedTimer.value,
timerRunning: !!props.task.isTimerRunning,
timerElapsed: timerElapsed.value,
isOvertime: isOvertime(),
overdueDays: overdueDays(),
overtimeText: overtimeText.value,
overdueText: overdueText.value,
daysText: daysText.value,
progressClass: progressClass.value,
}))
// 生命周期钩子 - 注册事件监听器 // 生命周期钩子 - 注册事件监听器
onMounted(() => { onMounted(() => {
window.addEventListener('splitter-drag-start', handleSplitterDragStart) window.addEventListener('splitter-drag-start', handleSplitterDragStart)
@@ -243,14 +276,11 @@ onUnmounted(() => {
class="task-row" class="task-row"
:class="{ :class="{
'task-row-hovered': isHovered, 'task-row-hovered': isHovered,
'parent-task': 'parent-task': isParentTask,
props.task.type === 'story' || 'milestone-group-row': isMilestoneGroup,
(props.task.children && props.task.children.length > 0) || 'task-type-story': isStoryTask,
props.task.type === 'milestone-group',
'milestone-group-row': props.task.type === 'milestone-group',
'task-type-story': props.task.type === 'story',
'task-type-task': props.task.type === 'task', 'task-type-task': props.task.type === 'task',
'task-type-milestone': props.task.type === 'milestone', 'task-type-milestone': isMilestoneTask
}" }"
@click="handleRowClick" @click="handleRowClick"
@dblclick="handleTaskRowDoubleClick" @dblclick="handleTaskRowDoubleClick"
@@ -260,11 +290,7 @@ onUnmounted(() => {
> >
<div class="col col-name" :style="{ paddingLeft: indent }"> <div class="col col-name" :style="{ paddingLeft: indent }">
<span <span
v-if=" v-if="(isStoryTask || hasChildren) && !isMilestoneGroup"
(props.task.type === 'story' ||
(props.task.children && props.task.children.length > 0)) &&
props.task.type !== 'milestone-group'
"
class="collapse-btn" class="collapse-btn"
@click.stop="handleToggle" @click.stop="handleToggle"
> >
@@ -282,13 +308,13 @@ onUnmounted(() => {
</span> </span>
<!-- 里程碑分组的占位空间用于与有折叠按钮的任务对齐 --> <!-- 里程碑分组的占位空间用于与有折叠按钮的任务对齐 -->
<span v-if="props.task.type === 'milestone-group'" class="milestone-spacer"></span> <span v-if="isMilestoneGroup" class="milestone-spacer"></span>
<!-- 任务图标 --> <!-- 任务图标 -->
<span class="task-icon"> <span class="task-icon">
<!-- 里程碑分组图标 - 使用菱形图标 --> <!-- 里程碑分组图标 - 使用菱形图标 -->
<svg <svg
v-if="props.task.type === 'milestone-group'" v-if="isMilestoneGroup"
width="16" width="16"
height="16" height="16"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -301,9 +327,7 @@ onUnmounted(() => {
</svg> </svg>
<!-- 父级任务图标 --> <!-- 父级任务图标 -->
<svg <svg
v-else-if=" v-else-if="isStoryTask || hasChildren"
props.task.type === 'story' || (props.task.children && props.task.children.length > 0)
"
width="16" width="16"
height="16" height="16"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -333,15 +357,18 @@ onUnmounted(() => {
<span <span
class="task-name-text" class="task-name-text"
:class="{ :class="{ 'parent-task': isParentTask }"
'parent-task':
props.task.type === 'story' ||
(props.task.children && props.task.children.length > 0) ||
props.task.type === 'milestone-group',
}"
:title="props.task.name" :title="props.task.name"
> >
<slot
v-if="hasContentSlot"
name="custom-task-content"
v-bind="slotPayload"
type="task-row"
/>
<template v-else>
{{ props.task.name }} {{ props.task.name }}
</template>
<!-- 计时器显示 --> <!-- 计时器显示 -->
<span <span
v-if="props.task.isTimerRunning || props.task.timerElapsedTime" v-if="props.task.isTimerRunning || props.task.timerElapsedTime"
@@ -359,7 +386,7 @@ onUnmounted(() => {
</div> </div>
<!-- 里程碑分组不显示详细信息 --> <!-- 里程碑分组不显示详细信息 -->
<template v-if="props.task.type === 'milestone-group'"> <template v-if="isMilestoneGroup">
<div class="col col-pre milestone-empty-col"></div> <div class="col col-pre milestone-empty-col"></div>
<div class="col col-assignee milestone-empty-col"></div> <div class="col col-assignee milestone-empty-col"></div>
<div class="col col-date milestone-empty-col"></div> <div class="col col-date milestone-empty-col"></div>
@@ -385,14 +412,14 @@ onUnmounted(() => {
<div class="col col-hours">{{ props.task.estimatedHours || '-' }}</div> <div class="col col-hours">{{ props.task.estimatedHours || '-' }}</div>
<div class="col col-hours">{{ props.task.actualHours || '-' }}</div> <div class="col col-hours">{{ props.task.actualHours || '-' }}</div>
<div class="col col-progress"> <div class="col col-progress">
<span class="progress-value" :class="getProgressClass()"> <span class="progress-value" :class="progressClass">
{{ props.task.progress != null ? props.task.progress + '%' : '-' }} {{ props.task.progress != null ? props.task.progress + '%' : '-' }}
</span> </span>
</div> </div>
</template> </template>
</div> </div>
<template <template
v-if="props.task.children && !props.task.collapsed && props.task.type !== 'milestone-group'" v-if="hasChildren && !props.task.collapsed && !isMilestoneGroup"
> >
<TaskRow <TaskRow
v-for="child in props.task.children" v-for="child in props.task.children"
@@ -410,7 +437,11 @@ onUnmounted(() => {
@add-predecessor="emit('add-predecessor', $event)" @add-predecessor="emit('add-predecessor', $event)"
@add-successor="emit('add-successor', $event)" @add-successor="emit('add-successor', $event)"
@delete="handleTaskDelete" @delete="handleTaskDelete"
/> >
<template v-if="hasContentSlot" #custom-task-content="childScope">
<slot name="custom-task-content" v-bind="childScope" />
</template>
</TaskRow>
</template> </template>
<TaskContextMenu <TaskContextMenu
+6 -2
View File
@@ -1617,7 +1617,7 @@ const handleMouseDown = (event: MouseEvent) => {
'input', 'input',
'select', 'select',
'textarea', 'textarea',
'.task-bar-content', '.custom-task-content',
'.progress-bar', '.progress-bar',
'.task-name', '.task-name',
'.task-controls', '.task-controls',
@@ -2754,7 +2754,11 @@ const handleAddSuccessor = (task: Task) => {
@add-predecessor="handleAddPredecessor" @add-predecessor="handleAddPredecessor"
@add-successor="handleAddSuccessor" @add-successor="handleAddSuccessor"
@delete="handleTaskDelete" @delete="handleTaskDelete"
/> >
<template v-if="$slots['custom-task-content']" #content="barScope">
<slot name="custom-task-content" v-bind="barScope" />
</template>
</TaskBar>
</div> </div>
</div> </div>
</div> </div>
+3
View File
@@ -19,4 +19,7 @@ export default defineConfig(({ command }) => ({
'@': '../src', '@': '../src',
}, },
}, },
server: {
port: 13000,
}
})) }))