From 4fff9d20f41c524d1fb54eeba0b513130166320d Mon Sep 17 00:00:00 2001 From: "LINING-PC\\lining" Date: Sat, 1 Nov 2025 15:46:12 +0800 Subject: [PATCH] v1.4.2 - enhancements --- CHANGELOG.md | 36 + README-EN.md | 2607 +++++++++++++---- README.md | 2594 ++++++++++++---- demo/App.vue | 1500 ++++++---- demo/VersionHistoryDrawer.vue | 2 +- demo/data.json | 12 +- demo/version-history.json | 38 + npm-demo/.gitignore | 6 + npm-demo/README.md | 63 + npm-demo/index.html | 12 + npm-demo/package-lock.json | 1302 ++++++++ npm-demo/package.json | 19 + npm-demo/src/App.vue | 15 + npm-demo/src/components/GanttTest.vue | 345 +++ npm-demo/src/components/StubGantt.vue | 29 + .../src/components/ThumbnailHtmlContent.vue | 39 + npm-demo/src/main.js | 9 + npm-demo/vite.config.js | 6 + package-lock.json | 227 +- package.json | 2 +- scripts/generate-css-export.js | 46 +- src/components/DatePicker.vue | 110 +- src/components/GanttChart.vue | 1009 +++++-- src/components/MilestoneDialog.vue | 35 +- src/components/MilestonePoint.vue | 409 ++- src/components/TaskBar.vue | 1170 +++++++- src/components/TaskDrawer.vue | 22 +- src/components/TaskList.vue | 23 +- src/components/TaskRow.vue | 11 +- src/components/Timeline.vue | 1100 +++++-- src/composables/useI18n.ts | 72 +- src/index.ts | 1 + src/models/classes/Task.ts | 3 + src/models/configs/TaskBarConfig.ts | 22 + src/models/configs/TaskListConfig.ts | 46 +- src/models/types/TimelineScale.ts | 8 +- vite.config.lib.ts | 10 +- 37 files changed, 10325 insertions(+), 2635 deletions(-) create mode 100644 npm-demo/.gitignore create mode 100644 npm-demo/README.md create mode 100644 npm-demo/index.html create mode 100644 npm-demo/package-lock.json create mode 100644 npm-demo/package.json create mode 100644 npm-demo/src/App.vue create mode 100644 npm-demo/src/components/GanttTest.vue create mode 100644 npm-demo/src/components/StubGantt.vue create mode 100644 npm-demo/src/components/ThumbnailHtmlContent.vue create mode 100644 npm-demo/src/main.js create mode 100644 npm-demo/vite.config.js create mode 100644 src/models/configs/TaskBarConfig.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 143878b..9ea069c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,42 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.4.2] - 2025-11-01 + +### Added +- TaskBar增加头像显示 +- TaskBar展示信息显示可配置 +- TaskBar长按聚焦高亮 +- Timeline Range自动优化计算, 提升显示效果 +- TaskListConfig支持像素和百分比设置方式 +- 支持拖拽和拉伸TaskBar/Milestone接近边界自动扩充时间轴范围 +- 支持通过属性配置限制TaskBar/Milestone拖拽和拉伸 +- 暴露更多外部可以使用属性和事件 +- 增加NPM包应用示例 +- Added avatar display on TaskBar +- Configurable display information on TaskBar +- Long-press to focus and highlight TaskBar +- Automatically optimized Timeline Range calculation for better visual performance +- TaskListConfig now supports both pixel and percentage-based sizing +- Support dragging and stretching TaskBar/Milestone to automatically expand timeline range near boundaries +- Support property-based configuration to limit dragging and stretching of TaskBar/Milestone +- Expose more external properties and events +- Added NPM package application examples + +### Changed +- 客制化多语言扩展支持 +- 说明文档更新,可读性更强 +- 增强Timeline时间轴的拖拽体验,增加防抖保护 +- Custom multi-language extension support +- Documentation updated for better readability +- Enhance dragging experience on Timeline, adding debounce protection + +### Fixed +- SonarQube代码质量检查问题修改 +- 缺陷修复 +- Modification of SonarQube code quality inspection issues +- Bugfix + ## [1.4.1] - 2025-10-26 ### Fixed diff --git a/README-EN.md b/README-EN.md index dcb62fa..ef96aa9 100644 --- a/README-EN.md +++ b/README-EN.md @@ -1,445 +1,117 @@ -# jordium-gantt-vue3 logo jordium-gantt-vue3 +# jordium-gantt-vue3 logo jordium-gantt-vue3 -**🌐 Languages**: [📖 English Documentation](./README-EN.md) | [📖 中文文档](./README.md) +

+ + npm version + + + MIT License + + + Vue.js + + + TypeScript + + + Nodejs + +

-[![npm version](https://img.shields.io/npm/v/jordium-gantt-vue3.svg?cacheBust=1)](https://www.npmjs.com/package/jordium-gantt-vue3) -[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) -[![Vue.Js](https://img.shields.io/badge/Vue.js->=3.5.13-4FC08D?style=flat-square&logo=vue.js&logoColor=white)](https://vuejs.org/) -[![TypeScript](https://img.shields.io/badge/TypeScript->=5.8.3-3178C6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) -[![Node.js](https://img.shields.io/badge/Node.js->=16.0.0-339933?style=flat-square&logo=node.js&logoColor=white)](https://nodejs.org/) +

+ 中文 | + English +

-> Modern Vue 3 Gantt chart component library providing complete solutions for project management and task scheduling +

A modern Vue 3 Gantt chart component library providing complete solutions for project management and task scheduling

-## 🌐 Online Demo +

+ + 📱 Live Demo + +   |   + + 📦 GitHub + +   |   + + 📚 npm + +

-🎯 **[Try Github Live Demo →](https://nelson820125.github.io/jordium-gantt-vue3/)** -Recommend to use DOVE VPN for fast and stable access if needed. (Note: Please use VPN resources legally.) +--- -*The online demo includes full feature showcase: task management, milestones, theme switching, internationalization, etc.* +## ✨ Introduction -## 🎨 Theme Support +jordium-gantt-vue3 is a modern Gantt chart component built with Vue 3 and TypeScript, designed specifically for project management and task scheduling scenarios. It provides rich interactive features, flexible configuration options, and elegant visual effects. -### Light Theme +### Core Features -![Light Theme](design/screenshots/light-theme-en.png) +- 📊 **Feature Complete** - Task management, milestones, dependencies, progress tracking +- 🎨 **Theme System** - Built-in light/dark themes with customizable styles +- 🖱️ **Smooth Interaction** - Drag & resize, zoom, double-click edit, context menu +- 🌍 **Internationalization** - Built-in Chinese/English with extensible language support +- ⚡ **High Performance** - Virtual scrolling, lazy loading for handling massive data +- 💎 **Type Safe** - Full TypeScript support -### Dark Theme +### Preview -![Dark Theme](design/screenshots/dark-theme-en.png) +#### Light Theme -## 🚀 Features +Light Theme -- 📊 **Complete Functionality**: Task management, milestone tracking, dependency relationships, progress visualization -- 🎨 **Theme Switching**: Built-in light/dark themes with customizable theme variables -- 🖱️ **Rich Interactions**: Drag adjustment, resizing, double-click editing, context menus -- 🌍 **Internationalization**: Built-in Chinese/English support with custom language pack capability -- 📱 **Responsive**: Desktop and mobile compatible with smooth touch experience -- ⚡ **High Performance**: Virtual scrolling, lazy loading, optimized rendering -- 🔧 **Extensible**: Rich API interfaces supporting custom components and events -- 💎 **Type Safe**: Complete TypeScript support for better development experience +#### Dark Theme + +Dark Theme + +--- ## 📦 Installation +Install using your preferred package manager: + ```bash # npm npm install jordium-gantt-vue3 -# yarn +# yarn yarn add jordium-gantt-vue3 # pnpm pnpm add jordium-gantt-vue3 ``` -## 📄 License - -[MIT License](./LICENSE) © 2025 JordiUM - --- -## 📁 Project Structure +## 🚀 Quick Start -``` -jordium-gantt-vue3/ -├── src/ # Source code directory -│ ├── components/ # Core Vue components -│ │ ├── GanttChart.vue # Main entry component -│ │ ├── TaskList.vue # Task list component -│ │ ├── Timeline.vue # Timeline component -│ │ ├── TaskBar.vue # Task bar component -│ │ ├── TaskDrawer.vue # Task edit drawer -│ │ ├── TaskContextMenu.vue # Task context menu -│ │ ├── GanttToolbar.vue # Toolbar component -│ │ ├── MilestonePoint.vue # Milestone point -│ │ ├── MilestoneDialog.vue # Milestone dialog -│ │ ├── DatePicker.vue # Date picker -│ │ └── ... # Other components -│ ├── models/ # Data models and configurations -│ │ ├── classes/ # Class definitions -│ │ │ ├── Task.ts # Task model -│ │ │ ├── Milestone.ts # Milestone model -│ │ │ └── Language.ts # Language configuration -│ │ ├── configs/ # Configuration interfaces -│ │ │ ├── TimelineConfig.ts # Timeline configuration -│ │ │ └── ToolbarConfig.ts # Toolbar configuration -│ │ └── types/ # Type definitions -│ │ └── TimelineScale.ts # Timeline scale types -│ ├── composables/ # Vue composable functions -│ │ ├── useI18n.ts # Internationalization utilities -│ │ └── useMessage.ts # Message utilities -│ ├── styles/ # Style files -│ │ ├── app.css # Main styles -│ │ └── theme-variables.css # Theme variables -│ ├── utils/ # Utility functions -│ │ └── predecessorUtils.ts # Predecessor utilities -│ └── index.ts # Export entry -├── demo/ # Development demo & interactive showcase -│ ├── App.vue # Demo application main component -│ ├── data.json # Demo data (includes clinical trial examples) -│ ├── main.ts # Demo application entry -│ └── ... # Other demo files -├── packageDemo/ # npm package integration demo -├── dist/ # Build output directory -├── docs/ # Documentation -├── design/ # Design resources and screenshots -│ └── screenshots/ # Theme screenshots -├── public/ # Public static resources -│ └── assets/ # Static asset files -├── README.md # Chinese documentation -├── README-EN.md # English documentation -├── package.json # Project configuration -├── vite.config.ts # Vite development configuration -├── vite.config.lib.ts # Vite library build configuration -├── tsconfig.json # TypeScript configuration -└── ... # Other configuration files and metadata -``` +### Import Component -### Directory Description - -- **`src/components/`**: Core Vue components containing all Gantt chart functionality -- **`src/models/`**: Data models, type definitions and configuration interfaces -- **`src/composables/`**: Vue 3 composable functions providing reusable logic -- **`src/styles/`**: Style files including theme system and CSS variables -- **`src/utils/`**: Utility functions for business logic and data transformation -- **`demo/`**: Local development and feature demonstration with complete interactive pages and clinical trial sample data -- **`packageDemo/`**: Simulates npm package integration in external projects -- **`dist/`**: Build output directory for npm publishing or static sites -- **`docs/`**: Project documentation including deployment guides and API references - -## 🔧 API Reference - -### GanttChart Properties - -| Property | Type | Default | Description | -|----------|------|---------|-------------| -| `tasks` | `Task[]` | `[]` | Task data array | -| `milestones` | `Task[]` | `[]` | Milestone data array | -| `editComponent` | `any` | - | Custom edit component | -| `useDefaultDrawer` | `boolean` | `true` | Use default edit drawer | -| `showToolbar` | `boolean` | `true` | Show toolbar | -| `toolbarConfig` | `ToolbarConfig` | `{}` | Toolbar configuration | -| `taskListConfig` | `TaskListConfig` | `{}` | Task list configuration (including default width, min/max width limits, etc.) | -| `localeMessages` | `Partial` | - | Custom locale messages | -| `workingHours` | `WorkingHours` | - | Working hours configuration | -| `onTaskDoubleClick` | `(task: Task) => void` | - | Task double-click event callback | -| `onTaskDelete` | `(task: Task, deleteChildren?: boolean) => void` | - | Task delete event callback | -| `onTaskUpdate` | `(task: Task) => void` | - | Task update event callback | -| `onTaskAdd` | `(task: Task) => void` | - | Task add event callback | -| `onMilestoneSave` | `(milestone: Task) => void` | - | Milestone save event callback | -| `onMilestoneDelete` | `(milestoneId: number) => void` | - | Milestone delete event callback | -| `onMilestoneIconChange` | `(milestoneId: number, icon: string) => void` | - | Milestone icon change event callback | -| `onAddTask` | `() => void` | - | Add task toolbar event callback | -| `onAddMilestone` | `() => void` | - | Add milestone toolbar event callback | -| `onTodayLocate` | `() => void` | - | Locate today toolbar event callback | -| `onExportCsv` | `() => boolean \| void` | - | Export CSV toolbar event callback | -| `onExportPdf` | `() => void` | - | Export PDF toolbar event callback | -| `onLanguageChange` | `(lang: 'zh-CN' \| 'en-US') => void` | - | Language change toolbar event callback | -| `onThemeChange` | `(isDark: boolean) => void` | - | Theme change toolbar event callback | -| `onFullscreenChange` | `(isFullscreen: boolean) => void` | - | Fullscreen change toolbar event callback | - -### GanttChart Events - -| Event | Parameters | Description | -|--------------------|----------------------------|------------------------------------| -| `taskbar-drag-end` | `task: Task` | Task bar drag end | -| `taskbar-resize-end` | `task: Task` | Task bar resize end | -| `milestone-drag-end` | `milestone: Task` | Milestone drag end | -| `predecessor-added`| `{ targetTask, newTask }` | Triggered after adding predecessor.
Parameters:
• `targetTask`: The task to which a predecessor was added (Task object)
• `newTask`: The newly added predecessor task (Task object) | -| `successor-added` | `{ targetTask, newTask }` | Triggered after adding successor.
Parameters:
• `targetTask`: The task to which a successor was added (Task object)
• `newTask`: The newly added successor task (Task object) | -| `task-deleted` | `{ task }` | Triggered after deleting a task | -| `task-added` | `{ task }` | Triggered after creating a task | -| `task-updated` | `{ task }` | Triggered after updating a task | - -#### Timer Event Usage Example +Import the `GanttChart` component and styles: ```vue - - - ``` -#### Task Event Usage Example +> **Tip**: The style file only needs to be imported once in your project. It's recommended to import it in `main.ts` or the root component. + +### First Example + +Create your first Gantt chart: ```vue - - - -``` - -### Data Types - -#### Core Types (src/models/classes) - -**Task Type** -```typescript -export interface Task { - id: number // Unique task ID - name: string // Task name - predecessor?: number[] // Predecessor task ID array - assignee?: string // Assignee - startDate?: string // Start date (ISO string) - endDate?: string // End date (ISO string) - progress?: number // Progress percentage 0-100 - estimatedHours?: number // Estimated hours (supports decimal, up to 2 decimal places) - actualHours?: number // Actual hours (supports decimal, up to 2 decimal places) - parentId?: number // Parent task ID - children?: Task[] // Subtask array - collapsed?: boolean // Collapsed state - isParent?: boolean // Is parent task - type?: string // Task type (e.g. task, story, milestone) - description?: string // Task description - icon?: string // Icon - level?: number // Level - // Timer related fields - isTimerRunning?: boolean // Is timer running - timerStartTime?: number // Timer start timestamp - timerEndTime?: number // Timer end timestamp - timerStartDesc?: string // Timer start description - timerElapsedTime?: number // Accumulated timer duration (seconds) -} -``` - -**Milestone Type** -```typescript -// Milestone is actually a special usage of Task type -// Task object with type: 'milestone' property -interface Milestone extends Task { - type: 'milestone' // Must be 'milestone' - startDate: string // Milestone date (required) - endDate?: string // End date (optional, usually same as startDate) -} -``` - -**Language Type** -```typescript -type Language = 'zh' | 'en' // Supported language types -type Locale = 'zh-CN' | 'en-US' // Complete language locale identifiers -``` - -#### Configuration Types (src/models/configs) - -**TimelineConfig** -```typescript -interface TimelineConfig { - startDate: Date // Timeline start date - endDate: Date // Timeline end date - zoomLevel: number // Zoom level -} -``` - -**ToolbarConfig** -```typescript -interface ToolbarConfig { - showAddTask?: boolean // Show add task button - showAddMilestone?: boolean // Show add milestone button - showTodayLocate?: boolean // Show locate today button - showExportCsv?: boolean // Show export CSV button - showExportPdf?: boolean // Show export PDF button - showLanguage?: boolean // Show language switch button - showTheme?: boolean // Show theme switch button - showFullscreen?: boolean // Show fullscreen toggle button - showTimeScale?: boolean // Show time scale toggle buttons (Day|Week|Month) -} -``` - -**TaskListConfig** -```typescript -interface TaskListConfig { - columns?: TaskListColumnConfig[] // Column configuration array - showAllColumns?: boolean // Show all columns, default true - defaultWidth?: number // Default expanded width in pixels, default 320px - minWidth?: number // Minimum width in pixels, default 280px, cannot be less than 280px - maxWidth?: number // Maximum width in pixels, default 1160px -} - -interface TaskListColumnConfig { - type?: TaskListColumnType // Column type - key: string // Key for internationalization, also used as identifier - label?: string // Display label - cssClass?: string // CSS class name - width?: number // Optional column width - visible?: boolean // Whether to display, default true -} - -type TaskListColumnType = - | 'name' | 'predecessor' | 'assignee' - | 'startDate' | 'endDate' | 'estimatedHours' - | 'actualHours' | 'progress' -``` - -**WorkingHours Configuration** -```typescript -interface WorkingHours { - morning?: { start: number; end: number } // Morning work hours, e.g. { start: 8, end: 11 } - afternoon?: { start: number; end: number } // Afternoon work hours, e.g. { start: 13, end: 17 } -} -``` - -**TimelineScale Types** -```typescript -// Timeline display scale types -type TimelineScale = 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year' - -// Timeline scale constants -export const TimelineScale = { - HOUR: 'hour', // Hour view - each column displays one hour - DAY: 'day', // Day view - each column displays one day - WEEK: 'week', // Week view - each column displays one week - MONTH: 'month', // Month view - each column displays one month - QUARTER: 'quarter', // Quarter view - each column displays one quarter - YEAR: 'year', // Year view - each column displays one year -} - -// Timeline scale configuration -interface TimelineScaleConfig { - scale: TimelineScale // Scale type - cellWidth: number // Width of each time unit (px) - headerLevels: number // Number of header levels - formatters: { - primary: string // Primary time label format - secondary?: string // Secondary time label format - } -} -``` - -### 🕐 Timeline Scale Features - -The component supports multiple timeline scale displays. Users can switch timeline granularity through the Day/Week/Month button group in the toolbar or programmatically: - -#### Built-in Scale Configurations - -| Scale Type | Cell Width | Primary Format | Secondary Format | Use Case | -|------------|------------|----------------|------------------|----------| -| `hour` | 40px | yyyy/MM/dd | HH | Short-term projects with hourly precision, such as drug clinical trials | -| `day` | 30px | yyyy/MM | dd | Standard view for daily project management | -| `week` | 120px | yyyy/MM | W | Weekly planning view for medium-term projects | -| `month` | 180px | yyyy | MM | Monthly view for long-term projects | -| `quarter` | 360px | yyyy | Q | Quarterly view for strategic planning | -| `year` | 360px | yyyy | First Half\|Second Half | Annual view for very long-term projects | - -#### Usage Example - -```vue - - -``` -#### Composable Functions (src/composables) - -**useI18n Internationalization Tool** -```typescript -// Provides multi-language support -const { - locale, // Current language - setLocale, // Switch language - t, // Translation function - formatYearMonth // Year-month formatting -} = useI18n() - -// Supported languages -type Locale = 'zh-CN' | 'en-US' -``` - -**useMessage Message Tool** -```typescript -// Provides global message notifications -const { showMessage } = useMessage() - -// Message types -type MessageType = 'success' | 'error' | 'warning' | 'info' - -// Usage example -showMessage('Operation successful', 'success') -``` - -## 💻 Basic Usage - -### Simple Example - -```vue +``` +🎯 **[Try Live Demo on Github →](https://nelson820125.github.io/jordium-gantt-vue3/)** +Recommended: DOVE VPN for fast and stable access. (Note: Please use VPN services legally) + +## 🌞 NPM Package Usage Example +Please refer to the npm-demo folder in the project. +It is a standalone project that can be opened and run independently using your IDE. +Before running, make sure to install the Element Plus library and the jordium-gantt-vue3 plugin package. + +```bash +# npm +npm install element-plus +npm install jordium-gantt-vue3 +npm run dev +``` +--- + +## 📖 Component Guide + +### GanttChart Component + +`GanttChart` is the core entry point of the library, providing complete Gantt chart functionality. + +#### Basic Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `tasks` | `Task[]` | `[]` | Array of task data | +| `milestones` | `Task[]` | `[]` | Array of milestone data (Note: Type is Task[], must set type='milestone') | +| `showToolbar` | `boolean` | `true` | Whether to show the toolbar | +| `useDefaultDrawer` | `boolean` | `true` | Whether to use the built-in task edit drawer (TaskDrawer) | +| `useDefaultMilestoneDialog` | `boolean` | `true` | Whether to use the built-in milestone edit dialog (MilestoneDialog) | +| `autoSortByStartDate` | `boolean` | `false` | Whether to automatically sort tasks by start date | +| `allowDragAndResize` | `boolean` | `true` | Whether to allow dragging and resizing tasks/milestones | + +#### Configuration Object Props + +For complete configuration object documentation, see [⚙️ Configuration & Customization](#⚙️-configuration--customization) section. + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `toolbarConfig` | `ToolbarConfig` | `{}` | Toolbar configuration | +| `taskListConfig` | `TaskListConfig` | `undefined` | Task list configuration | +| `taskBarConfig` | `TaskBarConfig` | `undefined` | Task bar style configuration | +| `localeMessages` | `Partial` | `undefined` | Custom localization messages | +| `workingHours` | `WorkingHours` | `{ morning: { start: 8, end: 11 }, afternoon: { start: 13, end: 17 } }` | Working hours configuration | + +#### Callback Props + +| Prop | Type | Description | +|------|------|-------------| +| `onTodayLocate` | `() => void` | Toolbar "Today" button click callback | +| `onExportCsv` | `() => boolean \| void` | Toolbar "Export CSV" button click callback, return `false` to prevent default export | +| `onExportPdf` | `() => void` | Toolbar "Export PDF" button click callback | +| `onLanguageChange` | `(lang: 'zh-CN' \| 'en-US') => void` | Language switch callback | +| `onThemeChange` | `(isDark: boolean) => void` | Theme switch callback | +| `onFullscreenChange` | `(isFullscreen: boolean) => void` | Fullscreen toggle callback | +| `onExpandAll` | `() => void` | Toolbar "Expand All" button click callback | +| `onCollapseAll` | `() => void` | Toolbar "Collapse All" button click callback | + +#### Component Events + +For complete event documentation, see: +- **Task-related events**: See [Task Management](#task-management) section below +- **Milestone-related events**: See [Milestone Management](#milestone-management) section below + +**Event List Overview:** + +| Event Name | Parameters | Description | +|------------|------------|-------------| +| `add-task` | - | Clicked toolbar "Add Task" button | +| `task-click` | `(task: Task, event: MouseEvent)` | Clicked task | +| `task-double-click` | `(task: Task)` | Double-clicked task | +| `task-added` | `{ task: Task }` | Triggered after task added | +| `task-updated` | `{ task: Task }` | Triggered after task updated | +| `task-deleted` | `{ task: Task }` | Triggered after task deleted | +| `taskbar-drag-end` | `(task: Task)` | Task drag ended | +| `taskbar-resize-end` | `(task: Task)` | Task resize ended | +| `predecessor-added` | `{ targetTask, newTask }` | Added predecessor task | +| `successor-added` | `{ targetTask, newTask }` | Added successor task | +| `timer-started` | `(task: Task)` | Task timer started | +| `timer-stopped` | `(task: Task)` | Task timer stopped | +| `add-milestone` | - | Clicked toolbar "Add Milestone" button | +| `milestone-saved` | `(milestone: Task)` | Milestone saved | +| `milestone-deleted` | `{ milestoneId: number }` | Milestone deleted | +| `milestone-icon-changed` | `{ milestoneId, icon }` | Milestone icon changed | +| `milestone-drag-end` | `(milestone: Task)` | Milestone drag ended | + +#### Example 1: Simplest Gantt Chart + +```vue -``` -### Custom Event Handling - -```vue - - - -``` - -### Theme and Internationalization - -```vue - - -``` - -### 🔧 Working Hours Configuration - -The component supports setting working hours, affecting task duration calculations and progress display: - -```vue - - - -``` - -### 📊 High-Precision Work Hours Management - -The component supports work hour recording precise to 2 decimal places, suitable for projects requiring precise billing: - -```vue - ``` -// Handle toolbar events -const handleLanguageChange = (lang) => { - console.log('Language changed to:', lang) -} - -const handleThemeChange = (isDark) => { - console.log('Theme changed to:', isDark ? 'dark' : 'light') -} - +#### Example 2: Gantt Chart with Milestones +```vue + + ``` -## 🤝 Contributing & Collaboration +#### Example 3: Hide Toolbar, Custom Control Buttons with Event Binding -### Contributing +```vue + -We welcome community contributions! If you want to participate in project development: + ``` --- -**🔗 Related Links** -- [GitHub Repository](https://github.com/nelson820125/jordium-gantt-vue3) -- [Changelog](./CHANGELOG.md) +### Task Management -> 💡 **Tip**: If this project helps you, please give us a ⭐ Star! +Tasks are the core elements of the Gantt chart. The component provides complete CRUD operation support for tasks, including adding, editing, deleting tasks, and rich interactive events. + +#### Task Data Structure + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `id` | `number` | ✅ | - | Unique task identifier | +| `name` | `string` | ✅ | - | Task name | +| `startDate` | `string` | - | - | Start date, format: 'YYYY-MM-DD' or 'YYYY-MM-DD HH:mm' | +| `endDate` | `string` | - | - | End date, format: 'YYYY-MM-DD' or 'YYYY-MM-DD HH:mm' | +| `progress` | `number` | - | `0` | Task progress, range 0-100 | +| `predecessor` | `number[]` | - | - | Array of predecessor task IDs, standard format: `[1, 2, 3]`
**Compatible formats**: Also supports string `'1,2,3'` or string array `['1', '2', '3']`, component will auto-parse | +| `assignee` | `string` | - | - | Task assignee | +| `avatar` | `string` | - | - | Avatar URL of task assignee | +| `estimatedHours` | `number` | - | - | Estimated hours | +| `actualHours` | `number` | - | - | Actual hours | +| `parentId` | `number` | - | - | Parent task ID, used for task grouping | +| `children` | `Task[]` | - | - | Array of child tasks | +| `collapsed` | `boolean` | - | `false` | Whether child tasks are collapsed | +| `isParent` | `boolean` | - | - | Whether this is a parent task | +| `type` | `string` | - | - | Task type, 'milestone' for milestone, 'milestone-group' for milestone group | +| `description` | `string` | - | - | Task description | +| `icon` | `string` | - | `'diamond'` | Task icon (for milestones), options: 'diamond', 'flag', 'star', 'rocket', etc. | +| `level` | `number` | - | `0` | Task level (auto-calculated) | +| `isTimerRunning` | `boolean` | - | `false` | Whether timer is running | +| `timerStartTime` | `number` | - | - | Timer start time (timestamp) | +| `timerEndTime` | `number` | - | - | Timer end time (timestamp) | +| `timerStartDesc` | `string` | - | - | Description filled when timer starts | +| `timerElapsedTime` | `number` | - | `0` | Elapsed time (milliseconds) | +| `isEditable` | `boolean` | - | `true` | Whether individual task is editable (draggable, resizable), overrides global `allowDragAndResize` | +| `[key: string]` | `unknown` | - | - | Supports custom property extensions, can add any additional fields | + +> **Custom Property Extensions**: The Task interface supports adding arbitrary custom fields, such as: `priority`, `tags`, `status`, `department`, and other business-related fields. +> +> **Predecessor Field Notes**: +> - **Standard format** (recommended): `predecessor: [1, 2, 3]` - number array +> **Compatible format 1**: `predecessor: '1,2,3'` - comma-separated string +> - **Compatible format 2**: `predecessor: ['1', '2', '3']` - string array +> - Component will automatically parse all formats into number array +> - No predecessors: use empty array `[]`, empty string `''`, or don't set this field + +#### Task-Related Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `tasks` | `Task[]` | `[]` | Array of task data | +| `useDefaultDrawer` | `boolean` | `true` | Whether to use built-in task edit drawer (TaskDrawer) | +| `taskBarConfig` | `TaskBarConfig` | `{}` | Task bar style configuration, see [TaskBarConfig Configuration](#taskbarconfig-configuration) | +| `taskListConfig` | `TaskListConfig` | `undefined` | Task list configuration, see [TaskListConfig Configuration](#tasklistconfig-configuration) | +| `autoSortByStartDate` | `boolean` | `false` | Whether to automatically sort tasks by start date | + +**Configuration Notes**: +- **Default mode**: `useDefaultDrawer=true` (default), double-click task to auto-open built-in TaskDrawer +- **Custom editor**: `useDefaultDrawer=false` disables built-in drawer, listen to `@task-double-click` event to open custom editor +- **Read-only mode**: `useDefaultDrawer=false` and don't listen to `@task-double-click` event, user double-click task has no response + +#### Task Events + +> **💡 Event-Driven Architecture**: Component adopts pure event-driven design. All user operations (add, edit, delete, drag, etc.) will trigger corresponding events for easy external listening and handling. + +| Event Name | Parameters | When Triggered | Description | +|------------|------------|----------------|-------------| +| `add-task` | - | When clicking toolbar "Add Task" button | Can be used for custom add task logic. If `useDefaultDrawer=true`, component will auto-open built-in TaskDrawer | +| `task-click` | `(task: Task, event: MouseEvent) => void` | When clicking task bar | Triggered on single-click task | +| `task-double-click` | `(task: Task) => void` | When double-clicking task bar | Double-click task **always triggers**. When `useDefaultDrawer=true`, component will additionally open built-in editor; when `false`, won't open. Event triggering is independent of property value | +| `task-added` | `{ task: Task }` | After task added | Triggered after adding task via built-in TaskDrawer. **Note**: Component has auto-updated `tasks` data, external only needs to listen to this event for additional processing (like calling API to save) | +| `task-updated` | `{ task: Task }` | After task updated | Triggered after updating task via built-in TaskDrawer or drag. **Note**: Component has auto-updated `tasks` data, external only needs to listen to this event for additional processing | +| `task-deleted` | `{ task: Task }` | After task deleted | Triggered after deleting task via built-in TaskDrawer. **Note**: Component has auto-updated `tasks` data, external only needs to listen to this event for additional processing | +| `taskbar-drag-end` | `(task: Task) => void` | When task bar drag ends | Task position changed, startDate and endDate updated. **Note**: Component has auto-updated `tasks` data | +| `taskbar-resize-end` | `(task: Task) => void` | When task bar resize ends | Task duration changed, endDate updated. **Note**: Component has auto-updated `tasks` data | +| `predecessor-added` | `{ targetTask: Task, newTask: Task }` | After adding predecessor via context menu | `targetTask` is the task to which predecessor is added, `newTask` is the newly created predecessor task | +| `successor-added` | `{ targetTask: Task, newTask: Task }` | After adding successor via context menu | `targetTask` is the original task, `newTask` is the newly created successor task (its predecessor already contains targetTask.id) | +| `timer-started` | `(task: Task) => void` | When task timer starts | Start recording task hours | +| `timer-stopped` | `(task: Task) => void` | When task timer stops | Stop recording task hours | + +**Data Synchronization Notes**: +- ✅ **Component auto-updates internally**: For all task CRUD operations, component will auto-update `props.tasks` data +- ✅ **Events are for notification only**: External event listeners are mainly for: showing messages, calling backend APIs, updating other related data, etc. +- ❌ **Avoid duplicate operations**: Don't modify `tasks` data again in event handlers, otherwise it will cause duplicate updates + +#### Example 1: Basic Task Operations + +```vue + + + +``` + +#### Example 2: Task Dependencies (Predecessors/Successors) + +Tasks can configure predecessors via the `predecessor` field, and the component will automatically draw dependency lines: + +```vue + + + +``` + +**Dependency Relationship Notes**: +- **`predecessor` field supports multiple formats**: + - Standard format (recommended): `[1, 2, 3]` - number array + - Compatible format 1: `'1,2,3'` - comma-separated string + - Compatible format 2: `['1', '2', '3']` - string array + - Component will automatically parse all formats +- Predecessor task: Task that must be completed first (e.g., design must be done before development) +- Successor task: Task that depends on current task (current task is a predecessor for other tasks) +- Component will automatically draw dependency lines from predecessor tasks to dependent tasks +- Can add/delete predecessor and successor tasks via built-in context menu +- When deleting tasks via built-in menu, component will automatically clean up related dependency references +- No predecessors: use empty array `[]`, empty string `''`, or don't set `predecessor` field + +#### Example 3: Hide Toolbar, Use Custom Buttons to Trigger Events + +Suitable for scenarios requiring complete custom control bar: + +```vue + + + +``` + +> **💡 Flexibility Design**: +> - Show toolbar + default editor: Simplest out-of-the-box approach +> - Hide toolbar + custom buttons + default editor: Custom control bar style while keeping default edit functionality +> - Hide toolbar + custom buttons + custom editor: Fully customize all interaction logic + +### Milestone Management + +### Milestone Management + +Milestones are used to mark important time points in a project, such as project kickoff, phase completion, product release, etc. The component provides flexible milestone editing configuration, using the built-in MilestoneDialog by default, and also supports fully custom editing behavior. + +> **Note**: Milestones and tasks are independent data collections with no direct association. Milestones are managed independently through the `milestones` prop. + +#### Milestone Data Structure + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `id` | `number` | ✅ | - | Unique milestone identifier | +| `name` | `string` | ✅ | - | Milestone name | +| `startDate` | `string` | ✅ | - | Milestone date, format: 'YYYY-MM-DD' or 'YYYY-MM-DD HH:mm' | +| `endDate` | `string` | - | - | End date (usually not needed for milestones, auto-set to same as startDate) | +| `assignee` | `string` | - | - | Assignee | +| `type` | `string` | ✅ | `'milestone'` | Type identifier, must be set to 'milestone' | +| `icon` | `string` | - | `'diamond'` | Milestone icon, options: 'diamond', 'flag', 'star', 'rocket', etc. | +| `description` | `string` | - | - | Milestone description | + +> **Note**: The `milestones` prop type is `Task[]`, ensure each milestone object's `type` field is set to `'milestone'`. + +#### Milestone-Related Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `milestones` | `Task[]` | `[]` | Array of milestone data (type is Task[], ensure type='milestone') | +| `useDefaultMilestoneDialog` | `boolean` | `true` | Whether to use built-in milestone edit dialog (MilestoneDialog) | + +**Configuration Notes**: +- **Default mode**: `useDefaultMilestoneDialog=true` (default), double-click milestone to auto-open built-in MilestoneDialog +- **Disable editor**: `useDefaultMilestoneDialog=false`, double-click milestone has no response (component doesn't open any editor) +- **Custom editor**: Can listen to `onMilestoneDoubleClick` callback or related events to implement custom editing logic + +> **💡 Differences Between Milestones and Tasks**: +> - Milestone data is managed independently via `milestones` prop, separate from `tasks` +> - Milestone object's `type` field must be set to `'milestone'` +> - Milestones don't support child tasks, dependency relationships, and other complex structures +> - Milestones are mainly used to mark key time points + +#### Milestone Callbacks (Backward Compatible) + +> **⚠️ Deprecated**: Please use the new event-driven API (see "Milestone Events" section below) + + +#### Milestone Events + +> **💡 Event-Driven Architecture**: Milestone management adopts event-driven design. Using event API is recommended over callback functions. + +| Event Name | Parameters | When Triggered | Description | +|------------|------------|----------------|-------------| +| `add-milestone` | - | When clicking toolbar "Add Milestone" button | Can be used for custom add milestone logic. If `useDefaultMilestoneDialog=true`, component will auto-open built-in MilestoneDialog | +| `milestone-saved` | `(milestone: Task) => void` | After milestone saved (add or edit) | Triggered after saving milestone via built-in MilestoneDialog. **Note**: Component has auto-updated `milestones` data, external only needs to listen to this event for additional processing (like calling API to save) | +| `milestone-deleted` | `{ milestoneId: number }` | After milestone deleted | Triggered after deleting milestone via built-in MilestoneDialog. **Note**: Component has auto-updated `milestones` data, external only needs to listen to this event for additional processing | +| `milestone-icon-changed` | `{ milestoneId: number, icon: string }` | After milestone icon changed | Triggered after modifying icon via built-in MilestoneDialog | +| `milestone-drag-end` | `(milestone: Task) => void` | When milestone drag ends | Milestone date updated. **Note**: Component has auto-updated `milestones` data | + +**Data Synchronization Notes**: +- ✅ **Component auto-updates internally**: For all milestone CRUD operations, component will auto-update `props.milestones` data +- ✅ **Events are for notification only**: External event listeners are mainly for: showing messages, calling backend APIs, updating other related data, etc. +- ❌ **Avoid duplicate operations**: Don't modify `milestones` data again in event handlers, otherwise it will cause duplicate updates + +#### Example 1: Using Event-Driven API (Recommended) + +Using the new event API, component auto-manages data, more concise: + +```vue + + + +``` + +#### Example 2: Using Custom Milestone Edit Dialog + +If you need to fully customize the milestone editing interface, you can disable the built-in dialog and use your own component: + +```vue + + + +``` + +**Custom Dialog Component Example** (`CustomMilestoneDialog.vue` - Using Element Plus): + +> **Note**: The following examplesUsing Element Plus UI framework. You can also use other UI frameworks (such as Ant Design Vue, Naive UI, etc.) or native HTML implementation. + +```vue + + + +``` + +> **💡 Custom Dialog Notes**: +> - Set `use-default-milestone-dialog="false"` to disable built-in dialog +> - Listen to `@add-milestone` event to open custom dialog +> - Need to manually manage `milestones` array CRUD operations +> - Can still listen to other events (like `@milestone-drag-end`) to handle drag operations +> - Suitable for scenarios requiring complex form validation, special UI design, or additional fields + +--- + +## ⚙️ Configuration & Customization + +This section details the configuration options and extension capabilities of the GanttChart component, including Component Configuration, Theme & Internationalization, and Custom Extensions. + +### Component Configuration + +#### ToolbarConfig (Toolbar Configuration) + +Customize the toolbar functional buttons and time scale options. + +**Type Definition:** + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `showAddTask` | `boolean` | `true` | Show "Add Task" button | +| `showAddMilestone` | `boolean` | `true` | Show "Add Milestone" button | +| `showTodayLocate` | `boolean` | `true` | Show "Locate to Today" button | +| `showExportCsv` | `boolean` | `true` | Show "Export CSV" button | +| `showExportPdf` | `boolean` | `true` | Show "Export PDF" button | +| `showLanguage` | `boolean` | `true` | Show "Language Switch" button (Chinese/English) | +| `showTheme` | `boolean` | `true` | Show "Theme Switch" button (Light/Dark) | +| `showFullscreen` | `boolean` | `true` | Show "Fullscreen" button | +| `showTimeScale` | `boolean` | `true` | Show time scale button group (controls entire group visibility) | +| `timeScaleDimensions` | `TimelineScale[]` | `['hour', 'day', 'week', 'month', 'quarter', 'year']` | Set time scale dimensions to display, options: `'hour'`, `'day'`, `'week'`, `'month'`, `'quarter'`, `'year'` | +| `defaultTimeScale` | `TimelineScale` | `'week'` | Default selected time scale | +| `showExpandCollapse` | `boolean` | `true` | Show "Expand All/Collapse All" button (for parent-child task tree structure) | + +**TimelineScale Type Description:** + +```typescript +type TimelineScale = 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year' + +// Can also use constant form +import { TimelineScale } from 'jordium-gantt-vue3' + +TimelineScale.HOUR // 'hour' - Hour view +TimelineScale.DAY // 'day' - Day view +TimelineScale.WEEK // 'week' - Week view +TimelineScale.MONTH // 'month' - Month view +TimelineScale.QUARTER // 'quarter' - Quarter view +TimelineScale.YEAR // 'year' - Year view +``` + +**Example 1: Complete Configuration (Show All Features)** + +```vue + + + +``` + +**Example 2: Simplified Configuration (Show Common Features Only)** + +```vue + +``` + +**Example 3: Using TimelineScale Constants** + +```vue + +``` + +**Example 4: Minimal Configuration (Suitable for Embedded Use)** + +```vue + +``` + +> **💡 Configuration Recommendations**: +> - **Default configuration**:When not passed, all buttons are shown by default +> - **Show as needed**: Hide unnecessary feature buttons based on business requirements +> - **Time scale**:`timeScaleDimensions` controls which time dimensions to display, recommend selecting 2-4 common dimensions +> - **Responsive layout**:toolbar will automatically adapt to container width, excessive buttons will collapse into more menu + +#### TaskListConfig(Task List Configuration) + +Customize task list display columns, width limits, etc. Task list is located on the left side of the Gantt chart, showing detailed task information. + +**Type Definition:** + +| Field | Type | Default | Description | +|--------|------|--------|------| +| `columns` | `TaskListColumnConfig[]` | Default 8 columns | Task list column configuration array, defines which columns to display and their properties | +| `showAllColumns` | `boolean` | `true` | Whether to show all columns. When `true`, ignores `visible` setting in `columns` | +| `defaultWidth` | `number \| string` | `320` | Default expanded width. Supports pixel number (like `320`) or percentage string (like `'30%'`) | +| `minWidth` | `number \| string` | `280` | Minimum width. Supports pixel number (like `280`) or percentage string (like `'20%'`). Cannot be less than 280px | +| `maxWidth` | `number \| string` | `1160` | Maximum width. Supports pixel number (like `1160`) or percentage string (like `'80%'`) | + +**TaskListColumnConfig Type Definition:** + +| Field | Type | Required | Description | +|--------|------|------|------| +| `key` | `string` | ✅ | Unique column identifier, used to access fields in Task object and for internationalization | +| `label` | `string` | - | Column display label (header text) | +| `cssClass` | `string` | - | Custom CSS class name | +| `width` | `number` | - | Column width (unit: pixels) | +| `visible` | `boolean` | - | Whether to show this column, default `true`. This setting is invalid when `showAllColumns=true` | + +**Example1:Basic Configuration (Adjust Width)** + +```vue + + + +``` + +**Example2:Using Percentage Width** + +```vue + + + +``` + +**Example3:Custom Display Columns (Standard Configuration)** + +Based on business requirements, you can customize columns to display, column widths, and display order. Recommend defining column configuration array first, then assign to `columns` prop. + +```vue + + + +``` + +**Example4:Simplified Column Configuration** + +Only show core information columns, suitable for scenarios with limited space or requiring concise display. + +```vue + +``` + +**Example5:Custom Business Columns** + +Add business-related custom columns, ensure Task object contains corresponding fields. + +```vue + +``` + +**Example6:Dynamic Column Configuration** + +Combine `ref` and `computed` to achieve dynamic show/hide and width adjustment of columns. + +```vue + + + +``` + +> **💡 Configuration Notes**: +> - **Default behavior**:When not passed, show all 8 default columns with width of 320px +> - **Width units**:Supports pixel (`number`) and percentage (`string`, like `'30%'`) methods +> - **Percentage calculation**:Based on total width of Gantt chart container, responsive adjustment +> - **Column order**: `columns` array order determines column display order +> - **Column configuration standards**:Recommend defining `TaskListColumnConfig[]` type column array first, then assign to `columns` prop +> - **Custom column support**:Task interface supports arbitrary custom fields through `[key: string]: unknown` index signature, component will dynamically read column values through `task[column.key]`, no need to modify Task interface to add custom columns +> - **Dynamic configuration**:Combine `ref` and `computed` to achieve dynamic show/hide and width adjustment of columns +> - **Minimum width limit**: `minWidth` cannot be less than 280px, this is the minimum value to ensure basic usability + +#### TaskBarConfig (Task Bar Configuration) + +Controls task bar display content and interaction behavior。 + +**Configuration Fields:** + +| Field | Type | Default | Description | +|--------|------|--------|------| +| `showAvatar` | `boolean` | `true` | Whether to show avatar | +| `showTitle` | `boolean` | `true` | Whether to show title text | +| `showProgress` | `boolean` | `true` | Whether to show progress text | +| `dragThreshold` | `number` | `5` | Drag trigger threshold (pixels) | +| `resizeHandleWidth` | `number` | `5` | Resize handle width (pixels), max 15px | +| `enableDragDelay` | `boolean` | `false` | Whether to enable drag delay (prevent accidental trigger) | +| `dragDelayTime` | `number` | `150` | Drag delay time (milliseconds) | + +> **💡 Edit Permission Control**: +> - **Global control**: Use `` to disable drag/resize for all tasks +> - **Individual task control**: Set task object `isEditable: false` property to control individual task + +**Example1:Complete Configuration** + +```vue + + + +``` + +**Example2:Global Read-Only Mode** + +Disable edit operations for all tasks. + +```vue + +``` + +**Example3:Individual Task Read-Only** + +Only certain tasks are non-editable, other tasks are normal. + +```vue + +``` + +**Example4:Simplified Display** + +Only show task bar, hide avatar, title and progress text. + +```vue + +``` + +**Example5:Anti-Accidental Touch Configuration** + +In mobile or touch screen scenarios, increase drag threshold and delay time. + +```vue + +``` +#### Timeline Container Auto-Fill Configuration + +The component has built-in intelligent timeline range calculation logic, ensuring that regardless of task data volume or task duration, the timeline always fills the container width, providing the best visual experience. + +**Core Design Principles:** + +1. **Base Buffer Mechanism**: Add fixed buffers based on the actual time range of tasks, varying by view type + - Hour view:±1 day task range + - Day view: ±30 days before/after task range + - Week view: ±8 weeks (approx. 2 months) before/after task range + - Month view: ±1 year before/after task range + - Quarter view: ±1 year before/after task range + - Year view: ±1 year before/after task range + +2. **Container Width Adaptation**: After base buffering, if calculated timeline width is less than container width, automatically extend the range + - Calculate time units (days/weeks/months/quarters/years) needed for container + - **Symmetrically extend** on both sides of base range to ensure timeline fills container + +3. **Empty Data Handling**: When no task data exists, calculate reasonable time range based on container width and time scale + - Center on current date + - Dynamically calculate time span to display based on container width + - Ensure minimum display range (e.g., at least 60 days for day view, at least 20 weeks for week view) + +4. **Independent Calculation on View Switch**: Each time scale switch triggers independent recalculation of optimal time range for that view + - Avoid unreasonable ranges caused by different views sharing cache + - Each view gets optimal display effect + +**Calculation Pattern Reference Table:** + +| View | Unit Width | Base Buffer | Empty Data Min Range | Container Auto-Fill? | +|------|-----------|--------------|----------------------|----------------| +| Hour View | 30px/hour | ±1 day | 3 days | ✅ | +| Day View | 30px/day | ±30 days | 60 days | ✅ | +| Week View | 60px/week | ±2 months | 20 weeks | ✅ | +| Month View | 60px/month | ±1 year | 3 years | ✅ | +| Quarter View | 60px/quarter (240px/year) | ±1 year | 5 years | ✅ | +| Year View | 360px/year | ±1 year | 5 years | ✅ | + +**Practical Application Scenarios:** + +- **Short-term Tasks** (e.g., 1-week project): + - Won't result in narrow timeline, automatically extends to fill container + - Day view: 1 week (7 days × 30px = 210px) → Extends to ≥1200px (approx. 40 days) + - Week view: 1 week (60px) → Extends to ≥1200px (approx. 20 weeks) + +- **Long-term Projects** (e.g., 2-year project): + - After adding fixed buffer, automatically adapts to container + - Month view: 24 months + buffer → Extends to container width if needed + - Quarter view: 8 quarters + buffer → Extends to container width if needed + +- **Empty Board** (no task data): + - Day view: Centered on today, displays at least 60 days + - Week view: Centered on today, displays at least 20 weeks + - Month view: Displays at least 3 years + - Quarter/Year view: Displays at least 5 years + +> **💡 Automation Advantages**: +> - No need to manually set `startDate` and `endDate`, component automatically calculates optimal range +> - Responsive to container width changes, timeline automatically recalculates +> - Different views independently optimized, auto-adjusts to best display effect when switching views +> - Avoids issues with timeline being too narrow or having excessive whitespace +> - Suitable for displaying at different resolutions + +### Theme & Internationalization + +#### Theme Switching + +Component has built-in light and dark themes, can switch via toolbar button, also can listen to switch events: + +```vue + + + +``` + +#### Custom Theme Variables + +Customize theme by overriding CSS variables: + +```css +/* Customize light theme */ +:root { + /* Primary colors */ + --gantt-primary-color: #409eff; + --gantt-success-color: #67c23a; + --gantt-warning-color: #e6a23c; + --gantt-danger-color: #f56c6c; + + /* Background colors */ + --gantt-bg-primary: #ffffff; + --gantt-bg-secondary: #f5f7fa; + --gantt-bg-hover: #ecf5ff; + + /* Text colors */ + --gantt-text-primary: #303133; + --gantt-text-secondary: #606266; + --gantt-text-placeholder: #c0c4cc; + + /* Border colors */ + --gantt-border-color: #dcdfe6; + --gantt-border-color-light: #e4e7ed; + + /* Task bar colors */ + --gantt-task-bg: #409eff; + --gantt-task-border: #66b1ff; + --gantt-task-text: #ffffff; +} + +/* Customize dark theme */ +.dark { + --gantt-bg-primary: #1a1a1a; + --gantt-bg-secondary: #2c2c2c; + --gantt-bg-hover: #3a3a3a; + + --gantt-text-primary: #e5e5e5; + --gantt-text-secondary: #b0b0b0; + + --gantt-border-color: #3a3a3a; + --gantt-border-color-light: #4a4a4a; + + --gantt-task-bg: #409eff; + --gantt-task-border: #66b1ff; + --gantt-task-text: #ffffff; +} +``` + +#### Language Switching + +Component has built-in Chinese (zh-CN) and English (en-US), can switch via toolbar button: + +```vue + + + +``` + +#### Custom Translations + +Override or extend default translations via `localeMessages` prop: + +```vue + + + +``` + +> **💡 Tip**: +> - `localeMessages` adopts **deep merge** strategy, only need to pass fields that need to be overridden +> - supports nested objects, like `taskList.name`, `toolbar.addTask`, etc. +> - For complete translation keys, please refer to built-in `messages['zh-CN']` object in component + +### Custom Extensions + +#### Slots (Slots) + +Component provides slots support, allowing custom task content rendering。 + +##### `custom-task-content` Slots + +Used to customize task display content in task list (TaskRow) and timeline (TaskBar). + +**Slot Parameters:** + +| Parameter | Type | Source | Description | +|--------|------|------|------| +| `type` | `'task-row'` \| `'task-bar'` | Common | Slot call position identifier | +| `task` | `Task` | Common | Current task object | + +**TaskRow specific parameters (when `type === 'task-row'`):** + +| Parameter | Type | Description | +|--------|------|------| +| `isRowContent` | `boolean` | Identified as row content | +| `level` | `number` | Task level | +| `indent` | `string` | Indent style | +| `isHovered` | `boolean` | Whether hovering | +| `hoveredTaskId` | `number \| null` | Current hovering task ID | +| `isParent` | `boolean` | Whether parent task | +| `hasChildren` | `boolean` | Whether has child tasks | +| `collapsed` | `boolean` | Whether collapsed | +| `formattedTimer` | `string` | Formatted timer text | +| `timerRunning` | `boolean` | Whether timer is running | +| `timerElapsed` | `number` | Elapsed time | +| `isOvertime` | `number \| boolean \| undefined` | Whether overtime | +| `overdueDays` | `number` | Overdue days | +| `overtimeText` | `string` | Overtime text | +| `overdueText` | `string` | Overdue text | +| `daysText` | `string` | Days text | +| `progressClass` | `string` | Progress CSS class name | + +**TaskBar specific parameters (when `type === 'task-bar'`):** + +| Parameter | Type | Description | +|--------|------|------| +| `status` | `object` | TaskStatus object, contains `type`, `color`, `bgColor`, `borderColor` | +| `statusType` | `string` | StatusType:`'completed'`, `'delayed'`, `'in-progress'`, `'not-started'`, `'parent'` | +| `isParent` | `boolean` | Whether parent task | +| `progress` | `number` | TaskProgress(0-100) | +| `currentTimeScale` | `TimelineScale` | Current time scale | +| `rowHeight` | `number` | Row height (pixels) | +| `dayWidth` | `number` | Width per day (pixels) | + +**Usage Example:** + +```vue + + + +``` + +**Custom Content Component Example:** + +```vue + + + + + + +``` + +> **💡 Usage Scenarios**: +> - Support HTML formatted task names +> - Add custom icons, tags or badges +> - Display different styles based on task status +> - Integrate third-party rich text rendering +> - Display additional business information + +> **⚠️ Notes**: +> - Slot content will be rendered in both TaskRow and TaskBar +> - Need to distinguish rendering position based on `type` parameter +> - TaskRow and TaskBar have different available space, need to adapt layout +> - Avoid using overly complex components in slot content, may affect performance + +--- + +## ❓ FAQ + +### How to integrate into existing project? + +1. Install dependencies +2. Import component and styles +3. Pass in data +4. Listen to events and handle business logic + +See [Contributing Guide](CONTRIBUTING.md) for details. + +### Which browsers are supported? + +- Chrome >= 90 +- Firefox >= 88 +- Safari >= 14 +- Edge >= 90 + +--- + +## 📁 Project Structure + +``` +jordium-gantt-vue3/ +├── src/ # Source code +│ ├── components/ # Vue Components +│ │ ├── GanttChart.vue # Main Gantt chart component +│ │ ├── TaskList.vue # Task list +│ │ ├── Timeline.vue # Timeline +│ │ └── ... +│ ├── models/ # Data models +│ │ ├── classes/ # Class definitions +│ │ ├── configs/ # Configuration interfaces +│ │ └── types/ # Type Definition +│ ├── composables/ # Composables +│ ├── styles/ # Style files +│ └── utils/ # Utilities +├── demo/ # Demo code +├── docs/ # Documentation +├── public/ # Public assets +└── package.json # Project configuration +``` + +--- + +## 🔗 Related Links + +- **Live Demo**: [https://nelson820125.github.io/jordium-gantt-vue3/](https://nelson820125.github.io/jordium-gantt-vue3/) +- **GitHub**: [https://github.com/nelson820125/jordium-gantt-vue3](https://github.com/nelson820125/jordium-gantt-vue3) +- **npm**: [https://www.npmjs.com/package/jordium-gantt-vue3](https://www.npmjs.com/package/jordium-gantt-vue3) +- **Changelog**: [CHANGELOG.md](./CHANGELOG.md) +- **Contributing Guide**: [CONTRIBUTING.md](./CONTRIBUTING.md) + +--- + +## 🤝 Contributing + +Welcome to submit Issues and Pull Requests! + +For detailed contributing guide, please see [CONTRIBUTING.md](./CONTRIBUTING.md)。 + +### Contributors + +Thanks to all developers who contributed to this project! + +View the complete [Contributors list](./CONTRIBUTORS.md)。 + +--- + +## 📄 Open Source License + +[MIT License](./LICENSE) © 2025 JORDIUM.COM + +--- + +

+ If this project helps you, please give it a ⭐️ to support it! +

diff --git a/README.md b/README.md index 52fff2d..787b404 100644 --- a/README.md +++ b/README.md @@ -1,459 +1,117 @@ # jordium-gantt-vue3 logo jordium-gantt-vue3 - -**🌐 Languages**: [📖 English Documentation](./README-EN.md) | [📖 中文文档](./README.md) +

+ + npm version + + + MIT License + + + Vue.js + + + TypeScript + + + Nodejs + +

-[![npm version](https://img.shields.io/npm/v/jordium-gantt-vue3.svg?cacheBust=1)](https://www.npmjs.com/package/jordium-gantt-vue3) -[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) -[![Vue.Js](https://img.shields.io/badge/Vue.js->=3.5.13-4FC08D?style=flat-square&logo=vue.js&logoColor=white)](https://vuejs.org/) -[![TypeScript](https://img.shields.io/badge/TypeScript->=5.8.3-3178C6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) -[![Node.js](https://img.shields.io/badge/Node.js->=16.0.0-339933?style=flat-square&logo=node.js&logoColor=white)](https://nodejs.org/) +

+ 中文 | + English +

-> 现代化的 Vue 3 甘特图组件库,为项目管理和任务调度提供完整解决方案 +

现代化的 Vue 3 甘特图组件库,为项目管理和任务调度提供完整解决方案

-## 🌐 在线体验 +

+ + 📱 在线演示 + +   |   + + 📦 GitHub + +   |   + + 📚 npm + +

-🎯 **[立即体验 Github在线Demo →](https://nelson820125.github.io/jordium-gantt-vue3/)** -推荐使用 DOVE VPN,快速、稳定。 (注意:请合法使用 VPN 资源) +--- -*在线 Demo 包含完整功能展示:任务管理、里程碑、主题切换、国际化等* +## ✨ 简介 -## 🎨 主题支持 +jordium-gantt-vue3 是一个基于 Vue 3 和 TypeScript 开发的现代化甘特图组件,专为项目管理和任务调度场景设计。它提供了丰富的交互功能、灵活的配置选项和优雅的视觉效果。 -### 亮色主题 +### 核心特性 -![亮色主题](design/screenshots/light-theme.png) +- 📊 **功能完整** - 任务管理、里程碑、依赖关系、进度追踪 +- 🎨 **主题系统** - 内置亮色/暗色主题,支持自定义样式 +- 🖱️ **交互流畅** - 拖拽调整、缩放、双击编辑、右键菜单 +- 🌍 **国际化** - 内置中英文,可扩展其他语言 +- ⚡ **高性能** - 虚拟滚动、懒加载,轻松处理大量数据 +- 💎 **类型安全** - 完整 TypeScript 支持 -### 暗色主题 +### 效果预览 -![暗色主题](design/screenshots/dark-theme.png) +#### 亮色主题 -## 🚀 插件特点 +亮色主题 -- 📊 **完整功能**: 任务管理、里程碑追踪、依赖关系、进度可视化 -- 🎨 **主题切换**: 内置亮色/暗色主题,支持自定义主题变量 -- 🖱️ **交互丰富**: 拖拽调整、大小缩放、双击编辑、右键菜单 -- 🌍 **国际化**: 内置中英文,支持自定义语言包 -- 📱 **响应式**: 适配桌面和移动端,流畅的触控体验 -- ⚡ **高性能**: 虚拟滚动、懒加载、优化渲染 -- 🔧 **可扩展**: 丰富的 API 接口,支持自定义组件和事件 -- 💎 **类型安全**: 完整 TypeScript 支持,开发体验更佳 +#### 暗色主题 + +暗色主题 + +--- ## 📦 安装 +使用你喜欢的包管理器安装: + ```bash # npm npm install jordium-gantt-vue3 -# yarn +# yarn yarn add jordium-gantt-vue3 # pnpm pnpm add jordium-gantt-vue3 ``` -## 📄 许可证 - -[MIT License](./LICENSE) © 2025 JORDIUM.COM - --- -## 📁 项目结构 +## 🚀 快速开始 -``` -jordium-gantt-vue3/ -├── src/ # 组件源码与核心逻辑 -│ ├── components/ # 主要 Vue 组件 -│ │ ├── GanttChart.vue # 主入口组件 -│ │ ├── TaskList.vue # 任务列表 -│ │ ├── Timeline.vue # 时间轴组件 -│ │ ├── TaskBar.vue # 任务条 -│ │ ├── TaskDrawer.vue # 任务编辑抽屉 -│ │ ├── TaskContextMenu.vue # 任务右键菜单 -│ │ ├── GanttToolbar.vue # 工具栏 -│ │ ├── MilestonePoint.vue # 里程碑点 -│ │ ├── MilestoneDialog.vue # 里程碑对话框 -│ │ ├── DatePicker.vue # 日期选择器 -│ │ └── ... # 其他组件 -│ ├── models/ # 数据模型与配置 -│ │ ├── classes/ # 类定义 -│ │ │ ├── Task.ts # 任务模型 -│ │ │ ├── Milestone.ts # 里程碑模型 -│ │ │ └── Language.ts # 语言配置 -│ │ ├── configs/ # 配置接口 -│ │ │ ├── TimelineConfig.ts # 时间轴配置 -│ │ │ └── ToolbarConfig.ts # 工具栏配置 -│ │ └── types/ # 类型定义 -│ │ └── TimelineScale.ts # 时间刻度类型 -│ ├── composables/ # 组合式函数 -│ │ ├── useI18n.ts # 国际化工具 -│ │ └── useMessage.ts # 消息提示工具 -│ ├── styles/ # 样式文件 -│ │ ├── app.css # 主样式 -│ │ └── theme-variables.css # 主题变量 -│ ├── utils/ # 工具函数 -│ │ └── predecessorUtils.ts # 前置依赖工具 -│ └── index.ts # 入口导出 -├── demo/ # 组件开发与交互演示(本地开发/预览用) -│ ├── App.vue # 演示应用主组件 -│ ├── data.json # 演示数据(包含药物临床试验案例) -│ ├── main.ts # 演示应用入口 -│ └── ... # 其他演示文件 -├── packageDemo/ # npm 包集成演示(模拟外部项目集成效果) -├── dist/ # 构建产物(发布/静态站点/打包输出) -├── docs/ # 相关文档(如部署、API 说明等) -├── design/ # 设计资源与截图 -│ └── screenshots/ # 主题截图 -├── public/ # 公共静态资源 -│ └── assets/ # 静态资源文件 -├── README.md # 中文说明文档 -├── README-EN.md # 英文说明文档 -├── package.json # 项目配置 -├── vite.config.ts # Vite开发配置 -├── vite.config.lib.ts # Vite库构建配置 -├── tsconfig.json # TypeScript配置 -└── ... # 其他配置、脚本与元数据 -``` +### 组件引入 -### 目录说明 - -- **`src/components/`**:核心Vue组件,包含甘特图的所有功能组件 -- **`src/models/`**:数据模型、类型定义和配置接口 -- **`src/composables/`**:Vue 3组合式函数,提供可复用的逻辑 -- **`src/styles/`**:样式文件,包含主题系统和CSS变量 -- **`src/utils/`**:工具函数,处理业务逻辑和数据转换 -- **`demo/`**:本地开发和功能演示,包含完整的交互页面和药物临床试验样例数据 -- **`packageDemo/`**:模拟npm包在外部项目中的集成与使用场景 -- **`dist/`**:构建输出目录,包含发布到npm或静态站点的产物 -- **`docs/`**:项目文档,包括部署说明、API参考等 - -## 🔧 API 参考 - -### GanttChart 属性 - -| 属性名 | 类型 | 默认值 | 说明 | -|--------|------|--------|------| -| `tasks` | `Task[]` | `[]` | 任务数据数组 | -| `milestones` | `Task[]` | `[]` | 里程碑数据数组 | -| `editComponent` | `any` | - | 自定义编辑组件 | -| `useDefaultDrawer` | `boolean` | `true` | 是否使用默认编辑抽屉 | -| `showToolbar` | `boolean` | `true` | 是否显示工具栏 | -| `toolbarConfig` | `ToolbarConfig` | `{}` | 工具栏配置 | -| `taskListConfig` | `TaskListConfig` | `{}` | 任务列表配置(包括默认宽度、最小最大宽度限制等) | -| `localeMessages` | `Partial` | - | 自定义多语言配置 | -| `workingHours` | `WorkingHours` | - | 工作时间配置 | -| `onTaskDoubleClick` | `(task: Task) => void` | - | 任务双击事件回调 | -| `onTaskDelete` | `(task: Task, deleteChildren?: boolean) => void` | - | 任务删除事件回调 | -| `onTaskUpdate` | `(task: Task) => void` | - | 任务更新事件回调 | -| `onTaskAdd` | `(task: Task) => void` | - | 任务添加事件回调 | -| `onMilestoneSave` | `(milestone: Task) => void` | - | 里程碑保存事件回调 | -| `onMilestoneDelete` | `(milestoneId: number) => void` | - | 里程碑删除事件回调 | -| `onMilestoneIconChange` | `(milestoneId: number, icon: string) => void` | - | 里程碑图标变更事件回调 | -| `onAddTask` | `() => void` | - | 新增任务工具栏事件回调 | -| `onAddMilestone` | `() => void` | - | 新增里程碑工具栏事件回调 | -| `onTodayLocate` | `() => void` | - | 定位今天工具栏事件回调 | -| `onExportCsv` | `() => boolean \| void` | - | 导出CSV工具栏事件回调 | -| `onExportPdf` | `() => void` | - | 导出PDF工具栏事件回调 | -| `onLanguageChange` | `(lang: 'zh-CN' \| 'en-US') => void` | - | 语言切换工具栏事件回调 | -| `onThemeChange` | `(isDark: boolean) => void` | - | 主题切换工具栏事件回调 | -| `onFullscreenChange` | `(isFullscreen: boolean) => void` | - | 全屏切换工具栏事件回调 | -| `onTaskUpdate` | `(task: Task) => void` | - | 任务更新事件回调 | -| `onTaskAdd` | `(task: Task) => void` | - | 任务添加事件回调 | -| `onMilestoneSave` | `(milestone: Task) => void` | - | 里程碑保存事件回调 | -| `onMilestoneDelete` | `(milestoneId: number) => void` | - | 里程碑删除事件回调 | -| `onMilestoneIconChange` | `(milestoneId: number, icon: string) => void` | - | 里程碑图标变更事件回调 | -| `onAddTask` | `() => void` | - | 新增任务工具栏事件回调 | -| `onAddMilestone` | `() => void` | - | 新增里程碑工具栏事件回调 | -| `onTodayLocate` | `() => void` | - | 定位今天工具栏事件回调 | -| `onExportCsv` | `() => boolean \| void` | - | 导出CSV工具栏事件回调 | -| `onExportPdf` | `() => void` | - | 导出PDF工具栏事件回调 | -| `onLanguageChange` | `(lang: 'zh-CN' \| 'en-US') => void` | - | 语言切换工具栏事件回调 | -| `onThemeChange` | `(isDark: boolean) => void` | - | 主题切换工具栏事件回调 | -| `onFullscreenChange` | `(isFullscreen: boolean) => void` | - | 全屏切换工具栏事件回调 | - -### GanttChart 事件 - -| 事件名 | 参数 | 说明 | -|----------------------|----------------------------|------------------------------| -| `taskbar-drag-end` | `task: Task` | 任务条拖拽结束 | -| `taskbar-resize-end` | `task: Task` | 任务条大小调整结束 | -| `milestone-drag-end` | `milestone: Task` | 里程碑拖拽结束 | -| `predecessor-added` | `{ targetTask, newTask }` | 添加前置任务后触发。
参数说明:
• `targetTask`:被添加前置任务的目标任务(Task对象)
• `newTask`:新添加的前置任务(Task对象) | -| `successor-added` | `{ targetTask, newTask }` | 添加后置任务后触发。
参数说明:
• `targetTask`:被添加后置任务的目标任务(Task对象)
• `newTask`:新添加的后置任务(Task对象) | -| `task-deleted` | `{ task }` | 删除任务后触发 | -| `task-added` | `{ task }` | 新建任务后触发 | -| `task-updated` | `{ task }` | 更新任务后触发 | - -#### 计时事件用法示例 +在组件中引入 `GanttChart` 组件和样式: ```vue - - - ``` -#### 任务事件用法示例 +> **提示**: 样式文件只需在项目中引入一次即可,建议在 `main.ts` 或根组件中引入。 + +### 第一个示例 + +创建你的第一个甘特图: ```vue - - - -``` - -### 数据类型 - -#### 核心类型 (src/models/classes) - -**Task 任务类型** -```typescript -export interface Task { - id: number // 任务唯一ID - name: string // 任务名称 - predecessor?: number[] // 前置任务ID数组 - assignee?: string // 负责人 - startDate?: string // 开始日期(ISO字符串) - endDate?: string // 结束日期(ISO字符串) - progress?: number // 进度百分比 0-100 - estimatedHours?: number // 预估工时(支持小数,最多2位) - actualHours?: number // 实际工时(支持小数,最多2位) - parentId?: number // 上级任务ID - children?: Task[] // 子任务数组 - collapsed?: boolean // 是否折叠 - isParent?: boolean // 是否为父任务 - type?: string // 任务类型(如 task、story、milestone 等) - description?: string // 任务描述 - icon?: string // 图标 - level?: number // 层级 - // 计时相关字段 - isTimerRunning?: boolean // 计时是否进行中 - timerStartTime?: number // 计时开始时间(时间戳) - timerEndTime?: number // 计时结束时间(时间戳) - timerStartDesc?: string // 计时开始时的描述 - timerElapsedTime?: number // 已累计计时时长(秒) -} -``` - -**Milestone 里程碑类型** -```typescript -// 里程碑实际上是 Task 类型的特殊用法 -// 具有 type: 'milestone' 属性的 Task 对象 -interface Milestone extends Task { - type: 'milestone' // 必须为 'milestone' - startDate: string // 里程碑日期 (必填) - endDate?: string // 结束日期 (可选,通常与startDate相同) -} -``` - -**Language 语言类型** -```typescript -type Language = 'zh' | 'en' // 支持的语言类型 -type Locale = 'zh-CN' | 'en-US' // 完整的语言区域标识 -``` - -#### 配置类型 (src/models/configs) - -**TimelineConfig 时间轴配置** -```typescript -interface TimelineConfig { - startDate: Date // 时间轴开始日期 - endDate: Date // 时间轴结束日期 - zoomLevel: number // 缩放级别 -} -``` - -**ToolbarConfig 工具栏配置** -```typescript -interface ToolbarConfig { - showAddTask?: boolean // 是否显示新增任务按钮 - showAddMilestone?: boolean // 是否显示新增里程碑按钮 - showTodayLocate?: boolean // 是否显示定位今天按钮 - showExportCsv?: boolean // 是否显示导出CSV按钮 - showExportPdf?: boolean // 是否显示导出PDF按钮 - showLanguage?: boolean // 是否显示语言切换按钮 - showTheme?: boolean // 是否显示主题切换按钮 - showFullscreen?: boolean // 是否显示全屏切换按钮 - showTimeScale?: boolean // 是否显示时间刻度切换按钮组(日|周|月) -} -``` - -**TaskListConfig 任务列表配置** -```typescript -interface TaskListConfig { - columns?: TaskListColumnConfig[] // 列配置数组 - showAllColumns?: boolean // 是否显示所有列,默认true - defaultWidth?: number // 默认展开宽度,单位像素,默认320px - minWidth?: number // 最小宽度,单位像素,默认280px,不能小于280px - maxWidth?: number // 最大宽度,单位像素,默认1160px -} - -interface TaskListColumnConfig { - type?: TaskListColumnType // 列类型 - key: string // 用于国际化的key,也可以作为识别符 - label?: string // 显示标签 - cssClass?: string // CSS类名 - width?: number // 可选的列宽度 - visible?: boolean // 是否显示,默认true -} - -type TaskListColumnType = - | 'name' | 'predecessor' | 'assignee' - | 'startDate' | 'endDate' | 'estimatedHours' - | 'actualHours' | 'progress' -``` - -**WorkingHours 工作时间配置** -```typescript -interface WorkingHours { - morning?: { start: number; end: number } // 上午工作时间,如 { start: 8, end: 11 } - afternoon?: { start: number; end: number } // 下午工作时间,如 { start: 13, end: 17 } -} -``` - -**TimelineScale 时间刻度类型** -```typescript -// 时间轴显示刻度类型 -type TimelineScale = 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year' - -// 时间刻度常量 -export const TimelineScale = { - HOUR: 'hour', // 小时视图 - 每列显示一小时 - DAY: 'day', // 日视图 - 每列显示一天 - WEEK: 'week', // 周视图 - 每列显示一周 - MONTH: 'month', // 月视图 - 每列显示一个月 - QUARTER: 'quarter', // 季度视图 - 每列显示一个季度 - YEAR: 'year', // 年视图 - 每列显示一年 -} - -// 时间刻度配置 -interface TimelineScaleConfig { - scale: TimelineScale // 刻度类型 - cellWidth: number // 每个时间单元的宽度(px) - headerLevels: number // 表头层级数 - formatters: { - primary: string // 主要时间标签格式 - secondary?: string // 次要时间标签格式 - } -} -``` - -### 🕐 时间刻度功能说明 - -组件支持多种时间刻度显示,用户可以通过工具栏的日/周/月按钮组或者编程方式切换时间轴的显示粒度: - -#### 内置刻度配置 - -| 刻度类型 | 单元宽度 | 主标签格式 | 副标签格式 | 适用场景 | -|----------|----------|------------|------------|----------| -| `hour` | 40px | yyyy/MM/dd | HH | 精确到小时的项目, 例如药物临床试验 | -| `day` | 30px | yyyy年MM月 | dd | 日常项目管理的标准视图 | -| `week` | 120px | yyyy年MM月 | W | 中期项目的周计划视图 | -| `month` | 180px | yyyy | MM | 长期项目的月度视图 | -| `quarter` | 360px | yyyy | Q | 战略规划的季度视图 | -| `year` | 360px | yyyy | 上半年\|下半年 | 超长期项目年度视图 | - -#### 使用示例 - -```vue - - -``` -#### 组合式函数 (src/composables) - -**useI18n 国际化工具** -```typescript -// 提供多语言支持 -const { - locale, // 当前语言 - setLocale, // 切换语言 - t, // 翻译函数 - formatYearMonth // 年月格式化 -} = useI18n() - -// 支持的语言 -type Locale = 'zh-CN' | 'en-US' -``` - -**useMessage 消息提示工具** -```typescript -// 提供全局消息提示 -const { showMessage } = useMessage() - -// 消息类型 -type MessageType = 'success' | 'error' | 'warning' | 'info' - -// 使用示例 -showMessage('操作成功', 'success') -``` - -## 💻 基本使用 - -### 简单示例 - -```vue +``` +🎯 **[立即体验 Github在线Demo →](https://nelson820125.github.io/jordium-gantt-vue3/)** +推荐使用 DOVE VPN,快速、稳定。 (注意:请合法使用 VPN 资源) + +## 🌞 NPM包使用示例 +请参考项目下的npm-demo,这是一个独立的项目,可以使用IDE单独浏览和启动,运行前请安装element plus以及jordium-gantt-vue3插件包 + +```bash +# npm +npm install element-plus +npm install jordium-gantt-vue3 +npm run dev +``` +--- + +## 组件指南 + +### GanttChart 组件 + +`GanttChart` 是组件库的核心入口,提供了完整的甘特图功能。 + +#### 基础属性 + +| 属性名 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `tasks` | `Task[]` | `[]` | 任务数据数组 | +| `milestones` | `Task[]` | `[]` | 里程碑数据数组(注意:类型为 Task[],需设置 type='milestone') | +| `showToolbar` | `boolean` | `true` | 是否显示工具栏 | +| `useDefaultDrawer` | `boolean` | `true` | 是否使用内置任务编辑抽屉(TaskDrawer) | +| `useDefaultMilestoneDialog` | `boolean` | `true` | 是否使用内置里程碑编辑对话框(MilestoneDialog) | +| `autoSortByStartDate` | `boolean` | `false` | 是否根据开始时间自动排序任务 | +| `allowDragAndResize` | `boolean` | `true` | 是否允许拖拽和调整任务/里程碑大小 | + +#### 配置对象属性 + +完整的配置对象说明请参考 [⚙️ 配置与扩展](#⚙️-配置与扩展) 章节。 + +| 属性名 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `toolbarConfig` | `ToolbarConfig` | `{}` | 工具栏配置 | +| `taskListConfig` | `TaskListConfig` | `undefined` | 任务列表配置 | +| `taskBarConfig` | `TaskBarConfig` | `undefined` | 任务条样式配置 | +| `localeMessages` | `Partial` | `undefined` | 自定义多语言配置 | +| `workingHours` | `WorkingHours` | `{ morning: { start: 8, end: 11 }, afternoon: { start: 13, end: 17 } }` | 工作时间配置 | + +#### 回调函数属性 + +| 属性名 | 类型 | 说明 | +|--------|------|------| +| `onTodayLocate` | `() => void` | 工具栏"今天"按钮点击回调 | +| `onExportCsv` | `() => boolean \| void` | 工具栏"导出CSV"按钮点击回调,返回 `false` 可阻止默认导出 | +| `onExportPdf` | `() => void` | 工具栏"导出PDF"按钮点击回调 | +| `onLanguageChange` | `(lang: 'zh-CN' \| 'en-US') => void` | 语言切换回调 | +| `onThemeChange` | `(isDark: boolean) => void` | 主题切换回调 | +| `onFullscreenChange` | `(isFullscreen: boolean) => void` | 全屏切换回调 | +| `onExpandAll` | `() => void` | 工具栏"全部展开"按钮点击回调 | +| `onCollapseAll` | `() => void` | 工具栏"全部折叠"按钮点击回调 | + +#### 组件事件(Events) + +完整的事件说明请分别参考: +- **任务相关事件**:参见下方 [任务管理](#任务管理) 章节 +- **里程碑相关事件**:参见下方 [里程碑管理](#里程碑管理) 章节 + +**事件列表总览:** + +| 事件名 | 参数 | 说明 | +|--------|------|------| +| `add-task` | - | 点击工具栏"添加任务"按钮 | +| `task-click` | `(task: Task, event: MouseEvent)` | 点击任务 | +| `task-double-click` | `(task: Task)` | 双击任务 | +| `task-added` | `{ task: Task }` | 任务添加后触发 | +| `task-updated` | `{ task: Task }` | 任务更新后触发 | +| `task-deleted` | `{ task: Task }` | 任务删除后触发 | +| `taskbar-drag-end` | `(task: Task)` | 拖拽任务结束 | +| `taskbar-resize-end` | `(task: Task)` | 调整任务大小结束 | +| `predecessor-added` | `{ targetTask, newTask }` | 添加前置任务 | +| `successor-added` | `{ targetTask, newTask }` | 添加后置任务 | +| `timer-started` | `(task: Task)` | 任务计时器启动 | +| `timer-stopped` | `(task: Task)` | 任务计时器停止 | +| `add-milestone` | - | 点击工具栏"添加里程碑"按钮 | +| `milestone-saved` | `(milestone: Task)` | 里程碑保存 | +| `milestone-deleted` | `{ milestoneId: number }` | 里程碑删除 | +| `milestone-icon-changed` | `{ milestoneId, icon }` | 里程碑图标变更 | +| `milestone-drag-end` | `(milestone: Task)` | 拖拽里程碑结束 | + +#### 示例1:最简单的甘特图 + +```vue -``` -### 自定义事件处理 - -```vue - - - -``` - -### 主题和国际化 - -```vue - - -``` - -### 🔧 工作时间配置 - -组件支持设置工作时间,影响任务时长计算和进度显示: - -```vue - - - -``` - -### 📊 高精度工时管理 - -组件支持精确到小数点后2位的工时记录,适合需要精确计费的项目: - -```vue - ``` -## 🤝 贡献与合作 +#### 示例2:带里程碑的甘特图 -### 参与贡献 +```vue + -我们欢迎社区贡献!如果你想参与项目开发: + +``` -如果你发现了 bug 或有功能建议: +#### 示例3:隐藏工具栏,自定义控制按钮绑定事件 -- 📬 [提交 Github Issue](https://github.com/nelson820125/jordium-gantt-vue3/issues) -- 📬 [提交 Gitee Issue](https://gitee.com/jordium/jordium-gantt-vue3/issues) -- 📧 发送邮件至:ning.li@jordium.com / nelson820125@gmail.com / lining820125@163.com +```vue + -### 商业合作 + ``` --- -**🔗 相关链接** -- [GitHub 仓库](https://github.com/nelson820125/jordium-gantt-vue3) -- [更新日志](./CHANGELOG.md) +### 任务管理 -> 💡 **提示**: 如果这个项目对你有帮助,请给我们一个 ⭐ Star! +任务是甘特图的核心元素,组件提供了完整的任务 CRUD 操作支持,包括添加、编辑、删除任务,以及丰富的交互事件。 + +#### Task 数据结构 + +| 字段名 | 类型 | 必填 | 默认值 | 说明 | +|--------|------|------|--------|------| +| `id` | `number` | ✅ | - | 任务唯一标识符 | +| `name` | `string` | ✅ | - | 任务名称 | +| `startDate` | `string` | - | - | 开始日期,格式:'YYYY-MM-DD' 或 'YYYY-MM-DD HH:mm' | +| `endDate` | `string` | - | - | 结束日期,格式:'YYYY-MM-DD' 或 'YYYY-MM-DD HH:mm' | +| `progress` | `number` | - | `0` | 任务进度,范围 0-100 | +| `predecessor` | `number[]` | - | - | 前置任务 ID 数组,标准格式:`[1, 2, 3]`
**兼容格式**:也支持字符串 `'1,2,3'` 或字符串数组 `['1', '2', '3']`,组件会自动解析 | +| `assignee` | `string` | - | - | 任务负责人 | +| `avatar` | `string` | - | - | 任务负责人头像 URL | +| `estimatedHours` | `number` | - | - | 预估工时(小时) | +| `actualHours` | `number` | - | - | 实际工时(小时) | +| `parentId` | `number` | - | - | 父任务 ID,用于任务分组 | +| `children` | `Task[]` | - | - | 子任务数组 | +| `collapsed` | `boolean` | - | `false` | 子任务是否折叠 | +| `isParent` | `boolean` | - | - | 是否为父任务 | +| `type` | `string` | - | - | 任务类型,'milestone' 表示里程碑,'milestone-group' 表示里程碑分组 | +| `description` | `string` | - | - | 任务描述 | +| `icon` | `string` | - | `'diamond'` | 任务图标(用于里程碑),可选值:'diamond', 'flag', 'star', 'rocket' 等 | +| `level` | `number` | - | `0` | 任务层级(自动计算) | +| `isTimerRunning` | `boolean` | - | `false` | 计时器是否运行中 | +| `timerStartTime` | `number` | - | - | 计时开始时间(时间戳) | +| `timerEndTime` | `number` | - | - | 计时结束时间(时间戳) | +| `timerStartDesc` | `string` | - | - | 计时开始时填写的描述 | +| `timerElapsedTime` | `number` | - | `0` | 已计时的时长(毫秒) | +| `isEditable` | `boolean` | - | `true` | 单个任务是否可编辑(可拖拽、拉伸),优先级高于全局 `allowDragAndResize` | +| `[key: string]` | `unknown` | - | - | 支持自定义属性扩展,可添加任意额外字段 | + +> **自定义属性扩展**:Task 接口支持添加任意自定义字段,例如:`priority`、`tags`、`status`、`department` 等业务相关字段。 +> +> **前置任务字段说明**: +> - **标准格式**(推荐):`predecessor: [1, 2, 3]` - number 数组 +> - **兼容格式1**:`predecessor: '1,2,3'` - 逗号分隔的字符串 +> - **兼容格式2**:`predecessor: ['1', '2', '3']` - 字符串数组 +> - 组件内部会自动将所有格式统一解析为 number 数组 +> - 无前置任务:使用空数组 `[]`、空字符串 `''` 或不设置该字段 + +#### 任务相关属性 + +| 属性名 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `tasks` | `Task[]` | `[]` | 任务数据数组 | +| `useDefaultDrawer` | `boolean` | `true` | 是否使用内置的任务编辑抽屉(TaskDrawer) | +| `taskBarConfig` | `TaskBarConfig` | `{}` | 任务条样式配置,详见 [TaskBarConfig 配置](#taskbarconfig-配置) | +| `taskListConfig` | `TaskListConfig` | `undefined` | 任务列表配置,详见 [TaskListConfig 配置](#tasklistconfig-配置) | +| `autoSortByStartDate` | `boolean` | `false` | 是否根据开始时间自动排序任务 | + +**配置说明**: +- **默认模式**:`useDefaultDrawer=true`(默认),双击任务自动打开内置 TaskDrawer +- **自定义编辑器**:`useDefaultDrawer=false` 禁用内置抽屉,监听 `@task-double-click` 事件打开自定义编辑器 +- **只读模式**:`useDefaultDrawer=false` 且不监听 `@task-double-click` 事件,用户双击任务无反应 + +#### 任务事件 + +> **💡 事件驱动架构**:组件采用纯事件驱动设计,所有用户操作(添加、编辑、删除、拖拽等)都会触发对应事件,方便外部监听和处理。 + +| 事件名 | 参数 | 触发时机 | 说明 | +|--------|------|---------|------| +| `add-task` | - | 点击工具栏"添加任务"按钮时 | 可用于自定义新增任务逻辑。如 `useDefaultDrawer=true`,组件会自动打开内置 TaskDrawer | +| `task-click` | `(task: Task, event: MouseEvent) => void` | 点击任务条时 | 单击任务触发 | +| `task-double-click` | `(task: Task) => void` | 双击任务条时 | 双击任务时**始终触发**。`useDefaultDrawer=true` 时组件会额外打开内置编辑器,`false` 时不打开。事件触发与属性值无关 | +| `task-added` | `{ task: Task }` | 任务添加后 | 通过内置 TaskDrawer 添加任务后触发。**注意**:组件已自动更新 `tasks` 数据,外部只需监听此事件做额外处理(如调用 API 保存) | +| `task-updated` | `{ task: Task }` | 任务更新后 | 通过内置 TaskDrawer 或拖拽更新任务后触发。**注意**:组件已自动更新 `tasks` 数据,外部只需监听此事件做额外处理 | +| `task-deleted` | `{ task: Task }` | 任务删除后 | 通过内置 TaskDrawer 删除任务后触发。**注意**:组件已自动更新 `tasks` 数据,外部只需监听此事件做额外处理 | +| `taskbar-drag-end` | `(task: Task) => void` | 拖拽任务条结束时 | 任务位置变化,startDate 和 endDate 已更新。**注意**:组件已自动更新 `tasks` 数据 | +| `taskbar-resize-end` | `(task: Task) => void` | 调整任务条大小结束时 | 任务时长变化,endDate 已更新。**注意**:组件已自动更新 `tasks` 数据 | +| `predecessor-added` | `{ targetTask: Task, newTask: Task }` | 通过右键菜单添加前置任务后 | `targetTask` 是被添加前置任务的任务,`newTask` 是新创建的前置任务 | +| `successor-added` | `{ targetTask: Task, newTask: Task }` | 通过右键菜单添加后置任务后 | `targetTask` 是原任务,`newTask` 是新创建的后置任务(其 predecessor 已包含 targetTask.id) | +| `timer-started` | `(task: Task) => void` | 任务计时器启动时 | 开始记录任务工时 | +| `timer-stopped` | `(task: Task) => void` | 任务计时器停止时 | 停止记录任务工时 | + +**数据同步说明**: +- ✅ **组件内部自动更新**:所有任务的增删改操作,组件都会自动更新 `props.tasks` 数据 +- ✅ **事件仅做通知**:外部监听事件主要用于:显示提示消息、调用后端 API、更新其他相关数据等 +- ❌ **避免重复操作**:不要在事件处理器中再次修改 `tasks` 数据,否则会导致重复更新 + +#### 示例1:基础任务操作 + +```vue + + + +``` + +#### 示例2:任务依赖关系(前置任务/后置任务) + +任务可以通过 `predecessor` 字段配置前置任务,组件会自动绘制依赖关系连线: + +```vue + + + +``` + +**依赖关系说明**: +- **`predecessor` 字段支持多种格式**: + - 标准格式(推荐):`[1, 2, 3]` - number 数组 + - 兼容格式1:`'1,2,3'` - 逗号分隔的字符串 + - 兼容格式2:`['1', '2', '3']` - 字符串数组 + - 组件会自动解析所有格式 +- 前置任务:必须先完成的任务(例如:设计完成后才能开发) +- 后置任务:依赖当前任务的任务(当前任务是其他任务的前置任务) +- 组件会自动绘制依赖关系连线,从前置任务指向依赖它的任务 +- 可以通过内置右键菜单添加/删除前置任务和后置任务 +- 内置菜单删除任务时,组件会自动清理相关的依赖关系引用 +- 无前置任务:使用空数组 `[]`、空字符串 `''` 或不设置 `predecessor` 字段 + +#### 示例3:隐藏工具栏,使用自定义按钮触发事件 + +适用于需要完全自定义控制栏的场景: + +```vue + + + +``` + +> **💡 灵活性设计**: +> - 显示工具栏 + 默认编辑器:最简单的开箱即用方式 +> - 隐藏工具栏 + 自定义按钮 + 默认编辑器:自定义控制栏样式,保留默认编辑功能 +> - 隐藏工具栏 + 自定义按钮 + 自定义编辑器:完全自定义所有交互逻辑 + +### 里程碑管理 + +里程碑用于标记项目中的重要时间节点,如项目启动、阶段完成、产品发布等。组件提供了灵活的里程碑编辑配置,默认使用内置的 MilestoneDialog,也支持完全自定义编辑行为。 + +> **注意**: 里程碑与任务是独立的数据集合,不存在直接关联关系。里程碑通过 `milestones` 属性独立管理。 + +#### Milestone 数据结构 + +| 字段名 | 类型 | 必填 | 默认值 | 说明 | +|--------|------|------|--------|------| +| `id` | `number` | ✅ | - | 里程碑唯一标识符 | +| `name` | `string` | ✅ | - | 里程碑名称 | +| `startDate` | `string` | ✅ | - | 里程碑日期,格式:'YYYY-MM-DD' 或 'YYYY-MM-DD HH:mm' | +| `endDate` | `string` | - | - | 结束日期(通常里程碑不需要,自动设置为与 startDate 相同) | +| `assignee` | `string` | - | - | 负责人 | +| `type` | `string` | ✅ | `'milestone'` | 类型标识,必须设为 'milestone' | +| `icon` | `string` | - | `'diamond'` | 里程碑图标,可选值:'diamond', 'flag', 'star', 'rocket' 等 | +| `description` | `string` | - | - | 里程碑描述 | + +> **注意**:`milestones` 属性的类型为 `Task[]`,需要确保每个里程碑对象的 `type` 字段设置为 `'milestone'`。 + +#### 里程碑相关属性 + +| 属性名 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `milestones` | `Task[]` | `[]` | 里程碑数据数组(类型为 Task[],需确保 type='milestone') | +| `useDefaultMilestoneDialog` | `boolean` | `true` | 是否使用内置的里程碑编辑对话框(MilestoneDialog) | + +**配置说明**: +- **默认模式**:`useDefaultMilestoneDialog=true`(默认),双击里程碑自动打开内置 MilestoneDialog +- **禁用编辑器**:`useDefaultMilestoneDialog=false`,双击里程碑无反应(组件不打开任何编辑器) +- **自定义编辑器**:可以监听 `onMilestoneDoubleClick` 回调或相关事件,实现自定义编辑逻辑 + +> **💡 里程碑与任务的区别**: +> - 里程碑数据通过 `milestones` 属性独立管理,与 `tasks` 分开 +> - 里程碑对象的 `type` 字段必须设置为 `'milestone'` +> - 里程碑不支持子任务、依赖关系等复杂结构 +> - 里程碑主要用于标记关键时间节点 + +#### 里程碑回调函数(向后兼容) + +> **⚠️ 已废弃**:请使用新的事件驱动 API(见下方"里程碑事件"章节) + + +#### 里程碑事件 + +> **💡 事件驱动架构**:里程碑管理采用事件驱动设计,推荐使用事件 API 替代回调函数。 + +| 事件名 | 参数 | 触发时机 | 说明 | +|--------|------|---------|------| +| `add-milestone` | - | 点击工具栏"添加里程碑"按钮时 | 可用于自定义新增里程碑逻辑。如 `useDefaultMilestoneDialog=true`,组件会自动打开内置 MilestoneDialog | +| `milestone-saved` | `(milestone: Task) => void` | 里程碑保存后(新增或编辑) | 通过内置 MilestoneDialog 保存里程碑后触发。**注意**:组件已自动更新 `milestones` 数据,外部只需监听此事件做额外处理(如调用 API 保存) | +| `milestone-deleted` | `{ milestoneId: number }` | 里程碑删除后 | 通过内置 MilestoneDialog 删除里程碑后触发。**注意**:组件已自动更新 `milestones` 数据,外部只需监听此事件做额外处理 | +| `milestone-icon-changed` | `{ milestoneId: number, icon: string }` | 里程碑图标变更后 | 通过内置 MilestoneDialog 修改图标后触发 | +| `milestone-drag-end` | `(milestone: Task) => void` | 拖拽里程碑结束时 | 里程碑日期已更新。**注意**:组件已自动更新 `milestones` 数据 | + +**数据同步说明**: +- ✅ **组件内部自动更新**:所有里程碑的增删改操作,组件都会自动更新 `props.milestones` 数据 +- ✅ **事件仅做通知**:外部监听事件主要用于:显示提示消息、调用后端 API、更新其他相关数据等 +- ❌ **避免重复操作**:不要在事件处理器中再次修改 `milestones` 数据,否则会导致重复更新 + +#### 示例1:使用事件驱动 API(推荐) + +使用新的事件 API,组件会自动管理数据,更加简洁: + +```vue + + + +``` + +#### 示例2:使用自定义里程碑编辑对话框 + +如果需要完全自定义里程碑编辑界面,可以禁用内置对话框并使用自己的组件: + +```vue + + + +``` + +**自定义对话框组件示例** (`CustomMilestoneDialog.vue` - 使用 Element Plus): + +> **注意**:以下示例使用 Element Plus UI 框架。你也可以使用其他 UI 框架(如 Ant Design Vue、Naive UI 等)或原生 HTML 实现。 + +```vue + + + +``` + +> **💡 自定义对话框说明**: +> - 设置 `use-default-milestone-dialog="false"` 禁用内置对话框 +> - 监听 `@add-milestone` 事件打开自定义对话框 +> - 需要手动管理 `milestones` 数组的增删改 +> - 仍然可以监听其他事件(如 `@milestone-drag-end`)处理拖拽等操作 +> - 适合需要复杂表单验证、特殊 UI 设计或额外字段的场景 + +--- + +## ⚙️ 配置与扩展 + +本章节详细介绍 GanttChart 组件的配置选项和扩展能力,包括组件配置、主题与国际化、自定义扩展三个部分。 + +### 组件配置 + +#### ToolbarConfig(工具栏配置) + +自定义工具栏显示的功能按钮和时间刻度选项。 + +**类型定义:** + +| 字段名 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `showAddTask` | `boolean` | `true` | 显示"添加任务"按钮 | +| `showAddMilestone` | `boolean` | `true` | 显示"添加里程碑"按钮 | +| `showTodayLocate` | `boolean` | `true` | 显示"定位到今天"按钮 | +| `showExportCsv` | `boolean` | `true` | 显示"导出 CSV"按钮 | +| `showExportPdf` | `boolean` | `true` | 显示"导出 PDF"按钮 | +| `showLanguage` | `boolean` | `true` | 显示"语言切换"按钮(中/英文) | +| `showTheme` | `boolean` | `true` | 显示"主题切换"按钮(亮色/暗色) | +| `showFullscreen` | `boolean` | `true` | 显示"全屏"按钮 | +| `showTimeScale` | `boolean` | `true` | 显示时间刻度按钮组(控制整组按钮的显隐) | +| `timeScaleDimensions` | `TimelineScale[]` | `['hour', 'day', 'week', 'month', 'quarter', 'year']` | 设置时间刻度按钮组要显示的维度,可选值:`'hour'`、`'day'`、`'week'`、`'month'`、`'quarter'`、`'year'` | +| `defaultTimeScale` | `TimelineScale` | `'week'` | 默认选中的时间刻度 | +| `showExpandCollapse` | `boolean` | `true` | 显示"全部展开/折叠"按钮(用于父子任务树形结构) | + +**TimelineScale 类型说明:** + +```typescript +type TimelineScale = 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year' + +// 也可以使用常量形式 +import { TimelineScale } from 'jordium-gantt-vue3' + +TimelineScale.HOUR // 'hour' - 小时视图 +TimelineScale.DAY // 'day' - 日视图 +TimelineScale.WEEK // 'week' - 周视图 +TimelineScale.MONTH // 'month' - 月视图 +TimelineScale.QUARTER // 'quarter' - 季度视图 +TimelineScale.YEAR // 'year' - 年视图 +``` + +**示例1:完整配置(显示所有功能)** + +```vue + + + +``` + +**示例2:精简配置(只显示常用功能)** + +```vue + +``` + +**示例3:使用 TimelineScale 常量** + +```vue + +``` + +**示例4:极简配置(适合嵌入式使用)** + +```vue + +``` + +> **💡 配置建议**: +> - **默认配置**:不传 `toolbar-config` 时,所有按钮默认显示 +> - **按需显示**:根据业务需求隐藏不需要的功能按钮 +> - **时间刻度**:`timeScaleDimensions` 控制显示哪些时间维度,建议选择 2-4 个常用维度 +> - **响应式布局**:工具栏会自动适配容器宽度,按钮过多时会折叠到更多菜单中 + +#### TaskListConfig(任务列表配置) + +自定义任务列表的显示列、宽度限制等。任务列表位于甘特图左侧,显示任务的详细信息。 + +**类型定义:** + +| 字段名 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `columns` | `TaskListColumnConfig[]` | 默认8列 | 任务列表的列配置数组,定义显示哪些列及其属性 | +| `showAllColumns` | `boolean` | `true` | 是否显示所有列。`true` 时忽略 `columns` 中的 `visible` 设置 | +| `defaultWidth` | `number \| string` | `320` | 默认展开宽度。支持像素数字(如 `320`)或百分比字符串(如 `'30%'`) | +| `minWidth` | `number \| string` | `280` | 最小宽度。支持像素数字(如 `280`)或百分比字符串(如 `'20%'`)。不能小于 280px | +| `maxWidth` | `number \| string` | `1160` | 最大宽度。支持像素数字(如 `1160`)或百分比字符串(如 `'80%'`) | + +**TaskListColumnConfig 类型定义:** + +| 字段名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| `key` | `string` | ✅ | 列的唯一标识符,用于访问 Task 对象中的字段,也用于国际化 | +| `label` | `string` | - | 列的显示标签(表头文字) | +| `cssClass` | `string` | - | 自定义 CSS 类名 | +| `width` | `number` | - | 列宽度(单位:像素) | +| `visible` | `boolean` | - | 是否显示该列,默认 `true`。当 `showAllColumns=true` 时此设置无效 | + +**示例1:基础配置(调整宽度)** + +```vue + + + +``` + +**示例2:使用百分比宽度** + +```vue + + + +``` + +**示例3:自定义显示列(标准配置)** + +根据业务需求,可以自定义要显示的列、列宽度和显示顺序。建议先定义列配置数组,再赋值给 `columns` 属性。 + +```vue + + + +``` + +**示例4:精简列配置** + +只显示核心信息列,适合空间有限或需要简洁展示的场景。 + +```vue + +``` + +**示例5:自定义业务列** + +添加业务相关的自定义列,需要确保 Task 对象中包含对应字段。 + +```vue + +``` + +**示例6:动态列配置** + +配合 `ref` 和 `computed` 实现列的动态显示/隐藏和宽度调整。 + +```vue + + + +``` + +> **💡 配置说明**: +> - **默认行为**:不传 `task-list-config` 时,显示所有 8 个默认列,宽度为 320px +> - **宽度单位**:支持像素(`number`)和百分比(`string`,如 `'30%'`)两种方式 +> - **百分比计算**:基于甘特图容器的总宽度,响应式调整 +> - **列顺序**:`columns` 数组的顺序决定列的显示顺序 +> - **列配置规范**:建议先定义 `TaskListColumnConfig[]` 类型的列数组,再赋值给 `columns` 属性 +> - **自定义列支持**:Task 接口通过 `[key: string]: unknown` 索引签名支持任意自定义字段,组件会通过 `task[column.key]` 动态读取列值,无需修改 Task 接口即可添加自定义列 +> - **动态配置**:配合 `ref` 和 `computed` 可实现列的动态显示/隐藏和宽度调整 +> - **最小宽度限制**:`minWidth` 不能小于 280px,这是保证基本可用性的最小值 + +#### TaskBarConfig(任务条配置) + +控制任务条的显示内容和交互行为。 + +**配置字段:** + +| 字段名 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `showAvatar` | `boolean` | `true` | 是否展示头像 | +| `showTitle` | `boolean` | `true` | 是否展示标题文字 | +| `showProgress` | `boolean` | `true` | 是否展示进度文字 | +| `dragThreshold` | `number` | `5` | 拖拽触发阈值(像素) | +| `resizeHandleWidth` | `number` | `5` | 拉伸手柄宽度(像素),最大 15px | +| `enableDragDelay` | `boolean` | `false` | 是否启用拖拽延迟(防止误触) | +| `dragDelayTime` | `number` | `150` | 拖拽延迟时间(毫秒) | + +> **💡 编辑权限控制**: +> - **全局控制**:使用 `` 禁用所有任务的拖拽/拉伸 +> - **单个任务控制**:设置任务对象的 `isEditable: false` 属性单独控制某个任务 + +**示例1:完整配置** + +```vue + + + +``` + +**示例2:全局只读模式** + +禁用所有任务的编辑操作。 + +```vue + +``` + +**示例3:单个任务只读** + +仅某些任务不可编辑,其他任务正常。 + +```vue + +``` + +**示例4:精简显示** + +仅显示任务条,隐藏头像、标题和进度文字。 + +```vue + +``` + +**示例5:防误触配置** + +移动端或触摸屏场景下,增加拖拽阈值和延迟时间。 + +```vue + +``` + +#### Timeline 容器自动填充配置 + +组件内置了智能的时间线范围计算逻辑,确保无论任务数据量多少、任务持续时间长短,时间线始终能够填充满容器宽度,提供最佳的视觉体验。 + +**核心设计思路:** + +1. **基础缓冲机制**:在任务的实际时间范围基础上,根据不同视图添加固定的缓冲区 + - 小时视图:任务范围前后各 ±1 天 + - 日视图:任务范围前后各 ±30 天 + - 周视图:任务范围前后各 ±8 周(约2个月) + - 月视图:任务范围前后各 ±1 年 + - 季度视图:任务范围前后各 ±1 年 + - 年视图:任务范围前后各 ±1 年 + +2. **容器宽度适配**:基础缓冲后,如果计算出的时间线宽度小于容器宽度,会自动扩展范围 + - 计算容器需要的时间单位数(天/周/月/季度/年) + - 在基础范围两侧**对称扩展**,确保时间线填充满容器 + +3. **空数据处理**:当没有任务数据时,根据容器宽度和时间刻度计算合理的时间范围 + - 以当前日期为中心 + - 根据容器宽度动态计算需要显示的时间跨度 + - 确保最小显示范围(如日视图至少60天,周视图至少20周等) + +4. **视图切换独立计算**:每次切换时间刻度时,都会独立重新计算该视图的最佳时间范围 + - 避免不同视图共享缓存导致的范围不合理 + - 每个视图都能获得最优的显示效果 + +**各视图计算模式对照表:** + +| 视图 | 单位宽度 | 基础缓冲 | 空数据最小范围 | 容器自动填充? | +|------|----------|----------|----------------|----------| +| 小时视图 | 30px/时 | ±1天 | 3天 | ✅ | +| 日视图 | 30px/天 | ±30天 | 60天 | ✅ | +| 周视图 | 60px/周 | ±2月 | 20周 | ✅ | +| 月视图 | 60px/月 | ±1年 | 3年 | ✅ | +| 季度视图 | 60px/季度 (240px/年) | ±1年 | 5年 | ✅ | +| 年视图 | 360px/年 | ±1年 | 5年 | ✅ | + +**实际应用场景:** + +- **短期任务**(如1周项目, 分辨率1080): + - 不会导致时间线过窄,自动扩展到填充满容器 + - 日视图:1周(7天×30px=210px) → 扩展至 ≥1200px(约40天) + - 周视图:1周(60px) → 扩展至 ≥1200px(约20周) + +- **长期项目**(如2年项目): + - 添加固定缓冲后,自动适配容器 + - 月视图:24个月 + 缓冲 → 如需要则扩展至容器宽度 + - 季度视图:8个季度 + 缓冲 → 如需要则扩展至容器宽度 + +- **空白看板**(无任务数据): + - 日视图:以今天为中心,显示至少60天 + - 周视图:以今天为中心,显示至少20周 + - 月视图:显示至少3年 + - 季度/年视图:显示至少5年 + +> **💡 自动化优势**: +> - 无需手动设置 `startDate` 和 `endDate`,组件会自动计算最优范围 +> - 响应式容器宽度变化,时间线自动重新计算 +> - 不同视图独立优化,切换视图时自动调整到最佳显示效果 +> - 避免出现时间线过窄或留白过多的问题 +> - 适用不同分辨率展示 + +### 主题与国际化 + +#### 主题切换 + +组件内置亮色和暗色两种主题,可通过工具栏按钮切换,也可监听切换事件: + +```vue + + + +``` + +#### 自定义主题变量 + +通过覆盖 CSS 变量实现主题自定义: + +```css +/* 自定义亮色主题 */ +:root { + /* 主色调 */ + --gantt-primary-color: #409eff; + --gantt-success-color: #67c23a; + --gantt-warning-color: #e6a23c; + --gantt-danger-color: #f56c6c; + + /* 背景色 */ + --gantt-bg-primary: #ffffff; + --gantt-bg-secondary: #f5f7fa; + --gantt-bg-hover: #ecf5ff; + + /* 文字颜色 */ + --gantt-text-primary: #303133; + --gantt-text-secondary: #606266; + --gantt-text-placeholder: #c0c4cc; + + /* 边框颜色 */ + --gantt-border-color: #dcdfe6; + --gantt-border-color-light: #e4e7ed; + + /* 任务条颜色 */ + --gantt-task-bg: #409eff; + --gantt-task-border: #66b1ff; + --gantt-task-text: #ffffff; +} + +/* 自定义暗色主题 */ +.dark { + --gantt-bg-primary: #1a1a1a; + --gantt-bg-secondary: #2c2c2c; + --gantt-bg-hover: #3a3a3a; + + --gantt-text-primary: #e5e5e5; + --gantt-text-secondary: #b0b0b0; + + --gantt-border-color: #3a3a3a; + --gantt-border-color-light: #4a4a4a; + + --gantt-task-bg: #409eff; + --gantt-task-border: #66b1ff; + --gantt-task-text: #ffffff; +} +``` + +#### 语言切换 + +组件内置中文(zh-CN)和英文(en-US)两种语言,可通过工具栏按钮切换: + +```vue + + + +``` + +#### 自定义翻译 + +通过 `localeMessages` 属性覆盖或扩展默认翻译: + +```vue + + + +``` + +> **💡 提示**: +> - `localeMessages` 采用**深度合并**策略,只需传递需要覆盖的字段即可 +> - 支持嵌套对象,如 `taskList.name`、`toolbar.addTask` 等 +> - 完整的翻译键请参考组件内置的 `messages['zh-CN']` 对象 + +### 自定义扩展 + +#### 插槽 (Slots) + +组件提供了插槽支持,允许自定义任务内容的渲染。 + +##### `custom-task-content` 插槽 + +用于自定义任务在任务列表(TaskRow)和时间轴(TaskBar)中的显示内容。 + +**插槽参数:** + +| 参数名 | 类型 | 来源 | 说明 | +|--------|------|------|------| +| `type` | `'task-row'` \| `'task-bar'` | 通用 | 插槽调用位置标识 | +| `task` | `Task` | 通用 | 当前任务对象 | + +**TaskRow 特有参数(当 `type === 'task-row'` 时):** + +| 参数名 | 类型 | 说明 | +|--------|------|------| +| `isRowContent` | `boolean` | 标识为行内容 | +| `level` | `number` | 任务层级 | +| `indent` | `string` | 缩进样式 | +| `isHovered` | `boolean` | 是否悬停 | +| `hoveredTaskId` | `number \| null` | 当前悬停任务ID | +| `isParent` | `boolean` | 是否为父任务 | +| `hasChildren` | `boolean` | 是否有子任务 | +| `collapsed` | `boolean` | 是否折叠 | +| `formattedTimer` | `string` | 格式化的计时文本 | +| `timerRunning` | `boolean` | 计时器是否运行 | +| `timerElapsed` | `number` | 已计时时长 | +| `isOvertime` | `number \| boolean \| undefined` | 是否超时 | +| `overdueDays` | `number` | 逾期天数 | +| `overtimeText` | `string` | 超时文本 | +| `overdueText` | `string` | 逾期文本 | +| `daysText` | `string` | 天数文本 | +| `progressClass` | `string` | 进度CSS类名 | + +**TaskBar 特有参数(当 `type === 'task-bar'` 时):** + +| 参数名 | 类型 | 说明 | +|--------|------|------| +| `status` | `object` | 任务状态对象,包含 `type`, `color`, `bgColor`, `borderColor` | +| `statusType` | `string` | 状态类型:`'completed'`, `'delayed'`, `'in-progress'`, `'not-started'`, `'parent'` | +| `isParent` | `boolean` | 是否为父任务 | +| `progress` | `number` | 任务进度(0-100) | +| `currentTimeScale` | `TimelineScale` | 当前时间刻度 | +| `rowHeight` | `number` | 行高(像素) | +| `dayWidth` | `number` | 每天宽度(像素) | + +**使用示例:** + +```vue + + + +``` + +**自定义内容组件示例:** + +```vue + + + + + + +``` + +> **💡 使用场景**: +> - 支持 HTML 格式的任务名称 +> - 添加自定义图标、标签或徽章 +> - 根据任务状态显示不同样式 +> - 集成第三方富文本渲染 +> - 显示额外的业务信息 + +> **⚠️ 注意事项**: +> - 插槽内容会同时在 TaskRow 和 TaskBar 中渲染 +> - 需要根据 `type` 参数区分渲染位置 +> - TaskRow 和 TaskBar 的可用空间不同,需要适配布局 +> - 避免在插槽内容中使用过于复杂的组件,可能影响性能 + +--- + +## ❓ 常见问题 + +### 如何集成到现有项目? + +1. 安装依赖 +2. 引入组件和样式 +3. 传入数据 +4. 监听事件处理业务逻辑 + +详见 [快速开始](#-快速开始) 章节。 + +### 支持哪些浏览器? + +- Chrome >= 90 +- Firefox >= 88 +- Safari >= 14 +- Edge >= 90 + +--- + +## 📁 项目结构 + +``` +jordium-gantt-vue3/ +├── src/ # 源代码 +│ ├── components/ # Vue 组件 +│ │ ├── GanttChart.vue # 甘特图主组件 +│ │ ├── TaskList.vue # 任务列表 +│ │ ├── Timeline.vue # 时间轴 +│ │ └── ... +│ ├── models/ # 数据模型 +│ │ ├── classes/ # 类定义 +│ │ ├── configs/ # 配置接口 +│ │ └── types/ # 类型定义 +│ ├── composables/ # 组合式函数 +│ ├── styles/ # 样式文件 +│ └── utils/ # 工具函数 +├── demo/ # 示例代码 +├── docs/ # 文档 +├── public/ # 公共资源 +└── package.json # 项目配置 +``` + +--- + +## 🔗 相关链接 + +- **在线演示**: [https://nelson820125.github.io/jordium-gantt-vue3/](https://nelson820125.github.io/jordium-gantt-vue3/) +- **GitHub**: [https://github.com/nelson820125/jordium-gantt-vue3](https://github.com/nelson820125/jordium-gantt-vue3) +- **npm**: [https://www.npmjs.com/package/jordium-gantt-vue3](https://www.npmjs.com/package/jordium-gantt-vue3) +- **更新日志**: [CHANGELOG.md](./CHANGELOG.md) +- **贡献指南**: [CONTRIBUTING.md](./CONTRIBUTING.md) + +--- + +## 🤝 贡献 + +欢迎提交 Issue 和 Pull Request! + +详细的贡献指南请查看 [CONTRIBUTING.md](./CONTRIBUTING.md)。 + +### 贡献者 + +感谢所有为本项目做出贡献的开发者! + +查看完整的 [贡献者名单](./CONTRIBUTORS.md)。 + +--- + +## 📄 开源协议 + +[MIT License](./LICENSE) © 2025 JORDIUM.COM + +--- + +

+ 如果这个项目对你有帮助,请给一个 ⭐️ 支持一下! +

diff --git a/demo/App.vue b/demo/App.vue index 7d79204..0a4bfed 100644 --- a/demo/App.vue +++ b/demo/App.vue @@ -14,6 +14,7 @@ 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() @@ -63,21 +64,104 @@ const taskListWidth = ref({ maxWidth: 1200, // 最大宽度1200px(比默认1160px略大) }) +// TaskList宽度单位配置(px 或百分比) +const widthUnit = ref<'px' | '%'>('px') +const widthPercentage = ref({ + defaultWidth: 25, + minWidth: 15, + maxWidth: 60, +}) + const taskListConfig = computed(() => ({ columns: availableColumns.value, - defaultWidth: taskListWidth.value.defaultWidth, - minWidth: taskListWidth.value.minWidth, - maxWidth: taskListWidth.value.maxWidth, + 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(() => ({ + 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(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 @@ -110,10 +194,9 @@ const handleCustomCsvExport = () => { // 其他工具栏事件处理器示例 const handleAddTask = () => { - // 打开TaskDrawer进行新建任务 - currentTask.value = null - isEditMode.value = false - showTaskDrawer.value = true + // GanttChart 内部会打开 TaskDrawer(如果 useDefaultDrawer=true) + // 这里可以添加自定义逻辑,比如显示提示消息 + showMessage('准备新增任务...', 'info', { closable: true }) } const handleAddMilestone = () => { @@ -137,397 +220,125 @@ const handleThemeChange = (isDark: boolean) => { }) } -// 里程碑保存处理器示例 -const handleMilestoneSave = (milestone: Task) => { - // 更新本地里程碑数据 - const milestoneIndex = milestones.value.findIndex(m => m.id === milestone.id) - if (milestoneIndex !== -1) { +// 从外部 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[milestoneIndex] = { ...milestone } + milestones.value.splice(existingIndex, 1, milestone) } else { - // 新增里程碑 - const newMilestone = { - ...milestone, - id: Date.now(), // 生成临时ID - type: 'milestone', - } - milestones.value.push(newMilestone) + // 添加新里程碑 + 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 handleMilestoneDelete = async (milestoneId: number) => { - // 从里程碑数据中删除 - const milestoneIndex = milestones.value.findIndex(m => m.id === milestoneId) - if (milestoneIndex !== -1) { - milestones.value.splice(milestoneIndex, 1) - showMessage(t.value.milestoneDeleteSuccess, 'success', { closable: false }) +// 任务更新事件处理器 +const handleTaskUpdateEvent = (e: { task: Task }) => { + const updatedTask = e.task + showMessage(`Demo 任务[${updatedTask.name}] 已更新`, 'success') - // 等待DOM更新完成 - await nextTick() + // 注意:GanttChart 内部已经通过 updateTaskInTree 更新了任务 + // 这里只需要显示消息,无需再次执行更新逻辑 - // 触发全局事件,通知其他组件里程碑已删除 - window.dispatchEvent( - new CustomEvent('milestone-deleted', { - detail: { milestoneId }, - }), - ) - - // 触发强制更新事件,确保Timeline重新渲染 - window.dispatchEvent( - new CustomEvent('milestone-data-changed', { - detail: { milestones: milestones.value }, - }), - ) - } - - // 关闭里程碑对话框 - showMilestoneDialog.value = false -} - -// 任务更新处理器 -const handleTaskUpdate = (updatedTask: Task) => { // 计时信息展示(无论来源于 TaskBar/TaskRow 还是 TaskDrawer header) if (updatedTask.timerStartTime) { - const msg = `任务【${updatedTask.name}】已更新` - showMessage(msg, 'success', { closable: true }) - } - - // 先找到原任务,检查parentId是否改变了 - const findOriginalTask = (taskArray: Task[]): Task | null => { - for (const task of taskArray) { - if (task.id === updatedTask.id) { - return task - } - if (task.children && task.children.length > 0) { - const found = findOriginalTask(task.children) - if (found) return found - } - } - return null - } - - const originalTask = findOriginalTask(tasks.value) - if (!originalTask) { - showMessage(formatTranslation('taskNotFound', { id: updatedTask.id }), 'warning', { - closable: true, - }) - return - } - - // 检查parentId是否改变了 - const parentIdChanged = originalTask.parentId !== updatedTask.parentId - - if (parentIdChanged) { - // parentId改变了,需要移除任务并重新添加到新位置 - const removeTaskFromArray = (taskArray: Task[]): Task | null => { - for (let i = 0; i < taskArray.length; i++) { - if (taskArray[i].id === updatedTask.id) { - const removedTask = taskArray.splice(i, 1)[0] - return removedTask - } - - if (taskArray[i].children && taskArray[i].children.length > 0) { - const removedTask = removeTaskFromArray(taskArray[i].children!) - if (removedTask) { - if (taskArray[i].children!.length === 0) { - delete taskArray[i].children - taskArray[i].isParent = taskArray[i].type === 'story' - } - return removedTask - } - } - } - return null - } - - const removedTask = removeTaskFromArray(tasks.value) - if (!removedTask) return - - const taskToAdd = { - ...updatedTask, - isParent: - updatedTask.type === 'story' || (updatedTask.children && updatedTask.children.length > 0), - } - - // 重新添加到新位置 - if (taskToAdd.parentId) { - const addToParentChildren = (taskArray: Task[]): boolean => { - for (const task of taskArray) { - if (task.id === taskToAdd.parentId) { - if (!task.children) task.children = [] - task.children.push(taskToAdd) - task.isParent = true - return true - } - if (task.children && task.children.length > 0) { - if (addToParentChildren(task.children)) return true - } - } - return false - } - - if (!addToParentChildren(tasks.value)) { - showMessage( - formatTranslation('newParentTaskNotFound', { parentId: taskToAdd.parentId }), - 'warning', - { closable: true }, - ) - tasks.value.push(taskToAdd) - } - } else { - tasks.value.push(taskToAdd) - } - } else { - // parentId没有改变,只是就地更新任务数据 - const updateTaskInPlace = (taskArray: Task[]): boolean => { - for (let i = 0; i < taskArray.length; i++) { - if (taskArray[i].id === updatedTask.id) { - // 保持原有的children和层级关系 - taskArray[i] = { - ...updatedTask, - children: taskArray[i].children, // 保持原有的children - isParent: - updatedTask.type === 'story' || - (taskArray[i].children && taskArray[i].children.length > 0), - } - return true - } - - if (taskArray[i].children && taskArray[i].children.length > 0) { - if (updateTaskInPlace(taskArray[i].children!)) { - return true - } - } - } - return false - } - - if (!updateTaskInPlace(tasks.value)) { - showMessage(formatTranslation('inPlaceUpdateFailed', { id: updatedTask.id }), 'warning', { - closable: true, - }) - } + const msg = `任务【${updatedTask.name}】计时已更新` + showMessage(msg, 'info', { closable: true }) } } -// 任务添加处理器 -const handleTaskAdd = (newTask: Task) => { - // 为新任务生成ID(如果没有的话) - if (!newTask.id) { - // 找到当前最大的ID,然后+1 - const maxId = Math.max( - ...tasks.value.map(t => t.id || 0), - ...milestones.value.map(m => m.id || 0), - 0, - ) - newTask.id = maxId + 1 - } +// 任务添加事件处理器 +const handleTaskAddEvent = (e: { task: Task }) => { + const newTask = e.task + showMessage(`Demo 任务[${newTask.name}] 已创建`, 'success') - // 设置isParent属性 - newTask.isParent = newTask.type === 'story' || (newTask.children && newTask.children.length > 0) - - // 处理父子关系 - if (newTask.parentId) { - // 如果有上级任务,需要将子任务添加到父任务的children中 - const addToParentChildren = (taskArray: Task[]): boolean => { - for (const task of taskArray) { - if (task.id === newTask.parentId) { - // 找到父任务 - if (!task.children) { - // 如果父任务没有children属性,创建一个 - task.children = [] - } - // 将新任务添加到父任务的children中 - task.children.push({ ...newTask }) - return true - } - // 递归查找父任务(支持多层嵌套) - if (task.children && task.children.length > 0) { - if (addToParentChildren(task.children)) { - return true - } - } - } - return false - } - - // 尝试添加到父任务的children中 - if (!addToParentChildren(tasks.value)) { - // 如果没找到父任务,作为顶级任务添加 - tasks.value.push({ ...newTask }) - } - } else { - // 没有父任务,作为顶级任务添加 - tasks.value.push({ ...newTask }) - } + // 注意:GanttChart 内部已经通过 insertTask 添加了任务 + // 这里只需要显示消息,无需再次执行添加逻辑 } -// 任务删除处理器 -const handleTaskDelete = (taskToDelete: Task) => { - // 收集要删除的所有任务ID(包括子任务) - const deletedTaskIds = collectAllTaskIds(taskToDelete) +// 任务删除事件处理器 +const handleTaskDeleteEvent = (e: { task: Task; deleteChildren?: boolean }) => { + const task = e.task - // 递归查找和删除任务(支持嵌套结构) - const deleteTaskFromArray = (taskArray: Task[]): boolean => { - for (let i = 0; i < taskArray.length; i++) { - if (taskArray[i].id === taskToDelete.id) { - // 找到任务,删除它 - taskArray.splice(i, 1) - showMessage(t.value.taskDeletedSuccess, 'success', { closable: false }) - return true - } + showMessage(`Demo 任务[${task.name}] 已删除`, 'success') - // 如果有子任务,递归查找 - if (taskArray[i].children && taskArray[i].children.length > 0) { - if (deleteTaskFromArray(taskArray[i].children!)) { - return true - } - } - } - return false - } + // 注意:GanttChart 内部已经通过 removeTaskFromTree 删除了任务 + // 这里只需要显示消息,无需再次执行删除逻辑 + // 如果需要在删除后清理其他数据(如 predecessor),可以在这里处理 - if (deleteTaskFromArray(tasks.value)) { - // 删除成功后,清理predecessor依赖关系 - cleanupPredecessorReferences(deletedTaskIds) - } else { - showMessage(formatTranslation('taskToDeleteNotFound', { id: taskToDelete.id }), 'warning', { - closable: true, - }) - } + // 收集被删除任务的所有ID(包括子任务) + const deletedTaskIds = collectAllTaskIds(task) + // 清理predecessor依赖关系 + cleanupPredecessorReferences(deletedTaskIds) } -// 删除story及其所有子任务 -const handleStoryDeleteWithChildren = (storyToDelete: Task) => { - // 收集要删除的所有任务ID(story及其所有子任务) - const deletedTaskIds = collectAllTaskIds(storyToDelete) +// 里程碑图标变更事件处理器(新的事件驱动 API) +const handleMilestoneIconChanged = (event: { milestoneId: number; icon: string }) => { + // 组件已自动更新 milestones 中的 icon,这里只需处理额外的业务逻辑 + const { icon } = event - // 递归查找和删除story(包含其所有子任务) - const deleteStoryFromArray = (taskArray: Task[]): boolean => { - for (let i = 0; i < taskArray.length; i++) { - if (taskArray[i].id === storyToDelete.id) { - // 找到story,直接删除它(包含所有子任务) - taskArray.splice(i, 1) - showMessage( - formatTranslation('storyDeleteAllSuccess', { name: storyToDelete.name }), - 'success', - { - closable: false, - }, - ) - return true - } - - // 如果有子任务,递归查找 - if (taskArray[i].children && taskArray[i].children.length > 0) { - if (deleteStoryFromArray(taskArray[i].children!)) { - return true - } - } - } - return false - } - - if (deleteStoryFromArray(tasks.value)) { - // 删除成功后,清理predecessor依赖关系 - cleanupPredecessorReferences(deletedTaskIds) - } else { - showMessage(formatTranslation('storyNotFound', { id: storyToDelete.id }), 'warning', { - closable: true, - }) - } -} - -// 仅删除story,保留子任务 -const handleStoryDeleteOnly = (storyToDelete: Task) => { - // 递归查找story并处理其子任务 - const deleteStoryOnlyFromArray = (taskArray: Task[]): boolean => { - for (let i = 0; i < taskArray.length; i++) { - if (taskArray[i].id === storyToDelete.id) { - // 找到story - const childrenToPromote = taskArray[i].children || [] - - // 步骤a: 克隆并升级子任务 - const upgradedChildren: Task[] = [] - if (childrenToPromote.length > 0) { - childrenToPromote.forEach(child => { - // 深度克隆子任务 - const clonedChild = JSON.parse(JSON.stringify(child)) - // 升级:移除parentId,使其成为顶级任务 - delete clonedChild.parentId - upgradedChildren.push(clonedChild) - }) - } - - // 步骤b: 删除story数据 - taskArray.splice(i, 1) - - // 将升级后的子任务push到tasks集合顶层 - if (upgradedChildren.length > 0) { - tasks.value.push(...upgradedChildren) - } - - showMessage( - formatTranslation('storyDeleteOnlySuccess', { - name: storyToDelete.name, - count: upgradedChildren.length, - }), - 'success', - { - closable: false, - }, - ) - return true - } - - // 如果有子任务,递归查找 - if (taskArray[i].children && taskArray[i].children.length > 0) { - if (deleteStoryOnlyFromArray(taskArray[i].children!)) { - return true - } - } - } - return false - } - - if (deleteStoryOnlyFromArray(tasks.value)) { - // 删除成功后,清理story的predecessor依赖关系(只清理story本身的ID,不影响子任务) - cleanupPredecessorReferences([storyToDelete.id]) - } else { - showMessage(formatTranslation('storyNotFound', { id: storyToDelete.id }), 'warning', { - closable: true, - }) - } -} - -// 里程碑图标变更处理器 -const handleMilestoneIconChange = (milestoneId: number, icon: string) => { - const milestoneIndex = milestones.value.findIndex(m => m.id === milestoneId) - if (milestoneIndex !== -1) { - milestones.value[milestoneIndex].icon = icon - } else { - showMessage(formatTranslation('milestoneIconUpdateNotFound', { id: milestoneId }), 'warning', { - closable: true, - }) - } -} - -const handleTaskDrawerDelete = (task: Task, deleteChildren?: boolean) => { - if (task.type === 'story' && deleteChildren === true) { - // 删除story及其所有子任务 - handleStoryDeleteWithChildren(task) - } else if (task.type === 'story' && deleteChildren === false) { - // 仅删除story,保留子任务 - handleStoryDeleteOnly(task) - } else { - // 普通任务删除 - handleTaskDelete(task) - } - showTaskDrawer.value = false + // 可以在这里添加其他业务逻辑,如: + // - 发送更新到服务器 + // - 显示成功提示 + // - 记录操作日志等 + showMessage(`里程碑图标已更新为 ${icon}`, 'info', { closable: false }) } // GitHub 文档处理函数 @@ -546,23 +357,21 @@ const handleGiteeDocsClick = (event: Event) => { // 任务拖拽/拉伸/里程碑拖拽监听 function handleTaskbarDragOrResizeEnd(newTask) { - const oldTask = findTaskDeep(tasks.value, newTask.id) - if (!oldTask) return + // 注意:GanttChart 已经更新了 props.tasks,所以这里只显示更新后的信息 showMessage( - `任务【${newTask.name}】\n` + - `开始: ${oldTask.startDate} → ${newTask.startDate}\n` + - `结束: ${oldTask.endDate} → ${newTask.endDate}`, - 'info', + `任务【${newTask.name}】已更新\n` + + `开始日期: ${newTask.startDate}\n` + + `结束日期: ${newTask.endDate}`, + 'success', { closable: true }, ) } function handleMilestoneDragEnd(newMilestone) { - const oldMilestone = findTaskDeep(milestones.value, newMilestone.id) - if (!oldMilestone) return + // 注意:GanttChart 已经更新了数据 showMessage( - `里程碑【${newMilestone.name}】\n` + - `开始: ${oldMilestone.endDate} → ${newMilestone.startDate}`, - 'info', + `里程碑【${newMilestone.name}】已更新\n` + + `日期: ${newMilestone.startDate || newMilestone.endDate}`, + 'success', { closable: true }, ) } @@ -572,18 +381,6 @@ onMounted(() => { milestones.value = demoData.milestones as Task[] }) -// 递归查找任务/里程碑,因为原始结构一致 -function findTaskDeep(taskArray: Task[], id: number): Task | null { - for (const task of taskArray) { - if (task.id === id) return task - if (task.children && task.children.length > 0) { - const found = findTaskDeep(task.children, id) - if (found) return found - } - } - return null -} - // 清理被删除任务的predecessor依赖关系 const cleanupPredecessorReferences = (deletedTaskIds: number[]) => { // 递归清理所有任务(包括嵌套的子任务)的predecessor @@ -646,7 +443,7 @@ function onTimerStopped(task: Task) { } function taskDebug(item: any) { - //console.log('Task Debug:', item) + // Placeholder for debugging } @@ -683,7 +480,7 @@ function taskDebug(item: any) { - +

@@ -693,9 +490,12 @@ function taskDebug(item: any) { fill="none" xmlns="http://www.w3.org/2000/svg" > - + - {{ t.taskListConfig.title }} + {{ t.configDemo }}

+
+ + + +
+ +
+
{{ t.taskListConfig.width.title }}
+ + +
+ +
-
- - - px + + +
+
+ + + px +
+
+ + + px +
+
+ + + px +
-
- - - px + + +
+
+ + + % +
+
+ + + % +
+
+ + + % +
+ + +
+
{{ t.taskListConfig.columns.title }}
+
+ +
+
+
+
- +
-

- +
+ + + + + + {{ t.taskBarConfig.title }} +
+

-
- + + + +
+ + + +
+ +
+
{{ t.taskBarConfig.display.title }}
+
+ + + +
+
+ + +
+
{{ t.taskBarConfig.mistouch.title }}
+
+
+ + + {{ t.taskBarConfig.mistouch.allowDragOnClickHint }} + +
+
+ + + + {{ t.taskBarConfig.mistouch.dragThresholdHint }} + +
+
+ + + + {{ t.taskBarConfig.mistouch.resizeHandleWidthHint }} + +
+
+ + + {{ t.taskBarConfig.mistouch.enableDragDelayHint }} + +
+
+ + + + {{ t.taskBarConfig.mistouch.dragDelayTimeHint }} + +
+
+
+
+
@@ -819,21 +824,21 @@ function taskDebug(item: any) { :milestones="milestones" :toolbar-config="toolbarConfig" :task-list-config="taskListConfig" + :task-bar-config="taskBarConfig" :working-hours="workingHoursConfig" - :on-add-task="handleAddTask" - :on-add-milestone="handleAddMilestone" + :use-default-milestone-dialog="true" + :allow-drag-and-resize="allowDragAndResize" :on-export-csv="handleCustomCsvExport" :on-language-change="handleLanguageChange" :on-theme-change="handleThemeChange" - :on-milestone-save="handleMilestoneSave" - :on-milestone-delete="handleMilestoneDelete" - :on-task-update="handleTaskUpdate" - :on-task-add="handleTaskAdd" - :on-task-delete="handleTaskDrawerDelete" - :on-milestone-icon-change="handleMilestoneIconChange" + @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" @@ -846,9 +851,9 @@ function taskDebug(item: any) { e => showMessage(`Demo 任务[${e.targetTask.name}] 添加后置任务 [${e.newTask.name}]`, 'info') " - @task-deleted="e => showMessage(`Demo 任务[${e.task.name}] 已删除`, 'info')" - @task-added="e => showMessage(`Demo 任务[${e.task.name}] 已创建`, 'info')" - @task-updated="e => showMessage(`Demo 任务[${e.task.name}] 已更新`, 'info')" + @task-deleted="handleTaskDeleteEvent" + @task-added="handleTaskAddEvent" + @task-updated="handleTaskUpdateEvent" > @@ -994,7 +1024,74 @@ function taskDebug(item: any) { } .config-section { - margin-bottom: 24px; + 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 { @@ -1003,16 +1100,42 @@ function taskDebug(item: any) { gap: 8px; font-size: 14px; font-weight: 600; - color: var(--text-primary); - margin-bottom: 12px; - padding-bottom: 6px; - border-bottom: 1px solid var(--border-color); + color: var(--gantt-text-primary, #333); + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 2px solid var(--gantt-primary-color, #409eff); } .section-icon { - width: 16px; - height: 16px; - color: var(--primary-color); + 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 { @@ -1098,6 +1221,138 @@ function taskDebug(item: any) { 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; @@ -1229,6 +1484,47 @@ function taskDebug(item: any) { 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); } @@ -1245,6 +1541,58 @@ function taskDebug(item: any) { 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); @@ -1406,4 +1754,156 @@ function taskDebug(item: any) { 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); +} diff --git a/demo/VersionHistoryDrawer.vue b/demo/VersionHistoryDrawer.vue index 5e6dd4c..7270706 100644 --- a/demo/VersionHistoryDrawer.vue +++ b/demo/VersionHistoryDrawer.vue @@ -61,7 +61,7 @@ onMounted(async () => { position: fixed; top: 0; left: 0; - width: 540px; /* 增加Drawer宽度,适中大气 */ + width: 50%; /* 增加Drawer宽度,适中大气 */ height: 100vh; background: var(--gantt-bg-primary, #fff); box-shadow: 2px 0 24px rgba(0, 0, 0, 0.1); diff --git a/demo/data.json b/demo/data.json index 6cceab7..ef26e32 100644 --- a/demo/data.json +++ b/demo/data.json @@ -31,6 +31,7 @@ "id": 1101, "name": "试验方案设计与伦理审查", "assignee": "方案设计师 李明", + "avatar": "https://i.pravatar.cc/150?img=1", "startDate": "2025-01-01", "endDate": "2025-02-28", "progress": 100, @@ -45,6 +46,7 @@ "id": 1102, "name": "受试者招募与筛选", "assignee": "招募专员 张丽", + "avatar": "https://i.pravatar.cc/150?img=5", "startDate": "2025-03-01", "endDate": "2025-04-30", "progress": 90, @@ -60,6 +62,7 @@ "id": 1103, "name": "药物给药与安全性监测", "assignee": "临床医生 Dr. Liu", + "avatar": "https://i.pravatar.cc/150?img=8", "startDate": "2025-05-01", "endDate": "2025-08-31", "progress": 40, @@ -92,6 +95,7 @@ "id": 1201, "name": "多中心试验启动", "assignee": "项目经理 王芳", + "avatar": "https://i.pravatar.cc/150?img=10", "startDate": "2025-09-01", "endDate": "2025-11-30", "progress": 25, @@ -107,6 +111,7 @@ "id": 1202, "name": "患者入组与随机化", "assignee": "数据管理员 陈静", + "avatar": "https://i.pravatar.cc/150?img=20", "startDate": "2025-12-01", "endDate": "2026-03-31", "progress": 0, @@ -122,6 +127,7 @@ "id": 1203, "name": "疗效评估与数据收集", "assignee": "统计师 赵磊", + "avatar": "https://i.pravatar.cc/150?img=15", "startDate": "2026-01-01", "endDate": "2026-08-31", "progress": 0, @@ -421,7 +427,7 @@ "startDate": "2025-08-31", "endDate": "2025-08-31", "type": "milestone", - "icon": "shield-check", + "icon": "diamond", "description": "完成I期临床试验,确认ADX-2024在人体的安全性和耐受性,为II期试验奠定基础。" }, { @@ -431,7 +437,7 @@ "startDate": "2026-04-30", "endDate": "2026-04-30", "type": "milestone", - "icon": "bar-chart", + "icon": "diamond", "description": "完成II期临床试验中期疗效分析,评估是否继续进行III期试验。" }, { @@ -441,7 +447,7 @@ "startDate": "2027-12-31", "endDate": "2027-12-31", "type": "milestone", - "icon": "trophy", + "icon": "diamond", "description": "完成III期临床试验最终分析,向监管部门提交新药上市申请(NDA)。" }, { diff --git a/demo/version-history.json b/demo/version-history.json index 6e05b8e..e7ccf24 100644 --- a/demo/version-history.json +++ b/demo/version-history.json @@ -230,5 +230,43 @@ "特别感谢 @Guduxwa", "Special thanks to @Guduxwa" ] + }, + { + "version": "1.4.2", + "date": "2025-11-01", + "notes": [ + "TaskBar增加头像显示", + "TaskBar展示信息显示可配置", + "TaskBar长按聚焦高亮", + "Timeline Range自动优化计算, 提升显示效果", + "TaskListConfig支持像素和百分比设置方式", + "客制化多语言扩展支持", + "暴露更多外部可以使用属性和事件", + "说明文档更新,可读性更强", + "增加NPM包应用示例", + "支持拖拽和拉伸TaskBar/Milestone接近边界自动扩充时间轴范围", + "支持通过属性配置限制TaskBar/Milestone拖拽和拉伸", + "增强Timeline时间轴的拖拽体验,增加防抖保护", + "SonarQube代码质量检查问题修改", + "缺陷修复", + "特别感谢 @SHENGLONG749/@xu-yiqi/@yfwork的使用及反馈的宝贵意见", + + "Added avatar display on TaskBar", + "Configurable display information on TaskBar", + "Long-press to focus and highlight TaskBar", + "Automatically optimized Timeline Range calculation for better visual performance", + "TaskListConfig now supports both pixel and percentage-based sizing", + "Custom multi-language extension support", + "Expose more external properties and events", + "Documentation updated for better readability", + "Added NPM package application examples", + "Support dragging and stretching TaskBar/Milestone to automatically expand timeline range near boundaries", + "Support property-based configuration to limit dragging and stretching of TaskBar/Milestone", + "Enhance dragging experience on Timeline, adding debounce protection", + "SonarQube code quality inspection issue modification", + "Bugfix", + "Special thanks to @SHENGLONG749/@xu-yiqi/@yfwork for their valuable use and feedback", + "Special thanks to @Guduxwa" + ] } ] diff --git a/npm-demo/.gitignore b/npm-demo/.gitignore new file mode 100644 index 0000000..8f494a6 --- /dev/null +++ b/npm-demo/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +.dscode/ +.vscode/ +dist/ +.idea/ +*.log diff --git a/npm-demo/README.md b/npm-demo/README.md new file mode 100644 index 0000000..5bba610 --- /dev/null +++ b/npm-demo/README.md @@ -0,0 +1,63 @@ +# Gantt Demo App + +基于 Vite + Vue3 的示例项目,用于本地调试 `jordium-gantt-vue3` 组件包。 + +## 调试方式 +支持两种: +1. npm link (推荐开发阶段) +2. 打包 tgz 后 file: 引用(已改为 link 模式,可回退) + +## 使用 npm link 调试流程 +在本地库目录(例如:`D:/02 Project/01 久元鼎晟/300 框架集/02 Plugins/01 Gantt/npm-package`)执行: +```powershell +# 进入库源码根目录 +npm install +npm run build:lib # 若需要生成 dist +npm link # 全局注册该包 +``` +回到示例项目目录执行: +```powershell +npm uninstall jordium-gantt-vue3 -D -S 2>$null | Out-Null # 忽略不存在情形 +npm install +npm link jordium-gantt-vue3 +npm run dev +``` +如果已经 link 过,只需: +```powershell +npm run dev +``` +修改库源码后,可在库目录重新 `npm run build:lib`,示例项目无需重新安装,只要刷新浏览器(若是构建产物路径不变)。 + +## 回退为 tgz 引用 +将 `package.json` 中: +```json +"jordium-gantt-vue3": "1.4.0" +``` +改回: +```json +"jordium-gantt-vue3": "file:绝对路径/jordium-gantt-vue3-1.4.0.tgz" +``` +然后: +```powershell +npm install --force +``` + +## 运行 +```powershell +npm install +npm run dev +``` +访问: http://localhost:5173/ + +## 占位组件 +当真实库无法加载时,显示 `StubGantt.vue` 占位以避免报错。 + +## 常见问题 +| 现象 | 处理 | +|------|------| +| Failed to resolve entry | 确认 dist 产物与 main/module 指向正确,重新 build:lib | +| 样式缺失 | 确认是否需要单独引入 css,如 `import 'jordium-gantt-vue3/dist/style.css'` | +| 代码未热更新 | 重新 build:lib 或使用源码 symlink + Vite optimizeDeps.exclude | + +## License +仅内部开发调试使用。 diff --git a/npm-demo/index.html b/npm-demo/index.html new file mode 100644 index 0000000..4b473f6 --- /dev/null +++ b/npm-demo/index.html @@ -0,0 +1,12 @@ + + + + + Gantt Demo + + + +
+ + + diff --git a/npm-demo/package-lock.json b/npm-demo/package-lock.json new file mode 100644 index 0000000..c3520fd --- /dev/null +++ b/npm-demo/package-lock.json @@ -0,0 +1,1302 @@ +{ + "name": "gantt-demo-app", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gantt-demo-app", + "version": "0.1.0", + "dependencies": { + "element-plus": "^2.11.5", + "vue": "^3.4.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "vite": "^5.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz", + "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", + "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", + "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", + "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", + "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", + "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", + "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", + "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", + "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", + "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", + "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", + "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", + "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", + "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", + "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", + "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", + "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", + "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", + "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", + "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", + "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", + "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", + "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz", + "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.22.tgz", + "integrity": "sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==", + "dependencies": { + "@babel/parser": "^7.28.4", + "@vue/shared": "3.5.22", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.22.tgz", + "integrity": "sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==", + "dependencies": { + "@vue/compiler-core": "3.5.22", + "@vue/shared": "3.5.22" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz", + "integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==", + "dependencies": { + "@babel/parser": "^7.28.4", + "@vue/compiler-core": "3.5.22", + "@vue/compiler-dom": "3.5.22", + "@vue/compiler-ssr": "3.5.22", + "@vue/shared": "3.5.22", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.19", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.22.tgz", + "integrity": "sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==", + "dependencies": { + "@vue/compiler-dom": "3.5.22", + "@vue/shared": "3.5.22" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.22.tgz", + "integrity": "sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==", + "dependencies": { + "@vue/shared": "3.5.22" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.22.tgz", + "integrity": "sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==", + "dependencies": { + "@vue/reactivity": "3.5.22", + "@vue/shared": "3.5.22" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.22.tgz", + "integrity": "sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==", + "dependencies": { + "@vue/reactivity": "3.5.22", + "@vue/runtime-core": "3.5.22", + "@vue/shared": "3.5.22", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.22.tgz", + "integrity": "sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==", + "dependencies": { + "@vue/compiler-ssr": "3.5.22", + "@vue/shared": "3.5.22" + }, + "peerDependencies": { + "vue": "3.5.22" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.22.tgz", + "integrity": "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==" + }, + "node_modules/@vueuse/core": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.13.0.tgz", + "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==", + "dependencies": { + "@types/web-bluetooth": "^0.0.16", + "@vueuse/metadata": "9.13.0", + "@vueuse/shared": "9.13.0", + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.13.0.tgz", + "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz", + "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==", + "dependencies": { + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==" + }, + "node_modules/element-plus": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.11.5.tgz", + "integrity": "sha512-O+bIVHQCjUDm4GiIznDXRoS8ar2TpWLwfOGnN/Aam0VXf5kbuc4SxdKKJdovWNxmxeqbcwjsSZPKgtXNcqys4A==", + "dependencies": { + "@ctrl/tinycolor": "^3.4.1", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.17.20", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "^9.1.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.18", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", + "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.4", + "@rollup/rollup-android-arm64": "4.52.4", + "@rollup/rollup-darwin-arm64": "4.52.4", + "@rollup/rollup-darwin-x64": "4.52.4", + "@rollup/rollup-freebsd-arm64": "4.52.4", + "@rollup/rollup-freebsd-x64": "4.52.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", + "@rollup/rollup-linux-arm-musleabihf": "4.52.4", + "@rollup/rollup-linux-arm64-gnu": "4.52.4", + "@rollup/rollup-linux-arm64-musl": "4.52.4", + "@rollup/rollup-linux-loong64-gnu": "4.52.4", + "@rollup/rollup-linux-ppc64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-musl": "4.52.4", + "@rollup/rollup-linux-s390x-gnu": "4.52.4", + "@rollup/rollup-linux-x64-gnu": "4.52.4", + "@rollup/rollup-linux-x64-musl": "4.52.4", + "@rollup/rollup-openharmony-arm64": "4.52.4", + "@rollup/rollup-win32-arm64-msvc": "4.52.4", + "@rollup/rollup-win32-ia32-msvc": "4.52.4", + "@rollup/rollup-win32-x64-gnu": "4.52.4", + "@rollup/rollup-win32-x64-msvc": "4.52.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vite": { + "version": "5.4.20", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", + "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz", + "integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==", + "dependencies": { + "@vue/compiler-dom": "3.5.22", + "@vue/compiler-sfc": "3.5.22", + "@vue/runtime-dom": "3.5.22", + "@vue/server-renderer": "3.5.22", + "@vue/shared": "3.5.22" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + } + } +} diff --git a/npm-demo/package.json b/npm-demo/package.json new file mode 100644 index 0000000..646bf1f --- /dev/null +++ b/npm-demo/package.json @@ -0,0 +1,19 @@ +{ + "name": "gantt-demo-app", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "element-plus": "^2.11.5", + "vue": "^3.4.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "vite": "^5.0.0" + } +} diff --git a/npm-demo/src/App.vue b/npm-demo/src/App.vue new file mode 100644 index 0000000..55d7f11 --- /dev/null +++ b/npm-demo/src/App.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/npm-demo/src/components/GanttTest.vue b/npm-demo/src/components/GanttTest.vue new file mode 100644 index 0000000..685c70b --- /dev/null +++ b/npm-demo/src/components/GanttTest.vue @@ -0,0 +1,345 @@ + + + + + diff --git a/npm-demo/src/components/StubGantt.vue b/npm-demo/src/components/StubGantt.vue new file mode 100644 index 0000000..0058f0e --- /dev/null +++ b/npm-demo/src/components/StubGantt.vue @@ -0,0 +1,29 @@ + + + diff --git a/npm-demo/src/components/ThumbnailHtmlContent.vue b/npm-demo/src/components/ThumbnailHtmlContent.vue new file mode 100644 index 0000000..7211996 --- /dev/null +++ b/npm-demo/src/components/ThumbnailHtmlContent.vue @@ -0,0 +1,39 @@ + + + + + diff --git a/npm-demo/src/main.js b/npm-demo/src/main.js new file mode 100644 index 0000000..9489de8 --- /dev/null +++ b/npm-demo/src/main.js @@ -0,0 +1,9 @@ +import { createApp } from 'vue' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import App from './App.vue' + +const app = createApp(App) + +app.use(ElementPlus) +app.mount('#app') diff --git a/npm-demo/vite.config.js b/npm-demo/vite.config.js new file mode 100644 index 0000000..1ebc4fc --- /dev/null +++ b/npm-demo/vite.config.js @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; + +export default defineConfig({ + plugins: [vue()], +}); diff --git a/package-lock.json b/package-lock.json index e82c5e0..48ddde8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "jordium-gantt-vue3", - "version": "1.3.0", + "version": "1.4.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "jordium-gantt-vue3", - "version": "1.3.0", + "version": "1.4.2", "license": "MIT", "dependencies": { "date-fns": "^4.1.0", @@ -80,9 +80,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", - "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", + "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", "cpu": [ "ppc64" ], @@ -96,9 +96,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.5.tgz", - "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", + "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", "cpu": [ "arm" ], @@ -112,9 +112,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", - "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", + "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", "cpu": [ "arm64" ], @@ -128,9 +128,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.5.tgz", - "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", + "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", "cpu": [ "x64" ], @@ -144,9 +144,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", - "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", + "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", "cpu": [ "arm64" ], @@ -160,9 +160,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", - "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", + "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", "cpu": [ "x64" ], @@ -176,9 +176,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", - "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", + "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", "cpu": [ "arm64" ], @@ -192,9 +192,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", - "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", + "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", "cpu": [ "x64" ], @@ -208,9 +208,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", - "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", + "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", "cpu": [ "arm" ], @@ -224,9 +224,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", - "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", + "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", "cpu": [ "arm64" ], @@ -240,9 +240,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", - "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", + "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", "cpu": [ "ia32" ], @@ -256,9 +256,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", - "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", + "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", "cpu": [ "loong64" ], @@ -272,9 +272,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", - "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", + "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", "cpu": [ "mips64el" ], @@ -288,9 +288,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", - "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", + "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", "cpu": [ "ppc64" ], @@ -304,9 +304,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", - "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", + "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", "cpu": [ "riscv64" ], @@ -320,9 +320,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", - "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", + "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", "cpu": [ "s390x" ], @@ -336,9 +336,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", - "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", + "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", "cpu": [ "x64" ], @@ -352,9 +352,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", - "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", + "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", "cpu": [ "arm64" ], @@ -368,9 +368,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", - "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", + "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", "cpu": [ "x64" ], @@ -384,9 +384,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", - "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", + "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", "cpu": [ "arm64" ], @@ -400,9 +400,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", - "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", + "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", "cpu": [ "x64" ], @@ -415,10 +415,26 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", + "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", - "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", + "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", "cpu": [ "x64" ], @@ -432,9 +448,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", - "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", + "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", "cpu": [ "arm64" ], @@ -448,9 +464,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", - "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", + "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", "cpu": [ "ia32" ], @@ -464,9 +480,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", - "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", + "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", "cpu": [ "x64" ], @@ -1736,9 +1752,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.5", - "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.5.tgz", - "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", + "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", "dev": true, "hasInstallScript": true, "bin": { @@ -1748,31 +1764,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.5", - "@esbuild/android-arm": "0.25.5", - "@esbuild/android-arm64": "0.25.5", - "@esbuild/android-x64": "0.25.5", - "@esbuild/darwin-arm64": "0.25.5", - "@esbuild/darwin-x64": "0.25.5", - "@esbuild/freebsd-arm64": "0.25.5", - "@esbuild/freebsd-x64": "0.25.5", - "@esbuild/linux-arm": "0.25.5", - "@esbuild/linux-arm64": "0.25.5", - "@esbuild/linux-ia32": "0.25.5", - "@esbuild/linux-loong64": "0.25.5", - "@esbuild/linux-mips64el": "0.25.5", - "@esbuild/linux-ppc64": "0.25.5", - "@esbuild/linux-riscv64": "0.25.5", - "@esbuild/linux-s390x": "0.25.5", - "@esbuild/linux-x64": "0.25.5", - "@esbuild/netbsd-arm64": "0.25.5", - "@esbuild/netbsd-x64": "0.25.5", - "@esbuild/openbsd-arm64": "0.25.5", - "@esbuild/openbsd-x64": "0.25.5", - "@esbuild/sunos-x64": "0.25.5", - "@esbuild/win32-arm64": "0.25.5", - "@esbuild/win32-ia32": "0.25.5", - "@esbuild/win32-x64": "0.25.5" + "@esbuild/aix-ppc64": "0.25.11", + "@esbuild/android-arm": "0.25.11", + "@esbuild/android-arm64": "0.25.11", + "@esbuild/android-x64": "0.25.11", + "@esbuild/darwin-arm64": "0.25.11", + "@esbuild/darwin-x64": "0.25.11", + "@esbuild/freebsd-arm64": "0.25.11", + "@esbuild/freebsd-x64": "0.25.11", + "@esbuild/linux-arm": "0.25.11", + "@esbuild/linux-arm64": "0.25.11", + "@esbuild/linux-ia32": "0.25.11", + "@esbuild/linux-loong64": "0.25.11", + "@esbuild/linux-mips64el": "0.25.11", + "@esbuild/linux-ppc64": "0.25.11", + "@esbuild/linux-riscv64": "0.25.11", + "@esbuild/linux-s390x": "0.25.11", + "@esbuild/linux-x64": "0.25.11", + "@esbuild/netbsd-arm64": "0.25.11", + "@esbuild/netbsd-x64": "0.25.11", + "@esbuild/openbsd-arm64": "0.25.11", + "@esbuild/openbsd-x64": "0.25.11", + "@esbuild/openharmony-arm64": "0.25.11", + "@esbuild/sunos-x64": "0.25.11", + "@esbuild/win32-arm64": "0.25.11", + "@esbuild/win32-ia32": "0.25.11", + "@esbuild/win32-x64": "0.25.11" } }, "node_modules/escape-string-regexp": { diff --git a/package.json b/package.json index 701b02f..9a0dafb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jordium-gantt-vue3", - "version": "1.4.1", + "version": "1.4.2", "type": "module", "main": "dist/jordium-gantt-vue3.cjs.js", "module": "dist/jordium-gantt-vue3.es.js", diff --git a/scripts/generate-css-export.js b/scripts/generate-css-export.js index 2551c8a..73ec745 100644 --- a/scripts/generate-css-export.js +++ b/scripts/generate-css-export.js @@ -9,13 +9,15 @@ const __dirname = path.dirname(__filename) // 构建后生成CSS导出文件 function generateCssExport() { - const distDir = path.resolve(__dirname, '../npm-package/dist') - const cssFile = path.join(distDir, 'assets/jordium-gantt-vue3.css') + const distDir = path.resolve(__dirname, '../../npm-package/dist') + const assetsDir = path.join(distDir, 'assets') + const expectedCssFile = path.join(assetsDir, 'jordium-gantt-vue3.css') const outputFile = path.join(distDir, 'jordium-gantt-vue3-styles.js') try { - if (fs.existsSync(cssFile)) { - const cssContent = fs.readFileSync(cssFile, 'utf8') + // 首先尝试查找固定名称的CSS文件 + if (fs.existsSync(expectedCssFile)) { + const cssContent = fs.readFileSync(expectedCssFile, 'utf8') // 生成JS导出文件 const jsContent = `// CSS styles for jordium-gantt-vue3 @@ -26,9 +28,41 @@ export default ganttStyles fs.writeFileSync(outputFile, jsContent, 'utf8') console.log('✅ Generated CSS export file:', outputFile) - } else { - console.warn('⚠️ CSS file not found:', cssFile) + console.log('📄 Source CSS file:', expectedCssFile) + return } + + // 如果固定名称文件不存在,检查assets目录 + if (!fs.existsSync(assetsDir)) { + console.warn('⚠️ Assets directory not found:', assetsDir) + console.warn('ℹ️ This is normal for library builds - CSS is typically inlined in JS files') + return + } + + // 查找任何CSS文件 + const files = fs.readdirSync(assetsDir) + const cssFiles = files.filter(file => file.endsWith('.css')) + + if (cssFiles.length === 0) { + console.warn('⚠️ No CSS files found in:', assetsDir) + console.warn('ℹ️ This is normal for library builds - CSS is typically inlined in JS files') + return + } + + // 使用第一个找到的CSS文件 + const cssFile = path.join(assetsDir, cssFiles[0]) + const cssContent = fs.readFileSync(cssFile, 'utf8') + + // 生成JS导出文件 + const jsContent = `// CSS styles for jordium-gantt-vue3 +// Auto-generated file - do not edit manually +export const ganttStyles = ${JSON.stringify(cssContent)} +export default ganttStyles +` + + fs.writeFileSync(outputFile, jsContent, 'utf8') + console.log('✅ Generated CSS export file:', outputFile) + console.log('📄 Source CSS file:', cssFile) } catch (error) { console.error('❌ Failed to generate CSS export:', error) process.exit(1) diff --git a/src/components/DatePicker.vue b/src/components/DatePicker.vue index b788ced..9ba40eb 100644 --- a/src/components/DatePicker.vue +++ b/src/components/DatePicker.vue @@ -1,6 +1,21 @@ + + +
+ + + + {{ t.disableTaskbarFocusMode }} +
@@ -1763,6 +2237,16 @@ function handleTaskDelete(task: Task, deleteChildren?: boolean) { @stop-timer="handleStopTimer" @delete="handleTaskDelete" /> + + + @@ -1812,6 +2296,55 @@ function handleTaskDelete(task: Task, deleteChildren?: boolean) { width: 100%; } +/* 关闭聚焦按钮 - 固定在gantt-panel-right底部居中 */ +.focus-close-button { + position: absolute; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + z-index: 1004; + display: flex; + align-items: center; + gap: 8px; + padding: 10px 20px; + background: #f56c6c; + border: 1px solid #f56c6c; + border-radius: 20px; + box-shadow: 0 4px 12px rgba(245, 108, 108, 0.4); + cursor: pointer; + transition: all 0.3s ease; + user-select: none; +} + +.focus-close-button:hover { + background: #f78989; + border-color: #f78989; + box-shadow: 0 6px 16px rgba(245, 108, 108, 0.5); + transform: translateX(-50%) translateY(-2px); +} + +.focus-close-button .close-icon { + width: 18px; + height: 18px; + color: #ffffff; + transition: color 0.3s ease; +} + +.focus-close-button:hover .close-icon { + color: #ffffff; +} + +.focus-close-button .close-text { + font-size: 14px; + color: #ffffff; + font-weight: 500; + transition: color 0.3s ease; +} + +.focus-close-button:hover .close-text { + color: #ffffff; +} + .gantt-splitter { position: relative; /* 添加相对定位以支持绝对定位的子元素 */ width: 6px; @@ -1932,6 +2465,28 @@ function handleTaskDelete(task: Task, deleteChildren?: boolean) { border-right-color: #4c4c4c !important; } +:global(html[data-theme='dark']) .focus-close-button { + background: #d85555; + border-color: #d85555; + box-shadow: 0 4px 12px rgba(216, 85, 85, 0.4); +} + +:global(html[data-theme='dark']) .focus-close-button:hover { + background: #e67676; + border-color: #e67676; + box-shadow: 0 6px 16px rgba(216, 85, 85, 0.5); +} + +:global(html[data-theme='dark']) .focus-close-button .close-icon, +:global(html[data-theme='dark']) .focus-close-button .close-text { + color: #ffffff; +} + +:global(html[data-theme='dark']) .focus-close-button:hover .close-icon, +:global(html[data-theme='dark']) .focus-close-button:hover .close-text { + color: #ffffff; +} + :global(html[data-theme='dark']) .gantt-splitter { background: linear-gradient( to right, diff --git a/src/components/MilestoneDialog.vue b/src/components/MilestoneDialog.vue index 0e2a449..a0ff143 100644 --- a/src/components/MilestoneDialog.vue +++ b/src/components/MilestoneDialog.vue @@ -4,6 +4,7 @@ import { useI18n } from '../composables/useI18n' import DatePicker from './DatePicker.vue' import GanttConfirmDialog from './GanttConfirmDialog.vue' import type { Milestone } from '../models/classes/Milestone' +import type { Task } from '../models/classes/Task' import '../styles/app.css' interface Props { @@ -16,12 +17,13 @@ const props = defineProps() const emit = defineEmits<{ 'update:visible': [visible: boolean] close: [] - save: [milestone: Milestone] + save: [milestone: Task] delete: [milestoneId: number] }>() // 表单数据 const formData = reactive({ + id: undefined, name: '', startDate: '', assignee: '', @@ -87,6 +89,28 @@ watch( { immediate: true }, ) +// 监听对话框打开,确保新建时重置表单 +watch( + () => props.visible, + (newVisible) => { + if (newVisible && !props.milestone) { + // 打开对话框且没有传入里程碑(新建模式),重置表单 + Object.assign(formData, { + id: undefined, + name: '', + startDate: '', + assignee: '', + type: 'milestone', + icon: 'diamond', + description: '', + }) + // 清空错误 + errors.name = '' + errors.startDate = '' + } + }, +) + // 表单验证 const validateForm = () => { errors.name = '' @@ -121,7 +145,13 @@ const selectIcon = (icon: string) => { // 保存处理 const handleSave = () => { if (validateForm()) { - emit('save', { ...formData }) + // 里程碑的 endDate 必须与 startDate 相同 + const milestoneData = { + ...formData, + endDate: formData.startDate, + id: formData.id!, // 确保id不为undefined + } as Task + emit('save', milestoneData) closeDialog() } } @@ -245,6 +275,7 @@ const t = (key: string) => { id="milestone-date" v-model="formData.startDate" type="date" + value-format="YYYY-MM-DD" placeholder="请选择里程碑日期" :class="{ error: errors.startDate }" /> diff --git a/src/components/MilestonePoint.vue b/src/components/MilestonePoint.vue index 20b5657..4075ac8 100644 --- a/src/components/MilestonePoint.vue +++ b/src/components/MilestonePoint.vue @@ -3,6 +3,7 @@ import { computed, ref, onUnmounted } from 'vue' import type { Milestone } from '../models/classes/Milestone' import { TimelineScale } from '../models/types/TimelineScale' +import type { TimelineMonth, TimelineYear, TimelineDay } from '../models/types/TimelineDataTypes' import { useI18n } from '../composables/useI18n' import { createLocalDate } from '../utils/predecessorUtils' const props = defineProps() @@ -21,29 +22,30 @@ const t = (key: string): string => { } interface Props { - date: string // 里程碑日期 + date: string + milestone: Milestone + name?: string rowHeight: number dayWidth: number startDate: Date - name?: string - milestone?: Milestone // 完整的里程碑数据 - // 新增:用于边界粘性显示的滚动位置信息 + timelineStart: Date + timelineEnd: Date scrollLeft?: number containerWidth?: number - // 新增:里程碑推挤效果所需的信息 - milestoneId?: string | number // 唯一标识符 + milestoneId?: number otherMilestones?: Array<{ - id: string | number - left: number - originalLeft: number // 原始位置(不考虑停靠) + id: number isSticky: boolean - stickyPosition: 'left' | 'right' | 'none' - priority: number // 推挤优先级 - }> // 其他里程碑的位置信息 - // 新增:时间线数据,用于精确计算subDays定位 - timelineData?: unknown[] - // 新增:当前时间刻度 - currentTimeScale?: TimelineScale + stickyPosition: string + left: number + originalLeft: number + priority: number + }> + currentTimeScale?: TimelineScale | null + timelineData?: TimelineMonth[] | TimelineYear[] | TimelineDay[] + periodWidth: number + isInHighlightMode?: boolean + allowDragAndResize: boolean } // 拖拽相关状态 @@ -102,8 +104,112 @@ const formatDateToLocalString = (date: Date): string => { return `${year}-${month}-${day}` } +// 根据像素位置计算日期(支持月度、季度视图) +const calculateDateFromPosition = ( + pixelPosition: number, + timelineData: Array<{ + year: number + month: number + startDate: Date + endDate: Date + days?: Array<{ date: Date; day: number }> + monthData?: { dayCount: number } + }>, + timeScale: TimelineScale, +): Date | null => { + if (!timelineData) { + return null + } + + let cumulativePosition = 0 + + if (timeScale === TimelineScale.DAY) { + // 日视图:基于 days 数组 + for (const periodData of timelineData) { + const days = periodData.days || [] + const periodWidth = days.length * 30 // 日视图每天30px + + if (pixelPosition >= cumulativePosition && pixelPosition < cumulativePosition + periodWidth) { + const relativePosition = pixelPosition - cumulativePosition + const dayIndex = Math.floor(relativePosition / 30) + + if (dayIndex >= 0 && dayIndex < days.length) { + return new Date(days[dayIndex].date) + } + } + + cumulativePosition += periodWidth + } + } else if (timeScale === TimelineScale.MONTH) { + // 月视图:每个月60px + for (const periodData of timelineData) { + const monthWidth = 60 + + if (pixelPosition >= cumulativePosition && pixelPosition < cumulativePosition + monthWidth) { + const relativePosition = pixelPosition - cumulativePosition + const daysInMonth = periodData.monthData?.dayCount || 30 + const dayWidth = monthWidth / daysInMonth + + const dayIndex = Math.floor(relativePosition / dayWidth) + const day = Math.min(dayIndex + 1, daysInMonth) + + return new Date(periodData.year, periodData.month - 1, day) + } + + cumulativePosition += monthWidth + } + } else if (timeScale === TimelineScale.QUARTER) { + // 季度视图:每个季度60px + for (const periodData of timelineData) { + const quarters = (periodData as Record).quarters as Array<{ + quarter: number + startDate: Date + endDate: Date + }> || [] + + for (const quarter of quarters) { + const quarterStart = new Date(quarter.startDate) + const quarterEnd = new Date(quarter.endDate) + const quarterWidth = 60 + + if ( + pixelPosition >= cumulativePosition && + pixelPosition < cumulativePosition + quarterWidth + ) { + const relativePosition = pixelPosition - cumulativePosition + const daysInQuarter = Math.ceil( + (quarterEnd.getTime() - quarterStart.getTime()) / (1000 * 60 * 60 * 24), + ) + const dayWidth = quarterWidth / daysInQuarter + + const dayIndex = Math.floor(relativePosition / dayWidth) + + const resultDate = new Date(quarterStart) + resultDate.setDate(resultDate.getDate() + dayIndex) + + return resultDate + } + + cumulativePosition += quarterWidth + } + } + } + + return null +} + // 拖拽事件处理 - 使用相对位置拖拽方案 const handleMouseDown = (e: MouseEvent) => { + // 如果禁用了拖拽和拉伸,直接返回 + if (props.allowDragAndResize === false) { + return + } + + // 年度视图禁止拖拽(与TaskBar保持一致) + if (props.currentTimeScale === TimelineScale.YEAR) { + return + } + // 如果是停靠状态或被推出边界,禁止拖拽 if ( milestoneVisibility.value.isSticky || @@ -162,7 +268,42 @@ const handleMouseMove = (e: MouseEvent) => { isDragging.value = true const newLeft = Math.max(0, dragStartLeft.value + deltaX) - const newStartDate = addDaysToLocalDate(props.startDate, newLeft / props.dayWidth) + let newStartDate: Date + + // 根据当前时间刻度使用不同的日期计算方法 + if ( + props.currentTimeScale === TimelineScale.MONTH || + props.currentTimeScale === TimelineScale.QUARTER || + props.currentTimeScale === TimelineScale.DAY + ) { + // 月度、季度、日视图:使用 timelineData 精确计算 + if (props.timelineData && props.currentTimeScale) { + const calculatedDate = calculateDateFromPosition( + newLeft, + props.timelineData as Array<{ + year: number + month: number + startDate: Date + endDate: Date + days?: Array<{ date: Date; day: number }> + monthData?: { dayCount: number } + }>, + props.currentTimeScale, + ) + if (calculatedDate) { + newStartDate = calculatedDate + } else { + // 如果计算失败,回退到简单计算 + newStartDate = addDaysToLocalDate(props.startDate, newLeft / props.dayWidth) + } + } else { + // 如果没有 timelineData,回退到简单计算 + newStartDate = addDaysToLocalDate(props.startDate, newLeft / props.dayWidth) + } + } else { + // 其他视图(周视图、小时视图):使用原有的简单计算 + newStartDate = addDaysToLocalDate(props.startDate, newLeft / props.dayWidth) + } // 只更新临时数据,不触发事件 tempMilestoneData.value = { @@ -246,6 +387,11 @@ const handleMilestoneClick = (e: MouseEvent) => { } } +// 判断是否应该显示为暗淡(处于高亮模式) +const isDimmed = computed(() => { + return props.isInHighlightMode === true +}) + // 计算菱形位置 - 考虑拖拽临时数据 const milestoneStyle = computed(() => { const currentMilestoneDate = tempMilestoneData.value?.startDate || props.date @@ -282,15 +428,8 @@ const milestoneStyle = computed(() => { size = Math.min(props.rowHeight, props.dayWidth * 1.2, 24) } - // 年度视图:使用专门的年度位置计算 - if (props.currentTimeScale === TimelineScale.YEAR) { - const centerPosition = calculateYearViewMilestonePosition(milestoneDate, props.startDate) - left = centerPosition - size / 2 // 从中心位置偏移到图标左上角 - } else if (props.currentTimeScale === TimelineScale.QUARTER) { - // 季度视图:使用类似年度视图的简单计算方法 - const centerPosition = calculateQuarterViewMilestonePosition(milestoneDate, props.startDate) - left = centerPosition - size / 2 // 从中心位置偏移到图标左上角 - } else if (props.currentTimeScale === TimelineScale.HOUR) { + // 小时视图:使用专门的小时位置计算 + if (props.currentTimeScale === TimelineScale.HOUR) { // 小时视图:精确到小时和分钟的定位 const centerPosition = calculateHourViewMilestonePosition(milestoneDate, props.startDate) left = centerPosition - size / 2 // 从中心位置偏移到图标左上角 @@ -298,9 +437,12 @@ const milestoneStyle = computed(() => { props.timelineData && props.currentTimeScale && (props.currentTimeScale === TimelineScale.WEEK || - props.currentTimeScale === TimelineScale.MONTH) + props.currentTimeScale === TimelineScale.MONTH || + props.currentTimeScale === TimelineScale.DAY || + props.currentTimeScale === TimelineScale.QUARTER || + props.currentTimeScale === TimelineScale.YEAR) ) { - // 优先使用基于timelineData的精确定位(适用于周视图和月视图) + // 优先使用基于timelineData的精确定位(适用于周视图、月视图、日视图、季度视图和年度视图) const centerPosition = calculateMilestonePositionFromTimelineData( milestoneDate, props.timelineData, @@ -309,7 +451,7 @@ const milestoneStyle = computed(() => { left = centerPosition - size / 2 // 从中心位置偏移到图标左上角 } else { - // 日视图:保持原有逻辑 + // 其他情况(没有 timelineData):保持原有逻辑 const startDiff = Math.floor( (milestoneDate.getTime() - props.startDate.getTime()) / (1000 * 60 * 60 * 24), ) @@ -543,58 +685,6 @@ onUnmounted(() => { }) // 年度视图里程碑位置计算函数 -const calculateYearViewMilestonePosition = (targetDate: Date, baseStartDate: Date): number => { - const targetYear = targetDate.getFullYear() - const baseYear = baseStartDate.getFullYear() - - // 每年的宽度是360px,每半年180px - const yearWidth = 360 - const halfYearWidth = 180 - - // 计算目标年份相对于基准年份的偏移 - const yearOffset = targetYear - baseYear - let position = yearOffset * yearWidth - - // 判断是上半年还是下半年 - const month = targetDate.getMonth() + 1 // getMonth()返回0-11,需要+1 - - if (month > 6) { - // 下半年,添加半年偏移 - position += halfYearWidth - } - - // 在半年内的具体位置计算 - let dayOffset = 0 - let startOfHalfYear: Date - - if (month <= 6) { - // 上半年:1-6月 - startOfHalfYear = new Date(targetYear, 0, 1) // 1月1日 - } else { - // 下半年:7-12月 - startOfHalfYear = new Date(targetYear, 6, 1) // 7月1日 - } - - dayOffset = Math.floor((targetDate.getTime() - startOfHalfYear.getTime()) / (1000 * 60 * 60 * 24)) - - // 半年大约181-184天,将天数映射到180px的宽度 - const daysInHalfYear = - month <= 6 - ? Math.floor( - (new Date(targetYear, 6, 1).getTime() - new Date(targetYear, 0, 1).getTime()) / - (1000 * 60 * 60 * 24), - ) - : Math.floor( - (new Date(targetYear + 1, 0, 1).getTime() - new Date(targetYear, 6, 1).getTime()) / - (1000 * 60 * 60 * 24), - ) - - const dayPositionInHalfYear = (dayOffset / daysInHalfYear) * halfYearWidth - position += dayPositionInHalfYear - - return position -} - // 小时视图里程碑位置计算 - 精确到小时和分钟 const calculateHourViewMilestonePosition = (targetDate: Date, baseStartDate: Date): number => { // 计算基础天数差 @@ -628,81 +718,94 @@ const calculateHourViewMilestonePosition = (targetDate: Date, baseStartDate: Dat return totalPosition } -// 季度视图里程碑位置计算 - 参考年度视图的简单算法 -const calculateQuarterViewMilestonePosition = (targetDate: Date, baseStartDate: Date): number => { - const targetYear = targetDate.getFullYear() - const baseYear = baseStartDate.getFullYear() - - // 每年的宽度是240px (4季度 * 60px),每季度60px - const yearWidth = 240 - const quarterWidth = 60 - - // 计算目标年份相对于基准年份的偏移 - const yearOffset = targetYear - baseYear - let position = yearOffset * yearWidth - - // 判断是哪个季度 - const month = targetDate.getMonth() + 1 // getMonth()返回0-11,需要+1 - let quarter = 1 - if (month >= 1 && month <= 3) { - // Q1: 1-3月 - } else if (month >= 4 && month <= 6) { - quarter = 2 // Q2: 4-6月 - } else if (month >= 7 && month <= 9) { - quarter = 3 // Q3: 7-9月 - } else { - quarter = 4 // Q4: 10-12月 - } - - // 添加季度偏移 (Q1=0px, Q2=60px, Q3=120px, Q4=180px) - position += (quarter - 1) * quarterWidth - - // 在季度内的具体位置计算 - let dayOffset = 0 - let startOfQuarter: Date - let endOfQuarter: Date - - if (quarter === 1) { - startOfQuarter = new Date(targetYear, 0, 1) // 1月1日 - endOfQuarter = new Date(targetYear, 2, 31) // 3月31日 - } else if (quarter === 2) { - startOfQuarter = new Date(targetYear, 3, 1) // 4月1日 - endOfQuarter = new Date(targetYear, 5, 30) // 6月30日 - } else if (quarter === 3) { - startOfQuarter = new Date(targetYear, 6, 1) // 7月1日 - endOfQuarter = new Date(targetYear, 8, 30) // 9月30日 - } else { - startOfQuarter = new Date(targetYear, 9, 1) // 10月1日 - endOfQuarter = new Date(targetYear, 11, 31) // 12月31日 - } - - dayOffset = Math.floor((targetDate.getTime() - startOfQuarter.getTime()) / (1000 * 60 * 60 * 24)) - - // 计算季度内的天数,将天数映射到60px的宽度 - const endTime = endOfQuarter.getTime() - const startTime = startOfQuarter.getTime() - const daysInQuarter = Math.floor((endTime - startTime) / (1000 * 60 * 60 * 24)) + 1 // 包含结束日期 - - const dayPositionInQuarter = (dayOffset / daysInQuarter) * quarterWidth - - position += dayPositionInQuarter - - return position -} - // 基于timelineData和subDays精确计算里程碑位置的函数 const calculateMilestonePositionFromTimelineData = ( targetDate: Date, - timelineData: any, + timelineData: TimelineMonth[] | TimelineYear[] | TimelineDay[], timeScale: TimelineScale, ) => { // 回退到原来的逻辑用于其他时间刻度 let cumulativePosition = 0 for (const periodData of timelineData) { - if (timeScale === TimelineScale.WEEK) { + if (timeScale === TimelineScale.DAY) { + // 日视图:处理days数组,返回中心位置 + // 类型保护:确保 periodData 是 TimelineMonth 并有 days 属性 + const days = ('days' in periodData && periodData.days) ? periodData.days : [] + + for (let i = 0; i < days.length; i++) { + const dayData = days[i] + const dayDate = new Date(dayData.date) + + // 比较日期(忽略时分秒) + if ( + dayDate.getFullYear() === targetDate.getFullYear() && + dayDate.getMonth() === targetDate.getMonth() && + dayDate.getDate() === targetDate.getDate() + ) { + // 找到目标日期,返回累计位置 + 当前天数索引 * 日宽度 + 半个日宽度(中心) + return cumulativePosition + i * 30 + 15 // 日视图每天30px,中心+15px + } + } + + // 累加当前月份所有天数的宽度 + cumulativePosition += days.length * 30 + } else if (timeScale === TimelineScale.QUARTER) { + // 季度视图:处理years数组,每个year包含quarters + // 类型保护:确保 periodData 是 TimelineYear 并有 quarters 属性 + const quarters = ('quarters' in periodData && periodData.quarters) ? periodData.quarters : [] + + for (const quarter of quarters) { + const quarterStart = new Date(quarter.startDate) + const quarterEnd = new Date(quarter.endDate) + + if (targetDate >= quarterStart && targetDate <= quarterEnd) { + // 找到目标日期所在的季度 + const quarterWidth = 60 + const daysInQuarter = Math.ceil( + (quarterEnd.getTime() - quarterStart.getTime()) / (1000 * 60 * 60 * 24), + ) + const dayWidth = quarterWidth / daysInQuarter + const dayInQuarter = Math.ceil( + (targetDate.getTime() - quarterStart.getTime()) / (1000 * 60 * 60 * 24), + ) + const finalPosition = cumulativePosition + dayInQuarter * dayWidth + dayWidth / 2 + + return finalPosition + } + + // 累加每季度的宽度 + cumulativePosition += 60 + } + } else if (timeScale === TimelineScale.YEAR) { + // 年度视图:处理years数组,每个year包含halfYears + // 类型保护:确保 periodData 是 TimelineYear 并有 halfYears 属性 + const halfYears = ('halfYears' in periodData && periodData.halfYears) ? periodData.halfYears : [] + + for (const halfYear of halfYears) { + const halfYearStart = new Date(halfYear.startDate) + const halfYearEnd = new Date(halfYear.endDate) + + if (targetDate >= halfYearStart && targetDate <= halfYearEnd) { + // 找到目标日期所在的半年 + const halfYearWidth = 180 // 年度视图每半年180px + const daysInHalfYear = Math.ceil( + (halfYearEnd.getTime() - halfYearStart.getTime()) / (1000 * 60 * 60 * 24), + ) + const dayWidth = halfYearWidth / daysInHalfYear + const dayInHalfYear = Math.ceil( + (targetDate.getTime() - halfYearStart.getTime()) / (1000 * 60 * 60 * 24), + ) + return cumulativePosition + dayInHalfYear * dayWidth + dayWidth / 2 + } + + // 累加每半年的宽度 + cumulativePosition += 180 + } + } else if (timeScale === TimelineScale.WEEK) { // 周视图:处理嵌套的weeks结构,返回中心位置 - const weeks = periodData.weeks || [] + // 类型保护:确保 periodData 是 TimelineMonth 并有 weeks 属性 + const weeks = ('weeks' in periodData && periodData.weeks) ? periodData.weeks : [] for (const week of weeks) { const weekStart = new Date(week.weekStart) @@ -741,13 +844,17 @@ const calculateMilestonePositionFromTimelineData = ( } } else if (timeScale === TimelineScale.MONTH) { // 月视图:处理扁平化的subDays结构,返回中心位置 + // 类型保护:确保 periodData 是 TimelineMonth + if (!('startDate' in periodData) || !('endDate' in periodData)) { + continue + } const periodStart = new Date(periodData.startDate) const periodEnd = new Date(periodData.endDate) if (targetDate >= periodStart && targetDate <= periodEnd) { // 找到目标日期所在的时间段 const monthWidth = 60 - const daysInMonth = periodData.monthData?.dayCount || 30 + const daysInMonth = ('monthData' in periodData && periodData.monthData?.dayCount) || 30 const dayWidth = monthWidth / daysInMonth const dayInMonth = targetDate.getDate() const finalPosition = cumulativePosition + (dayInMonth - 1) * dayWidth + dayWidth / 2 @@ -777,6 +884,7 @@ const calculateMilestonePositionFromTimelineData = ( 'milestone-sticky-left': milestoneVisibility.stickyPosition === 'left', 'milestone-sticky-right': milestoneVisibility.stickyPosition === 'right', 'milestone-pushed-out': milestoneVisibility.isPushedOut, + dimmed: isDimmed, }" @click.stop="handleMilestoneClick" > @@ -875,6 +983,13 @@ const calculateMilestonePositionFromTimelineData = ( user-select: none; } +/* 高亮模式下,非高亮的Milestone变暗淡 */ +.milestone.dimmed { + opacity: 0.35 !important; + filter: grayscale(0.3) !important; + transition: all 0.3s ease !important; +} + /* 里程碑SVG发光效果 */ .milestone svg { filter: drop-shadow(0 0 8px var(--gantt-danger, #f56c6c)); diff --git a/src/components/TaskBar.vue b/src/components/TaskBar.vue index 7d4ef92..39e44ab 100644 --- a/src/components/TaskBar.vue +++ b/src/components/TaskBar.vue @@ -6,6 +6,8 @@ import { TimelineScale } from '../models/types/TimelineScale' import TaskContextMenu from './TaskContextMenu.vue' import { useI18n } from '../composables/useI18n' +import type { TaskBarConfig } from '../models/configs/TaskBarConfig' +import { DEFAULT_TASK_BAR_CONFIG } from '../models/configs/TaskBarConfig' interface Props { task: Task @@ -13,7 +15,7 @@ interface Props { dayWidth: number startDate: Date isParent?: boolean - onDoubleClick?: (task: Task) => void + onClick?: (task: Task, event: MouseEvent) => void // 新增:用于粘性文字显示的滚动位置信息 scrollLeft?: number containerWidth?: number @@ -23,6 +25,16 @@ interface Props { timelineData?: any // 新增:当前时间刻度 currentTimeScale?: TimelineScale + // TaskBar 配置 + taskBarConfig?: TaskBarConfig + // 是否允许拖拽和拉伸(默认为 true) + allowDragAndResize?: boolean + // 是否被高亮显示(前置或后置任务) + isHighlighted?: boolean + // 是否是主要高亮(被长按的任务) + isPrimaryHighlight?: boolean + // 是否处于高亮模式(有任务被高亮) + isInHighlightMode?: boolean } interface TaskStatus { @@ -49,6 +61,7 @@ const props = defineProps() const emit = defineEmits([ 'update:task', 'bar-mounted', + 'click', 'dblclick', 'drag-end', 'resize-end', @@ -59,6 +72,7 @@ const emit = defineEmits([ 'add-successor', 'delete', 'context-menu', + 'long-press', ]) defineSlots<{ @@ -73,6 +87,12 @@ const t = (key: string): string => { const hasContentSlot = computed(() => Boolean(slots['custom-task-content'])) +// 合并默认配置和用户配置 +const barConfig = computed(() => ({ + ...DEFAULT_TASK_BAR_CONFIG, + ...props.taskBarConfig, +})) + // 日期工具函数 - 处理时区安全的日期创建和操作 const createLocalDate = (dateString: string | Date | undefined | null): Date | null => { if (!dateString) return null @@ -132,8 +152,13 @@ const getMinutesDiff = (startDate: Date, endDate: Date): number => { return Math.round((endDate.getTime() - startDate.getTime()) / (1000 * 60)) } -// 计算是否应该禁用拖拽和调整大小(年度视图下禁用) +// 计算是否应该禁用拖拽和调整大小(年度视图下禁用或任务不可编辑时禁用) const isInteractionDisabled = computed(() => { + // 检查任务的 isEditable 属性 + if (props.task.isEditable === false) { + return true + } + // 年度视图下禁用 return props.currentTimeScale === TimelineScale.YEAR }) @@ -141,6 +166,7 @@ const isInteractionDisabled = computed(() => { const isDragging = ref(false) const isResizingLeft = ref(false) const isResizingRight = ref(false) +const justFinishedDragOrResize = ref(false) // 标记刚刚完成拖拽或调整大小 const dragStartX = ref(0) const dragStartLeft = ref(0) const dragStartWidth = ref(0) @@ -148,6 +174,18 @@ const resizeStartX = ref(0) const resizeStartWidth = ref(0) const resizeStartLeft = ref(0) +// 长按检测状态 +const longPressTimer = ref(null) +const longPressTriggered = ref(false) +const LONG_PRESS_DURATION = 1000 // 1秒(缩短了) + +// 防误触配置 - 使用配置项或默认值 +const dragThreshold = computed(() => barConfig.value.dragThreshold ?? 5) +const isDragThresholdMet = ref(false) // 是否达到拖拽阈值 +const dragType = ref<'drag' | 'resize-left' | 'resize-right' | null>(null) // 记录操作类型 +const dragDelayTimer = ref(null) // 延迟拖拽定时器 +const isDelayPassed = ref(false) // 延迟是否已过 + // 相对位置拖拽方案:记录鼠标相对于TaskBar的位置 const mouseOffsetX = ref(0) // 鼠标在TaskBar内的相对位置 @@ -253,21 +291,15 @@ const taskBarStyle = computed(() => { baseStart.getDate(), ) - if (props.currentTimeScale === TimelineScale.YEAR) { - // 年度视图计算逻辑 - const startPosition = calculateYearViewPosition(startDateOnly, baseStartOnly) - const endPosition = calculateYearViewPosition(endDateOnly, baseStartOnly) - - left = startPosition - width = Math.max(endPosition - startPosition, 4) // 确保最小4px宽度 - } else if ( + if ( props.timelineData && props.currentTimeScale && (props.currentTimeScale === TimelineScale.WEEK || props.currentTimeScale === TimelineScale.MONTH || - props.currentTimeScale === TimelineScale.QUARTER) + props.currentTimeScale === TimelineScale.QUARTER || + props.currentTimeScale === TimelineScale.YEAR) ) { - // 优先使用基于timelineData的精确定位(适用于周视图、月视图和季度视图) + // 优先使用基于timelineData的精确定位(适用于周视图、月视图、季度视图和年度视图) // 计算开始位置 const startPosition = calculatePositionFromTimelineData( startDateOnly, @@ -290,6 +322,8 @@ const taskBarStyle = computed(() => { dayWidth = 60 / 7 } else if (props.currentTimeScale === TimelineScale.QUARTER) { dayWidth = 60 / 90 // 季度视图:每季度60px,约90天 + } else if (props.currentTimeScale === TimelineScale.YEAR) { + dayWidth = 180 / 182 // 年度视图:每半年180px,约182天 } endPosition = calculatePositionFromTimelineData( @@ -299,10 +333,41 @@ const taskBarStyle = computed(() => { ) + dayWidth } + left = startPosition + width = Math.max(endPosition - startPosition, 4) // 确保最小4px宽度 + } else if ( + props.timelineData && + props.currentTimeScale === TimelineScale.DAY + ) { + // 日视图:使用 timelineData 精确定位 + const startPosition = calculatePositionFromTimelineData( + startDateOnly, + props.timelineData, + props.currentTimeScale, + ) + + // 计算结束位置:为结束日期添加一天来获取正确的结束位置 + const nextDay = new Date(endDateOnly) + nextDay.setDate(nextDay.getDate() + 1) + let endPosition = calculatePositionFromTimelineData( + nextDay, + props.timelineData, + props.currentTimeScale, + ) + + // 如果结束日期+1天超出范围,使用结束日期的位置+一天的宽度 + if (endPosition === startPosition) { + endPosition = calculatePositionFromTimelineData( + endDateOnly, + props.timelineData, + props.currentTimeScale, + ) + 30 // 日视图每天30px + } + left = startPosition width = Math.max(endPosition - startPosition, 4) // 确保最小4px宽度 } else { - // 日视图:基于日期的简单计算 + // 其他情况(没有 timelineData):基于日期的简单计算 const startDiff = Math.floor( (startDateOnly.getTime() - baseStartOnly.getTime()) / (1000 * 60 * 60 * 24), ) @@ -398,6 +463,11 @@ const slotPayload = computed(() => ({ // 判断是否已完成 const isCompleted = computed(() => (props.task.progress || 0) >= 100) +// 判断是否应该显示为暗淡(处于高亮模式但自己不是高亮的) +const isDimmed = computed(() => { + return props.isInHighlightMode && !props.isHighlighted && !props.isPrimaryHighlight +}) + // 计算完成部分的宽度 const progressWidth = computed(() => { const progress = props.task.progress || 0 @@ -417,8 +487,20 @@ const isShortTaskBar = computed(() => { // 判断是否需要溢出效果(周视图且短TaskBar) const needsOverflowEffect = computed(() => isWeekView.value && isShortTaskBar.value) -// 鼠标事件处理 - 使用相对位置拖拽方案 +// 鼠标事件处理 - 使用相对位置拖拽方案(带防误触机制) const handleMouseDown = (e: MouseEvent, type: 'drag' | 'resize-left' | 'resize-right') => { + // 如果处于高亮状态,不阻止事件传播,让Timeline可以滚动 + if (props.isHighlighted || props.isPrimaryHighlight) { + // 不调用 e.preventDefault() 和 e.stopPropagation() + // 让事件继续传播到父容器,从而触发 Timeline 的滚动 + return + } + + // 如果禁用了拖拽和拉伸,直接返回 + if (props.allowDragAndResize === false) { + return + } + // 如果已完成或是父级任务或年度视图,禁用所有交互 if (isCompleted.value || props.isParent || isInteractionDisabled.value) { return @@ -430,6 +512,46 @@ const handleMouseDown = (e: MouseEvent, type: 'drag' | 'resize-left' | 'resize-r // 清空之前的临时数据 tempTaskData.value = null + // 重置防误触状态 + isDragThresholdMet.value = false + isDelayPassed.value = false + dragType.value = type + + // 重置长按状态 + longPressTriggered.value = false + if (longPressTimer.value) { + clearTimeout(longPressTimer.value) + longPressTimer.value = null + } + + // 启动长按检测定时器(仅在拖拽模式下) + if (type === 'drag') { + longPressTimer.value = window.setTimeout(() => { + // 检查是否发生了拖拽或拉伸(通过检查是否达到拖拽阈值) + const noDragOrResize = + !isDragThresholdMet.value && + !isDragging.value && + !isResizingLeft.value && + !isResizingRight.value + if (noDragOrResize) { + longPressTriggered.value = true + emit('long-press', props.task.id) + } + longPressTimer.value = null + }, LONG_PRESS_DURATION) + } + + // 如果启用了拖拽延迟,设置定时器 + if (barConfig.value.enableDragDelay && barConfig.value.dragDelayTime) { + dragDelayTimer.value = window.setTimeout(() => { + isDelayPassed.value = true + dragDelayTimer.value = null + }, barConfig.value.dragDelayTime) + } else { + // 未启用延迟,立即标记为已过延迟 + isDelayPassed.value = true + } + // 获取TaskBar相对于Timeline容器的位置 const timelineContainer = document.querySelector('.timeline') as HTMLElement if (!timelineContainer || !barRef.value) return @@ -439,18 +561,16 @@ const handleMouseDown = (e: MouseEvent, type: 'drag' | 'resize-left' | 'resize-r // 计算鼠标相对于TaskBar的位置 mouseOffsetX.value = e.clientX - barRect.left - if (type === 'drag') { - isDragging.value = true - dragStartX.value = e.clientX - dragStartLeft.value = parseInt(taskBarStyle.value.left) - dragStartWidth.value = parseInt(taskBarStyle.value.width) - } else if (type === 'resize-left') { - isResizingLeft.value = true + // 记录初始状态,但不立即激活拖拽 + dragStartX.value = e.clientX + dragStartLeft.value = parseInt(taskBarStyle.value.left) + dragStartWidth.value = parseInt(taskBarStyle.value.width) + + if (type === 'resize-left') { resizeStartX.value = e.clientX resizeStartWidth.value = parseInt(taskBarStyle.value.width) resizeStartLeft.value = parseInt(taskBarStyle.value.left) } else if (type === 'resize-right') { - isResizingRight.value = true resizeStartX.value = e.clientX resizeStartWidth.value = parseInt(taskBarStyle.value.width) resizeStartLeft.value = parseInt(taskBarStyle.value.left) @@ -494,6 +614,41 @@ const dragTooltipPosition = ref({ x: 0, y: 0 }) const dragTooltipContent = ref({ startDate: '', endDate: '' }) const handleMouseMove = (e: MouseEvent) => { + // 如果处于高亮状态,立即返回,不执行任何拖拽/拉伸操作 + if (props.isHighlighted || props.isPrimaryHighlight) { + return + } + + // 检查延迟是否已过(如果启用了延迟) + if (barConfig.value.enableDragDelay && !isDelayPassed.value) { + return + } + + // 检查是否达到拖拽阈值 + if (!isDragThresholdMet.value) { + const deltaX = Math.abs(e.clientX - dragStartX.value) + + // 如果移动距离小于阈值,不执行任何操作 + if (deltaX < dragThreshold.value) { + return + } + + // 达到阈值,激活对应的拖拽/拉伸状态 + isDragThresholdMet.value = true + if (dragType.value === 'drag') { + isDragging.value = true + } else if (dragType.value === 'resize-left') { + isResizingLeft.value = true + } else if (dragType.value === 'resize-right') { + isResizingRight.value = true + } + } + + // 只有达到阈值后才执行边界检测和提示框显示 + if (!isDragThresholdMet.value) { + return + } + // 发送边界检测事件给Timeline window.dispatchEvent( new CustomEvent('drag-boundary-check', { @@ -602,22 +757,62 @@ const handleMouseMove = (e: MouseEvent) => { endDate: formatDateToLocalString(newEndDate), } } else { - // 其他视图:保持原有逻辑 + // 其他视图(包括日视图、周视图、月视图、季度视图、年度视图):保持原有逻辑 const newLeft = Math.max(0, dragStartLeft.value + deltaX) - const newStartDate = addDaysToLocalDate(props.startDate, newLeft / props.dayWidth) - const duration = dragStartWidth.value / props.dayWidth - const newEndDate = addDaysToLocalDate(newStartDate, duration - 1) - // 只更新临时数据,不触发事件 - tempTaskData.value = { - startDate: formatDateToLocalString(newStartDate), - endDate: formatDateToLocalString(newEndDate), - } + // 日视图、月视图、季度视图或年度视图:如果有 timelineData,使用精确计算 + if ( + (props.currentTimeScale === TimelineScale.DAY || + props.currentTimeScale === TimelineScale.MONTH || + props.currentTimeScale === TimelineScale.QUARTER || + props.currentTimeScale === TimelineScale.YEAR) && + props.timelineData + ) { + const newStartDate = calculateDateFromPosition( + newLeft, + props.timelineData, + props.currentTimeScale, + ) - // 更新拖拽提示框内容 - dragTooltipContent.value = { - startDate: formatDateToLocalString(newStartDate), - endDate: formatDateToLocalString(newEndDate), + if (newStartDate) { + // 计算任务持续天数 + const originalStartDate = createLocalDate(props.task.startDate) || props.startDate + const originalEndDate = createLocalDate(props.task.endDate) || props.startDate + const durationMs = originalEndDate.getTime() - originalStartDate.getTime() + const duration = Math.ceil(durationMs / (1000 * 60 * 60 * 24)) + + const newEndDate = new Date(newStartDate) + newEndDate.setDate(newEndDate.getDate() + Math.max(0, duration)) + + // 只更新临时数据,不触发事件 + tempTaskData.value = { + startDate: formatDateToLocalString(newStartDate), + endDate: formatDateToLocalString(newEndDate), + } + + // 更新拖拽提示框内容 + dragTooltipContent.value = { + startDate: formatDateToLocalString(newStartDate), + endDate: formatDateToLocalString(newEndDate), + } + } + } else { + // 其他情况:使用原有的简单计算 + const newStartDate = addDaysToLocalDate(props.startDate, newLeft / props.dayWidth) + const duration = dragStartWidth.value / props.dayWidth + const newEndDate = addDaysToLocalDate(newStartDate, duration - 1) + + // 只更新临时数据,不触发事件 + tempTaskData.value = { + startDate: formatDateToLocalString(newStartDate), + endDate: formatDateToLocalString(newEndDate), + } + + // 更新拖拽提示框内容 + dragTooltipContent.value = { + startDate: formatDateToLocalString(newStartDate), + endDate: formatDateToLocalString(newEndDate), + } } } } else if (isResizingLeft.value) { @@ -683,20 +878,51 @@ const handleMouseMove = (e: MouseEvent) => { endDate: props.task.endDate || '', } } else { - // 其他视图:保持原有逻辑 + // 其他视图(包括日视图、周视图、月视图、季度视图、年度视图):保持原有逻辑 const newLeft = Math.max(0, resizeStartLeft.value + deltaX) - const newStartDate = addDaysToLocalDate(props.startDate, newLeft / props.dayWidth) - // 只更新临时数据,不触发事件 - tempTaskData.value = { - startDate: formatDateToLocalString(newStartDate), - endDate: props.task.endDate, // 保持原来的结束日期 - } + // 日视图、月视图、季度视图或年度视图:如果有 timelineData,使用精确计算 + if ( + (props.currentTimeScale === TimelineScale.DAY || + props.currentTimeScale === TimelineScale.MONTH || + props.currentTimeScale === TimelineScale.QUARTER || + props.currentTimeScale === TimelineScale.YEAR) && + props.timelineData + ) { + const newStartDate = calculateDateFromPosition( + newLeft, + props.timelineData, + props.currentTimeScale, + ) - // 更新拖拽提示框内容 - dragTooltipContent.value = { - startDate: formatDateToLocalString(newStartDate), - endDate: props.task.endDate || '', + if (newStartDate) { + // 只更新临时数据,不触发事件 + tempTaskData.value = { + startDate: formatDateToLocalString(newStartDate), + endDate: props.task.endDate, // 保持原来的结束日期 + } + + // 更新拖拽提示框内容 + dragTooltipContent.value = { + startDate: formatDateToLocalString(newStartDate), + endDate: props.task.endDate || '', + } + } + } else { + // 其他情况:使用原有的简单计算 + const newStartDate = addDaysToLocalDate(props.startDate, newLeft / props.dayWidth) + + // 只更新临时数据,不触发事件 + tempTaskData.value = { + startDate: formatDateToLocalString(newStartDate), + endDate: props.task.endDate, // 保持原来的结束日期 + } + + // 更新拖拽提示框内容 + dragTooltipContent.value = { + startDate: formatDateToLocalString(newStartDate), + endDate: props.task.endDate || '', + } } } } else if (isResizingRight.value) { @@ -761,30 +987,75 @@ const handleMouseMove = (e: MouseEvent) => { endDate: formatDateToLocalString(newEndDate), } } else { - // 其他视图:保持原有逻辑 + // 其他视图(包括日视图、周视图、月视图、季度视图、年度视图):保持原有逻辑 const newWidth = Math.max(props.dayWidth, resizeStartWidth.value + deltaX) - const newDurationDays = newWidth / props.dayWidth - const newEndDate = addDaysToLocalDate( - props.startDate, - resizeStartLeft.value / props.dayWidth + newDurationDays - 1, - ) - // 只更新临时数据,不触发事件 - tempTaskData.value = { - startDate: props.task.startDate, // 保持原来的开始日期 - endDate: formatDateToLocalString(newEndDate), - } + // 日视图、月视图、季度视图或年度视图:如果有 timelineData,使用精确计算 + if ( + (props.currentTimeScale === TimelineScale.DAY || + props.currentTimeScale === TimelineScale.MONTH || + props.currentTimeScale === TimelineScale.QUARTER || + props.currentTimeScale === TimelineScale.YEAR) && + props.timelineData + ) { + // 计算新的结束位置(左侧位置 + 新宽度) + const newRightPosition = resizeStartLeft.value + newWidth + const newEndDate = calculateDateFromPosition( + newRightPosition, + props.timelineData, + props.currentTimeScale, + ) - // 更新拖拽提示框内容 - dragTooltipContent.value = { - startDate: props.task.startDate || '', - endDate: formatDateToLocalString(newEndDate), + if (newEndDate) { + // 只更新临时数据,不触发事件 + tempTaskData.value = { + startDate: props.task.startDate, // 保持原来的开始日期 + endDate: formatDateToLocalString(newEndDate), + } + + // 更新拖拽提示框内容 + dragTooltipContent.value = { + startDate: props.task.startDate || '', + endDate: formatDateToLocalString(newEndDate), + } + } + } else { + // 其他情况:使用原有的简单计算 + const newDurationDays = newWidth / props.dayWidth + const newEndDate = addDaysToLocalDate( + props.startDate, + resizeStartLeft.value / props.dayWidth + newDurationDays - 1, + ) + + // 只更新临时数据,不触发事件 + tempTaskData.value = { + startDate: props.task.startDate, // 保持原来的开始日期 + endDate: formatDateToLocalString(newEndDate), + } + + // 更新拖拽提示框内容 + dragTooltipContent.value = { + startDate: props.task.startDate || '', + endDate: formatDateToLocalString(newEndDate), + } } } } } const handleMouseUp = () => { + // 清除延迟定时器 + if (dragDelayTimer.value !== null) { + window.clearTimeout(dragDelayTimer.value) + dragDelayTimer.value = null + } + + // 清除长按定时器 + if (longPressTimer.value !== null) { + clearTimeout(longPressTimer.value) + longPressTimer.value = null + } + // 隐藏拖拽提示框 dragTooltipVisible.value = false @@ -801,8 +1072,8 @@ const handleMouseUp = () => { }), ) - // 如果有临时数据,说明发生了拖拽或拉伸,提交数据更新 - if (tempTaskData.value) { + // 只有达到拖拽阈值且有临时数据时才提交更新 + if (isDragThresholdMet.value && tempTaskData.value) { const updatedTask = { ...props.task, ...tempTaskData.value, @@ -828,9 +1099,23 @@ const handleMouseUp = () => { // 清理自动滚动监听器 window.removeEventListener('timeline-auto-scroll', handleAutoScroll as EventListener) + // 如果发生了拖拽或调整大小,设置标志防止触发 click 事件 + if (isDragging.value || isResizingLeft.value || isResizingRight.value) { + justFinishedDragOrResize.value = true + // 200ms 后清除标志 + setTimeout(() => { + justFinishedDragOrResize.value = false + }, 200) + } + + // 重置所有拖拽状态 isDragging.value = false isResizingLeft.value = false isResizingRight.value = false + isDragThresholdMet.value = false + isDelayPassed.value = false + dragType.value = null + document.removeEventListener('mousemove', handleMouseMove) document.removeEventListener('mouseup', handleMouseUp) } @@ -871,6 +1156,12 @@ onMounted(() => { window.removeEventListener('timeline-scale-updated', handleTimelineScaleUpdate) window.removeEventListener('timeline-force-recalculate', handleForceRecalculate) window.removeEventListener('close-all-taskbar-menus', closeContextMenu) + + // 清除长按定时器 + if (longPressTimer.value !== null) { + clearTimeout(longPressTimer.value) + longPressTimer.value = null + } }) }) @@ -885,12 +1176,101 @@ watch( { deep: true }, ) +// 监听高亮状态变化(调试) +watch( + () => [props.isHighlighted, props.isPrimaryHighlight], + ([highlighted, primary]) => { + if (highlighted || primary) { + // 当TaskBar变为高亮状态时,立即清理所有拖拽状态和事件监听器 + // 无条件清理,即使没有正在拖拽也要重置状态 + isDragging.value = false + isResizingLeft.value = false + isResizingRight.value = false + isDragThresholdMet.value = false + isDelayPassed.value = false + dragType.value = null + dragTooltipVisible.value = false + + // 清理事件监听器 + document.removeEventListener('mousemove', handleMouseMove) + document.removeEventListener('mouseup', handleMouseUp) + window.removeEventListener('timeline-auto-scroll', handleAutoScroll as EventListener) + + // 清理定时器 + if (longPressTimer.value) { + clearTimeout(longPressTimer.value) + longPressTimer.value = null + } + if (dragDelayTimer.value) { + clearTimeout(dragDelayTimer.value) + dragDelayTimer.value = null + } + + // 触发自定义事件,通知Timeline启动拖拽滚动 + window.dispatchEvent( + new CustomEvent('taskbar-highlighted', { + detail: { + taskId: props.task.id, + }, + }), + ) + } + }, +) + +// 单击/双击延迟处理 +let clickTimer: number | null = null +const clickDelay = 250 // 延迟时间(毫秒) + +// 处理TaskBar单击事件(使用延迟以区分双击) +const handleTaskBarClick = (e: MouseEvent) => { + // 如果正在拖拽或调整大小,或者刚刚完成拖拽/调整大小,不触发点击事件 + if ( + isDragging.value || + isResizingLeft.value || + isResizingRight.value || + justFinishedDragOrResize.value + ) { + return + } + + // 如果刚刚触发了长按,不触发点击事件 + if (longPressTriggered.value) { + longPressTriggered.value = false // 重置标记 + return + } + + // 如果已有定时器,说明是双击的第二次点击,清除定时器不触发单击 + if (clickTimer !== null) { + return + } + + // 设置延迟触发单击事件 + clickTimer = window.setTimeout(() => { + clickTimer = null + + // 优先调用外部传入的单击处理器 + if (props.onClick && typeof props.onClick === 'function') { + props.onClick(props.task, e) + } else { + // 默认行为:发出单击事件给父组件 + emit('click', props.task, e) + } + }, clickDelay) +} + // 处理TaskBar双击事件 const handleTaskBarDoubleClick = (e: MouseEvent) => { // 阻止事件冒泡,避免触发拖拽等其他事件 e.stopPropagation() e.preventDefault() + // 清除单击定时器,防止触发单击事件 + if (clickTimer !== null) { + clearTimeout(clickTimer) + clickTimer = null + } + // 清理任何可能残留的拖拽状态和临时数据 isDragging.value = false isResizingLeft.value = false @@ -901,13 +1281,8 @@ const handleTaskBarDoubleClick = (e: MouseEvent) => { document.removeEventListener('mousemove', handleMouseMove) document.removeEventListener('mouseup', handleMouseUp) - // 优先调用外部传入的双击处理器 - if (props.onDoubleClick && typeof props.onDoubleClick === 'function') { - props.onDoubleClick(props.task) - } else { - // 默认行为:发出双击事件给父组件 - emit('dblclick', props.task) - } + // 发出双击事件给父组件 + emit('dblclick', props.task, e) } // 计算粘性样式 - 支持左右边界的精细控制 @@ -923,6 +1298,8 @@ const stickyStyles = computed(() => { progressLeft: '', progressPosition: '', progressTop: '', + avatarLeft: '', + avatarPosition: '', } } @@ -940,55 +1317,122 @@ const stickyStyles = computed(() => { let progressLeft = '' let progressPosition = '' let progressTop = '' + let avatarLeft = '' + let avatarPosition = '' // 估算文字内容的实际位置 const nameWidth = Math.max(nameTextWidth.value, 40) // 最小40px const progressWidth = 35 + const avatarWidth = 22 // avatar 宽度 + const handleWidth = actualHandleWidth.value // 拉伸手柄宽度 - // 计算名称和进度在默认居中状态下的位置 + // === 第一步:检测 Avatar 是否需要粘性定位 === + const avatarDefaultLeft = handleWidth + 3 // 手柄宽度 + 3px 间距 + const avatarLeftPos = taskLeft + avatarDefaultLeft + const avatarRightPos = taskLeft + avatarDefaultLeft + avatarWidth + + // Avatar 左侧粘性逻辑 + const avatarNeedsLeftSticky = + (avatarLeftPos < leftBoundary && taskRight > leftBoundary) || + (taskCenterX < leftBoundary && taskRight > leftBoundary) + + // Avatar 右侧粘性逻辑:当 avatar 接近右边框 + name/progress 宽度 + 15px 时触发 + const maxContentWidth = Math.max(nameWidth, progressWidth) + const avatarNeedsRightSticky = + (avatarRightPos + maxContentWidth + 15 > rightBoundary && taskLeft < rightBoundary) + + // 计算 avatar 粘性时的偏移量 + let avatarStickyOffset = 0 + if (avatarNeedsLeftSticky) { + const offset = leftBoundary - taskLeft + avatarLeft = `${offset + handleWidth + 3}px` // 手柄宽度 + 3px 间距 + avatarPosition = 'absolute' + avatarStickyOffset = avatarWidth + 8 // avatar 宽度 + 右侧间距 + } else if (avatarNeedsRightSticky) { + // avatar 应该停靠在 name/progress 左侧 15px 的位置 + // 计算 name/progress 在右边界时的位置(它们会贴在右边框上) + const maxContentWidth = Math.max(nameWidth, progressWidth) + // 内容贴右边框时的左侧位置,考虑手柄宽度 + const contentRightPos = rightBoundary - taskLeft - maxContentWidth - handleWidth - 3 + const offset = contentRightPos - avatarWidth - 15 // avatar 在内容左侧 15px + avatarLeft = `${offset}px` + avatarPosition = 'absolute' + avatarStickyOffset = -(avatarWidth + 15) // 负值表示在右侧,avatar宽度 + 15px间距 + } + + // === 第二步:处理名称粘性定位(考虑 avatar 偏移) === const nameLeftPos = taskCenterX - nameWidth / 2 const nameRightPos = taskCenterX + nameWidth / 2 - const progressLeftPos = taskCenterX - progressWidth / 2 - const progressRightPos = taskCenterX + progressWidth / 2 - // 左侧边界粘性逻辑 - const nameNeedsLeftSticky = - nameLeftPos < leftBoundary && taskRight > leftBoundary && taskCenterX < leftBoundary + // 判断是否显示 avatar(只要配置开启就显示,包括默认头像) + const hasAvatar = barConfig.value.showAvatar + + // Name 左侧磁吸触发条件优化 + let nameNeedsLeftSticky = false + if (hasAvatar) { + // 如果 avatar 存在:当 name 左侧接近左边界(预留 avatar+间距 的空间)时触发 + // name 默认居中,当它向左移动到需要为 avatar 留出空间时触发磁吸 + const nameActualLeft = nameLeftPos // name 实际左侧位置 + const avatarReservedSpace = avatarWidth + 15 // avatar 需要的空间 + nameNeedsLeftSticky = + nameActualLeft < leftBoundary + avatarReservedSpace && taskRight > leftBoundary + } else { + // 如果 avatar 不存在:保留原逻辑 + nameNeedsLeftSticky = + nameLeftPos < leftBoundary && taskRight > leftBoundary && taskCenterX < leftBoundary + } - // 右侧边界粘性逻辑 const nameNeedsRightSticky = nameRightPos > rightBoundary && taskLeft < rightBoundary && taskCenterX > rightBoundary - // 名称粘性处理 if (nameNeedsLeftSticky) { const offset = leftBoundary - taskLeft - nameLeft = `${offset + 8}px` + // 如果 avatar 也在左侧粘性,则 title 需要在 avatar 右侧 + const extraOffset = avatarNeedsLeftSticky ? avatarStickyOffset : 0 + nameLeft = `${offset + handleWidth + 3 + extraOffset}px` // 考虑手柄宽度 + 间距 namePosition = 'absolute' nameTop = '2px' } else if (nameNeedsRightSticky) { const offset = rightBoundary - taskLeft - nameWidth - nameLeft = `${offset - 8}px` + // name 右侧磁吸时应始终保持与右边框固定距离,需要减去右侧手柄宽度 + nameLeft = `${offset - handleWidth - 3}px` // 考虑手柄宽度 + 间距 namePosition = 'absolute' nameTop = '2px' } - // 进度左侧边界粘性逻辑 - const progressNeedsLeftSticky = - progressLeftPos < leftBoundary && taskRight > leftBoundary && taskCenterX < leftBoundary + // === 第三步:处理进度粘性定位(考虑 avatar 偏移) === + const progressLeftPos = taskCenterX - progressWidth / 2 + const progressRightPos = taskCenterX + progressWidth / 2 + + // Progress 左侧磁吸触发条件优化 + let progressNeedsLeftSticky = false + if (hasAvatar) { + // 如果 avatar 存在:当 progress 左侧接近左边界(预留 avatar+间距 的空间)时触发 + // progress 默认居中,当它向左移动到需要为 avatar 留出空间时触发磁吸 + const progressActualLeft = progressLeftPos // progress 实际左侧位置 + const avatarReservedSpace = avatarWidth + 15 // avatar 需要的空间 + progressNeedsLeftSticky = + progressActualLeft < leftBoundary + avatarReservedSpace && taskRight > leftBoundary + } else { + // 如果 avatar 不存在:保留原逻辑 + progressNeedsLeftSticky = + progressLeftPos < leftBoundary && taskRight > leftBoundary && taskCenterX < leftBoundary + } - // 进度右侧边界粘性逻辑 const progressNeedsRightSticky = progressRightPos > rightBoundary && taskLeft < rightBoundary && taskCenterX > rightBoundary - // 进度粘性处理 if (progressNeedsLeftSticky) { const offset = leftBoundary - taskLeft - progressLeft = `${offset + 8}px` + // 如果 avatar 也在左侧粘性,则进度需要在 avatar 右侧 + const extraOffset = avatarNeedsLeftSticky ? avatarStickyOffset : 0 + progressLeft = `${offset + handleWidth + 3 + extraOffset}px` // 考虑手柄宽度 + 间距 progressPosition = 'absolute' progressTop = '18px' } else if (progressNeedsRightSticky) { const offset = rightBoundary - taskLeft - progressWidth - progressLeft = `${offset - 8}px` + // progress 右侧磁吸时应始终保持与右边框固定距离,需要减去右侧手柄宽度 + progressLeft = `${offset - handleWidth - 3}px` // 考虑手柄宽度 + 间距 progressPosition = 'absolute' progressTop = '18px' } @@ -1000,6 +1444,8 @@ const stickyStyles = computed(() => { progressLeft, progressPosition, progressTop, + avatarLeft, + avatarPosition, } }) @@ -1085,7 +1531,11 @@ const isAutoScrolling = ref(false) // 跟踪自动滚动状态(如定位到今 const bubbleHidden = ref(false) // 控制半圆的强制隐藏状态 // 气泡点击事件 - 将TaskBar定位到Timeline中间 -const handleBubbleClick = () => { +const handleBubbleClick = (e: MouseEvent) => { + // 阻止事件冒泡,避免触发 TaskBar 的 click 事件 + e.stopPropagation() + e.preventDefault() + const scrollLeft = props.scrollLeft || 0 const containerWidth = props.containerWidth || 0 @@ -1227,6 +1677,9 @@ onUnmounted(() => { // 处理气泡悬停 const handleBubbleMouseEnter = (event: MouseEvent) => { + // 阻止事件冒泡 + event.stopPropagation() + showTooltip.value = true // 智能定位:右侧气泡在左侧显示tooltip,左侧气泡在右侧显示 @@ -1239,7 +1692,10 @@ const handleBubbleMouseEnter = (event: MouseEvent) => { } } -const handleBubbleMouseLeave = () => { +const handleBubbleMouseLeave = (event: MouseEvent) => { + // 阻止事件冒泡 + event.stopPropagation() + showTooltip.value = false } @@ -1314,59 +1770,36 @@ const getProgressStyles = () => { return result } -// 年度视图位置计算函数 -const calculateYearViewPosition = (targetDate: Date, baseStartDate: Date): number => { - const targetYear = targetDate.getFullYear() - const baseYear = baseStartDate.getFullYear() +const getAvatarStyles = () => { + const styles = stickyStyles.value + const result: Record = {} - // 每年的宽度是360px,每半年180px - const yearWidth = 360 - const halfYearWidth = 180 + if (styles.avatarLeft) result.left = styles.avatarLeft + if (styles.avatarPosition) result.position = styles.avatarPosition - // 计算目标年份相对于基准年份的偏移 - const yearOffset = targetYear - baseYear - let position = yearOffset * yearWidth - - // 判断是上半年还是下半年 - const month = targetDate.getMonth() + 1 // getMonth()返回0-11,需要+1 - - if (month > 6) { - // 下半年,添加半年偏移 - position += halfYearWidth - } - - // 在半年内的具体位置计算 - let dayOffset = 0 - let startOfHalfYear: Date - - if (month <= 6) { - // 上半年:1-6月 - startOfHalfYear = new Date(targetYear, 0, 1) // 1月1日 - } else { - // 下半年:7-12月 - startOfHalfYear = new Date(targetYear, 6, 1) // 7月1日 - } - - dayOffset = Math.floor((targetDate.getTime() - startOfHalfYear.getTime()) / (1000 * 60 * 60 * 24)) - - // 半年大约181-184天,将天数映射到180px的宽度 - const daysInHalfYear = - month <= 6 - ? Math.floor( - (new Date(targetYear, 6, 1).getTime() - new Date(targetYear, 0, 1).getTime()) / - (1000 * 60 * 60 * 24), - ) - : Math.floor( - (new Date(targetYear + 1, 0, 1).getTime() - new Date(targetYear, 6, 1).getTime()) / - (1000 * 60 * 60 * 24), - ) - - const dayPositionInHalfYear = (dayOffset / daysInHalfYear) * halfYearWidth - position += dayPositionInHalfYear - - return position + return result } +// 计算 avatar 是否应该渲染在外框边缘 +const shouldRenderAvatarOutside = computed(() => { + const taskWidth = parseInt(taskBarStyle.value.width) + const avatarWidth = 22 + return taskWidth - 10 < avatarWidth +}) + +// 计算 resize-handle 的样式 +const resizeHandleStyle = computed(() => { + const width = Math.min(barConfig.value.resizeHandleWidth ?? 5, 15) // 最大15px + return { + width: `${width}px`, + } +}) + +// 计算实际手柄宽度(用于位置计算) +const actualHandleWidth = computed(() => { + return Math.min(barConfig.value.resizeHandleWidth ?? 5, 15) +}) + // 基于timelineData和subDays精确计算日期位置的函数 const calculatePositionFromTimelineData = ( targetDate: Date, @@ -1375,6 +1808,7 @@ const calculatePositionFromTimelineData = ( month: number startDate: Date endDate: Date + days?: Array<{ date: Date; day: number }> subDays?: Array<{ date: Date; dayOfWeek?: number }> monthData?: { dayCount: number } weeks?: Array<{ @@ -1388,14 +1822,34 @@ const calculatePositionFromTimelineData = ( let cumulativePosition = 0 for (const periodData of timelineData) { - if (timeScale === TimelineScale.QUARTER) { - // 季度视图:处理quarters结构 - const quarters = ((periodData as Record).quarters as unknown[]) || [] + if (timeScale === TimelineScale.DAY) { + // 日视图:处理days数组 + const days = periodData.days || [] + + for (let i = 0; i < days.length; i++) { + const dayData = days[i] + const dayDate = new Date(dayData.date) + + // 比较日期(忽略时分秒) + if ( + dayDate.getFullYear() === targetDate.getFullYear() && + dayDate.getMonth() === targetDate.getMonth() && + dayDate.getDate() === targetDate.getDate() + ) { + // 找到目标日期,返回累计位置 + 当前天数索引 * 日宽度 + return cumulativePosition + i * 30 // 日视图每天30px + } + } + + // 累加当前月份所有天数的宽度 + cumulativePosition += days.length * 30 + } else if (timeScale === TimelineScale.QUARTER) { + // 季度视图:处理years数组,每个year包含quarters + const quarters = (periodData as any).quarters || [] for (const quarter of quarters) { - const quarterObj = quarter as Record - const quarterStart = new Date(quarterObj.startDate as string) - const quarterEnd = new Date(quarterObj.endDate as string) + const quarterStart = new Date(quarter.startDate) + const quarterEnd = new Date(quarter.endDate) if (targetDate >= quarterStart && targetDate <= quarterEnd) { // 找到目标日期所在的季度 @@ -1465,10 +1919,175 @@ const calculatePositionFromTimelineData = ( // 累加每月的宽度 cumulativePosition += 60 + } else if (timeScale === TimelineScale.YEAR) { + // 年度视图:处理years数组,每个year包含halfYears + const halfYears = (periodData as any).halfYears || [] + + for (const halfYear of halfYears) { + const halfYearStart = new Date(halfYear.startDate) + const halfYearEnd = new Date(halfYear.endDate) + + if (targetDate >= halfYearStart && targetDate <= halfYearEnd) { + // 找到目标日期所在的半年 + const halfYearWidth = 180 // 年度视图每半年180px + const daysInHalfYear = Math.ceil( + (halfYearEnd.getTime() - halfYearStart.getTime()) / (1000 * 60 * 60 * 24), + ) + const dayWidth = halfYearWidth / daysInHalfYear + const dayInHalfYear = Math.ceil( + (targetDate.getTime() - halfYearStart.getTime()) / (1000 * 60 * 60 * 24), + ) + return cumulativePosition + dayInHalfYear * dayWidth + } + + // 累加每半年的宽度 + cumulativePosition += 180 + } } } - return cumulativePosition // 如果没找到,返回累计位置 + // 周视图没找到目标日期时返回-1 + if (timeScale === TimelineScale.WEEK) { + // eslint-disable-next-line no-console + console.warn('[Gantt Debug] 周视图定位失败,任务日期未找到 week 匹配,返回 -1', { targetDate }) + return -1 + } + return cumulativePosition // 其他视图保持原逻辑 +} + +// 反向函数:从像素位置计算日期(基于 timelineData) +const calculateDateFromPosition = ( + pixelPosition: number, + timelineData: Array<{ + year: number + month: number + startDate: Date + endDate: Date + days?: Array<{ date: Date; day: number }> + monthData?: { dayCount: number } + }>, + timeScale: TimelineScale, +): Date | null => { + if (!timelineData) { + return null + } + + let cumulativePosition = 0 + + if (timeScale === TimelineScale.DAY) { + // 日视图:基于 days 数组 + for (const periodData of timelineData) { + const days = periodData.days || [] + const periodWidth = days.length * 30 // 日视图每天30px + + // 检查像素位置是否在当前时间段内 + if (pixelPosition >= cumulativePosition && pixelPosition < cumulativePosition + periodWidth) { + // 计算在当前时间段内的相对位置 + const relativePosition = pixelPosition - cumulativePosition + const dayIndex = Math.floor(relativePosition / 30) + + // 确保索引在范围内 + if (dayIndex >= 0 && dayIndex < days.length) { + return new Date(days[dayIndex].date) + } + } + + cumulativePosition += periodWidth + } + } else if (timeScale === TimelineScale.MONTH) { + // 月视图:每个月60px + for (const periodData of timelineData) { + const monthWidth = 60 + + // 检查像素位置是否在当前月份内 + if (pixelPosition >= cumulativePosition && pixelPosition < cumulativePosition + monthWidth) { + // 计算在当前月份内的相对位置 + const relativePosition = pixelPosition - cumulativePosition + const daysInMonth = periodData.monthData?.dayCount || 30 + const dayWidth = monthWidth / daysInMonth + + // 计算是该月的第几天 + const dayIndex = Math.floor(relativePosition / dayWidth) + const day = Math.min(dayIndex + 1, daysInMonth) // 1-based,确保不超过该月天数 + + return new Date(periodData.year, periodData.month - 1, day) + } + + cumulativePosition += monthWidth + } + } else if (timeScale === TimelineScale.QUARTER) { + // 季度视图:每个季度60px + for (const periodData of timelineData) { + const quarters = (periodData as any).quarters || [] + + for (const quarter of quarters) { + const quarterStart = new Date(quarter.startDate) + const quarterEnd = new Date(quarter.endDate) + const quarterWidth = 60 + + // 检查像素位置是否在当前季度内 + if ( + pixelPosition >= cumulativePosition && + pixelPosition < cumulativePosition + quarterWidth + ) { + // 计算在当前季度内的相对位置 + const relativePosition = pixelPosition - cumulativePosition + const daysInQuarter = Math.ceil( + (quarterEnd.getTime() - quarterStart.getTime()) / (1000 * 60 * 60 * 24), + ) + const dayWidth = quarterWidth / daysInQuarter + + // 计算是该季度的第几天 + const dayIndex = Math.floor(relativePosition / dayWidth) + + // 从季度开始日期加上天数 + const resultDate = new Date(quarterStart) + resultDate.setDate(resultDate.getDate() + dayIndex) + + return resultDate + } + + cumulativePosition += quarterWidth + } + } + } else if (timeScale === TimelineScale.YEAR) { + // 年度视图:每半年180px + for (const periodData of timelineData) { + const halfYears = (periodData as any).halfYears || [] + + for (const halfYear of halfYears) { + const halfYearStart = new Date(halfYear.startDate) + const halfYearEnd = new Date(halfYear.endDate) + const halfYearWidth = 180 + + // 检查像素位置是否在当前半年内 + if ( + pixelPosition >= cumulativePosition && + pixelPosition < cumulativePosition + halfYearWidth + ) { + // 计算在当前半年内的相对位置 + const relativePosition = pixelPosition - cumulativePosition + const daysInHalfYear = Math.ceil( + (halfYearEnd.getTime() - halfYearStart.getTime()) / (1000 * 60 * 60 * 24), + ) + const dayWidth = halfYearWidth / daysInHalfYear + + // 计算是该半年的第几天 + const dayIndex = Math.floor(relativePosition / dayWidth) + + // 从半年开始日期加上天数 + const resultDate = new Date(halfYearStart) + resultDate.setDate(resultDate.getDate() + dayIndex) + + return resultDate + } + + cumulativePosition += halfYearWidth + } + } + } + + return null // 如果没找到,返回 null } // 处理右键菜单 @@ -1477,6 +2096,12 @@ const contextMenuPosition = ref({ x: 0, y: 0 }) const contextMenuTask = computed(() => props.task) function handleContextMenu(event: MouseEvent) { + // 高亮模式下禁用右键菜单 + if (props.isHighlighted || props.isPrimaryHighlight) { + event.preventDefault() + return + } + // 先广播关闭所有TaskBar菜单 window.dispatchEvent(new CustomEvent('close-all-taskbar-menus')) if (props.task.type !== 'task' && props.task.type !== 'story') { @@ -1505,6 +2130,12 @@ onMounted(() => { }) onUnmounted(() => { window.removeEventListener('close-all-taskbar-menus', closeContextMenu) + + // 清理单击定时器 + if (clickTimer !== null) { + clearTimeout(clickTimer) + clickTimer = null + } }) @@ -1519,6 +2150,7 @@ onUnmounted(() => { color: taskStatus.color, cursor: isCompleted || isParent ? 'default' : 'move', '--row-height': `${rowHeight}px` /* 传递行高给CSS变量 */, + '--handle-width': `${actualHandleWidth}px` /* 传递手柄宽度给CSS变量 */, }" :class="{ dragging: isDragging, @@ -1528,7 +2160,11 @@ onUnmounted(() => { 'week-view': isWeekView, 'short-task-bar': isShortTaskBar, 'overflow-effect': needsOverflowEffect, + highlighted: isHighlighted, + 'primary-highlight': isPrimaryHighlight, + dimmed: isDimmed, }" + @click="handleTaskBarClick" @contextmenu="handleContextMenu" @dblclick="handleTaskBarDoubleClick" > @@ -1550,8 +2186,16 @@ onUnmounted(() => {
@@ -1559,11 +2203,51 @@ onUnmounted(() => {
+ +
+ + + + + {{ task.assignee.charAt(0).toUpperCase() }} + + + + + +
+ -
+
{{ task.name }} @@ -1571,15 +2255,27 @@ onUnmounted(() => {
-
+
{{ task.progress || 0 }}%
@@ -1596,10 +2292,10 @@ onUnmounted(() => { backgroundColor: bubbleIndicator.color, borderColor: bubbleIndicator.color, }" - @mouseenter="handleBubbleMouseEnter" - @mouseleave="handleBubbleMouseLeave" - @mousedown="handleBubbleMouseDown" - @click="handleBubbleClick" + @mouseenter.stop="handleBubbleMouseEnter" + @mouseleave.stop="handleBubbleMouseLeave" + @mousedown.stop="handleBubbleMouseDown" + @click.stop="handleBubbleClick" >
{ border-radius: 4px; user-select: none; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - transition: box-shadow 0.2s; + transition: + box-shadow 0.2s, + transform 0.3s, + filter 0.3s; z-index: 100; border: 2px solid; overflow: visible; /* 允许内容超出 TaskBar */ @@ -1711,6 +2410,45 @@ onUnmounted(() => { z-index: 1000; } +/* 高亮模式下,非高亮的TaskBar变暗淡 */ +.task-bar.dimmed { + opacity: 0.35 !important; + filter: grayscale(0.3) !important; + transition: all 0.3s ease !important; +} + +/* 高亮样式 - 前置/后置任务 */ +.task-bar.highlighted { + z-index: 1002 !important; + box-shadow: + 0 8px 24px rgba(64, 158, 255, 0.5), + 0 6px 16px rgba(0, 0, 0, 0.3) !important; + transform: translateY(-5px) scale(1.05) !important; + transition: all 0.3s ease !important; + cursor: grab !important; +} + +/* 高亮TaskBar的内容加粗 */ +.task-bar.highlighted .task-bar-content { + font-weight: bold !important; +} + +/* 主要高亮样式 - 被长按的任务 */ +.task-bar.primary-highlight { + z-index: 1003 !important; + box-shadow: + 0 12px 32px rgba(64, 158, 255, 0.6), + 0 8px 20px rgba(0, 0, 0, 0.35) !important; + transform: translateY(-8px) scale(1.08) !important; + transition: all 0.3s ease !important; + cursor: grab !important; +} + +/* 主要高亮TaskBar的内容加粗 */ +.task-bar.primary-highlight .task-bar-content { + font-weight: bold !important; +} + .task-bar.parent-task { position: relative; border-radius: 0; /* 移除圆角,使用线性设计 */ @@ -1725,6 +2463,23 @@ onUnmounted(() => { overflow: visible; /* 确保伪元素可见 */ } +/* 高亮的父任务覆盖默认样式 */ +.task-bar.parent-task.highlighted { + box-shadow: + 0 8px 24px rgba(64, 158, 255, 0.5), + 0 6px 16px rgba(0, 0, 0, 0.3) !important; + filter: brightness(1.2) drop-shadow(0 0 8px rgba(64, 158, 255, 0.4)) !important; + transform: translateY(-50%) translateY(-5px) scale(1.05) !important; +} + +.task-bar.parent-task.primary-highlight { + box-shadow: + 0 12px 32px rgba(64, 158, 255, 0.6), + 0 8px 20px rgba(0, 0, 0, 0.35) !important; + filter: brightness(1.25) drop-shadow(0 0 12px rgba(64, 158, 255, 0.6)) !important; + transform: translateY(-50%) translateY(-8px) scale(1.08) !important; +} + /* 父级任务的标签 */ .task-bar.parent-task .parent-label { position: absolute; @@ -1791,6 +2546,78 @@ onUnmounted(() => { z-index: 1; } +.task-avatar { + position: absolute; + left: calc(var(--handle-width, 5px) + 3px); /* 手柄宽度 + 3px 间距 */ + top: 50%; + transform: translateY(-50%); + width: 22px; + height: 22px; + border-radius: 50%; + overflow: hidden; + background: #f0f0f0; + border: 2px solid rgba(255, 255, 255, 0.9); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + z-index: 15; + pointer-events: none; /* 不阻止拖拽 */ + flex-shrink: 0; /* 防止被压缩 */ + display: flex; + align-items: center; + justify-content: center; +} + +.task-avatar img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +/* 默认头像样式(灰色背景) */ +.task-avatar.avatar-default { + background: var(--gantt-bg-tertiary, #e0e0e0); + color: var(--gantt-text-tertiary, #9e9e9e); +} + +/* 文字头像样式 */ +.task-avatar .avatar-text { + font-size: 11px; + font-weight: 600; + color: var(--gantt-text-white, #ffffff); + background: var(--gantt-primary-color, #409eff); + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; +} + +/* 默认图标样式 */ +.task-avatar .avatar-icon { + width: 14px; + height: 14px; + color: var(--gantt-text-tertiary, #9e9e9e); +} + +/* 当 taskbar 较窄时,将 avatar 渲染到外框左边缘 */ +.task-avatar.avatar-outside { + left: -12px; /* 位于 taskbar 左侧外框边缘 */ + z-index: 20; /* 提高层级确保在最上层 */ + border-width: 2px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); +} + +/* 当任务条较窄时,调整内容padding以避免与头像重叠 */ +.task-bar-content:has(.task-avatar) { + padding-left: 36px; /* 为头像留出空间 */ +} + +/* 当 avatar 在外框时,不需要额外的 padding */ +.task-bar-content:has(.task-avatar.avatar-outside) { + padding-left: 8px; +} + .task-name { white-space: nowrap; overflow: visible; @@ -1812,17 +2639,18 @@ onUnmounted(() => { .resize-handle { position: absolute; top: 0; - width: 6px; + /* 宽度通过内联样式设置,默认10px */ height: 100%; cursor: ew-resize; background: rgba(0, 0, 0, 0.1); border-radius: 2px; - transition: background 0.2s; + transition: all 0.2s; z-index: 2; } .resize-handle:hover { - background: rgba(0, 0, 0, 0.2); + background: rgba(0, 0, 0, 0.25); /* 悬停时更明显 */ + transform: scaleX(1.2); /* 悬停时稍微增宽 */ } .resize-handle-left { @@ -1841,7 +2669,7 @@ onUnmounted(() => { .task-bar.overflow-effect .resize-handle:hover { background: rgba(0, 0, 0, 0.3); - width: 8px; /* 悬停时稍微加宽 */ + transform: scaleX(1.3); /* 悬停时进一步增宽 */ } /* 溢出模式下左右handle的位置调整 */ @@ -2309,4 +3137,26 @@ onUnmounted(() => { :global(html[data-theme='dark']) .task-bar.week-view { border-color: var(--gantt-border-light, #555555); } + +/* 暗色主题下的头像样式 */ +:global(html[data-theme='dark']) .task-avatar { + border-color: rgba(255, 255, 255, 0.3); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); +} + +/* 暗色主题下的默认头像 */ +:global(html[data-theme='dark']) .task-avatar.avatar-default { + background: var(--gantt-bg-tertiary, #4a5568); + color: var(--gantt-text-tertiary, #718096); +} + +/* 暗色主题下的图标颜色 */ +:global(html[data-theme='dark']) .task-avatar .avatar-icon { + color: var(--gantt-text-tertiary, #718096); +} + +:global(html[data-theme='dark']) .task-avatar.avatar-outside { + border-color: rgba(255, 255, 255, 0.4); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.7); +} diff --git a/src/components/TaskDrawer.vue b/src/components/TaskDrawer.vue index 7636486..e569639 100644 --- a/src/components/TaskDrawer.vue +++ b/src/components/TaskDrawer.vue @@ -1,5 +1,5 @@