v 1.6.0.preview1 - TaskList Slots
This commit is contained in:
76
demo/App.vue
76
demo/App.vue
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||
import GanttChart from '../src/components/GanttChart.vue'
|
||||
// GanttChart 和 TaskListColumn 已经通过 app.use(JordiumGantt) 全局注册,无需导入
|
||||
// import TaskDrawer from '../src/components/TaskDrawer.vue' // 移除
|
||||
import MilestoneDialog from '../src/components/MilestoneDialog.vue'
|
||||
import normalData from './data.json'
|
||||
@@ -1126,6 +1126,7 @@ const handleTaskRowMoved = async (payload: {
|
||||
:on-export-csv="handleCustomCsvExport"
|
||||
:on-language-change="handleLanguageChange"
|
||||
:on-theme-change="handleThemeChange"
|
||||
task-list-column-render-mode="declarative"
|
||||
@milestone-saved="handleMilestoneSaved"
|
||||
@milestone-deleted="handleMilestoneDeleted"
|
||||
@milestone-icon-changed="handleMilestoneIconChanged"
|
||||
@@ -1162,12 +1163,16 @@ const handleTaskRowMoved = async (payload: {
|
||||
</template>
|
||||
|
||||
<!-- 列级 Slot 示例:'name'列的渲染 -->
|
||||
<template #header-name>
|
||||
<div style="display: flex; align-items: center; gap: 6px;">
|
||||
<img src="https://foruda.gitee.com/avatar/1764902889653058860/565633_nelson820125_1764902889.png!avatar200" width="32" height="32" style="border-radius: 50%;" />
|
||||
<strong style="font-size: 14px;">{{ t.taskName }}</strong>
|
||||
</div>
|
||||
</template>
|
||||
<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="
|
||||
@@ -1243,6 +1248,41 @@ const handleTaskRowMoved = async (payload: {
|
||||
</div>
|
||||
</template>
|
||||
-->
|
||||
|
||||
<!-- 使用 TaskListColumn 组件自定义列 声明式模式 -->
|
||||
<TaskListColumn prop="name" :label="t.taskName" width="300" align="center">
|
||||
<template #header>
|
||||
<img src="https://foruda.gitee.com/avatar/1764902889653058860/565633_nelson820125_1764902889.png!avatar200" width="32" height="32" style="border-radius: 50%;" />
|
||||
<strong style="font-size: 14px;">{{ t.taskName }}</strong>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center; gap: 6px;">
|
||||
<!-- 里程碑分组图标 - 使用菱形图标 -->
|
||||
<svg
|
||||
v-if="scope.row.type === 'milestone-group'"
|
||||
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>
|
||||
<img
|
||||
v-if="scope.row.avatar && scope.row.type === 'task'"
|
||||
:src="scope.row.avatar"
|
||||
width="20"
|
||||
height="20"
|
||||
style="border-radius: 50%;"
|
||||
/>
|
||||
<span v-html="scope.row.name"></span>
|
||||
</div>
|
||||
</template>
|
||||
</TaskListColumn>
|
||||
<TaskListColumn prop="startDate" :label="t.startDate" width="200" align="center" />
|
||||
<TaskListColumn prop="endDate" :label="t.endDate" width="200" align="center" />
|
||||
</GanttChart>
|
||||
</div>
|
||||
<div class="license-info">
|
||||
@@ -2221,6 +2261,22 @@ const handleTaskRowMoved = async (payload: {
|
||||
brightness(0.7);
|
||||
}
|
||||
|
||||
/* 里程碑分组图标样式 - 统一使用红色并添加发光效果 */
|
||||
.milestone-group-icon {
|
||||
color: var(--gantt-danger, #f56c6c);
|
||||
fill: var(--gantt-danger, #f56c6c);
|
||||
opacity: 0.9;
|
||||
filter: drop-shadow(0 0 6px var(--gantt-danger, #f56c6c));
|
||||
animation: milestone-icon-glow 2.5s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
/* 里程碑图标悬停效果 */
|
||||
.task-row:hover .milestone-group-icon {
|
||||
filter: drop-shadow(0 0 10px var(--gantt-danger, #f56c6c))
|
||||
drop-shadow(0 0 16px rgba(245, 108, 108, 0.4));
|
||||
animation: milestone-icon-glow-intense 1.8s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
/* 移除旧的基于 SVG color 的样式,现在使用 filter */
|
||||
|
||||
/* 暗黑模式下覆盖所有链接样式 */
|
||||
@@ -2262,6 +2318,20 @@ const handleTaskRowMoved = async (payload: {
|
||||
brightness(1.2);
|
||||
}
|
||||
|
||||
/* 暗黑模式下的里程碑图标发光效果 */
|
||||
:global(html[data-theme='dark']) .milestone-group-icon {
|
||||
color: var(--gantt-danger, #f67c7c);
|
||||
fill: var(--gantt-danger, #f67c7c);
|
||||
filter: drop-shadow(0 0 6px var(--gantt-danger, #f67c7c));
|
||||
animation: milestone-icon-glow-dark 2.5s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
:global(html[data-theme='dark']) .task-row:hover .milestone-group-icon {
|
||||
filter: drop-shadow(0 0 10px var(--gantt-danger, #f67c7c))
|
||||
drop-shadow(0 0 16px rgba(246, 124, 124, 0.4));
|
||||
animation: milestone-icon-glow-intense-dark 1.8s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
/* Task Click Dialog 样式 */
|
||||
.task-click-dialog-overlay {
|
||||
position: fixed;
|
||||
|
||||
347
demo/DeclarativeColumnsDemo.vue
Normal file
347
demo/DeclarativeColumnsDemo.vue
Normal file
@@ -0,0 +1,347 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { GanttChart } from '../src/index'
|
||||
import type { Task } from '../src/models/classes/Task'
|
||||
|
||||
// 示例任务数据
|
||||
const tasks = ref<Task[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: '项目规划',
|
||||
type: 'story',
|
||||
startDate: '2024-01-01',
|
||||
endDate: '2024-01-15',
|
||||
progress: 100,
|
||||
assignee: '张三',
|
||||
collapsed: false,
|
||||
children: [
|
||||
{
|
||||
id: 11,
|
||||
name: '需求分析',
|
||||
type: 'task',
|
||||
startDate: '2024-01-01',
|
||||
endDate: '2024-01-05',
|
||||
progress: 100,
|
||||
assignee: '李四',
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: '技术方案设计',
|
||||
type: 'task',
|
||||
startDate: '2024-01-06',
|
||||
endDate: '2024-01-15',
|
||||
progress: 100,
|
||||
assignee: '王五',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '开发阶段',
|
||||
type: 'story',
|
||||
startDate: '2024-01-16',
|
||||
endDate: '2024-02-28',
|
||||
progress: 65,
|
||||
assignee: '张三',
|
||||
collapsed: false,
|
||||
children: [
|
||||
{
|
||||
id: 21,
|
||||
name: '前端开发',
|
||||
type: 'task',
|
||||
startDate: '2024-01-16',
|
||||
endDate: '2024-02-15',
|
||||
progress: 80,
|
||||
assignee: '赵六',
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
name: '后端开发',
|
||||
type: 'task',
|
||||
startDate: '2024-01-16',
|
||||
endDate: '2024-02-20',
|
||||
progress: 70,
|
||||
assignee: '孙七',
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
name: '数据库设计',
|
||||
type: 'task',
|
||||
startDate: '2024-01-20',
|
||||
endDate: '2024-02-10',
|
||||
progress: 50,
|
||||
assignee: '周八',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '测试上线',
|
||||
type: 'story',
|
||||
startDate: '2024-03-01',
|
||||
endDate: '2024-03-15',
|
||||
progress: 30,
|
||||
assignee: '张三',
|
||||
collapsed: false,
|
||||
children: [
|
||||
{
|
||||
id: 31,
|
||||
name: '功能测试',
|
||||
type: 'task',
|
||||
startDate: '2024-03-01',
|
||||
endDate: '2024-03-08',
|
||||
progress: 40,
|
||||
assignee: '吴九',
|
||||
},
|
||||
{
|
||||
id: 32,
|
||||
name: '性能优化',
|
||||
type: 'task',
|
||||
startDate: '2024-03-05',
|
||||
endDate: '2024-03-12',
|
||||
progress: 20,
|
||||
assignee: '郑十',
|
||||
},
|
||||
{
|
||||
id: 33,
|
||||
name: '生产部署',
|
||||
type: 'task',
|
||||
startDate: '2024-03-13',
|
||||
endDate: '2024-03-15',
|
||||
progress: 0,
|
||||
assignee: '钱十一',
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
const milestones = ref<Task[]>([])
|
||||
|
||||
// Helper function for progress bar background
|
||||
const getProgressColor = (progress: number) => {
|
||||
if (progress >= 100) return '#67c23a'
|
||||
if (progress >= 50) return '#409eff'
|
||||
return '#e6a23c'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="declarative-demo">
|
||||
<h1>声明式列定义演示 (Declarative Columns Demo)</h1>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2>示例 1: 基础声明式列</h2>
|
||||
<GanttChart
|
||||
:tasks="tasks"
|
||||
:milestones="milestones"
|
||||
task-list-column-render-mode="declarative"
|
||||
:show-toolbar="false"
|
||||
style="height: 400px"
|
||||
>
|
||||
<TaskListColumn prop="assignee" label="负责人" width="120" align="center" />
|
||||
<TaskListColumn prop="name" label="任务名称" width="300" />
|
||||
|
||||
<TaskListColumn prop="startDate" label="开始日期" width="140" />
|
||||
<TaskListColumn prop="endDate" label="结束日期" width="140" />
|
||||
<TaskListColumn prop="progress" label="进度" width="100" align="center" />
|
||||
</GanttChart>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2>示例 2: 使用默认 Slot 自定义列内容</h2>
|
||||
<GanttChart
|
||||
:tasks="tasks"
|
||||
:milestones="milestones"
|
||||
task-list-column-render-mode="declarative"
|
||||
:show-toolbar="false"
|
||||
style="height: 400px"
|
||||
>
|
||||
<TaskListColumn prop="name" label="任务名称" width="300" />
|
||||
|
||||
<!-- 自定义负责人列显示 -->
|
||||
<TaskListColumn prop="assignee" label="负责人" width="150" align="center">
|
||||
<template #default="scope">
|
||||
<div class="assignee-cell">
|
||||
<div class="avatar">
|
||||
{{ scope.row.assignee ? scope.row.assignee.charAt(0).toUpperCase() : '?' }}
|
||||
</div>
|
||||
<span>{{ scope.row.assignee || '未分配' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</TaskListColumn>
|
||||
|
||||
<!-- 自定义进度列显示 -->
|
||||
<TaskListColumn prop="progress" label="进度" width="150" align="center">
|
||||
<template #default="scope">
|
||||
<div class="progress-cell">
|
||||
<div class="progress-bar-container">
|
||||
<div
|
||||
class="progress-bar"
|
||||
:style="{
|
||||
width: `${scope.row.progress || 0}%`,
|
||||
background: getProgressColor(scope.row.progress || 0),
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<span class="progress-text">{{ scope.row.progress || 0 }}%</span>
|
||||
</div>
|
||||
</template>
|
||||
</TaskListColumn>
|
||||
|
||||
<TaskListColumn prop="startDate" label="开始" width="120" />
|
||||
<TaskListColumn prop="endDate" label="结束" width="120" />
|
||||
</GanttChart>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2>示例 3: 自定义表头</h2>
|
||||
<GanttChart
|
||||
:tasks="tasks"
|
||||
:milestones="milestones"
|
||||
task-list-column-render-mode="declarative"
|
||||
:show-toolbar="false"
|
||||
style="height: 400px"
|
||||
>
|
||||
<TaskListColumn prop="name" width="300">
|
||||
<template #header>
|
||||
<div class="header-with-icon">
|
||||
<svg
|
||||
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>
|
||||
<span class="header-title">任务信息</span>
|
||||
</div>
|
||||
</template>
|
||||
</TaskListColumn>
|
||||
|
||||
<TaskListColumn prop="assignee" width="150">
|
||||
<template #header>
|
||||
<div class="header-success">👤 负责人</div>
|
||||
</template>
|
||||
</TaskListColumn>
|
||||
|
||||
<TaskListColumn prop="progress" width="150">
|
||||
<template #header>
|
||||
<div class="header-warning">📊 完成度</div>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<span :class="scope.row.progress >= 100 ? 'text-success' : 'text-warning'">
|
||||
{{ scope.row.progress || 0 }}%
|
||||
</span>
|
||||
</template>
|
||||
</TaskListColumn>
|
||||
|
||||
<TaskListColumn prop="startDate" label="开始日期" width="140" />
|
||||
<TaskListColumn prop="endDate" label="结束日期" width="140" />
|
||||
</GanttChart>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.declarative-demo {
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.demo-section h2 {
|
||||
color: #409eff;
|
||||
margin-bottom: 15px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.assignee-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: #409eff;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.progress-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.header-with-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.header-success {
|
||||
color: #67c23a;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.header-warning {
|
||||
color: #e6a23c;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: #e6a23c;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import JordiumGantt from '../src/index'
|
||||
// import DeclarativeColumnsDemo from './DeclarativeColumnsDemo.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
// 使用 DeclarativeColumnsDemo 替代 App 来演示声明式列功能
|
||||
// 如果要切换回原来的演示,取消注释 App 并注释掉 DeclarativeColumnsDemo
|
||||
const app = createApp(App)
|
||||
app.use(JordiumGantt) // 全局注册 GanttChart 和 TaskListColumn
|
||||
app.mount('#app')
|
||||
|
||||
@@ -44,6 +44,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
afternoon: { start: 13, end: 17 },
|
||||
}),
|
||||
taskListConfig: undefined,
|
||||
taskListColumnRenderMode: 'default',
|
||||
taskBarConfig: undefined,
|
||||
autoSortByStartDate: false,
|
||||
allowDragAndResize: true,
|
||||
@@ -84,11 +85,6 @@ const emit = defineEmits([
|
||||
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)
|
||||
|
||||
@@ -135,6 +131,8 @@ interface Props {
|
||||
}
|
||||
// 任务列表配置
|
||||
taskListConfig?: TaskListConfig
|
||||
// 任务列表列渲染模式:'default' 使用 taskListConfig.columns 配置,'declarative' 使用声明式 <task-list-column> 标签
|
||||
taskListColumnRenderMode?: 'default' | 'declarative'
|
||||
// TaskBar 配置
|
||||
taskBarConfig?: TaskBarConfig
|
||||
// 是否启用自动排序(根据开始时间排序任务)
|
||||
@@ -2307,6 +2305,7 @@ function handleMilestoneDialogDelete(milestoneId: number) {
|
||||
:tasks="tasksForTaskList"
|
||||
:use-default-drawer="props.useDefaultDrawer"
|
||||
:task-list-config="props.taskListConfig"
|
||||
:task-list-column-render-mode="props.taskListColumnRenderMode"
|
||||
:enable-task-row-move="props.enableTaskRowMove"
|
||||
@task-collapse-change="handleTaskCollapseChange"
|
||||
@start-timer="handleStartTimer"
|
||||
@@ -2316,6 +2315,10 @@ function handleMilestoneDialogDelete(milestoneId: number) {
|
||||
@delete="handleTaskDelete"
|
||||
@task-row-moved="handleTaskRowMoved"
|
||||
>
|
||||
<!-- 传递默认 slot (用于声明式列定义) -->
|
||||
<template v-if="$slots.default" #default>
|
||||
<slot />
|
||||
</template>
|
||||
<!-- 传递 custom-task-content slot -->
|
||||
<template v-if="$slots['custom-task-content']" #custom-task-content="rowScope">
|
||||
<slot name="custom-task-content" v-bind="rowScope" />
|
||||
|
||||
@@ -8,15 +8,20 @@ import { DEFAULT_TASK_LIST_COLUMNS } from '../models/configs/TaskListConfig'
|
||||
import { useTaskRowDrag } from '../composables/useTaskRowDrag'
|
||||
import { moveTask } from '../utils/taskTreeUtils'
|
||||
import type { Slots } from 'vue'
|
||||
import { useTaskListColumns } from '../composables/useTaskListColumns'
|
||||
import type { DeclarativeColumnConfig } from '../composables/useTaskListColumns'
|
||||
|
||||
interface Props {
|
||||
tasks?: Task[]
|
||||
useDefaultDrawer?: boolean
|
||||
taskListConfig?: TaskListConfig
|
||||
taskListColumnRenderMode?: 'default' | 'declarative'
|
||||
enableTaskRowMove?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
taskListColumnRenderMode: 'default',
|
||||
})
|
||||
|
||||
// 定义emit事件
|
||||
// const emit = defineEmits(['task-collapse-change', 'start-timer', 'stop-timer'])
|
||||
@@ -49,6 +54,24 @@ const columnSlots = inject<Slots>('gantt-column-slots', {})
|
||||
// 多语言支持
|
||||
const { t } = useI18n()
|
||||
|
||||
// 使用声明式列管理 composable
|
||||
const { declarativeColumns, finalColumns, getColumnWidthStyle: getDeclarativeColumnWidth } =
|
||||
useTaskListColumns(
|
||||
props.taskListColumnRenderMode || 'default',
|
||||
slots,
|
||||
props.taskListConfig?.columns || DEFAULT_TASK_LIST_COLUMNS,
|
||||
)
|
||||
|
||||
// 计算实际使用的列配置
|
||||
const columnsToUse = computed(() => {
|
||||
if (props.taskListColumnRenderMode === 'declarative') {
|
||||
return declarativeColumns.value
|
||||
}
|
||||
// 默认模式:使用 taskListConfig 中的列配置
|
||||
const columns = props.taskListConfig?.columns || DEFAULT_TASK_LIST_COLUMNS
|
||||
return columns.filter(col => col.visible !== false)
|
||||
})
|
||||
|
||||
// TaskList 容器引用
|
||||
const taskListRef = ref<HTMLElement | null>(null)
|
||||
const taskListBodyRef = ref<HTMLElement | null>(null)
|
||||
@@ -68,6 +91,12 @@ const taskListBodyHeight = ref(0)
|
||||
|
||||
// 获取列宽度样式(百分比转像素)
|
||||
const getColumnWidthStyle = (column: { width?: number | string }) => {
|
||||
// 声明式模式:使用 composable 中的方法
|
||||
if (props.taskListColumnRenderMode === 'declarative') {
|
||||
return getDeclarativeColumnWidth(column, cachedContainerWidth.value)
|
||||
}
|
||||
|
||||
// 默认模式:原有逻辑
|
||||
if (!column.width) return {}
|
||||
|
||||
let widthPx: string
|
||||
@@ -94,13 +123,8 @@ const getColumnWidthStyle = (column: { width?: number | string }) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 计算可见的列配置
|
||||
const visibleColumns = computed(() => {
|
||||
const columns = props.taskListConfig?.columns || DEFAULT_TASK_LIST_COLUMNS
|
||||
|
||||
// 过滤出可见的列(visible !== false)
|
||||
return columns.filter(col => col.visible !== false)
|
||||
})
|
||||
// 计算可见的列配置(已被 columnsToUse 替代,保留以兼容)
|
||||
const visibleColumns = computed(() => columnsToUse.value)
|
||||
|
||||
// 使用 props.tasks 的引用,不创建副本
|
||||
// 这样可以直接修改对象,触发响应式更新,不需要外部监听事件
|
||||
@@ -596,20 +620,58 @@ onUnmounted(() => {
|
||||
<template>
|
||||
<div ref="taskListRef" class="task-list">
|
||||
<div class="task-list-header">
|
||||
<!-- 任务名称列,始终显示 -->
|
||||
<div class="col col-name">
|
||||
{{ (t as any).taskName || '任务名称' }}
|
||||
</div>
|
||||
<!-- 可配置的其他列 -->
|
||||
<div
|
||||
v-for="column in visibleColumns"
|
||||
:key="column.key"
|
||||
class="col"
|
||||
:class="column.cssClass || `col-${column.key}`"
|
||||
:style="getColumnWidthStyle(column)"
|
||||
>
|
||||
{{ (t as any)[column.key] || column.label }}
|
||||
</div>
|
||||
<!-- 声明式模式 -->
|
||||
<template v-if="taskListColumnRenderMode === 'declarative'">
|
||||
<div
|
||||
v-for="(column, index) in columnsToUse"
|
||||
:key="index"
|
||||
class="col"
|
||||
:class="column.cssClass"
|
||||
:style="{
|
||||
...getColumnWidthStyle(column),
|
||||
justifyContent: column.align === 'center' ? 'center' : column.align === 'right' ? 'flex-end' : 'flex-start',
|
||||
textAlign: column.align || 'left'
|
||||
}"
|
||||
>
|
||||
<!-- 使用 header slot 或显示 label -->
|
||||
<template v-if="column.headerSlot">
|
||||
<component :is="column.headerSlot" />
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ column.label }}
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 默认模式:使用配置的列 -->
|
||||
<template v-else>
|
||||
<!-- 任务名称列,始终显示 -->
|
||||
<div class="col col-name">
|
||||
<!-- 检查是否有 header-name slot -->
|
||||
<template v-if="columnSlots['header-name']">
|
||||
<component :is="columnSlots['header-name']" />
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ (t as any).taskName || '任务名称' }}
|
||||
</template>
|
||||
</div>
|
||||
<!-- 可配置的其他列 -->
|
||||
<div
|
||||
v-for="column in visibleColumns"
|
||||
:key="column.key"
|
||||
class="col"
|
||||
:class="column.cssClass || `col-${column.key}`"
|
||||
:style="getColumnWidthStyle(column)"
|
||||
>
|
||||
<!-- 检查是否有对应的 header-{key} slot -->
|
||||
<template v-if="columnSlots[`header-${column.key}`]">
|
||||
<component :is="columnSlots[`header-${column.key}`]" />
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ (t as any)[column.key] || column.label }}
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div ref="taskListBodyRef" class="task-list-body" @scroll="handleTaskListScroll">
|
||||
<div class="task-list-body-spacer" :style="{ height: `${startSpacerHeight}px` }"></div>
|
||||
@@ -623,6 +685,8 @@ onUnmounted(() => {
|
||||
:hovered-task-id="hoveredTaskId"
|
||||
:on-hover="handleTaskRowHover"
|
||||
:columns="visibleColumns"
|
||||
:declarative-columns="taskListColumnRenderMode === 'declarative' ? columnsToUse : undefined"
|
||||
:render-mode="taskListColumnRenderMode"
|
||||
:get-column-width-style="getColumnWidthStyle"
|
||||
:disable-children-render="true"
|
||||
:show-task-icon="props.taskListConfig?.showTaskIcon"
|
||||
@@ -691,6 +755,7 @@ onUnmounted(() => {
|
||||
color: var(--gantt-text-header);
|
||||
border-right-color: var(--gantt-border-medium);
|
||||
padding: 0 10px;
|
||||
box-sizing: border-box; /* 确保 padding 包含在宽度内 */
|
||||
}
|
||||
|
||||
.task-list-body {
|
||||
|
||||
44
src/components/TaskListColumn.vue
Normal file
44
src/components/TaskListColumn.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* TaskListColumn 声明式列组件
|
||||
* 用于在 GanttChart 中声明式定义任务列表的列
|
||||
* 类似于 Element Plus 的 el-table-column
|
||||
*/
|
||||
|
||||
// 显式定义组件名称,用于识别
|
||||
defineOptions({
|
||||
name: 'TaskListColumn',
|
||||
})
|
||||
|
||||
interface Props {
|
||||
// 列的属性名(用于访问任务数据)
|
||||
prop?: string
|
||||
// 列的显示标签
|
||||
label?: string
|
||||
// 列宽度,支持像素数字或百分比字符串
|
||||
width?: number | string
|
||||
// 列对齐方式
|
||||
align?: 'left' | 'center' | 'right'
|
||||
// CSS 类名
|
||||
cssClass?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
align: 'left',
|
||||
})
|
||||
|
||||
// 定义 slot
|
||||
defineSlots<{
|
||||
// 列头部插槽
|
||||
header?: () => any
|
||||
// 列内容默认插槽,接收 scope 对象 { row: Task, $index: number }
|
||||
default?: (scope: { row: any; $index: number }) => any
|
||||
}>()
|
||||
|
||||
// 注意:此组件不渲染任何内容,仅用于声明列配置
|
||||
// 实际渲染由 TaskList 和 TaskRow 处理
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 此组件不渲染内容 -->
|
||||
</template>
|
||||
@@ -5,6 +5,7 @@ import { useI18n } from '../composables/useI18n'
|
||||
import { formatPredecessorDisplay } from '../utils/predecessorUtils'
|
||||
import type { Task } from '../models/classes/Task'
|
||||
import type { TaskListColumnConfig } from '../models/configs/TaskListConfig'
|
||||
import type { DeclarativeColumnConfig } from '../composables/useTaskListColumns'
|
||||
import TaskContextMenu from './TaskContextMenu.vue'
|
||||
|
||||
interface TaskRowSlotProps {
|
||||
@@ -36,6 +37,8 @@ interface Props {
|
||||
hoveredTaskId?: number | null
|
||||
onHover?: (taskId: number | null) => void
|
||||
columns: TaskListColumnConfig[]
|
||||
declarativeColumns?: DeclarativeColumnConfig[]
|
||||
renderMode?: 'default' | 'declarative'
|
||||
getColumnWidthStyle?: (column: { width?: number | string }) => StyleValue
|
||||
disableChildrenRender?: boolean
|
||||
showTaskIcon?: boolean // 是否显示任务图标,默认true
|
||||
@@ -43,7 +46,9 @@ interface Props {
|
||||
dragStart?: (task: Task, element: HTMLElement, event: MouseEvent) => void
|
||||
dragOver?: (task: Task, element: HTMLElement, event: MouseEvent) => void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
renderMode: 'default',
|
||||
})
|
||||
const emit = defineEmits([
|
||||
'toggle',
|
||||
'dblclick',
|
||||
@@ -85,6 +90,142 @@ const renderColumnSlot = (columnKey: string, slotProps: any) => {
|
||||
return null
|
||||
}
|
||||
|
||||
// 计算实际要渲染的列
|
||||
const columnsToRender = computed(() => {
|
||||
if (props.renderMode === 'declarative' && props.declarativeColumns) {
|
||||
return props.declarativeColumns
|
||||
}
|
||||
return props.columns
|
||||
})
|
||||
|
||||
// 判断是否是第一列
|
||||
const isFirstColumn = (index: number) => index === 0
|
||||
|
||||
// 获取声明式列的对齐样式
|
||||
const getDeclarativeColumnAlign = (column: DeclarativeColumnConfig) => {
|
||||
// if (!column.align || column.align === 'left') return {}
|
||||
return {
|
||||
justifyContent: column.align === 'center' ? 'center' : column.align === 'right' ? 'flex-end' : 'flex-start',
|
||||
textAlign: column.align || 'left',
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染声明式列的内容
|
||||
const renderDeclarativeColumn = (
|
||||
column: DeclarativeColumnConfig,
|
||||
index: number,
|
||||
): any => {
|
||||
// 第一列特殊处理:需要显示折叠按钮、图标等
|
||||
if (isFirstColumn(index)) {
|
||||
// 获取列对应的值(使用 prop 或默认为 name)
|
||||
const columnValue = column.prop ? (props.task as any)[column.prop] : props.task.name
|
||||
|
||||
// 如果有自定义 slot
|
||||
if (column.defaultSlot) {
|
||||
return h('div', { class: 'task-name-content' }, [
|
||||
// 自定义内容
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
class: ['task-name-text', { 'parent-task': isParentTask.value }],
|
||||
title: columnValue,
|
||||
},
|
||||
column.defaultSlot({
|
||||
row: props.task,
|
||||
$index: index,
|
||||
}),
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
// 默认显示列的值(使用 prop)
|
||||
return h('div', { class: 'task-name-content' }, [
|
||||
// 任务图标
|
||||
props.showTaskIcon !== false
|
||||
? h(
|
||||
'span',
|
||||
{ class: 'task-icon' },
|
||||
isMilestoneGroup.value
|
||||
? h(
|
||||
'svg',
|
||||
{
|
||||
width: 16,
|
||||
height: 16,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
stroke: 'currentColor',
|
||||
'stroke-width': 2,
|
||||
class: 'milestone-group-icon',
|
||||
},
|
||||
h('polygon', { points: '12,2 22,12 12,22 2,12' }),
|
||||
)
|
||||
: isStoryTask.value || hasChildren.value
|
||||
? h(
|
||||
'svg',
|
||||
{
|
||||
width: 16,
|
||||
height: 16,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
stroke: 'currentColor',
|
||||
'stroke-width': 2,
|
||||
},
|
||||
h('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',
|
||||
}),
|
||||
)
|
||||
: h(
|
||||
'svg',
|
||||
{
|
||||
width: 16,
|
||||
height: 16,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
stroke: 'currentColor',
|
||||
'stroke-width': 2,
|
||||
},
|
||||
[
|
||||
h('path', {
|
||||
d: 'M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z',
|
||||
}),
|
||||
h('polyline', { points: '14,2 14,8 20,8' }),
|
||||
h('line', { x1: 16, y1: 13, x2: 8, y2: 13 }),
|
||||
h('line', { x1: 16, y1: 17, x2: 8, y2: 17 }),
|
||||
h('polyline', { points: '10,9 9,9 8,9' }),
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
// 列的值(使用 prop 或默认 name)
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
class: ['task-name-text', { 'parent-task': isParentTask.value }],
|
||||
title: columnValue,
|
||||
},
|
||||
columnValue || '-',
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
// 其他列:正常处理
|
||||
// 如果有自定义 slot,使用 slot
|
||||
if (column.defaultSlot) {
|
||||
return column.defaultSlot({
|
||||
row: props.task,
|
||||
$index: index,
|
||||
})
|
||||
}
|
||||
|
||||
// 否则使用 prop 访问任务数据
|
||||
if (column.prop) {
|
||||
const value = (props.task as any)[column.prop]
|
||||
return value !== undefined && value !== null ? value : '-'
|
||||
}
|
||||
|
||||
return '-'
|
||||
}
|
||||
|
||||
const baseIndent = 10
|
||||
const indent = computed(() => `${baseIndent + props.level * 20}px`)
|
||||
const hasChildren = computed(() => !!(props.task.children && props.task.children.length > 0))
|
||||
@@ -373,33 +514,85 @@ onUnmounted(() => {
|
||||
@mouseleave="handleMouseLeave"
|
||||
@contextmenu="handleContextMenu"
|
||||
>
|
||||
<div class="col col-name" :style="{ paddingLeft: indent }">
|
||||
<span
|
||||
v-if="(isStoryTask || hasChildren) && !isMilestoneGroup"
|
||||
class="collapse-btn"
|
||||
@click.stop="handleToggle"
|
||||
<!-- 声明式渲染模式 -->
|
||||
<template v-if="renderMode === 'declarative' && declarativeColumns">
|
||||
<div
|
||||
v-for="(column, index) in columnsToRender"
|
||||
:key="index"
|
||||
class="col"
|
||||
:class="[column.cssClass, { 'col-name': isFirstColumn(index) }]"
|
||||
:style="{
|
||||
...(getColumnWidthStyle ? getColumnWidthStyle(column) : {}),
|
||||
...getDeclarativeColumnAlign(column),
|
||||
}"
|
||||
>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
<!-- 第一列:保留 collapse-btn, milestone-spacer, leaf-spacer -->
|
||||
<div v-if="isFirstColumn(index)"
|
||||
:style="{ paddingLeft: indent }"
|
||||
class="first-col-wrapper">
|
||||
<span
|
||||
v-if="(isStoryTask || hasChildren) && !isMilestoneGroup"
|
||||
class="collapse-btn"
|
||||
@click.stop="handleToggle"
|
||||
>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polyline v-if="props.task.collapsed" points="9,18 15,12 9,6" />
|
||||
<polyline v-else points="18,15 12,9 6,15" />
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<span v-if="isMilestoneGroup" class="milestone-spacer"></span>
|
||||
|
||||
<span
|
||||
v-if="!isParentTask && !isMilestoneGroup && showTaskIcon === false"
|
||||
class="leaf-spacer"
|
||||
></span>
|
||||
|
||||
<!-- 渲染列内容 -->
|
||||
<component :is="() => renderDeclarativeColumn(column, index)" />
|
||||
</div>
|
||||
|
||||
<!-- 其他列:直接渲染内容 -->
|
||||
<component v-else :is="() => renderDeclarativeColumn(column, index)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 默认渲染模式 -->
|
||||
<template v-else>
|
||||
<div class="col col-name" :style="{ paddingLeft: indent }">
|
||||
<span
|
||||
v-if="(isStoryTask || hasChildren) && !isMilestoneGroup"
|
||||
class="collapse-btn"
|
||||
@click.stop="handleToggle"
|
||||
>
|
||||
<polyline v-if="props.task.collapsed" points="9,18 15,12 9,6" />
|
||||
<polyline v-else points="18,15 12,9 6,15" />
|
||||
</svg>
|
||||
</span>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polyline v-if="props.task.collapsed" points="9,18 15,12 9,6" />
|
||||
<polyline v-else points="18,15 12,9 6,15" />
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<!-- 里程碑分组的占位空间,用于与有折叠按钮的任务对齐 -->
|
||||
<span v-if="isMilestoneGroup" class="milestone-spacer"></span>
|
||||
<!-- 里程碑分组的占位空间,用于与有折叠按钮的任务对齐 -->
|
||||
<span v-if="isMilestoneGroup" class="milestone-spacer"></span>
|
||||
|
||||
<!-- 叶子节点的占位空间(无折叠按钮且无图标显示时) -->
|
||||
<span
|
||||
v-if="!isParentTask && !isMilestoneGroup && showTaskIcon === false"
|
||||
class="leaf-spacer"
|
||||
></span>
|
||||
<!-- 叶子节点的占位空间(无折叠按钮且无图标显示时) -->
|
||||
<span
|
||||
v-if="!isParentTask && !isMilestoneGroup && showTaskIcon === false"
|
||||
class="leaf-spacer"
|
||||
></span>
|
||||
|
||||
<!-- 优先级1: 列级自定义 Slot (#column-name) - 覆盖整列内容(图标+文本+徽章) -->
|
||||
<component
|
||||
@@ -642,6 +835,8 @@ onUnmounted(() => {
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 结束默认渲染模式 -->
|
||||
</div>
|
||||
<template
|
||||
v-if="
|
||||
@@ -657,6 +852,8 @@ onUnmounted(() => {
|
||||
:hovered-task-id="props.hoveredTaskId"
|
||||
:on-hover="props.onHover"
|
||||
:columns="props.columns"
|
||||
:declarative-columns="props.declarativeColumns"
|
||||
:render-mode="props.renderMode"
|
||||
:get-column-width-style="props.getColumnWidthStyle"
|
||||
:disable-children-render="props.disableChildrenRender"
|
||||
:show-task-icon="props.showTaskIcon"
|
||||
@@ -1151,4 +1348,17 @@ onUnmounted(() => {
|
||||
:global(html[data-theme='dark']) .task-row-drop-target.drop-child {
|
||||
background-color: rgba(125, 180, 240, 0.1) !important;
|
||||
}
|
||||
|
||||
/* 声明式列第一列的包装器 */
|
||||
.first-col-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 声明式列的 padding,与 header 保持一致 */
|
||||
.task-row .col {
|
||||
padding: 0 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export { default as GanttChart } from './GanttChart.vue'
|
||||
export { default as GanttToolbar } from './GanttToolbar.vue'
|
||||
export { default as TaskList } from './TaskList.vue'
|
||||
export { default as TaskListColumn } from './TaskListColumn.vue'
|
||||
export { default as TaskRow } from './TaskRow.vue'
|
||||
export { default as Timeline } from './Timeline.vue'
|
||||
export { default as TaskBar } from './TaskBar.vue'
|
||||
|
||||
191
src/composables/useTaskListColumns.ts
Normal file
191
src/composables/useTaskListColumns.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { computed, type VNode, type Slots, type VNodeChild } from 'vue'
|
||||
import type { Task } from '../models/classes/Task'
|
||||
import type { TaskListColumnConfig } from '../models/configs/TaskListConfig'
|
||||
|
||||
/**
|
||||
* 声明式列配置接口
|
||||
* 从 task-list-column 组件解析出的列配置
|
||||
*/
|
||||
export interface DeclarativeColumnConfig {
|
||||
prop?: string
|
||||
label?: string
|
||||
width?: number | string
|
||||
align?: 'left' | 'center' | 'right'
|
||||
cssClass?: string
|
||||
// slot 函数
|
||||
headerSlot?: () => VNodeChild
|
||||
defaultSlot?: (scope: { row: Task; $index: number }) => VNodeChild
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 VNode 中提取 task-list-column 的配置
|
||||
*/
|
||||
export function extractColumnConfig(vnode: VNode): DeclarativeColumnConfig | null {
|
||||
// 检查是否是 task-list-column 组件
|
||||
// 支持多种识别方式
|
||||
let isTaskListColumn = false
|
||||
|
||||
if (typeof vnode.type === 'object') {
|
||||
const component = vnode.type as any
|
||||
// 检查组件名称的多种可能性
|
||||
isTaskListColumn =
|
||||
component.name === 'TaskListColumn' ||
|
||||
component.__name === 'TaskListColumn' ||
|
||||
// 检查文件名(setup script 组件)
|
||||
(component.__file && component.__file.includes('TaskListColumn'))
|
||||
}
|
||||
|
||||
if (!isTaskListColumn) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 提取 props
|
||||
const props = vnode.props || {}
|
||||
type ChildrenType = {
|
||||
header?: () => VNodeChild
|
||||
default?: (scope: { row: Task; $index: number }) => VNodeChild
|
||||
} | null
|
||||
const children = vnode.children as ChildrenType
|
||||
|
||||
// 提取 slots
|
||||
const headerSlot = children?.header
|
||||
const defaultSlot = children?.default
|
||||
|
||||
return {
|
||||
prop: props.prop,
|
||||
label: props.label,
|
||||
width: props.width,
|
||||
align: props.align || 'left',
|
||||
cssClass: props.cssClass,
|
||||
headerSlot,
|
||||
defaultSlot,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析声明式列配置
|
||||
* 从默认 slot 的 VNode 中提取所有 task-list-column 的配置
|
||||
*/
|
||||
export function parseDeclarativeColumns(slots: Slots): DeclarativeColumnConfig[] {
|
||||
const defaultSlot = slots.default?.()
|
||||
|
||||
if (!defaultSlot || !Array.isArray(defaultSlot)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const columns: DeclarativeColumnConfig[] = []
|
||||
|
||||
// 递归提取所有 task-list-column
|
||||
const extractColumns = (vnodes: VNode[]) => {
|
||||
for (const vnode of vnodes) {
|
||||
// 跳过注释节点和文本节点,但继续处理 Fragment
|
||||
if (!vnode) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 如果是 Fragment 或其他 Symbol 类型,直接处理子节点
|
||||
if (typeof vnode.type === 'symbol') {
|
||||
if (vnode.children && Array.isArray(vnode.children)) {
|
||||
extractColumns(vnode.children as VNode[])
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const config = extractColumnConfig(vnode)
|
||||
if (config) {
|
||||
columns.push(config)
|
||||
}
|
||||
|
||||
// 递归处理子节点
|
||||
if (vnode.children && Array.isArray(vnode.children)) {
|
||||
extractColumns(vnode.children as VNode[])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extractColumns(defaultSlot)
|
||||
|
||||
return columns
|
||||
}/**
|
||||
* Task List Columns Composable
|
||||
* 用于管理任务列表的列配置(支持声明式和配置式)
|
||||
*/
|
||||
export function useTaskListColumns(
|
||||
renderMode: 'default' | 'declarative',
|
||||
slots: Slots,
|
||||
defaultColumns?: TaskListColumnConfig[],
|
||||
) {
|
||||
// 声明式列配置
|
||||
const declarativeColumns = computed(() => {
|
||||
if (renderMode !== 'declarative') {
|
||||
return []
|
||||
}
|
||||
return parseDeclarativeColumns(slots)
|
||||
})
|
||||
|
||||
// 最终使用的列配置
|
||||
const finalColumns = computed(() => {
|
||||
if (renderMode === 'declarative') {
|
||||
return declarativeColumns.value
|
||||
}
|
||||
return defaultColumns || []
|
||||
})
|
||||
|
||||
// 获取列宽度样式
|
||||
const getColumnWidthStyle = (
|
||||
column: { width?: number | string },
|
||||
containerWidth?: number,
|
||||
) => {
|
||||
if (!column.width) return {}
|
||||
|
||||
let widthPx: string
|
||||
|
||||
// 如果是百分比,转换为像素
|
||||
if (typeof column.width === 'string' && column.width.includes('%')) {
|
||||
if (containerWidth && containerWidth > 0) {
|
||||
const percentage = parseFloat(column.width) / 100
|
||||
const pixels = Math.floor(containerWidth * percentage)
|
||||
widthPx = `${pixels}px`
|
||||
} else {
|
||||
return {} // 容器宽度未知时返回空
|
||||
}
|
||||
} else {
|
||||
// 像素值:处理数字或字符串数字
|
||||
if (typeof column.width === 'number') {
|
||||
widthPx = `${column.width}px`
|
||||
} else if (typeof column.width === 'string') {
|
||||
// 如果字符串已经包含单位(px, rem, em等),直接使用
|
||||
if (/\d+(px|rem|em|%)/.test(column.width)) {
|
||||
widthPx = column.width
|
||||
} else {
|
||||
// 纯数字字符串,添加 px 单位
|
||||
widthPx = `${column.width}px`
|
||||
}
|
||||
} else {
|
||||
widthPx = String(column.width)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
flex: `0 0 ${widthPx}`,
|
||||
minWidth: widthPx,
|
||||
maxWidth: widthPx,
|
||||
}
|
||||
}
|
||||
|
||||
// 获取列对齐样式
|
||||
const getColumnAlignStyle = (align?: 'left' | 'center' | 'right') => {
|
||||
if (!align || align === 'left') return {}
|
||||
return {
|
||||
justifyContent: align === 'center' ? 'center' : 'flex-end',
|
||||
textAlign: align,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
declarativeColumns,
|
||||
finalColumns,
|
||||
getColumnWidthStyle,
|
||||
getColumnAlignStyle,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// 导出所有组件
|
||||
export { default as GanttChart } from './components/GanttChart.vue'
|
||||
export { default as TaskList } from './components/TaskList.vue'
|
||||
export { default as TaskListColumn } from './components/TaskListColumn.vue'
|
||||
export { default as Timeline } from './components/Timeline.vue'
|
||||
export { default as TaskBar } from './components/TaskBar.vue'
|
||||
export { default as TaskDrawer } from './components/TaskDrawer.vue'
|
||||
@@ -26,9 +27,11 @@ import './styles/theme-variables.css'
|
||||
// 导出安装函数(可选,用于Vue.use())
|
||||
import type { App } from 'vue'
|
||||
import GanttChart from './components/GanttChart.vue'
|
||||
import TaskListColumn from './components/TaskListColumn.vue'
|
||||
|
||||
export const install = (app: App) => {
|
||||
app.component('GanttChart', GanttChart)
|
||||
app.component('TaskListColumn', TaskListColumn)
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
Reference in New Issue
Block a user