beta 1.0.0 - 正式外测版本
This commit is contained in:
@@ -97,7 +97,7 @@ watch(
|
||||
singleValue.value = (newValue as string) || ''
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 处理单日期输入变化(预留,当前版本不使用)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onUnmounted, onMounted, computed, watch, nextTick } from 'vue'
|
||||
import { ref, onUnmounted, onMounted, computed, watch, nextTick, defineEmits } from 'vue'
|
||||
import TaskList from './TaskList.vue'
|
||||
import Timeline from './Timeline.vue'
|
||||
import GanttToolbar from './GanttToolbar.vue'
|
||||
@@ -8,6 +8,36 @@ import jsPDF from 'jspdf'
|
||||
import html2canvas from 'html2canvas'
|
||||
import type { Task } from '../models/classes/Task'
|
||||
import type { ToolbarConfig } from '../models/configs/ToolbarConfig'
|
||||
import { useMessage } from '../composables/useMessage'
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
tasks: () => [],
|
||||
milestones: () => [],
|
||||
onTaskDoubleClick: undefined,
|
||||
editComponent: undefined,
|
||||
useDefaultDrawer: true,
|
||||
onTaskDelete: undefined,
|
||||
onMilestoneSave: undefined,
|
||||
onMilestoneDelete: undefined,
|
||||
onTaskUpdate: undefined,
|
||||
onTaskAdd: undefined,
|
||||
onMilestoneIconChange: undefined,
|
||||
toolbarConfig: () => ({}),
|
||||
showToolbar: true,
|
||||
onAddTask: undefined,
|
||||
onAddMilestone: undefined,
|
||||
onTodayLocate: undefined,
|
||||
onExportCsv: undefined,
|
||||
onExportPdf: undefined,
|
||||
onLanguageChange: undefined,
|
||||
onThemeChange: undefined,
|
||||
onFullscreenChange: undefined,
|
||||
localeMessages: undefined,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['taskbar-drag-end', 'taskbar-resize-end', 'milestone-drag-end'])
|
||||
|
||||
const { showMessage } = useMessage()
|
||||
|
||||
interface Props {
|
||||
// 任务数据
|
||||
@@ -55,31 +85,6 @@ interface Props {
|
||||
localeMessages?: Partial<import('../composables/useI18n').Messages['zh-CN']>
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
tasks: () => [],
|
||||
milestones: () => [],
|
||||
onTaskDoubleClick: undefined,
|
||||
editComponent: undefined,
|
||||
useDefaultDrawer: true,
|
||||
onTaskDelete: undefined,
|
||||
onMilestoneSave: undefined,
|
||||
onMilestoneDelete: undefined,
|
||||
onTaskUpdate: undefined,
|
||||
onTaskAdd: undefined,
|
||||
onMilestoneIconChange: undefined,
|
||||
toolbarConfig: () => ({}),
|
||||
showToolbar: true,
|
||||
onAddTask: undefined,
|
||||
onAddMilestone: undefined,
|
||||
onTodayLocate: undefined,
|
||||
onExportCsv: undefined,
|
||||
onExportPdf: undefined,
|
||||
onLanguageChange: undefined,
|
||||
onThemeChange: undefined,
|
||||
onFullscreenChange: undefined,
|
||||
localeMessages: undefined,
|
||||
})
|
||||
|
||||
const leftPanelWidth = ref(320)
|
||||
|
||||
// Timeline组件的引用
|
||||
@@ -229,10 +234,29 @@ const handleToggleTaskList = (event: CustomEvent) => {
|
||||
isTaskListVisible.value = event.detail
|
||||
}
|
||||
|
||||
// --- 事件链路:监听 Timeline 传递上来的拖拽/拉伸事件,并通过 props 回调暴露 ---
|
||||
function handleTaskBarDragEnd(event: CustomEvent) {
|
||||
emit('taskbar-drag-end', event.detail)
|
||||
}
|
||||
function handleTaskBarResizeEnd(event: CustomEvent) {
|
||||
emit('taskbar-resize-end', event.detail)
|
||||
}
|
||||
function handleMilestoneDragEnd(event: CustomEvent) {
|
||||
emit('milestone-drag-end', event.detail)
|
||||
}
|
||||
onMounted(() => {
|
||||
window.addEventListener('taskbar-drag-end', handleTaskBarDragEnd as EventListener)
|
||||
window.addEventListener('taskbar-resize-end', handleTaskBarResizeEnd as EventListener)
|
||||
window.addEventListener('milestone-drag-end', handleMilestoneDragEnd as EventListener)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('taskbar-drag-end', handleTaskBarDragEnd as EventListener)
|
||||
window.removeEventListener('taskbar-resize-end', handleTaskBarResizeEnd as EventListener)
|
||||
window.removeEventListener('milestone-drag-end', handleMilestoneDragEnd as EventListener)
|
||||
})
|
||||
|
||||
// 处理TaskList的任务折叠状态变化
|
||||
const handleTaskCollapseChange = (task: Task) => {
|
||||
console.log('任务折叠状态变化:', task)
|
||||
|
||||
// 递归查找并更新原始数据中的任务折叠状态
|
||||
const updateTaskCollapsedState = (
|
||||
tasks: Task[],
|
||||
@@ -242,7 +266,6 @@ const handleTaskCollapseChange = (task: Task) => {
|
||||
for (const t of tasks) {
|
||||
if (t.id === targetId) {
|
||||
t.collapsed = collapsed
|
||||
console.log(`已更新任务 ${t.name} 的折叠状态为: ${collapsed}`)
|
||||
return true
|
||||
}
|
||||
if (t.children && t.children.length > 0) {
|
||||
@@ -385,7 +408,6 @@ const tasksForTaskList = computed(() => {
|
||||
result.push(...props.tasks)
|
||||
}
|
||||
|
||||
console.log('TaskList数据(保持层级结构):', result)
|
||||
return result
|
||||
})
|
||||
|
||||
@@ -489,7 +511,6 @@ const tasksForTimeline = computed(() => {
|
||||
startDate,
|
||||
endDate,
|
||||
}
|
||||
console.log(`父任务 ${task.name} 时间范围已更新: ${startDate} - ${endDate}`)
|
||||
}
|
||||
|
||||
return updatedTask
|
||||
@@ -528,11 +549,9 @@ const tasksForTimeline = computed(() => {
|
||||
// 因为tasksForTaskList返回的是props.tasks的副本,我们需要确保Timeline能看到最新的折叠状态
|
||||
// 通过触发器强制重新计算来实现状态同步
|
||||
const flattenedTasks = smartFlattenTasks(tasksWithUpdatedDates)
|
||||
console.log('Timeline智能扁平化任务数据(考虑折叠状态):', flattenedTasks)
|
||||
result.push(...flattenedTasks)
|
||||
}
|
||||
|
||||
console.log('Timeline最终数据:', result)
|
||||
return result
|
||||
})
|
||||
|
||||
@@ -645,6 +664,7 @@ const defaultExportCsv = () => {
|
||||
URL.revokeObjectURL(url)
|
||||
} catch (error) {
|
||||
console.error('CSV导出失败:', error)
|
||||
showMessage('CSV导出失败', 'error', { closable: false })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -877,8 +897,6 @@ const handleMilestoneSave = (milestone: Task) => {
|
||||
|
||||
// 如果没有外部处理器,执行默认的更新逻辑
|
||||
// 这里可以添加默认的里程碑数据更新逻辑,比如更新本地数据或者触发重新渲染
|
||||
|
||||
console.log('Milestone saved:', milestone)
|
||||
}
|
||||
|
||||
// 处理任务更新事件
|
||||
@@ -907,7 +925,6 @@ const handleMilestoneIconChangeEvent = (event: CustomEvent) => {
|
||||
if (props.onMilestoneIconChange && typeof props.onMilestoneIconChange === 'function') {
|
||||
props.onMilestoneIconChange(milestoneId, icon)
|
||||
}
|
||||
console.log('Milestone icon changed:', { milestoneId, icon })
|
||||
}
|
||||
|
||||
// 处理里程碑删除事件
|
||||
@@ -980,8 +997,6 @@ const defaultAddMilestone = () => {
|
||||
|
||||
// 默认今日定位功能
|
||||
const defaultTodayLocate = () => {
|
||||
console.log('执行默认今日定位逻辑defaultTodayLocate')
|
||||
|
||||
// 优先使用Timeline组件的scrollToToday方法(已优化的定位逻辑)
|
||||
if (timelineRef.value && typeof timelineRef.value.scrollToToday === 'function') {
|
||||
timelineRef.value.scrollToToday()
|
||||
|
||||
@@ -17,6 +17,7 @@ const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
'milestone-double-click': [milestone: Milestone]
|
||||
'update:milestone': [milestone: Milestone] // 新增里程碑更新事件
|
||||
'drag-end': [milestone: Milestone] // 新增
|
||||
}>()
|
||||
|
||||
// 拖拽相关状态
|
||||
@@ -89,17 +90,11 @@ const handleMouseUp = () => {
|
||||
...props.milestone,
|
||||
...tempMilestoneData.value,
|
||||
}
|
||||
|
||||
console.log('里程碑拖拽完成,提交数据更新:', updatedMilestone)
|
||||
emit('update:milestone', updatedMilestone)
|
||||
|
||||
// 清空临时数据
|
||||
emit('drag-end', updatedMilestone) // 新增
|
||||
tempMilestoneData.value = null
|
||||
}
|
||||
|
||||
isDragging.value = false
|
||||
|
||||
// 移除全局事件监听器
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,13 @@ interface Props {
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits(['update:task', 'bar-mounted', 'dblclick'])
|
||||
const emit = defineEmits([
|
||||
'update:task',
|
||||
'bar-mounted',
|
||||
'dblclick',
|
||||
'drag-end', // 新增
|
||||
'resize-end', // 新增
|
||||
])
|
||||
|
||||
// 日期工具函数 - 处理时区安全的日期创建和操作
|
||||
const createLocalDate = (dateString: string | Date | undefined | null): Date | null => {
|
||||
@@ -255,7 +261,12 @@ const handleMouseUp = () => {
|
||||
...tempTaskData.value,
|
||||
}
|
||||
|
||||
console.log('TaskBar操作完成,提交数据更新:', updatedTask)
|
||||
// 判断是拖拽还是拉伸
|
||||
if (isDragging.value) {
|
||||
emit('drag-end', updatedTask)
|
||||
} else if (isResizingLeft.value || isResizingRight.value) {
|
||||
emit('resize-end', updatedTask)
|
||||
}
|
||||
emit('update:task', updatedTask)
|
||||
|
||||
// 清空临时数据
|
||||
|
||||
+19
-22
@@ -230,8 +230,6 @@ const handleMilestoneDelete = (milestoneId: number) => {
|
||||
|
||||
// 处理里程碑拖拽更新事件
|
||||
const handleMilestoneUpdate = (updatedMilestone: any) => {
|
||||
console.log('Timeline 接收到里程碑拖拽更新事件:', updatedMilestone)
|
||||
|
||||
// 通知父组件里程碑数据已更新
|
||||
if (props.onMilestoneSave && typeof props.onMilestoneSave === 'function') {
|
||||
props.onMilestoneSave(updatedMilestone)
|
||||
@@ -345,15 +343,6 @@ const scrollToTodayCenter = (retry = 0) => {
|
||||
const scrollContainer = document.querySelector('.timeline') as HTMLElement
|
||||
|
||||
const containerWidth = scrollContainer.clientWidth
|
||||
console.log('[scrollToTodayCenter]', {
|
||||
retry,
|
||||
today: todayNormalized,
|
||||
timelineStart: startNormalized,
|
||||
daysDiff,
|
||||
todayPosition,
|
||||
containerWidth,
|
||||
containerClass: scrollContainer.className,
|
||||
})
|
||||
// 若宽度为0,递归重试,最多10次
|
||||
if (containerWidth === 0 && retry < 10) {
|
||||
setTimeout(() => scrollToTodayCenter(retry + 1), 60)
|
||||
@@ -367,19 +356,8 @@ const scrollToTodayCenter = (retry = 0) => {
|
||||
} else {
|
||||
scrollContainer.scrollLeft = Math.max(0, centeredScrollPosition)
|
||||
}
|
||||
console.log(
|
||||
'[scrollToTodayCenter] scrollLeft set:',
|
||||
Math.max(0, centeredScrollPosition),
|
||||
'on',
|
||||
scrollContainer.className,
|
||||
)
|
||||
console.log(
|
||||
'[scrollToTodayCenter] scrollContainer.scrollLeft updated:',
|
||||
scrollContainer.scrollLeft,
|
||||
)
|
||||
}
|
||||
|
||||
// 聚焦到任务所在位置
|
||||
const scrollToTasks = () => {
|
||||
if (tasks.value.length === 0) {
|
||||
// 如果没有任务,滚动到今天
|
||||
@@ -565,6 +543,22 @@ function handleBarMounted(payload: {
|
||||
updateSvgSize()
|
||||
}
|
||||
|
||||
// 向上传递 TaskBar 拖拽/拉伸事件
|
||||
const handleTaskBarDragEnd = (updatedTask: Task) => {
|
||||
// 通过全局事件或 emit/props 回调传递给 GanttChart
|
||||
window.dispatchEvent(new CustomEvent('taskbar-drag-end', { detail: updatedTask }))
|
||||
// TODO: 你可以在这里加 emit/props 回调
|
||||
}
|
||||
const handleTaskBarResizeEnd = (updatedTask: Task) => {
|
||||
window.dispatchEvent(new CustomEvent('taskbar-resize-end', { detail: updatedTask }))
|
||||
// TODO: 你可以在这里加 emit/props 回调
|
||||
}
|
||||
// 向上传递 MilestonePoint 拖拽事件
|
||||
const handleMilestoneDragEnd = (updatedMilestone: any) => {
|
||||
window.dispatchEvent(new CustomEvent('milestone-drag-end', { detail: updatedMilestone }))
|
||||
// TODO: 你可以在这里加 emit/props 回调
|
||||
}
|
||||
|
||||
// 计算所有连线
|
||||
const links = computed(() => {
|
||||
const result: { from: number; to: number; path: string }[] = []
|
||||
@@ -987,6 +981,7 @@ const convertTaskToMilestone = (task: Task): Milestone => {
|
||||
:milestone="convertTaskToMilestone(milestone)"
|
||||
@milestone-double-click="handleMilestoneDoubleClick"
|
||||
@update:milestone="handleMilestoneUpdate"
|
||||
@drag-end="handleMilestoneDragEnd"
|
||||
/>
|
||||
</template>
|
||||
<!-- 普通任务条 - 排除里程碑分组和普通里程碑 -->
|
||||
@@ -1002,6 +997,8 @@ const convertTaskToMilestone = (task: Task): Milestone => {
|
||||
@update:task="updateTask"
|
||||
@bar-mounted="handleBarMounted"
|
||||
@dblclick="handleTaskBarDoubleClick(task)"
|
||||
@drag-end="handleTaskBarDragEnd"
|
||||
@resize-end="handleTaskBarResizeEnd"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
// src/composables/useMessage.ts
|
||||
export type MessageType = 'success' | 'error' | 'warning' | 'info'
|
||||
|
||||
// 消息类型对应的颜色,参考 element plus
|
||||
const typeColorMap: Record<MessageType, string> = {
|
||||
success: '#67c23a', // 绿色
|
||||
warning: '#e6a23c', // 黄色
|
||||
error: '#f56c6c', // 红色
|
||||
info: '#909399', // 灰色
|
||||
}
|
||||
|
||||
// 全局消息容器
|
||||
let container: HTMLDivElement | null = null
|
||||
function getContainer() {
|
||||
if (!container) {
|
||||
container = document.createElement('div')
|
||||
container.className = 'message-container'
|
||||
container.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
pointer-events: none;
|
||||
`
|
||||
document.body.appendChild(container)
|
||||
}
|
||||
return container
|
||||
}
|
||||
|
||||
export function useMessage() {
|
||||
/**
|
||||
* @param message 消息内容
|
||||
* @param type 消息类型
|
||||
* @param options 可选项:
|
||||
* - duration: 自动关闭的毫秒数(默认3000,0表示不自动关闭)
|
||||
* - closable: 是否显示关闭按钮(true=手动关闭,false=自动关闭)
|
||||
*/
|
||||
const showMessage = (
|
||||
message: string,
|
||||
type: MessageType = 'success',
|
||||
options?: { duration?: number; closable?: boolean },
|
||||
) => {
|
||||
const messageEl = document.createElement('div')
|
||||
messageEl.className = `message ${type}`
|
||||
messageEl.textContent = message
|
||||
messageEl.style.cssText = `
|
||||
min-width: 180px;
|
||||
max-width: 360px;
|
||||
margin: 0;
|
||||
padding: 12px 20px 12px 20px;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
background: ${typeColorMap[type]};
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
pointer-events: auto;
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
transform: translateY(-20px);
|
||||
transition: opacity 0.25s, transform 0.25s;
|
||||
position: relative;
|
||||
`
|
||||
let closed = false
|
||||
// 关闭按钮
|
||||
const closable = options?.closable ?? false
|
||||
if (closable) {
|
||||
const closeBtn = document.createElement('button')
|
||||
closeBtn.innerHTML =
|
||||
'<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>'
|
||||
closeBtn.style.cssText = `
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
margin-left: 8px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
`
|
||||
closeBtn.onmouseenter = () => (closeBtn.style.opacity = '1')
|
||||
closeBtn.onmouseleave = () => (closeBtn.style.opacity = '0.7')
|
||||
closeBtn.onclick = () => close()
|
||||
messageEl.appendChild(closeBtn)
|
||||
}
|
||||
// 插入到容器(始终插入到末尾,保证新消息在下方)
|
||||
const parent = getContainer()
|
||||
parent.appendChild(messageEl)
|
||||
// 入场动画
|
||||
setTimeout(() => {
|
||||
messageEl.style.opacity = '0.98'
|
||||
messageEl.style.transform = 'translateY(0)'
|
||||
}, 10)
|
||||
// 自动关闭
|
||||
const duration = options?.duration ?? (closable ? 0 : 3000)
|
||||
let timer: number | undefined
|
||||
if (!closable && duration > 0) {
|
||||
timer = window.setTimeout(() => close(), duration)
|
||||
}
|
||||
// 关闭逻辑,带动画
|
||||
function close() {
|
||||
if (closed) return
|
||||
closed = true
|
||||
messageEl.style.opacity = '0'
|
||||
messageEl.style.transform = 'translateY(-20px)'
|
||||
if (timer) clearTimeout(timer)
|
||||
setTimeout(() => {
|
||||
if (messageEl.parentNode) {
|
||||
parent.removeChild(messageEl)
|
||||
// 关闭后自动上移(flex布局已保证)
|
||||
// 若无消息,移除容器
|
||||
if (parent.childElementCount === 0) {
|
||||
parent.remove()
|
||||
container = null
|
||||
}
|
||||
}
|
||||
}, 250)
|
||||
}
|
||||
}
|
||||
return { showMessage }
|
||||
}
|
||||
Reference in New Issue
Block a user