Files
jordium-gantt-vue3/demo/App.vue

1909 lines
55 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { ref, computed, onMounted, nextTick } from 'vue'
import GanttChart from '../src/components/GanttChart.vue'
// import TaskDrawer from '../src/components/TaskDrawer.vue' // 移除
import MilestoneDialog from '../src/components/MilestoneDialog.vue'
import demoData from './data.json'
import packageInfo from '../package.json'
// 导入主题变量
import '../src/styles/theme-variables.css'
import VersionHistoryDrawer from './VersionHistoryDrawer.vue'
import HtmlContent from './HtmlContent.vue'
import { useMessage } from '../src/composables/useMessage'
import { useI18n } from '../src/composables/useI18n'
import { getPredecessorIds, predecessorIdsToString } from '../src/utils/predecessorUtils'
import type { Task } from '../src/models/Task'
import type { TaskListConfig, TaskListColumnConfig } from '../src/models/configs/TaskListConfig'
import type { TaskBarConfig } from '../src/models/configs/TaskBarConfig'
const { showMessage } = useMessage()
const { t, formatTranslation } = useI18n()
const tasks = ref<Task[]>([])
const milestones = ref<Task[]>([])
// MilestoneDialog状态管理
const showMilestoneDialog = ref(false)
const currentMilestone = ref<Task | null>(null)
const isMilestoneEditMode = ref(false)
// 版本历史Drawer状态
const showVersionDrawer = ref(false)
const toolbarConfig = {
showAddTask: true,
showAddMilestone: true,
showTodayLocate: true,
showExportCsv: true,
showExportPdf: true,
showLanguage: true,
showTheme: true,
showFullscreen: true,
showTimeScale: true, // 控制日|周|月时间刻度按钮组的可见性
timeScaleDimensions: ['hour', 'day', 'week', 'month', 'quarter', 'year'], // 设置时间刻度按钮的展示维度,包含所有时间维度
defaultTimeScale: 'week',
showExpandCollapse: true, // 显示全部展开/折叠按钮
}
// TaskList列配置
const availableColumns = ref<TaskListColumnConfig[]>([
{ key: 'predecessor', label: '前置任务', visible: true },
{ key: 'assignee', label: '负责人', visible: true, width: 250 },
{ key: 'startDate', label: '开始日期', visible: true },
{ key: 'endDate', label: '结束日期', visible: true },
{ key: 'estimatedHours', label: '预估工时', visible: true },
{ key: 'actualHours', label: '实际工时', visible: true },
{ key: 'progress', label: '进度', visible: true },
{ key: 'custom', label: '自定义列', visible: true, width: '30%' }, // 添加默认宽度120px
])
// TaskList宽度配置
const taskListWidth = ref({
defaultWidth: 450, // 默认宽度400px比默认320px更宽
minWidth: 300, // 最小宽度300px比默认280px略大
maxWidth: 1200, // 最大宽度1200px比默认1160px略大
})
// TaskList宽度单位配置px 或百分比)
const widthUnit = ref<'px' | '%'>('px')
const widthPercentage = ref({
defaultWidth: 25,
minWidth: 15,
maxWidth: 60,
})
const taskListConfig = computed<TaskListConfig>(() => ({
columns: availableColumns.value,
defaultWidth:
widthUnit.value === '%'
? `${widthPercentage.value.defaultWidth}%`
: taskListWidth.value.defaultWidth,
minWidth:
widthUnit.value === '%' ? `${widthPercentage.value.minWidth}%` : taskListWidth.value.minWidth,
maxWidth:
widthUnit.value === '%' ? `${widthPercentage.value.maxWidth}%` : taskListWidth.value.maxWidth,
}))
// 控制是否允许拖拽和拉伸
const allowDragAndResize = ref(true)
// TaskBar配置
const taskBarOptions = ref({
showAvatar: true,
showTitle: true,
showProgress: true,
dragThreshold: 5, // 拖拽触发阈值(像素)
resizeHandleWidth: 5, // 拉伸手柄宽度像素默认5px最大15px
enableDragDelay: false, // 是否启用拖拽延迟
dragDelayTime: 150, // 拖拽延迟时间(毫秒)
})
const taskBarConfig = computed<TaskBarConfig>(() => ({
showAvatar: taskBarOptions.value.showAvatar,
showTitle: taskBarOptions.value.showTitle,
showProgress: taskBarOptions.value.showProgress,
dragThreshold: taskBarOptions.value.dragThreshold,
resizeHandleWidth: taskBarOptions.value.resizeHandleWidth,
enableDragDelay: taskBarOptions.value.enableDragDelay,
dragDelayTime: taskBarOptions.value.dragDelayTime,
}))
// 配置面板折叠状态
const isConfigPanelCollapsed = ref(false)
// TaskList 配置区域折叠状态(默认收起)
const isTaskListConfigCollapsed = ref(true)
// TaskBar 配置区域折叠状态(默认收起)
const isTaskBarConfigCollapsed = ref(true)
// 切换配置面板折叠状态
const toggleConfigPanel = () => {
isConfigPanelCollapsed.value = !isConfigPanelCollapsed.value
}
// 切换 TaskList 配置区域
const toggleTaskListConfig = () => {
isTaskListConfigCollapsed.value = !isTaskListConfigCollapsed.value
}
// 切换 TaskBar 配置区域
const toggleTaskBarConfig = () => {
isTaskBarConfigCollapsed.value = !isTaskBarConfigCollapsed.value
}
// Task Click Dialog 状态管理
const showTaskClickDialog = ref(false)
const clickedTask = ref<Task | null>(null)
// 处理任务点击事件
const handleTaskClick = (task: Task) => {
clickedTask.value = task
showTaskClickDialog.value = true
}
// 关闭 Task Click Dialog
const closeTaskClickDialog = () => {
showTaskClickDialog.value = false
clickedTask.value = null
}
// 格式化属性值用于显示
const formatPropertyValue = (value: unknown): string => {
if (value === null || value === undefined) {
return 'null'
}
if (Array.isArray(value)) {
return JSON.stringify(value)
}
if (typeof value === 'object') {
return JSON.stringify(value, null, 2)
}
return String(value)
}
// 切换列显示状态
const toggleColumn = (columnKey: string, event: Event) => {
const target = event.target as HTMLInputElement
const visible = target?.checked ?? false
const column = availableColumns.value.find(col => col.key === columnKey)
if (column) {
column.visible = visible
}
} // 工作时间配置示例
const workingHoursConfig = {
morning: { start: 8, end: 11 }, // 上午8:00-11:59为工作时间
afternoon: { start: 13, end: 17 }, // 下午13:00-17:00为工作时间
// 其他时间12:00-12:59, 18:00-07:59为休息时间显示为灰色背景
}
// 自定义CSV导出处理器可选
const handleCustomCsvExport = () => {
showMessage(t.value.customCsvExportCalled, 'info', { closable: true })
// 这里可以实现自定义的CSV导出逻辑
// 例如:添加额外的数据处理、格式化、或发送到服务器等
// 如果不实现自定义逻辑,组件会使用内置的默认导出功能
// return false // 返回false让组件使用默认实现
// 示例:这里直接使用默认实现
return false
}
// 其他工具栏事件处理器示例
const handleAddTask = () => {
// GanttChart 内部会打开 TaskDrawer如果 useDefaultDrawer=true
// 这里可以添加自定义逻辑,比如显示提示消息
showMessage('准备新增任务...', 'info', { closable: true })
}
const handleAddMilestone = () => {
// 打开MilestoneDialog进行新建里程碑
currentMilestone.value = null
isMilestoneEditMode.value = false
showMilestoneDialog.value = true
}
const handleLanguageChange = (lang: 'zh' | 'en') => {
const languageText = lang === 'zh' ? '中文' : 'English'
showMessage(formatTranslation('languageSwitchedTo', { language: languageText }), 'info', {
closable: true,
})
}
const handleThemeChange = (isDark: boolean) => {
const themeText = isDark ? t.value.darkModeText : t.value.lightModeText
showMessage(formatTranslation('themeSwitchedTo', { theme: themeText }), 'info', {
closable: true,
})
}
// 从外部 MilestoneDialog 保存里程碑(新建里程碑按钮打开的对话框)
const handleMilestoneSaveFromDialog = (milestone: Task) => {
// 如果是新建里程碑没有id生成一个临时ID
if (!milestone.id) {
milestone.id = Date.now()
}
// 确保里程碑有必要的属性
milestone.type = 'milestone'
// 查找是否已存在该里程碑
const existingIndex = milestones.value.findIndex(m => m.id === milestone.id)
if (existingIndex !== -1) {
// 更新现有里程碑
milestones.value.splice(existingIndex, 1, milestone)
} else {
// 添加新里程碑
milestones.value.push(milestone)
}
// 关闭对话框
showMilestoneDialog.value = false
// 显示成功提示
showMessage(`里程碑 "${milestone.name}" 已保存`, 'success', { closable: false })
}
// 里程碑保存事件处理器(新的事件驱动 API
const handleMilestoneSaved = (milestone: Task) => {
// 组件已自动更新 milestones 数据,这里只需处理额外的业务逻辑
// 关闭里程碑对话框
showMilestoneDialog.value = false
// 可以在这里添加其他业务逻辑,如:
// - 发送数据到服务器
// - 显示成功提示
// - 记录操作日志等
showMessage(`里程碑 "${milestone.name}" 已保存`, 'success', { closable: false })
}
// 里程碑删除事件处理器(新的事件驱动 API
const handleMilestoneDeleted = async (event: { milestoneId: number }) => {
// 组件已自动从 milestones 中删除数据,这里只需处理额外的业务逻辑
const { milestoneId } = event
showMessage(t.value.milestoneDeleteSuccess, 'success', { closable: false })
// 等待DOM更新完成
await nextTick()
// 触发全局事件,通知其他组件里程碑已删除
window.dispatchEvent(
new CustomEvent('milestone-deleted', {
detail: { milestoneId },
}),
)
// 触发强制更新事件确保Timeline重新渲染
window.dispatchEvent(
new CustomEvent('milestone-data-changed', {
detail: { milestones: milestones.value },
}),
)
// 关闭里程碑对话框
showMilestoneDialog.value = false
}
// 任务更新事件处理器
const handleTaskUpdateEvent = (e: { task: Task }) => {
const updatedTask = e.task
showMessage(`Demo 任务[${updatedTask.name}] 已更新`, 'success')
// 注意GanttChart 内部已经通过 updateTaskInTree 更新了任务
// 这里只需要显示消息,无需再次执行更新逻辑
// 计时信息展示(无论来源于 TaskBar/TaskRow 还是 TaskDrawer header
if (updatedTask.timerStartTime) {
const msg = `任务【${updatedTask.name}】计时已更新`
showMessage(msg, 'info', { closable: true })
}
}
// 任务添加事件处理器
const handleTaskAddEvent = (e: { task: Task }) => {
const newTask = e.task
showMessage(`Demo 任务[${newTask.name}] 已创建`, 'success')
// 注意GanttChart 内部已经通过 insertTask 添加了任务
// 这里只需要显示消息,无需再次执行添加逻辑
}
// 任务删除事件处理器
const handleTaskDeleteEvent = (e: { task: Task; deleteChildren?: boolean }) => {
const task = e.task
showMessage(`Demo 任务[${task.name}] 已删除`, 'success')
// 注意GanttChart 内部已经通过 removeTaskFromTree 删除了任务
// 这里只需要显示消息,无需再次执行删除逻辑
// 如果需要在删除后清理其他数据(如 predecessor可以在这里处理
// 收集被删除任务的所有ID包括子任务
const deletedTaskIds = collectAllTaskIds(task)
// 清理predecessor依赖关系
cleanupPredecessorReferences(deletedTaskIds)
}
// 里程碑图标变更事件处理器(新的事件驱动 API
const handleMilestoneIconChanged = (event: { milestoneId: number; icon: string }) => {
// 组件已自动更新 milestones 中的 icon这里只需处理额外的业务逻辑
const { icon } = event
// 可以在这里添加其他业务逻辑,如:
// - 发送更新到服务器
// - 显示成功提示
// - 记录操作日志等
showMessage(`里程碑图标已更新为 ${icon}`, 'info', { closable: false })
}
// GitHub 文档处理函数
const handleGithubDocsClick = (event: Event) => {
event.preventDefault()
// 打开GitHub仓库的README页面
window.open('https://github.com/nelson820125/jordium-gantt-vue3', '_blank')
}
// Gitee 文档处理函数
const handleGiteeDocsClick = (event: Event) => {
event.preventDefault()
// 打开Gitee仓库的README页面
window.open('https://gitee.com/jordium/jordium-gantt-vue3.git', '_blank')
}
// 任务拖拽/拉伸/里程碑拖拽监听
function handleTaskbarDragOrResizeEnd(newTask) {
// 注意GanttChart 已经更新了 props.tasks所以这里只显示更新后的信息
showMessage(
`任务【${newTask.name}】已更新\n` +
`开始日期: ${newTask.startDate}\n` +
`结束日期: ${newTask.endDate}`,
'success',
{ closable: true },
)
}
function handleMilestoneDragEnd(newMilestone) {
// 注意GanttChart 已经更新了数据
showMessage(
`里程碑【${newMilestone.name}】已更新\n` +
`日期: ${newMilestone.startDate || newMilestone.endDate}`,
'success',
{ closable: true },
)
}
onMounted(() => {
tasks.value = demoData.tasks as Task[]
milestones.value = demoData.milestones as Task[]
})
// 清理被删除任务的predecessor依赖关系
const cleanupPredecessorReferences = (deletedTaskIds: number[]) => {
// 递归清理所有任务包括嵌套的子任务的predecessor
const cleanupTaskArray = (taskArray: Task[]) => {
taskArray.forEach(task => {
if (task.predecessor) {
// 使用工具函数获取前置任务ID数组
const predecessorIds = getPredecessorIds(task.predecessor)
// 过滤掉被删除的任务ID
const validPredecessorIds = predecessorIds.filter(id => !deletedTaskIds.includes(id))
// 更新predecessor属性
if (validPredecessorIds.length === 0) {
delete task.predecessor
} else {
task.predecessor = predecessorIdsToString(validPredecessorIds)
}
}
// 递归处理子任务
if (task.children && task.children.length > 0) {
cleanupTaskArray(task.children)
}
})
}
cleanupTaskArray(tasks.value)
}
// 收集被删除任务及其所有子任务的ID
const collectAllTaskIds = (task: Task): number[] => {
const ids = [task.id]
if (task.children && task.children.length > 0) {
task.children.forEach(child => {
ids.push(...collectAllTaskIds(child))
})
}
return ids
}
// Timer事件处理
function onTimerStarted(task: Task) {
showMessage(
`Demo 任务【${task.name}\n开始计时${new Date(task.timerStartTime).toLocaleString()}\n计时说明${task.timerStartDesc ? task.timerStartDesc : ''}`,
'info',
{ closable: true },
)
}
function onTimerStopped(task: Task) {
let msg = `Demo 任务【${task.name}`
if (task.timerStartTime) {
msg += `\n开始计时${new Date(task.timerStartTime).toLocaleString()}`
msg += `\n结束计时${new Date().toLocaleString()}`
if (task.timerStartDesc) msg += `\n计时说明${task.timerStartDesc}`
} else {
msg += `\n结束计时${new Date().toLocaleString()}`
}
showMessage(msg, 'info', { closable: true })
}
function taskDebug(item: any) {
// Placeholder for debugging
}
</script>
<template>
<div class="app-container">
<h1 class="page-title">
<div class="title-left">
<svg class="gantt-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="4" width="20" height="2" rx="1" fill="#409eff" />
<rect x="2" y="8" width="12" height="2" rx="1" fill="#67c23a" />
<rect x="2" y="12" width="16" height="2" rx="1" fill="#e6a23c" />
<rect x="2" y="16" width="8" height="2" rx="1" fill="#f56c6c" />
<rect x="2" y="20" width="14" height="2" rx="1" fill="#909399" />
<circle cx="22" cy="5" r="1" fill="#409eff" />
<circle cx="16" cy="9" r="1" fill="#67c23a" />
<circle cx="20" cy="13" r="1" fill="#e6a23c" />
<circle cx="12" cy="17" r="1" fill="#f56c6c" />
<circle cx="18" cy="21" r="1" fill="#909399" />
</svg>
Jordium Gantt Vue3 Demo
<span class="version-badge" style="cursor: pointer" @click="showVersionDrawer = true">{{
packageInfo.version
}}</span>
</div>
<div class="docs-links">
<a href="#github-docs" class="doc-link github-link" @click="handleGithubDocsClick">
<img class="doc-icon" src="./public/github.svg" alt="GitHub" />
</a>
<span class="docs-divider"></span>
<a href="#gitee-docs" class="doc-link gitee-link" @click="handleGiteeDocsClick">
<img class="doc-icon" src="./public/gitee.svg" alt="Gitee" />
</a>
</div>
</h1>
<VersionHistoryDrawer :visible="showVersionDrawer" @close="showVersionDrawer = false" />
<!-- 配置说明面板 - 可折叠 -->
<div class="config-panel" :class="{ collapsed: isConfigPanelCollapsed }">
<div class="config-header" @click="toggleConfigPanel">
<h3 class="config-title">
<svg
class="config-icon"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"
fill="currentColor"
/>
</svg>
{{ t.configDemo }}
</h3>
<button class="collapse-button" :class="{ collapsed: isConfigPanelCollapsed }">
<svg
class="collapse-icon"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7 10l5 5 5-5"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
<!-- 可折叠内容区域 -->
<transition name="config-content">
<div v-show="!isConfigPanelCollapsed" class="config-content">
<!-- TaskList 配置区域 -->
<div class="config-section">
<div class="section-header" @click="toggleTaskListConfig">
<div class="section-header-title">
<svg
class="section-icon"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M3 6h18v2H3V6zm0 5h18v2H3v-2zm0 5h18v2H3v-2z" fill="currentColor" />
</svg>
{{ t.taskListConfig.title }}
</div>
<button
class="section-collapse-button"
:class="{ collapsed: isTaskListConfigCollapsed }"
>
<svg
class="collapse-icon"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7 10l5 5 5-5"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
<!-- TaskList 配置内容 -->
<transition name="section-content">
<div v-show="!isTaskListConfigCollapsed" class="section-content">
<!-- 宽度配置 -->
<div class="subsection">
<h5 class="subsection-title">{{ t.taskListConfig.width.title }}</h5>
<!-- 单位切换器 -->
<div
class="width-unit-toggle"
style="margin-bottom: 12px; display: flex; align-items: center; gap: 16px"
>
<label class="taskbar-control">
<input v-model="widthUnit" type="radio" value="px" />
<span class="taskbar-label">
{{ t.taskListConfig.width.pixelsModel }}
</span>
</label>
<label class="taskbar-control">
<input v-model="widthUnit" type="radio" value="%" />
<span class="taskbar-label">{{ t.taskListConfig.width.percentageModel }}</span>
</label>
</div>
<!-- 像素值配置 -->
<div v-if="widthUnit === 'px'" class="width-controls">
<div class="width-control">
<label class="width-label">{{ t.taskListConfig.width.defaultWidth }}:</label>
<input
v-model.number="taskListWidth.defaultWidth"
type="number"
:min="taskListWidth.minWidth"
:max="taskListWidth.maxWidth"
step="10"
class="width-input"
/>
<span class="width-unit">px</span>
</div>
<div class="width-control">
<label class="width-label">{{ t.taskListConfig.width.minWidth }}:</label>
<input
v-model.number="taskListWidth.minWidth"
type="number"
min="280"
:max="taskListWidth.defaultWidth"
step="10"
class="width-input"
/>
<span class="width-unit">px</span>
</div>
<div class="width-control">
<label class="width-label">{{ t.taskListConfig.width.maxWidth }}:</label>
<input
v-model.number="taskListWidth.maxWidth"
type="number"
:min="taskListWidth.defaultWidth"
max="2000"
step="10"
class="width-input"
/>
<span class="width-unit">px</span>
</div>
</div>
<!-- 百分比配置 -->
<div v-else class="width-controls">
<div class="width-control">
<label class="width-label">{{ t.taskListConfig.width.defaultWidth }}:</label>
<input
v-model.number="widthPercentage.defaultWidth"
type="number"
:min="widthPercentage.minWidth"
:max="widthPercentage.maxWidth"
step="1"
class="width-input"
/>
<span class="width-unit">%</span>
</div>
<div class="width-control">
<label class="width-label">{{ t.taskListConfig.width.minWidth }}:</label>
<input
v-model.number="widthPercentage.minWidth"
type="number"
min="10"
:max="widthPercentage.defaultWidth"
step="1"
class="width-input"
/>
<span class="width-unit">%</span>
</div>
<div class="width-control">
<label class="width-label">{{ t.taskListConfig.width.maxWidth }}:</label>
<input
v-model.number="widthPercentage.maxWidth"
type="number"
:min="widthPercentage.defaultWidth"
max="80"
step="1"
class="width-input"
/>
<span class="width-unit">%</span>
</div>
</div>
</div>
<!-- 列配置 -->
<div class="subsection">
<h5 class="subsection-title">{{ t.taskListConfig.columns.title }}</h5>
<div class="column-controls">
<label v-for="column in availableColumns" :key="column.key" class="column-control">
<input
type="checkbox"
:checked="column.visible"
@change="toggleColumn(column.key, $event)"
/>
<span class="column-label">{{ (t as any)[column.key] || column.label }}</span>
</label>
</div>
</div>
</div>
</transition>
</div>
<!-- TaskBar配置区域 -->
<div class="config-section">
<div class="section-header" @click="toggleTaskBarConfig">
<div class="section-header-title">
<svg
class="section-icon"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect x="2" y="6" width="20" height="3" rx="1.5" fill="currentColor" />
<rect x="2" y="11" width="15" height="3" rx="1.5" fill="currentColor" />
<rect x="2" y="16" width="18" height="3" rx="1.5" fill="currentColor" />
</svg>
{{ t.taskBarConfig.title }}
</div>
<button
class="section-collapse-button"
:class="{ collapsed: isTaskBarConfigCollapsed }"
>
<svg
class="collapse-icon"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7 10l5 5 5-5"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
<!-- TaskBar 配置内容 -->
<transition name="section-content">
<div v-show="!isTaskBarConfigCollapsed" class="section-content">
<!-- 显示选项 -->
<div class="subsection">
<h5 class="subsection-title">{{ t.taskBarConfig.display.title }}</h5>
<div class="taskbar-controls">
<label class="taskbar-control">
<input v-model="taskBarOptions.showAvatar" type="checkbox" />
<span class="taskbar-label">{{ t.taskBarConfig.display.showAvatar }}</span>
</label>
<label class="taskbar-control">
<input v-model="taskBarOptions.showTitle" type="checkbox" />
<span class="taskbar-label">{{ t.taskBarConfig.display.showTitle }}</span>
</label>
<label class="taskbar-control">
<input v-model="taskBarOptions.showProgress" type="checkbox" />
<span class="taskbar-label">{{ t.taskBarConfig.display.showProgress }}</span>
</label>
</div>
</div>
<!-- 防误触配置 -->
<div class="subsection">
<h5 class="subsection-title">{{ t.taskBarConfig.mistouch.title }}</h5>
<div class="taskbar-advanced-controls">
<div class="control-row">
<label class="taskbar-control">
<input v-model="allowDragAndResize" type="checkbox" />
<span class="taskbar-label">
{{ t.taskBarConfig.mistouch.allowDragOnClick }}
</span>
</label>
<span class="control-hint">
{{ t.taskBarConfig.mistouch.allowDragOnClickHint }}
</span>
</div>
<div class="control-row">
<label class="control-label">
{{ t.taskBarConfig.mistouch.dragThreshold }}:
</label>
<input
v-model.number="taskBarOptions.dragThreshold"
type="number"
min="0"
max="20"
step="1"
class="control-input"
/>
<span class="control-hint">
{{ t.taskBarConfig.mistouch.dragThresholdHint }}
</span>
</div>
<div class="control-row">
<label class="control-label">
{{ t.taskBarConfig.mistouch.resizeHandleWidth }}:
</label>
<input
v-model.number="taskBarOptions.resizeHandleWidth"
type="number"
min="5"
max="15"
step="1"
class="control-input"
/>
<span class="control-hint">
{{ t.taskBarConfig.mistouch.resizeHandleWidthHint }}
</span>
</div>
<div class="control-row">
<label class="taskbar-control">
<input v-model="taskBarOptions.enableDragDelay" type="checkbox" />
<span class="taskbar-label">
{{ t.taskBarConfig.mistouch.enableDragDelay }}
</span>
</label>
<span class="control-hint">
{{ t.taskBarConfig.mistouch.enableDragDelayHint }}
</span>
</div>
<div v-if="taskBarOptions.enableDragDelay" class="control-row control-indent">
<label class="control-label">
{{ t.taskBarConfig.mistouch.dragDelayTime }}:
</label>
<input
v-model.number="taskBarOptions.dragDelayTime"
type="number"
min="50"
max="500"
step="50"
class="control-input"
/>
<span class="control-hint">
{{ t.taskBarConfig.mistouch.dragDelayTimeHint }}
</span>
</div>
</div>
</div>
</div>
</transition>
</div>
</div>
</transition>
</div>
<div class="gantt-wrapper">
<GanttChart
:tasks="tasks"
:milestones="milestones"
:toolbar-config="toolbarConfig"
:task-list-config="taskListConfig"
:task-bar-config="taskBarConfig"
:working-hours="workingHoursConfig"
:use-default-milestone-dialog="true"
:allow-drag-and-resize="allowDragAndResize"
:on-export-csv="handleCustomCsvExport"
:on-language-change="handleLanguageChange"
:on-theme-change="handleThemeChange"
@milestone-saved="handleMilestoneSaved"
@milestone-deleted="handleMilestoneDeleted"
@milestone-icon-changed="handleMilestoneIconChanged"
@add-task="handleAddTask"
@taskbar-drag-end="handleTaskbarDragOrResizeEnd"
@taskbar-resize-end="handleTaskbarDragOrResizeEnd"
@milestone-drag-end="handleMilestoneDragEnd"
@task-click="handleTaskClick"
@edit-task="task => showMessage(`进入任务编辑:${task.name}`)"
@close="() => showMessage('已关闭任务编辑', 'info')"
@timer-started="onTimerStarted"
@timer-stopped="onTimerStopped"
@predecessor-added="
e =>
showMessage(`Demo 任务[${e.targetTask.name}] 添加前置任务 [${e.newTask.name}]`, 'info')
"
@successor-added="
e =>
showMessage(`Demo 任务[${e.targetTask.name}] 添加后置任务 [${e.newTask.name}]`, 'info')
"
@task-deleted="handleTaskDeleteEvent"
@task-added="handleTaskAddEvent"
@task-updated="handleTaskUpdateEvent"
>
<template #custom-task-content="item">
<HtmlContent
:item="taskDebug(item)"
:task="item.task"
:type="item.type"
:style="item.dynamicStyles"
/>
</template>
</GanttChart>
</div>
<div class="license-info">
<p>MIT License @JORDIUM.COM</p>
</div>
<!-- MilestoneDialog用于新建/编辑里程碑 -->
<MilestoneDialog
:visible="showMilestoneDialog"
:milestone="currentMilestone"
@update:visible="showMilestoneDialog = $event"
@save="handleMilestoneSaveFromDialog"
@delete="handleMilestoneDeleted"
@close="showMilestoneDialog = false"
/>
<!-- Task Click Dialog - 显示任务详细信息 -->
<div v-if="showTaskClickDialog" class="task-click-dialog-overlay" @click="closeTaskClickDialog">
<div class="task-click-dialog" @click.stop>
<div class="task-click-dialog-header">
<h3>Task</h3>
<button class="close-button" @click="closeTaskClickDialog">×</button>
</div>
<div class="task-click-dialog-content">
<div v-if="clickedTask" class="task-properties">
<div
v-for="[key, value] in Object.entries(clickedTask)"
:key="key"
class="property-row"
>
<span class="property-key">{{ key }}:</span>
<span class="property-value">{{ formatPropertyValue(value) }}</span>
</div>
</div>
</div>
<div class="task-click-dialog-footer">
<button class="confirm-button" @click="closeTaskClickDialog">确认</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.app-container {
width: 100%;
height: 100%;
padding: 10px;
box-sizing: border-box;
background: var(--gantt-bg-secondary, #f0f2f5);
display: flex;
flex-direction: column;
}
/* TaskList列配置面板样式 - 可折叠 */
.config-panel {
background: var(--gantt-bg-primary, #ffffff);
border: 1px solid var(--gantt-border-color, #e4e7ed);
border-radius: 8px;
margin-bottom: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
overflow: hidden;
}
.config-panel:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.config-panel.collapsed {
border-radius: 8px;
}
.config-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
cursor: pointer;
user-select: none;
transition: background-color 0.2s ease;
border-bottom: 1px solid var(--gantt-border-color, #e4e7ed);
}
.config-panel.collapsed .config-header {
border-bottom: none;
}
.config-header:hover {
background-color: var(--gantt-hover-bg, #f8f9fa);
}
.config-content {
padding: 0 16px 16px;
overflow: hidden;
}
.collapse-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
background: transparent;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
color: var(--gantt-text-secondary, #666);
}
.collapse-button:hover {
background-color: var(--gantt-hover-bg, #e8f4fd);
color: var(--gantt-primary-color, #409eff);
}
.collapse-icon {
width: 20px;
height: 20px;
transition: transform 0.3s ease;
}
.collapse-button.collapsed .collapse-icon {
transform: rotate(-90deg);
}
/* 过渡动画 */
.config-content-enter-active,
.config-content-leave-active {
transition: all 0.3s ease;
overflow: hidden;
}
.config-content-enter-from,
.config-content-leave-to {
height: 0;
opacity: 0;
}
.config-content-enter-to,
.config-content-leave-from {
opacity: 1;
}
.config-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--gantt-text-primary, #333);
display: flex;
align-items: center;
gap: 8px;
}
.config-icon {
width: 20px;
height: 20px;
color: var(--gantt-primary-color, #409eff);
}
.config-section {
margin-bottom: 16px;
}
/* 折叠区域样式 */
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 0 8px 0;
margin-bottom: 16px;
background-color: transparent;
border-bottom: 2px solid var(--gantt-primary-color, #409eff);
cursor: pointer;
transition: all 0.3s ease;
user-select: none;
}
.section-header:hover {
border-bottom-color: var(--gantt-primary-color-light, #66b3ff);
}
.section-header-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: var(--gantt-text-primary, #333);
}
.section-collapse-button {
background: none;
border: none;
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
color: var(--gantt-text-secondary, #666);
transition: transform 0.3s ease, color 0.2s ease;
}
.section-collapse-button:hover {
color: var(--gantt-primary-color, #409eff);
}
.section-collapse-button.collapsed {
transform: rotate(-90deg);
}
.section-content-enter-active,
.section-content-leave-active {
transition: all 0.3s ease;
overflow: hidden;
}
.section-content-enter-from,
.section-content-leave-to {
max-height: 0;
opacity: 0;
margin-bottom: 0;
}
.section-content-enter-to,
.section-content-leave-from {
max-height: 2000px;
opacity: 1;
margin-bottom: 16px;
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: var(--gantt-text-primary, #333);
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 2px solid var(--gantt-primary-color, #409eff);
}
.section-icon {
width: 18px;
height: 18px;
color: var(--gantt-primary-color, #409eff);
}
/* 子区域样式 */
.subsection {
margin-bottom: 16px;
padding-left: 12px;
border-left: 2px solid var(--gantt-border-color, #e4e7ed);
}
.subsection-title {
font-size: 13px;
font-weight: 600;
color: var(--gantt-text-secondary, #666);
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 6px;
}
.subsection-title::before {
content: '';
display: inline-block;
width: 4px;
height: 4px;
border-radius: 50%;
background-color: var(--gantt-primary-color, #409eff);
}
.width-controls {
display: flex;
flex-direction: row;
gap: 12px;
}
.width-control {
display: flex;
align-items: center;
gap: 8px;
}
.width-label {
flex: 0 0 100px;
font-size: 13px;
color: var(--text-secondary);
}
.width-input {
flex: 1;
padding: 4px 8px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--input-background, #fff);
color: var(--text-primary);
font-size: 13px;
max-width: 100px;
}
.width-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
.width-unit {
flex: 0 0 20px;
font-size: 12px;
color: var(--text-tertiary);
}
.column-controls {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.column-control {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 6px 12px;
border-radius: 6px;
transition: all 0.2s ease;
background: var(--gantt-bg-secondary, #f8f9fa);
border: 1px solid transparent;
}
.column-control:hover {
background: var(--gantt-hover-bg, #e8f4fd);
border-color: var(--gantt-primary-color, #409eff);
}
.column-control input[type='checkbox'] {
width: 16px;
height: 16px;
cursor: pointer;
accent-color: var(--gantt-primary-color, #409eff);
}
.column-label {
font-size: 14px;
font-weight: 500;
color: var(--gantt-text-primary, #333);
user-select: none;
transition: color 0.2s ease;
}
.column-control:hover .column-label {
color: var(--gantt-primary-color, #409eff);
}
/* TaskBar配置样式 */
.taskbar-controls {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: flex-start;
}
.taskbar-control {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 6px 12px;
border-radius: 6px;
transition: all 0.2s ease;
background: var(--gantt-bg-secondary, #f8f9fa);
border: 1px solid transparent;
}
.taskbar-control:hover {
background: var(--gantt-hover-bg, #e8f4fd);
border-color: var(--gantt-primary-color, #409eff);
}
.taskbar-control input[type='checkbox'] {
width: 16px;
height: 16px;
cursor: pointer;
accent-color: var(--gantt-primary-color, #409eff);
}
.taskbar-label {
font-size: 14px;
font-weight: 500;
color: var(--gantt-text-primary, #333);
user-select: none;
transition: color 0.2s ease;
}
.taskbar-control:hover .taskbar-label {
color: var(--gantt-primary-color, #409eff);
}
/* TaskBar 高级配置样式 */
.taskbar-advanced-controls {
display: flex;
flex-direction: column;
gap: 12px;
}
.control-row {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
background: var(--gantt-bg-secondary, #f8f9fa);
border-radius: 6px;
border: 1px solid var(--gantt-border-color, #e4e7ed);
}
.control-indent {
margin-left: 24px;
}
.control-label {
flex: 0 0 140px;
font-size: 13px;
font-weight: 500;
color: var(--gantt-text-secondary, #666);
}
.control-input {
flex: 0 0 80px;
padding: 4px 8px;
border: 1px solid var(--gantt-border-color, #e4e7ed);
border-radius: 4px;
background: var(--gantt-bg-primary, #fff);
color: var(--gantt-text-primary, #333);
font-size: 13px;
transition: all 0.2s ease;
}
.control-input:focus {
outline: none;
border-color: var(--gantt-primary-color, #409eff);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
.control-hint {
flex: 1;
font-size: 12px;
color: var(--gantt-text-muted, #999);
font-style: italic;
}
.taskbar-field-control {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 6px;
background: var(--gantt-bg-secondary, #f8f9fa);
border: 1px solid var(--gantt-border-color, #e4e7ed);
min-width: 300px;
}
.field-label {
font-size: 13px;
font-weight: 500;
color: var(--gantt-text-secondary, #666);
white-space: nowrap;
}
.field-select {
flex: 1;
padding: 4px 8px;
border: 1px solid var(--gantt-border-color, #e4e7ed);
border-radius: 4px;
background: var(--gantt-bg-primary, #fff);
color: var(--gantt-text-primary, #333);
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
}
.field-select:focus {
outline: none;
border-color: var(--gantt-primary-color, #409eff);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
.page-title {
margin: 20px 0;
font-size: 1.8rem;
font-weight: 600;
color: var(--gantt-text-primary, #333);
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.title-left {
display: flex;
align-items: center;
gap: 12px;
}
.gantt-icon {
width: 32px;
height: 32px;
flex-shrink: 0;
transition: all 0.3s ease;
}
.gantt-icon:hover {
transform: scale(1.05);
}
.version-badge {
display: inline-block;
background: linear-gradient(135deg, #409eff 0%, #36d1dc 50%, #667eea 100%);
color: white;
font-size: 0.7rem;
font-weight: 700;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
padding: 6px 12px;
border-radius: 16px;
line-height: 1;
margin-left: 8px;
position: relative;
overflow: hidden;
text-transform: uppercase;
letter-spacing: 0.5px;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow:
0 0 20px rgba(64, 158, 255, 0.3),
0 4px 15px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.version-badge::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transition: left 0.6s ease;
}
.version-badge:hover {
transform: scale(1.05) translateY(-1px);
box-shadow:
0 0 30px rgba(64, 158, 255, 0.5),
0 8px 25px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
background: linear-gradient(135deg, #4dabf7 0%, #40c9ff 50%, #74c0fc 100%);
}
.version-badge:hover::before {
left: 100%;
}
/* 科技感呼吸动画 */
@keyframes glow-pulse {
0%,
100% {
box-shadow:
0 0 20px rgba(64, 158, 255, 0.3),
0 4px 15px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
50% {
box-shadow:
0 0 30px rgba(64, 158, 255, 0.5),
0 4px 15px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
}
.gantt-wrapper {
flex: 1 1 0%;
min-height: 0;
min-width: 0;
display: flex;
align-items: flex-start;
justify-content: flex-start;
}
.license-info {
text-align: center;
color: var(--gantt-text-muted, #c0c4cc);
font-size: 14px;
font-weight: 400;
letter-spacing: 0.5px;
}
/* 全局暗色主题支持 */
:global(html[data-theme='dark']) {
background: #1e1e1e !important;
}
:global(html[data-theme='dark']) body {
background: #1e1e1e !important;
color: #e5e5e5 !important;
}
/* 暗色主题下的配置面板样式 */
:global(html[data-theme='dark']) .config-panel {
background: var(--gantt-bg-primary, #2d3748);
border-color: var(--gantt-border-color, #4a5568);
}
:global(html[data-theme='dark']) .config-title {
color: var(--gantt-text-primary, #e2e8f0);
}
:global(html[data-theme='dark']) .section-title {
color: var(--gantt-text-primary, #e2e8f0);
border-bottom-color: var(--gantt-primary-color, #66b3ff);
}
:global(html[data-theme='dark']) .section-header {
border-bottom-color: var(--gantt-primary-color, #66b3ff);
}
:global(html[data-theme='dark']) .section-header:hover {
border-bottom-color: var(--gantt-primary-color-light, #74c0fc);
}
:global(html[data-theme='dark']) .section-header-title {
color: var(--gantt-text-primary, #e2e8f0);
}
:global(html[data-theme='dark']) .section-collapse-button {
color: var(--gantt-text-secondary, #a0aec0);
}
:global(html[data-theme='dark']) .section-collapse-button:hover {
color: var(--gantt-primary-color, #66b3ff);
}
:global(html[data-theme='dark']) .section-icon {
color: var(--gantt-primary-color, #66b3ff);
}
:global(html[data-theme='dark']) .subsection {
border-left-color: var(--gantt-border-color, #4a5568);
}
:global(html[data-theme='dark']) .subsection-title {
color: var(--gantt-text-secondary, #a0aec0);
}
:global(html[data-theme='dark']) .subsection-title::before {
background-color: var(--gantt-primary-color, #66b3ff);
}
:global(html[data-theme='dark']) .column-control {
background: var(--gantt-bg-secondary, #1a202c);
}
:global(html[data-theme='dark']) .column-control:hover {
background: var(--gantt-hover-bg, #2d3748);
}
:global(html[data-theme='dark']) .column-label {
color: var(--gantt-text-primary, #e2e8f0);
}
:global(html[data-theme='dark']) .column-control:hover .column-label {
color: var(--gantt-primary-color, #66b3ff);
}
/* 暗色主题下的TaskBar配置样式 */
:global(html[data-theme='dark']) .taskbar-control {
background: var(--gantt-bg-secondary, #1a202c);
}
:global(html[data-theme='dark']) .taskbar-control:hover {
background: var(--gantt-hover-bg, #2d3748);
}
:global(html[data-theme='dark']) .taskbar-label {
color: var(--gantt-text-primary, #e2e8f0);
}
:global(html[data-theme='dark']) .taskbar-control:hover .taskbar-label {
color: var(--gantt-primary-color, #66b3ff);
}
/* 暗色主题下的 TaskBar 高级配置 */
:global(html[data-theme='dark']) .control-row {
background: var(--gantt-bg-secondary, #1a202c);
border-color: var(--gantt-border-color, #4a5568);
}
:global(html[data-theme='dark']) .control-label {
color: var(--gantt-text-secondary, #a0aec0);
}
:global(html[data-theme='dark']) .control-input {
background: var(--gantt-bg-primary, #2d3748);
color: var(--gantt-text-primary, #e2e8f0);
border-color: var(--gantt-border-color, #4a5568);
}
:global(html[data-theme='dark']) .control-hint {
color: var(--gantt-text-muted, #718096);
}
:global(html[data-theme='dark']) .taskbar-field-control {
background: var(--gantt-bg-secondary, #1a202c);
border-color: var(--gantt-border-color, #4a5568);
}
:global(html[data-theme='dark']) .field-label {
color: var(--gantt-text-secondary, #a0aec0);
}
:global(html[data-theme='dark']) .field-select {
background: var(--gantt-bg-primary, #2d3748);
color: var(--gantt-text-primary, #e2e8f0);
border-color: var(--gantt-border-color, #4a5568);
}
/* 暗色主题下的折叠面板样式 */
:global(html[data-theme='dark']) .config-header {
border-bottom-color: var(--gantt-border-color, #4a5568);
}
:global(html[data-theme='dark']) .config-header:hover {
background-color: var(--gantt-hover-bg, #2d3748);
}
:global(html[data-theme='dark']) .collapse-button {
color: var(--gantt-text-secondary, #a0aec0);
}
:global(html[data-theme='dark']) .collapse-button:hover {
background-color: var(--gantt-hover-bg, #2d3748);
color: var(--gantt-primary-color, #66b3ff);
}
/* 暗黑模式下的版本标签 */
:global(html[data-theme='dark']) .version-badge {
background: linear-gradient(135deg, #1a73e8 0%, #00bcd4 50%, #3f51b5 100%);
box-shadow:
0 0 25px rgba(102, 177, 255, 0.4),
0 4px 15px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
:global(html[data-theme='dark']) .version-badge:hover {
background: linear-gradient(135deg, #2196f3 0%, #00e5ff 50%, #5c6bc0 100%);
box-shadow:
0 0 35px rgba(102, 177, 255, 0.6),
0 8px 25px rgba(0, 0, 0, 0.5),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
border-color: rgba(102, 177, 255, 0.5);
}
/* 暗黑模式的呼吸动画 */
@keyframes glow-pulse-dark {
0%,
100% {
box-shadow:
0 0 25px rgba(102, 177, 255, 0.4),
0 4px 15px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
50% {
box-shadow:
0 0 35px rgba(102, 177, 255, 0.6),
0 4px 15px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
}
.docs-links {
display: flex;
align-items: center;
gap: 8px;
}
.docs-divider {
display: inline-block;
width: 1px;
height: 24px;
border-left: 1.5px dashed #bbb;
margin: 0 8px;
background: none;
}
.doc-link {
display: flex;
align-items: center;
gap: 6px;
color: var(--gantt-text-primary, #333333);
text-decoration: none;
font-size: 1rem;
font-weight: 500;
transition: color 0.2s ease;
padding: 4px 8px;
border-radius: 4px;
}
.doc-link:hover {
color: var(--gantt-text-primary, #333333);
background-color: rgba(0, 0, 0, 0.1);
}
.doc-link:nth-child(2) {
color: #c71d23;
}
.doc-link:nth-child(2):hover {
color: #a91b1b;
background-color: rgba(199, 29, 35, 0.1);
}
.doc-icon {
width: 24px;
height: 24px;
flex-shrink: 0;
transition: filter 0.2s ease;
}
/* GitHub 图标样式 - 黑色 */
.github-link .doc-icon {
filter: brightness(0) saturate(100%);
}
.github-link:hover .doc-icon {
filter: brightness(0) saturate(100%) invert(20%) sepia(15%) saturate(1500%) hue-rotate(200deg);
}
/* Gitee 图标样式 - 红色 */
.gitee-link .doc-icon {
filter: brightness(0) saturate(100%) invert(20%) sepia(100%) saturate(2000%) hue-rotate(350deg)
brightness(0.8);
}
.gitee-link:hover .doc-icon {
filter: brightness(0) saturate(100%) invert(15%) sepia(100%) saturate(2500%) hue-rotate(350deg)
brightness(0.7);
}
/* 移除旧的基于 SVG color 的样式,现在使用 filter */
/* 暗黑模式下覆盖所有链接样式 */
:global(html[data-theme='dark']) .doc-link {
color: #ffffff;
}
:global(html[data-theme='dark']) .doc-link:hover {
color: #ffffff;
background-color: rgba(255, 255, 255, 0.1);
}
:global(html[data-theme='dark']) .doc-link:nth-child(2) {
color: #ffffff !important;
}
:global(html[data-theme='dark']) .doc-link:nth-child(2):hover {
color: #ffffff !important;
background-color: rgba(199, 29, 35, 0.1);
}
/* 暗黑模式下图标样式 */
:global(html[data-theme='dark']) .github-link .doc-icon {
filter: brightness(0) saturate(100%) invert(100%);
}
:global(html[data-theme='dark']) .github-link:hover .doc-icon {
filter: brightness(0) saturate(100%) invert(70%) sepia(50%) saturate(2000%) hue-rotate(190deg)
brightness(1.2);
}
:global(html[data-theme='dark']) .gitee-link .doc-icon {
filter: brightness(0) saturate(100%) invert(45%) sepia(100%) saturate(1500%) hue-rotate(340deg)
brightness(1.1);
}
:global(html[data-theme='dark']) .gitee-link:hover .doc-icon {
filter: brightness(0) saturate(100%) invert(50%) sepia(100%) saturate(1800%) hue-rotate(340deg)
brightness(1.2);
}
/* Task Click Dialog 样式 */
.task-click-dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.task-click-dialog {
background: var(--gantt-bg-primary, #ffffff);
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
width: 600px;
max-width: 90vw;
max-height: 80vh;
display: flex;
flex-direction: column;
}
.task-click-dialog-header {
padding: 20px;
border-bottom: 1px solid var(--gantt-border-color, #e4e7ed);
display: flex;
justify-content: space-between;
align-items: center;
}
.task-click-dialog-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--gantt-text-primary, #303133);
}
.close-button {
background: none;
border: none;
font-size: 28px;
color: var(--gantt-text-secondary, #909399);
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.3s;
}
.close-button:hover {
background: var(--gantt-bg-hover, #f5f7fa);
color: var(--gantt-text-primary, #303133);
}
.task-click-dialog-content {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.task-properties {
display: flex;
flex-direction: column;
gap: 12px;
}
.property-row {
display: grid;
grid-template-columns: 150px 1fr;
gap: 12px;
padding: 8px 0;
border-bottom: 1px solid var(--gantt-border-light, #f0f0f0);
}
.property-row:last-child {
border-bottom: none;
}
.property-key {
font-weight: 600;
color: var(--gantt-text-secondary, #606266);
word-break: break-word;
}
.property-value {
color: var(--gantt-text-primary, #303133);
word-break: break-all;
white-space: pre-wrap;
font-family: 'Courier New', monospace;
font-size: 13px;
}
.task-click-dialog-footer {
padding: 16px 20px;
border-top: 1px solid var(--gantt-border-color, #e4e7ed);
display: flex;
justify-content: flex-end;
}
.confirm-button {
padding: 8px 20px;
background: var(--gantt-primary-color, #409eff);
color: #ffffff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s;
}
.confirm-button:hover {
background: var(--gantt-primary-color-hover, #66b1ff);
}
.confirm-button:active {
background: var(--gantt-primary-color-active, #3a8ee6);
}
/* 暗黑模式适配 */
:global(html[data-theme='dark']) .task-click-dialog {
background: var(--gantt-bg-primary, #1a1a1a);
}
:global(html[data-theme='dark']) .task-click-dialog-header h3 {
color: var(--gantt-text-primary, #e0e0e0);
}
:global(html[data-theme='dark']) .close-button {
color: var(--gantt-text-secondary, #b0b0b0);
}
:global(html[data-theme='dark']) .close-button:hover {
background: var(--gantt-bg-hover, #2a2a2a);
color: var(--gantt-text-primary, #e0e0e0);
}
:global(html[data-theme='dark']) .property-key {
color: var(--gantt-text-secondary, #b0b0b0);
}
:global(html[data-theme='dark']) .property-value {
color: var(--gantt-text-primary, #e0e0e0);
}
</style>