Merge branch 'develop' into pages
This commit is contained in:
@@ -5,6 +5,22 @@ 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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.7.1] - 2026-01-11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- 🎉 新增:GanttChart新增属性,locale, theme, timeScale, fullscreen, expandAll供外部调用
|
||||||
|
- 🎉 新增:GanttChart暴露国际化相关方法,setLocale & currentLocale
|
||||||
|
- 🎉 新增:GanttChart暴露主题相关方法,setTheme & currentTheme,setTimeScale & currentScale
|
||||||
|
- 🎉 新增:GanttChart暴露全屏相关方法,toggleFullscreen & enterFullscreen & exitFullscreen & isFullscreen
|
||||||
|
- 🎉 新增:GanttChart暴露展开/收起相关方法,toggleExpandAll & expandAll & collapseAll & isExpandAll
|
||||||
|
- 🎉 新增:GanttChart暴露滑至今日/任务/指定日期相关方法,scrollToToday & scrollToTask & scrollToDate
|
||||||
|
- 🎉 Added: New properties in GanttChart - locale, theme, timeScale, fullscreen, expandAll for external calls
|
||||||
|
- 🎉 Added: Exposed internationalization related methods in GanttChart - setLocale & currentLocale
|
||||||
|
- 🎉 Added: Exposed theme related methods in GanttChart - setTheme & currentTheme, setTimeScale & currentScale
|
||||||
|
- 🎉 Added: Exposed fullscreen related methods in GanttChart - toggleFullscreen & enterFullscreen & exitFullscreen & isFullscreen
|
||||||
|
- 🎉 Added: Exposed expand/collapse related methods in GanttChart - toggleExpandAll & expandAll & collapseAll & isExpandAll
|
||||||
|
- 🎉 Added: Exposed methods in GanttChart to scroll to today/task/specified date - scrollToToday & scrollToTask & scrollToDate
|
||||||
|
|
||||||
## [1.7.0] - 2026-01-10
|
## [1.7.0] - 2026-01-10
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+238
@@ -1,5 +1,21 @@
|
|||||||
# <img src="public/assets/jordium-gantt-vue3-logo.svg" alt="jordium-gantt-vue3 logo" width="32" style="vertical-align:middle;margin-right:8px;" /> jordium-gantt-vue3
|
# <img src="public/assets/jordium-gantt-vue3-logo.svg" alt="jordium-gantt-vue3 logo" width="32" style="vertical-align:middle;margin-right:8px;" /> jordium-gantt-vue3
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.version-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1px 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #409eff;
|
||||||
|
background-color: #ecf5ff;
|
||||||
|
border: 1px solid #d9ecff;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-left: 4px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://www.npmjs.com/package/jordium-gantt-vue3">
|
<a href="https://www.npmjs.com/package/jordium-gantt-vue3">
|
||||||
<img src="https://img.shields.io/npm/v/jordium-gantt-vue3?style=flat-square" alt="npm version">
|
<img src="https://img.shields.io/npm/v/jordium-gantt-vue3?style=flat-square" alt="npm version">
|
||||||
@@ -200,6 +216,11 @@ npm run dev
|
|||||||
| `enableTaskListContextMenu` | `boolean` | `true` | Whether to enable TaskList (TaskRow) context menu. When `true`: uses built-in menu if `task-list-context-menu` slot is not declared, uses custom menu if slot is declared; when `false`: context menu is completely disabled |
|
| `enableTaskListContextMenu` | `boolean` | `true` | Whether to enable TaskList (TaskRow) context menu. When `true`: uses built-in menu if `task-list-context-menu` slot is not declared, uses custom menu if slot is declared; when `false`: context menu is completely disabled |
|
||||||
| `enableTaskBarContextMenu` | `boolean` | `true` | Whether to enable TaskBar context menu. When `true`: uses built-in menu if `task-bar-context-menu` slot is not declared, uses custom menu if slot is declared; when `false`: context menu is completely disabled |
|
| `enableTaskBarContextMenu` | `boolean` | `true` | Whether to enable TaskBar context menu. When `true`: uses built-in menu if `task-bar-context-menu` slot is not declared, uses custom menu if slot is declared; when `false`: context menu is completely disabled |
|
||||||
| `assigneeOptions` | `Array<{ key?: string \| number; value: string \| number; label: string }>` | `[]` | Assignee dropdown options in task edit drawer |
|
| `assigneeOptions` | `Array<{ key?: string \| number; value: string \| number; label: string }>` | `[]` | Assignee dropdown options in task edit drawer |
|
||||||
|
| `locale` <sup class="version-badge">1.7.1</sup> | `'zh-CN' \| 'en-US'` | `'zh-CN'` | Language setting (reactive). Component's internal language will follow changes |
|
||||||
|
| `theme` <sup class="version-badge">1.7.1</sup> | `'light' \| 'dark'` | `'light'` | Theme mode (reactive). Component's theme will follow changes |
|
||||||
|
| `timeScale` <sup class="version-badge">1.7.1</sup> | `'hour' \| 'day' \| 'week' \| 'month' \| 'quarter' \| 'year'` | `'week'` | Time scale (reactive). Timeline scale will follow changes |
|
||||||
|
| `fullscreen` <sup class="version-badge">1.7.1</sup> | `boolean` | `false` | Fullscreen state control (reactive). Component's fullscreen state will follow changes |
|
||||||
|
| `expandAll` <sup class="version-badge">1.7.1</sup> | `boolean` | `true` | Expand/collapse all tasks (reactive). All tasks' expand state will follow changes |
|
||||||
|
|
||||||
#### TaskListColumn Component Props
|
#### TaskListColumn Component Props
|
||||||
|
|
||||||
@@ -537,6 +558,61 @@ const handleMilestoneSaved = milestone => {
|
|||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Example 4: External Component State Control (TimeScale, Fullscreen, Expand/Collapse, Locale, Theme)
|
||||||
|
|
||||||
|
Control component state through reactive Props binding. Component state will automatically follow Props changes.
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- External control panel -->
|
||||||
|
<div class="control-panel">
|
||||||
|
<button @click="propsFullscreen = !propsFullscreen">Toggle Fullscreen</button>
|
||||||
|
<button @click="propsExpandAll = !propsExpandAll">Expand/Collapse All</button>
|
||||||
|
<button @click="propsLocale = 'zh-CN'">中文</button>
|
||||||
|
<button @click="propsLocale = 'en-US'">English</button>
|
||||||
|
<button @click="propsTimeScale = 'day'">Day View</button>
|
||||||
|
<button @click="propsTimeScale = 'week'">Week View</button>
|
||||||
|
<button @click="propsTimeScale = 'month'">Month View</button>
|
||||||
|
<button @click="propsTheme = 'light'">Light Theme</button>
|
||||||
|
<button @click="propsTheme = 'dark'">Dark Theme</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gantt chart component -->
|
||||||
|
<div style="height: 600px;">
|
||||||
|
<GanttChart
|
||||||
|
:tasks="tasks"
|
||||||
|
:milestones="milestones"
|
||||||
|
:locale="propsLocale"
|
||||||
|
:theme="propsTheme"
|
||||||
|
:time-scale="propsTimeScale"
|
||||||
|
:fullscreen="propsFullscreen"
|
||||||
|
:expand-all="propsExpandAll"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { GanttChart } from 'jordium-gantt-vue3'
|
||||||
|
import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
|
||||||
|
|
||||||
|
const tasks = ref([
|
||||||
|
{ id: 1, name: 'Task 1', startDate: '2025-01-01', endDate: '2025-01-10', progress: 50 },
|
||||||
|
{ id: 2, name: 'Task 2', startDate: '2025-01-05', endDate: '2025-01-15', progress: 30 },
|
||||||
|
])
|
||||||
|
const milestones = ref([])
|
||||||
|
|
||||||
|
// Props control variables
|
||||||
|
const propsLocale = ref<'zh-CN' | 'en-US'>('zh-CN')
|
||||||
|
const propsTheme = ref<'light' | 'dark'>('light')
|
||||||
|
const propsTimeScale = ref<'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'>('week')
|
||||||
|
const propsFullscreen = ref(false)
|
||||||
|
const propsExpandAll = ref(false)
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Task Management
|
### Task Management
|
||||||
@@ -2041,6 +2117,168 @@ The component has built-in intelligent timeline range calculation logic, ensurin
|
|||||||
> - Avoids issues with timeline being too narrow or having excessive whitespace
|
> - Avoids issues with timeline being too narrow or having excessive whitespace
|
||||||
> - Suitable for displaying at different resolutions
|
> - Suitable for displaying at different resolutions
|
||||||
|
|
||||||
|
### Expose Methods
|
||||||
|
|
||||||
|
The GanttChart component exposes a series of methods through `defineExpose`, allowing parent components to directly call these methods via template references (`ref`) to control component behavior. This imperative control approach is suitable for scenarios requiring precise timing control.
|
||||||
|
|
||||||
|
#### Available Expose Methods
|
||||||
|
|
||||||
|
| Method | Parameters | Return Value | Description |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `setLocale` <sup class="version-badge">1.7.1</sup> | `locale: 'zh-CN' \| 'en-US'` | `void` | Set component language |
|
||||||
|
| `currentLocale` <sup class="version-badge">1.7.1</sup> | - | `'zh-CN' \| 'en-US'` | Get current language setting |
|
||||||
|
| `setTheme` <sup class="version-badge">1.7.1</sup> | `mode: 'light' \| 'dark'` | `void` | Set theme mode |
|
||||||
|
| `currentTheme` <sup class="version-badge">1.7.1</sup> | - | `'light' \| 'dark'` | Get current theme mode |
|
||||||
|
| `setTimeScale` <sup class="version-badge">1.7.1</sup> | `scale: TimelineScale` | `void` | Set time scale (`'hour' \| 'day' \| 'week' \| 'month' \| 'quarter' \| 'year'`) |
|
||||||
|
| `currentScale` <sup class="version-badge">1.7.1</sup> | - | `TimelineScale` | Get current time scale |
|
||||||
|
| `toggleFullscreen` <sup class="version-badge">1.7.1</sup> | - | `void` | Toggle fullscreen state |
|
||||||
|
| `enterFullscreen` <sup class="version-badge">1.7.1</sup> | - | `void` | Enter fullscreen mode |
|
||||||
|
| `exitFullscreen` <sup class="version-badge">1.7.1</sup> | - | `void` | Exit fullscreen mode |
|
||||||
|
| `isFullscreen` <sup class="version-badge">1.7.1</sup> | - | `boolean` | Get current fullscreen state |
|
||||||
|
| `toggleExpandAll` <sup class="version-badge">1.7.1</sup> | - | `void` | Toggle expand/collapse all tasks |
|
||||||
|
| `expandAll` <sup class="version-badge">1.7.1</sup> | - | `void` | Expand all tasks |
|
||||||
|
| `collapseAll` <sup class="version-badge">1.7.1</sup> | - | `void` | Collapse all tasks |
|
||||||
|
| `isExpandAll` <sup class="version-badge">1.7.1</sup> | - | `boolean` | Get current expand all state |
|
||||||
|
| `scrollToToday` <sup class="version-badge">1.7.1</sup> | - | `void` | Scroll to today's position |
|
||||||
|
| `scrollToTask` <sup class="version-badge">1.7.1</sup> | `taskId: number \| string` | `void` | Scroll to specified task (task will auto-expand to visible state) |
|
||||||
|
| `scrollToDate` <sup class="version-badge">1.7.1</sup> | `date: string \| Date` | `void` | Scroll to specified date position (format: `'YYYY-MM-DD'` or Date object) |
|
||||||
|
|
||||||
|
#### Usage Example
|
||||||
|
|
||||||
|
**Basic Usage: Imperative Control**
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- External control buttons -->
|
||||||
|
<div class="control-panel">
|
||||||
|
<button @click="handleSetLocale('zh-CN')">中文</button>
|
||||||
|
<button @click="handleSetLocale('en-US')">English</button>
|
||||||
|
<button @click="handleSetTheme('light')">Light Theme</button>
|
||||||
|
<button @click="handleSetTheme('dark')">Dark Theme</button>
|
||||||
|
<button @click="handleSetTimeScale('day')">Day View</button>
|
||||||
|
<button @click="handleSetTimeScale('week')">Week View</button>
|
||||||
|
<button @click="handleSetTimeScale('month')">Month View</button>
|
||||||
|
<button @click="ganttRef?.toggleFullscreen()">Toggle Fullscreen</button>
|
||||||
|
<button @click="ganttRef?.toggleExpandAll()">Expand/Collapse All</button>
|
||||||
|
<button @click="ganttRef?.scrollToToday()">Locate Today</button>
|
||||||
|
<button @click="handleScrollToTask">Scroll to Task 2</button>
|
||||||
|
<button @click="handleScrollToDate">Scroll to 2025-06-01</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status display -->
|
||||||
|
<div class="status-panel">
|
||||||
|
<p>Current Language: {{ currentLang }}</p>
|
||||||
|
<p>Current Theme: {{ currentThemeMode }}</p>
|
||||||
|
<p>Current Scale: {{ currentTimeScale }}</p>
|
||||||
|
<p>Fullscreen: {{ isFullscreenMode ? 'Yes' : 'No' }}</p>
|
||||||
|
<p>Expand State: {{ isAllExpanded ? 'All Expanded' : 'Partially Collapsed' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gantt chart component -->
|
||||||
|
<div style="height: 600px;">
|
||||||
|
<GanttChart
|
||||||
|
ref="ganttRef"
|
||||||
|
:tasks="tasks"
|
||||||
|
:milestones="milestones"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { GanttChart } from 'jordium-gantt-vue3'
|
||||||
|
import type { TimelineScale } from 'jordium-gantt-vue3'
|
||||||
|
import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
|
||||||
|
|
||||||
|
// Component reference
|
||||||
|
const ganttRef = ref<InstanceType<typeof GanttChart>>()
|
||||||
|
|
||||||
|
// State variables
|
||||||
|
const currentLang = ref<'zh-CN' | 'en-US'>('zh-CN')
|
||||||
|
const currentThemeMode = ref<'light' | 'dark'>('light')
|
||||||
|
const currentTimeScale = ref<TimelineScale>('week')
|
||||||
|
const isFullscreenMode = ref(false)
|
||||||
|
const isAllExpanded = ref(true)
|
||||||
|
|
||||||
|
// Task data
|
||||||
|
const tasks = ref([
|
||||||
|
{ id: 1, name: 'Task 1', startDate: '2025-01-01', endDate: '2025-01-10', progress: 50 },
|
||||||
|
{ id: 2, name: 'Task 2', startDate: '2025-01-05', endDate: '2025-01-15', progress: 30 },
|
||||||
|
])
|
||||||
|
const milestones = ref([])
|
||||||
|
|
||||||
|
// Language control
|
||||||
|
const handleSetLocale = (locale: 'zh-CN' | 'en-US') => {
|
||||||
|
ganttRef.value?.setLocale(locale)
|
||||||
|
currentLang.value = ganttRef.value?.currentLocale() || locale
|
||||||
|
}
|
||||||
|
|
||||||
|
// Theme control
|
||||||
|
const handleSetTheme = (mode: 'light' | 'dark') => {
|
||||||
|
ganttRef.value?.setTheme(mode)
|
||||||
|
currentThemeMode.value = ganttRef.value?.currentTheme() || mode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time scale control
|
||||||
|
const handleSetTimeScale = (scale: TimelineScale) => {
|
||||||
|
ganttRef.value?.setTimeScale(scale)
|
||||||
|
currentTimeScale.value = ganttRef.value?.currentScale() || scale
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll to specified task
|
||||||
|
const handleScrollToTask = () => {
|
||||||
|
ganttRef.value?.scrollToTask(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll to specified date
|
||||||
|
const handleScrollToDate = () => {
|
||||||
|
ganttRef.value?.scrollToDate('2025-06-01')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.control-panel {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-panel {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-panel p {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Best Practices
|
||||||
|
|
||||||
|
1. **Imperative vs Reactive**
|
||||||
|
- Use **Expose Methods**: When you need precise control over timing, such as button clicks or specific event triggers
|
||||||
|
- Use **Props Binding**: When state needs to automatically update following data source, such as syncing with URL parameters
|
||||||
|
|
||||||
|
2. **Getting State**
|
||||||
|
- Provides paired getter methods (like `currentLocale()`, `currentTheme()`)
|
||||||
|
- Can immediately get the latest state for verification after calling setters
|
||||||
|
|
||||||
|
3. **Error Handling**
|
||||||
|
- Check if `ref` is mounted before calling: `ganttRef.value?.methodName()`
|
||||||
|
- Safer to call after `onMounted` lifecycle
|
||||||
|
|
||||||
|
**Complete examples can be found in:**
|
||||||
|
- npm-demo project: `npm-demo/src/components/GanttTest.vue`
|
||||||
|
- npm-webpack-demo project: `npm-webpack-demo/src/App.vue`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Theme & Internationalization
|
### Theme & Internationalization
|
||||||
|
|
||||||
#### Theme Switching
|
#### Theme Switching
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# <img src="public/assets/jordium-gantt-vue3-logo.svg" alt="jordium-gantt-vue3 logo" width="32" style="vertical-align:middle;margin-right:8px;" /> jordium-gantt-vue3
|
# <img src="public/assets/jordium-gantt-vue3-logo.svg" alt="jordium-gantt-vue3 logo" width="32" style="vertical-align:middle;margin-right:8px;" /> jordium-gantt-vue3
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.version-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1px 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #409eff;
|
||||||
|
background-color: #ecf5ff;
|
||||||
|
border: 1px solid #d9ecff;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-left: 4px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://www.npmjs.com/package/jordium-gantt-vue3">
|
<a href="https://www.npmjs.com/package/jordium-gantt-vue3">
|
||||||
<img src="https://img.shields.io/npm/v/jordium-gantt-vue3?style=flat-square" alt="npm version">
|
<img src="https://img.shields.io/npm/v/jordium-gantt-vue3?style=flat-square" alt="npm version">
|
||||||
@@ -197,7 +213,12 @@ npm run dev
|
|||||||
| `enableTaskRowMove` | `boolean` | `false` | 是否允许拖拽和摆放TaskRow |
|
| `enableTaskRowMove` | `boolean` | `false` | 是否允许拖拽和摆放TaskRow |
|
||||||
| `enableTaskListContextMenu` | `boolean` | `true` | 是否启用 TaskList(TaskRow)右键菜单功能。为 `true` 时:未声明 `task-list-context-menu` 插槽则使用内置菜单,声明了插槽则使用自定义菜单;为 `false` 时右键菜单完全禁用 |
|
| `enableTaskListContextMenu` | `boolean` | `true` | 是否启用 TaskList(TaskRow)右键菜单功能。为 `true` 时:未声明 `task-list-context-menu` 插槽则使用内置菜单,声明了插槽则使用自定义菜单;为 `false` 时右键菜单完全禁用 |
|
||||||
| `enableTaskBarContextMenu` | `boolean` | `true` | 是否启用 TaskBar 右键菜单功能。为 `true` 时:未声明 `task-bar-context-menu` 插槽则使用内置菜单,声明了插槽则使用自定义菜单;为 `false` 时右键菜单完全禁用 |
|
| `enableTaskBarContextMenu` | `boolean` | `true` | 是否启用 TaskBar 右键菜单功能。为 `true` 时:未声明 `task-bar-context-menu` 插槽则使用内置菜单,声明了插槽则使用自定义菜单;为 `false` 时右键菜单完全禁用 |
|
||||||
| `assigneeOptions` | `Array<{ key?: string \| number; value: string \| number; label: string }>` | `[]` | 任务编辑抽屉中负责人下拉菜单的选项列表 |
|
| `assigneeOptions` | `Array<{ key?: string \| number; value: string \| number; label: string }>` | `[]` | 任务编辑抽屉中负责人下拉菜单的选项列表 |
|
||||||
|
| `locale` <sup class="version-badge">1.7.1</sup> | `'zh-CN' \| 'en-US'` | `'zh-CN'` | 语言设置(响应式)。设置后组件内部语言将跟随变化 |
|
||||||
|
| `theme` <sup class="version-badge">1.7.1</sup> | `'light' \| 'dark'` | `'light'` | 主题模式(响应式)。设置后组件主题将跟随变化 |
|
||||||
|
| `timeScale` <sup class="version-badge">1.7.1</sup> | `'hour' \| 'day' \| 'week' \| 'month' \| 'quarter' \| 'year'` | `'week'` | 时间刻度(响应式)。设置后时间线刻度将跟随变化 |
|
||||||
|
| `fullscreen` <sup class="version-badge">1.7.1</sup> | `boolean` | `false` | 全屏状态控制(响应式)。设置后组件全屏状态将跟随变化 |
|
||||||
|
| `expandAll` <sup class="version-badge">1.7.1</sup> | `boolean` | `true` | 展开/收起所有任务(响应式)。设置后所有任务的展开状态将跟随变化 |
|
||||||
|
|
||||||
#### TaskListColumn 属性
|
#### TaskListColumn 属性
|
||||||
|
|
||||||
@@ -535,6 +556,61 @@ const handleMilestoneSaved = milestone => {
|
|||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 示例4:外部组件控制状态(TimeScale、Fullscreen、Expand/Collapse、Locale、Theme)
|
||||||
|
|
||||||
|
通过响应式Props绑定来控制组件状态,组件状态会自动跟随Props变化。
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- 外部控制面板 -->
|
||||||
|
<div class="control-panel">
|
||||||
|
<button @click="propsFullscreen = !propsFullscreen">切换全屏</button>
|
||||||
|
<button @click="propsExpandAll = !propsExpandAll">展开/收起所有</button>
|
||||||
|
<button @click="propsLocale = 'zh-CN'">中文</button>
|
||||||
|
<button @click="propsLocale = 'en-US'">English</button>
|
||||||
|
<button @click="propsTimeScale = 'day'">日视图</button>
|
||||||
|
<button @click="propsTimeScale = 'week'">周视图</button>
|
||||||
|
<button @click="propsTimeScale = 'month'">月视图</button>
|
||||||
|
<button @click="propsTheme = 'light'">亮色主题</button>
|
||||||
|
<button @click="propsTheme = 'dark'">暗色主题</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 甘特图组件 -->
|
||||||
|
<div style="height: 600px;">
|
||||||
|
<GanttChart
|
||||||
|
:tasks="tasks"
|
||||||
|
:milestones="milestones"
|
||||||
|
:locale="propsLocale"
|
||||||
|
:theme="propsTheme"
|
||||||
|
:time-scale="propsTimeScale"
|
||||||
|
:fullscreen="propsFullscreen"
|
||||||
|
:expand-all="propsExpandAll"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { GanttChart } from 'jordium-gantt-vue3'
|
||||||
|
import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
|
||||||
|
|
||||||
|
const tasks = ref([
|
||||||
|
{ id: 1, name: '任务1', startDate: '2025-01-01', endDate: '2025-01-10', progress: 50 },
|
||||||
|
{ id: 2, name: '任务2', startDate: '2025-01-05', endDate: '2025-01-15', progress: 30 },
|
||||||
|
])
|
||||||
|
const milestones = ref([])
|
||||||
|
|
||||||
|
// Props控制变量
|
||||||
|
const propsLocale = ref<'zh-CN' | 'en-US'>('zh-CN')
|
||||||
|
const propsTheme = ref<'light' | 'dark'>('light')
|
||||||
|
const propsTimeScale = ref<'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'>('week')
|
||||||
|
const propsFullscreen = ref(false)
|
||||||
|
const propsExpandAll = ref(false)
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 任务管理
|
### 任务管理
|
||||||
@@ -2032,6 +2108,168 @@ const taskBarConfig = computed<TaskBarConfig>(() => ({
|
|||||||
> - 避免出现时间线过窄或留白过多的问题
|
> - 避免出现时间线过窄或留白过多的问题
|
||||||
> - 适用不同分辨率展示
|
> - 适用不同分辨率展示
|
||||||
|
|
||||||
|
### Expose 方法
|
||||||
|
|
||||||
|
GanttChart 组件通过 `defineExpose` 暴露了一系列方法,允许父组件通过模板引用 (`ref`) 直接调用这些方法来控制组件行为。这种命令式的控制方式适合需要精确控制时机的场景。
|
||||||
|
|
||||||
|
#### 可用的 Expose 方法
|
||||||
|
|
||||||
|
| 方法名 | 参数 | 返回值 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `setLocale` <sup class="version-badge">1.7.1</sup> | `locale: 'zh-CN' \| 'en-US'` | `void` | 设置组件语言 |
|
||||||
|
| `currentLocale` <sup class="version-badge">1.7.1</sup> | - | `'zh-CN' \| 'en-US'` | 获取当前语言设置 |
|
||||||
|
| `setTheme` <sup class="version-badge">1.7.1</sup> | `mode: 'light' \| 'dark'` | `void` | 设置主题模式 |
|
||||||
|
| `currentTheme` <sup class="version-badge">1.7.1</sup> | - | `'light' \| 'dark'` | 获取当前主题模式 |
|
||||||
|
| `setTimeScale` <sup class="version-badge">1.7.1</sup> | `scale: TimelineScale` | `void` | 设置时间刻度(`'hour' \| 'day' \| 'week' \| 'month' \| 'quarter' \| 'year'`) |
|
||||||
|
| `currentScale` <sup class="version-badge">1.7.1</sup> | - | `TimelineScale` | 获取当前时间刻度 |
|
||||||
|
| `toggleFullscreen` <sup class="version-badge">1.7.1</sup> | - | `void` | 切换全屏状态 |
|
||||||
|
| `enterFullscreen` <sup class="version-badge">1.7.1</sup> | - | `void` | 进入全屏模式 |
|
||||||
|
| `exitFullscreen` <sup class="version-badge">1.7.1</sup> | - | `void` | 退出全屏模式 |
|
||||||
|
| `isFullscreen` <sup class="version-badge">1.7.1</sup> | - | `boolean` | 获取当前是否处于全屏状态 |
|
||||||
|
| `toggleExpandAll` <sup class="version-badge">1.7.1</sup> | - | `void` | 切换展开/收起所有任务 |
|
||||||
|
| `expandAll` <sup class="version-badge">1.7.1</sup> | - | `void` | 展开所有任务 |
|
||||||
|
| `collapseAll` <sup class="version-badge">1.7.1</sup> | - | `void` | 收起所有任务 |
|
||||||
|
| `isExpandAll` <sup class="version-badge">1.7.1</sup> | - | `boolean` | 获取当前是否全部展开 |
|
||||||
|
| `scrollToToday` <sup class="version-badge">1.7.1</sup> | - | `void` | 滚动到今天的位置 |
|
||||||
|
| `scrollToTask` <sup class="version-badge">1.7.1</sup> | `taskId: number \| string` | `void` | 滚动到指定任务(任务会自动展开到可见状态) |
|
||||||
|
| `scrollToDate` <sup class="version-badge">1.7.1</sup> | `date: string \| Date` | `void` | 滚动到指定日期位置(格式:`'YYYY-MM-DD'` 或 Date 对象) |
|
||||||
|
|
||||||
|
#### 使用示例
|
||||||
|
|
||||||
|
**基础用法:命令式控制**
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- 外部控制按钮 -->
|
||||||
|
<div class="control-panel">
|
||||||
|
<button @click="handleSetLocale('zh-CN')">中文</button>
|
||||||
|
<button @click="handleSetLocale('en-US')">English</button>
|
||||||
|
<button @click="handleSetTheme('light')">亮色主题</button>
|
||||||
|
<button @click="handleSetTheme('dark')">暗色主题</button>
|
||||||
|
<button @click="handleSetTimeScale('day')">日视图</button>
|
||||||
|
<button @click="handleSetTimeScale('week')">周视图</button>
|
||||||
|
<button @click="handleSetTimeScale('month')">月视图</button>
|
||||||
|
<button @click="ganttRef?.toggleFullscreen()">切换全屏</button>
|
||||||
|
<button @click="ganttRef?.toggleExpandAll()">展开/收起所有</button>
|
||||||
|
<button @click="ganttRef?.scrollToToday()">定位到今天</button>
|
||||||
|
<button @click="handleScrollToTask">滚动到任务2</button>
|
||||||
|
<button @click="handleScrollToDate">滚动到2025-06-01</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 状态显示 -->
|
||||||
|
<div class="status-panel">
|
||||||
|
<p>当前语言: {{ currentLang }}</p>
|
||||||
|
<p>当前主题: {{ currentThemeMode }}</p>
|
||||||
|
<p>当前刻度: {{ currentTimeScale }}</p>
|
||||||
|
<p>全屏状态: {{ isFullscreenMode ? '是' : '否' }}</p>
|
||||||
|
<p>展开状态: {{ isAllExpanded ? '全部展开' : '部分收起' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 甘特图组件 -->
|
||||||
|
<div style="height: 600px;">
|
||||||
|
<GanttChart
|
||||||
|
ref="ganttRef"
|
||||||
|
:tasks="tasks"
|
||||||
|
:milestones="milestones"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { GanttChart } from 'jordium-gantt-vue3'
|
||||||
|
import type { TimelineScale } from 'jordium-gantt-vue3'
|
||||||
|
import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
|
||||||
|
|
||||||
|
// 组件引用
|
||||||
|
const ganttRef = ref<InstanceType<typeof GanttChart>>()
|
||||||
|
|
||||||
|
// 状态变量
|
||||||
|
const currentLang = ref<'zh-CN' | 'en-US'>('zh-CN')
|
||||||
|
const currentThemeMode = ref<'light' | 'dark'>('light')
|
||||||
|
const currentTimeScale = ref<TimelineScale>('week')
|
||||||
|
const isFullscreenMode = ref(false)
|
||||||
|
const isAllExpanded = ref(true)
|
||||||
|
|
||||||
|
// 任务数据
|
||||||
|
const tasks = ref([
|
||||||
|
{ id: 1, name: '任务1', startDate: '2025-01-01', endDate: '2025-01-10', progress: 50 },
|
||||||
|
{ id: 2, name: '任务2', startDate: '2025-01-05', endDate: '2025-01-15', progress: 30 },
|
||||||
|
])
|
||||||
|
const milestones = ref([])
|
||||||
|
|
||||||
|
// 语言控制
|
||||||
|
const handleSetLocale = (locale: 'zh-CN' | 'en-US') => {
|
||||||
|
ganttRef.value?.setLocale(locale)
|
||||||
|
currentLang.value = ganttRef.value?.currentLocale() || locale
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主题控制
|
||||||
|
const handleSetTheme = (mode: 'light' | 'dark') => {
|
||||||
|
ganttRef.value?.setTheme(mode)
|
||||||
|
currentThemeMode.value = ganttRef.value?.currentTheme() || mode
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间刻度控制
|
||||||
|
const handleSetTimeScale = (scale: TimelineScale) => {
|
||||||
|
ganttRef.value?.setTimeScale(scale)
|
||||||
|
currentTimeScale.value = ganttRef.value?.currentScale() || scale
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滚动到指定任务
|
||||||
|
const handleScrollToTask = () => {
|
||||||
|
ganttRef.value?.scrollToTask(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滚动到指定日期
|
||||||
|
const handleScrollToDate = () => {
|
||||||
|
ganttRef.value?.scrollToDate('2025-06-01')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.control-panel {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-panel {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-panel p {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 最佳实践
|
||||||
|
|
||||||
|
1. **命令式 vs 响应式**
|
||||||
|
- 使用 **Expose 方法**:需要精确控制调用时机,如按钮点击、特定事件触发
|
||||||
|
- 使用 **Props 绑定**:状态需要跟随数据源自动更新,如与 URL 参数同步
|
||||||
|
|
||||||
|
2. **获取状态**
|
||||||
|
- 提供了成对的 getter 方法(如 `currentLocale()`、`currentTheme()`)
|
||||||
|
- 可在调用 setter 后立即获取最新状态进行验证
|
||||||
|
|
||||||
|
3. **错误处理**
|
||||||
|
- 调用前检查 `ref` 是否已挂载:`ganttRef.value?.methodName()`
|
||||||
|
- 在 `onMounted` 生命周期之后调用更安全
|
||||||
|
|
||||||
|
**完整示例可参考:**
|
||||||
|
- npm-demo 项目:`npm-demo/src/components/GanttTest.vue`
|
||||||
|
- npm-webpack-demo 项目:`npm-webpack-demo/src/App.vue`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### 主题与国际化
|
### 主题与国际化
|
||||||
|
|
||||||
#### 主题切换
|
#### 主题切换
|
||||||
|
|||||||
+1001
-12
File diff suppressed because it is too large
Load Diff
@@ -24,5 +24,92 @@
|
|||||||
"story": "Story [{name}]",
|
"story": "Story [{name}]",
|
||||||
"task": "Task [{name}]"
|
"task": "Task [{name}]"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"toolSettings": {
|
||||||
|
"title": "Tool Settings",
|
||||||
|
"currentStatus": {
|
||||||
|
"title": "Current Status",
|
||||||
|
"fullscreen": "Fullscreen",
|
||||||
|
"expandAll": "Expand All",
|
||||||
|
"locale": "Locale",
|
||||||
|
"timeScale": "Time Scale",
|
||||||
|
"theme": "Theme",
|
||||||
|
"controlMode": "Control Mode",
|
||||||
|
"active": "Active",
|
||||||
|
"inactive": "Inactive",
|
||||||
|
"expanded": "Expanded",
|
||||||
|
"collapsed": "Collapsed"
|
||||||
|
},
|
||||||
|
"controlMode": {
|
||||||
|
"title": "Control Mode",
|
||||||
|
"expose": "Expose Methods",
|
||||||
|
"props": "Props Control",
|
||||||
|
"exposeHint": "Call component methods via ref.value.method()",
|
||||||
|
"propsHint": "Control component state by modifying Props"
|
||||||
|
},
|
||||||
|
"exposeMethods": {
|
||||||
|
"sectionTitle": "Expose Methods Control",
|
||||||
|
"fullscreen": {
|
||||||
|
"title": "Fullscreen",
|
||||||
|
"enter": "Enter",
|
||||||
|
"exit": "Exit",
|
||||||
|
"toggle": "Toggle"
|
||||||
|
},
|
||||||
|
"expand": {
|
||||||
|
"title": "Expand",
|
||||||
|
"all": "All",
|
||||||
|
"none": "None",
|
||||||
|
"toggle": "Toggle"
|
||||||
|
},
|
||||||
|
"timeScale": {
|
||||||
|
"title": "Scale",
|
||||||
|
"hour": "Hour",
|
||||||
|
"day": "Day",
|
||||||
|
"week": "Week",
|
||||||
|
"month": "Month",
|
||||||
|
"quarter": "Quarter",
|
||||||
|
"year": "Year",
|
||||||
|
"zoomIn": "Zoom In",
|
||||||
|
"zoomOut": "Zoom Out"
|
||||||
|
},
|
||||||
|
"locale": {
|
||||||
|
"title": "Locale",
|
||||||
|
"zhCN": "中文",
|
||||||
|
"enUS": "EN"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"title": "Theme",
|
||||||
|
"light": "Light",
|
||||||
|
"dark": "Dark"
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"title": "Nav",
|
||||||
|
"today": "Today",
|
||||||
|
"taskIdPlaceholder": "Task ID",
|
||||||
|
"go": "Go"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"propsControl": {
|
||||||
|
"sectionTitle": "Props Control",
|
||||||
|
"locale": {
|
||||||
|
"title": "Locale Prop"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"title": "Theme Prop"
|
||||||
|
},
|
||||||
|
"timeScale": {
|
||||||
|
"title": "TimeScale Prop"
|
||||||
|
},
|
||||||
|
"fullscreen": {
|
||||||
|
"title": "Fullscreen Prop",
|
||||||
|
"true": "True",
|
||||||
|
"false": "False"
|
||||||
|
},
|
||||||
|
"expandAll": {
|
||||||
|
"title": "ExpandAll Prop",
|
||||||
|
"true": "True",
|
||||||
|
"false": "False"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,5 +24,92 @@
|
|||||||
"story": "需求【{name}】",
|
"story": "需求【{name}】",
|
||||||
"task": "任务【{name}】"
|
"task": "任务【{name}】"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"toolSettings": {
|
||||||
|
"title": "工具设置",
|
||||||
|
"currentStatus": {
|
||||||
|
"title": "当前状态",
|
||||||
|
"fullscreen": "全屏",
|
||||||
|
"expandAll": "展开所有",
|
||||||
|
"locale": "语言",
|
||||||
|
"timeScale": "时间刻度",
|
||||||
|
"theme": "主题",
|
||||||
|
"controlMode": "控制模式",
|
||||||
|
"active": "激活",
|
||||||
|
"inactive": "未激活",
|
||||||
|
"expanded": "已展开",
|
||||||
|
"collapsed": "已收起"
|
||||||
|
},
|
||||||
|
"controlMode": {
|
||||||
|
"title": "控制模式",
|
||||||
|
"expose": "Expose 方法",
|
||||||
|
"props": "Props 控制",
|
||||||
|
"exposeHint": "通过 ref.value.method() 调用组件方法",
|
||||||
|
"propsHint": "通过修改 Props 控制组件状态"
|
||||||
|
},
|
||||||
|
"exposeMethods": {
|
||||||
|
"sectionTitle": "Expose 方法控制",
|
||||||
|
"fullscreen": {
|
||||||
|
"title": "全屏",
|
||||||
|
"enter": "进入",
|
||||||
|
"exit": "退出",
|
||||||
|
"toggle": "切换"
|
||||||
|
},
|
||||||
|
"expand": {
|
||||||
|
"title": "展开",
|
||||||
|
"all": "全部",
|
||||||
|
"none": "无",
|
||||||
|
"toggle": "切换"
|
||||||
|
},
|
||||||
|
"timeScale": {
|
||||||
|
"title": "时间刻度",
|
||||||
|
"hour": "小时",
|
||||||
|
"day": "日",
|
||||||
|
"week": "周",
|
||||||
|
"month": "月",
|
||||||
|
"quarter": "季度",
|
||||||
|
"year": "年",
|
||||||
|
"zoomIn": "放大",
|
||||||
|
"zoomOut": "缩小"
|
||||||
|
},
|
||||||
|
"locale": {
|
||||||
|
"title": "语言",
|
||||||
|
"zhCN": "中文",
|
||||||
|
"enUS": "英文"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"title": "主题",
|
||||||
|
"light": "明亮",
|
||||||
|
"dark": "暗黑"
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"title": "导航",
|
||||||
|
"today": "今日",
|
||||||
|
"taskIdPlaceholder": "任务ID",
|
||||||
|
"go": "前往"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"propsControl": {
|
||||||
|
"sectionTitle": "Props 属性控制",
|
||||||
|
"locale": {
|
||||||
|
"title": "语言属性"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"title": "主题属性"
|
||||||
|
},
|
||||||
|
"timeScale": {
|
||||||
|
"title": "时间刻度属性"
|
||||||
|
},
|
||||||
|
"fullscreen": {
|
||||||
|
"title": "全屏属性",
|
||||||
|
"true": "真",
|
||||||
|
"false": "假"
|
||||||
|
},
|
||||||
|
"expandAll": {
|
||||||
|
"title": "展开所有属性",
|
||||||
|
"true": "真",
|
||||||
|
"false": "假"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -436,5 +436,25 @@
|
|||||||
"🔧 Fixed: Upgraded vulnerabilities in dependent packages happy-dom, jspdf, vitest",
|
"🔧 Fixed: Upgraded vulnerabilities in dependent packages happy-dom, jspdf, vitest",
|
||||||
"<span style=\"font-weight: bold; color: #f00;\">Special thanks to fhjfhj@gitee for their use and feedback</span>"
|
"<span style=\"font-weight: bold; color: #f00;\">Special thanks to fhjfhj@gitee for their use and feedback</span>"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.7.1",
|
||||||
|
"date": "2026-01-11",
|
||||||
|
"notes": [
|
||||||
|
"🎉 新增:GanttChart新增属性,locale, theme, timeScale, fullscreen, expandAll供外部调用",
|
||||||
|
"🎉 新增:GanttChart暴露国际化相关方法,setLocale & currentLocale",
|
||||||
|
"🎉 新增:GanttChart暴露主题相关方法,setTheme & currentTheme,setTimeScale & currentScale",
|
||||||
|
"🎉 新增:GanttChart暴露全屏相关方法,toggleFullscreen & enterFullscreen & exitFullscreen & isFullscreen",
|
||||||
|
"🎉 新增:GanttChart暴露展开/收起相关方法,toggleExpandAll & expandAll & collapseAll & isExpandAll",
|
||||||
|
"🎉 新增:GanttChart暴露滑至今日/任务/指定日期相关方法,scrollToToday & scrollToTask & scrollToDate",
|
||||||
|
"<span style=\"font-weight: bold; color: #f00;\">特别感谢 name-hard@github的使用及建议</span>",
|
||||||
|
"🎉 Added: New properties in GanttChart - locale, theme, timeScale, fullscreen, expandAll for external calls",
|
||||||
|
"🎉 Added: Exposed internationalization related methods in GanttChart - setLocale & currentLocale",
|
||||||
|
"🎉 Added: Exposed theme related methods in GanttChart - setTheme & currentTheme, setTimeScale & currentScale",
|
||||||
|
"🎉 Added: Exposed fullscreen related methods in GanttChart - toggleFullscreen & enterFullscreen & exitFullscreen & isFullscreen",
|
||||||
|
"🎉 Added: Exposed expand/collapse related methods in GanttChart - toggleExpandAll & expandAll & collapseAll & isExpandAll",
|
||||||
|
"🎉 Added: Exposed methods in GanttChart to scroll to today/task/specified date - scrollToToday & scrollToTask & scrollToDate",
|
||||||
|
"<span style=\"font-weight: bold; color: #f00;\">Special thanks to name-hard@github for their use and suggestions</span>"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,10 +1,89 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { GanttChart, TaskListColumn, useI18n, TaskListContextMenu, TaskBarContextMenu } from 'jordium-gantt-vue3'
|
import { GanttChart, TaskListColumn, useI18n, TaskListContextMenu, TaskBarContextMenu } from 'jordium-gantt-vue3'
|
||||||
import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
|
import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
|
||||||
|
|
||||||
const { t, getTranslation } = useI18n();
|
const { t, getTranslation } = useI18n();
|
||||||
|
|
||||||
|
// GanttChart ref
|
||||||
|
const ganttRef = ref(null)
|
||||||
|
|
||||||
|
// 控制模式:'expose' 使用expose方法,'props' 使用Props
|
||||||
|
const controlMode = ref<'expose' | 'props'>('expose')
|
||||||
|
|
||||||
|
// 状态变量
|
||||||
|
const fullscreenStatus = ref(false)
|
||||||
|
const expandStatus = ref(false)
|
||||||
|
const currentLocaleStatus = ref<'zh-CN' | 'en-US'>('zh-CN')
|
||||||
|
const currentScaleStatus = ref<'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'>('week')
|
||||||
|
const currentThemeStatus = ref<'light' | 'dark'>('light')
|
||||||
|
|
||||||
|
// Props控制变量
|
||||||
|
const propsLocale = ref<'zh-CN' | 'en-US'>('zh-CN')
|
||||||
|
const propsTheme = ref<'light' | 'dark'>('light')
|
||||||
|
const propsTimeScale = ref<'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'>('week')
|
||||||
|
const propsFullscreen = ref(false)
|
||||||
|
const propsExpandAll = ref(false)
|
||||||
|
|
||||||
|
// 监听 Props 变化,同步 status
|
||||||
|
watch(propsLocale, (newLocale) => {
|
||||||
|
currentLocaleStatus.value = newLocale
|
||||||
|
})
|
||||||
|
watch(propsTheme, (newTheme) => {
|
||||||
|
currentThemeStatus.value = newTheme
|
||||||
|
})
|
||||||
|
watch(propsTimeScale, (newScale) => {
|
||||||
|
currentScaleStatus.value = newScale
|
||||||
|
})
|
||||||
|
watch(propsFullscreen, (newFullscreen) => {
|
||||||
|
fullscreenStatus.value = newFullscreen
|
||||||
|
})
|
||||||
|
watch(propsExpandAll, (newExpandAll) => {
|
||||||
|
expandStatus.value = newExpandAll
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新状态函数
|
||||||
|
const updateStatus = () => {
|
||||||
|
if (ganttRef.value) {
|
||||||
|
fullscreenStatus.value = ganttRef.value.isFullscreen()
|
||||||
|
expandStatus.value = ganttRef.value.isExpandAll()
|
||||||
|
currentLocaleStatus.value = ganttRef.value.currentLocale()
|
||||||
|
currentScaleStatus.value = ganttRef.value.currentScale()
|
||||||
|
currentThemeStatus.value = ganttRef.value.currentTheme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose 方法处理器
|
||||||
|
const handleToggleFullscreen = () => {
|
||||||
|
ganttRef.value?.toggleFullscreen()
|
||||||
|
updateStatus()
|
||||||
|
propsFullscreen.value = ganttRef.value?.isFullscreen() ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToggleExpandAll = () => {
|
||||||
|
ganttRef.value?.toggleExpandAll()
|
||||||
|
updateStatus()
|
||||||
|
propsExpandAll.value = ganttRef.value?.isExpandAll() ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSetLocale = (locale: 'zh-CN' | 'en-US') => {
|
||||||
|
ganttRef.value?.setLocale(locale)
|
||||||
|
currentLocaleStatus.value = locale
|
||||||
|
propsLocale.value = locale
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSetTimeScale = (scale: 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year') => {
|
||||||
|
ganttRef.value?.setTimeScale(scale)
|
||||||
|
updateStatus()
|
||||||
|
propsTimeScale.value = scale
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSetTheme = (mode: 'light' | 'dark') => {
|
||||||
|
ganttRef.value?.setTheme(mode)
|
||||||
|
updateStatus()
|
||||||
|
propsTheme.value = mode
|
||||||
|
}
|
||||||
|
|
||||||
const tasks = ref([
|
const tasks = ref([
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -180,10 +259,10 @@ const handleTaskRowMoved = async (payload: {
|
|||||||
newParent: Task | null
|
newParent: Task | null
|
||||||
}) => {
|
}) => {
|
||||||
const { draggedTask, targetTask, position, oldParent, newParent } = payload
|
const { draggedTask, targetTask, position, oldParent, newParent } = payload
|
||||||
|
|
||||||
// 组件已自动完成任务移动、parentId更新和TaskList/Timeline同步
|
// 组件已自动完成任务移动、parentId更新和TaskList/Timeline同步
|
||||||
// 监听此事件为完全可选,仅用于:
|
// 监听此事件为完全可选,仅用于:
|
||||||
|
|
||||||
// 1. 调用后端 API 保存新的任务层级关系(示例)
|
// 1. 调用后端 API 保存新的任务层级关系(示例)
|
||||||
// 取消注释并替换为你的实际 API 调用
|
// 取消注释并替换为你的实际 API 调用
|
||||||
/*
|
/*
|
||||||
@@ -205,7 +284,7 @@ const handleTaskRowMoved = async (payload: {
|
|||||||
alert('保存失败,请刷新页面')
|
alert('保存失败,请刷新页面')
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 3. 触发其他业务逻辑(如更新关联数据、记录操作日志等)
|
// 3. 触发其他业务逻辑(如更新关联数据、记录操作日志等)
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
@@ -216,7 +295,7 @@ const handleTaskRowMoved = async (payload: {
|
|||||||
const onTaskAdded = (res) => {
|
const onTaskAdded = (res) => {
|
||||||
// 组件已自动添加任务,这里只需要找到并更新额外字段
|
// 组件已自动添加任务,这里只需要找到并更新额外字段
|
||||||
const addedTask = tasks.value.find(t => t.id === res.task.id);
|
const addedTask = tasks.value.find(t => t.id === res.task.id);
|
||||||
|
|
||||||
if (addedTask && addedTask.assignee) {
|
if (addedTask && addedTask.assignee) {
|
||||||
// 根据assignee值查找对应的label并赋值给assigneeName
|
// 根据assignee值查找对应的label并赋值给assigneeName
|
||||||
const assigneeOption = assigneeOptions.value.find(option => option.value === addedTask.assignee);
|
const assigneeOption = assigneeOptions.value.find(option => option.value === addedTask.assignee);
|
||||||
@@ -224,7 +303,7 @@ const onTaskAdded = (res) => {
|
|||||||
addedTask.assigneeName = assigneeOption.label;
|
addedTask.assigneeName = assigneeOption.label;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 不需要手动push,组件已处理
|
// 不需要手动push,组件已处理
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -236,10 +315,238 @@ const handleCustomMenuAction = (action: string, task: Task, onClose: () => void)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div style="height: 600px;">
|
<!-- 工具设置面板 -->
|
||||||
<GanttChart
|
<div class="tool-settings-panel">
|
||||||
:tasks="tasks"
|
<h3>🔧 External Control Demo</h3>
|
||||||
|
|
||||||
|
<!-- 当前状态显示 -->
|
||||||
|
<div class="status-section">
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">Fullscreen:</span>
|
||||||
|
<span :class="['status-value', { active: fullscreenStatus }]">
|
||||||
|
{{ fullscreenStatus ? '✓ Yes' : '✗ No' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">Expand All:</span>
|
||||||
|
<span :class="['status-value', { active: expandStatus }]">
|
||||||
|
{{ expandStatus ? '✓ Yes' : '✗ No' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">Locale:</span>
|
||||||
|
<span class="status-value active">{{ currentLocaleStatus }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">Time Scale:</span>
|
||||||
|
<span class="status-value active">{{ currentScaleStatus }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">Theme:</span>
|
||||||
|
<span class="status-value active">{{ currentThemeStatus }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">Control Mode:</span>
|
||||||
|
<span class="status-value active" :style="{ color: controlMode === 'props' ? '#67c23a' : '#409eff' }">
|
||||||
|
{{ controlMode === 'props' ? '📝 Props' : '⚡ Expose' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 控制模式切换 -->
|
||||||
|
<div class="control-mode-section">
|
||||||
|
<h4>🎛️ Control Mode</h4>
|
||||||
|
<div class="button-group">
|
||||||
|
<button
|
||||||
|
class="mode-button"
|
||||||
|
:class="{ active: controlMode === 'expose' }"
|
||||||
|
@click="controlMode = 'expose'"
|
||||||
|
>
|
||||||
|
⚡ Expose Methods
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="mode-button"
|
||||||
|
:class="{ active: controlMode === 'props' }"
|
||||||
|
@click="controlMode = 'props'"
|
||||||
|
>
|
||||||
|
📝 Props Control
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Expose 方法控制 -->
|
||||||
|
<div v-show="controlMode === 'expose'" class="control-section">
|
||||||
|
<h4>⚡ Expose Methods Control</h4>
|
||||||
|
|
||||||
|
<div class="controls-flow">
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Fullscreen:</label>
|
||||||
|
<button class="control-btn" @click="handleToggleFullscreen">Toggle Fullscreen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Expand All:</label>
|
||||||
|
<button class="control-btn" @click="handleToggleExpandAll">Toggle Expand All</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Locale:</label>
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="control-btn" @click="handleSetLocale('zh-CN')">中文</button>
|
||||||
|
<button class="control-btn" @click="handleSetLocale('en-US')">English</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Time Scale:</label>
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="control-btn" @click="handleSetTimeScale('day')">Day</button>
|
||||||
|
<button class="control-btn" @click="handleSetTimeScale('week')">Week</button>
|
||||||
|
<button class="control-btn" @click="handleSetTimeScale('month')">Month</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Theme:</label>
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="control-btn" @click="handleSetTheme('light')">☀️ Light</button>
|
||||||
|
<button class="control-btn" @click="handleSetTheme('dark')">🌙 Dark</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Props 控制 -->
|
||||||
|
<div v-show="controlMode === 'props'" class="control-section">
|
||||||
|
<h4>📝 Props Control</h4>
|
||||||
|
|
||||||
|
<div class="controls-flow">
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Locale Prop:</label>
|
||||||
|
<div class="button-group">
|
||||||
|
<button
|
||||||
|
class="control-btn"
|
||||||
|
:class="{ primary: propsLocale === 'zh-CN' }"
|
||||||
|
@click="propsLocale = 'zh-CN'"
|
||||||
|
>
|
||||||
|
中文
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="control-btn"
|
||||||
|
:class="{ primary: propsLocale === 'en-US' }"
|
||||||
|
@click="propsLocale = 'en-US'"
|
||||||
|
>
|
||||||
|
English
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="prop-info">:locale="{{ propsLocale }}"</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Theme Prop:</label>
|
||||||
|
<div class="button-group">
|
||||||
|
<button
|
||||||
|
class="control-btn"
|
||||||
|
:class="{ primary: propsTheme === 'light' }"
|
||||||
|
@click="propsTheme = 'light'"
|
||||||
|
>
|
||||||
|
☀️ Light
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="control-btn"
|
||||||
|
:class="{ primary: propsTheme === 'dark' }"
|
||||||
|
@click="propsTheme = 'dark'"
|
||||||
|
>
|
||||||
|
🌙 Dark
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="prop-info">:theme="{{ propsTheme }}"</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Time Scale Prop:</label>
|
||||||
|
<div class="button-group">
|
||||||
|
<button
|
||||||
|
class="control-btn"
|
||||||
|
:class="{ primary: propsTimeScale === 'day' }"
|
||||||
|
@click="propsTimeScale = 'day'"
|
||||||
|
>
|
||||||
|
Day
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="control-btn"
|
||||||
|
:class="{ primary: propsTimeScale === 'week' }"
|
||||||
|
@click="propsTimeScale = 'week'"
|
||||||
|
>
|
||||||
|
Week
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="control-btn"
|
||||||
|
:class="{ primary: propsTimeScale === 'month' }"
|
||||||
|
@click="propsTimeScale = 'month'"
|
||||||
|
>
|
||||||
|
Month
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="prop-info">:time-scale="{{ propsTimeScale }}"</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Fullscreen Prop:</label>
|
||||||
|
<div class="button-group">
|
||||||
|
<button
|
||||||
|
class="control-btn"
|
||||||
|
:class="{ primary: propsFullscreen }"
|
||||||
|
@click="propsFullscreen = true"
|
||||||
|
>
|
||||||
|
✓ True
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="control-btn"
|
||||||
|
:class="{ primary: !propsFullscreen }"
|
||||||
|
@click="propsFullscreen = false"
|
||||||
|
>
|
||||||
|
✗ False
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="prop-info">:fullscreen="{{ propsFullscreen }}"</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Expand All Prop:</label>
|
||||||
|
<div class="button-group">
|
||||||
|
<button
|
||||||
|
class="control-btn"
|
||||||
|
:class="{ primary: propsExpandAll }"
|
||||||
|
@click="propsExpandAll = true"
|
||||||
|
>
|
||||||
|
✓ True
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="control-btn"
|
||||||
|
:class="{ primary: !propsExpandAll }"
|
||||||
|
@click="propsExpandAll = false"
|
||||||
|
>
|
||||||
|
✗ False
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="prop-info">:expand-all="{{ propsExpandAll }}"</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gantt Chart -->
|
||||||
|
<div style="height: 600px; margin-top: 20px;">
|
||||||
|
<GanttChart
|
||||||
|
ref="ganttRef"
|
||||||
|
:tasks="tasks"
|
||||||
:milestones="milestones"
|
:milestones="milestones"
|
||||||
|
:locale="controlMode === 'props' ? propsLocale : undefined"
|
||||||
|
:theme="controlMode === 'props' ? propsTheme : undefined"
|
||||||
|
:time-scale="controlMode === 'props' ? propsTimeScale : undefined"
|
||||||
|
:fullscreen="controlMode === 'props' ? propsFullscreen : undefined"
|
||||||
|
:expand-all="controlMode === 'props' ? propsExpandAll : undefined"
|
||||||
:task-list-config="taskListConfig"
|
:task-list-config="taskListConfig"
|
||||||
:toolbar-config="toolbarConfig"
|
:toolbar-config="toolbarConfig"
|
||||||
:use-default-drawer="true"
|
:use-default-drawer="true"
|
||||||
@@ -305,7 +612,7 @@ const handleCustomMenuAction = (action: string, task: Task, onClose: () => void)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</TaskBarContextMenu>
|
</TaskBarContextMenu>
|
||||||
</GanttChart>
|
</GanttChart>
|
||||||
</div>
|
</div>
|
||||||
<!-- 自定义添加任务按钮 -->
|
<!-- 自定义添加任务按钮 -->
|
||||||
@@ -314,7 +621,7 @@ const handleCustomMenuAction = (action: string, task: Task, onClose: () => void)
|
|||||||
<button class="btn btn-primary" @click="showAddMilestoneDialog = true">添加里程碑</button>
|
<button class="btn btn-primary" @click="showAddMilestoneDialog = true">添加里程碑</button>
|
||||||
<button class="btn btn-primary" @click="showTodayLocate = !showTodayLocate">开启/关闭今日按钮</button>
|
<button class="btn btn-primary" @click="showTodayLocate = !showTodayLocate">开启/关闭今日按钮</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 自定义抽屉组件 (原生HTML替代 el-drawer) -->
|
<!-- 自定义抽屉组件 (原生HTML替代 el-drawer) -->
|
||||||
<div v-if="showAddTaskDrawer" class="drawer-overlay" @click="showAddTaskDrawer = false">
|
<div v-if="showAddTaskDrawer" class="drawer-overlay" @click="showAddTaskDrawer = false">
|
||||||
<div class="drawer-container" @click.stop>
|
<div class="drawer-container" @click.stop>
|
||||||
@@ -322,24 +629,24 @@ const handleCustomMenuAction = (action: string, task: Task, onClose: () => void)
|
|||||||
<h3>自定义添加任务组件</h3>
|
<h3>自定义添加任务组件</h3>
|
||||||
<button class="close-btn" @click="showAddTaskDrawer = false">×</button>
|
<button class="close-btn" @click="showAddTaskDrawer = false">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="drawer-body">
|
<div class="drawer-body">
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<label>任务名称:</label>
|
<label>任务名称:</label>
|
||||||
<input v-model="newTask.name" type="text" placeholder="请输入任务名称" />
|
<input v-model="newTask.name" type="text" placeholder="请输入任务名称" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<label>开始日期:</label>
|
<label>开始日期:</label>
|
||||||
<input v-model="newTask.startDate" type="date" />
|
<input v-model="newTask.startDate" type="date" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<label>结束日期:</label>
|
<label>结束日期:</label>
|
||||||
<input v-model="newTask.endDate" type="date" />
|
<input v-model="newTask.endDate" type="date" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="drawer-footer">
|
<div class="drawer-footer">
|
||||||
<button class="btn btn-primary" @click="addTask">确定</button>
|
<button class="btn btn-primary" @click="addTask">确定</button>
|
||||||
<button class="btn btn-default" @click="showAddTaskDrawer = false">取消</button>
|
<button class="btn btn-default" @click="showAddTaskDrawer = false">取消</button>
|
||||||
@@ -348,9 +655,9 @@ const handleCustomMenuAction = (action: string, task: Task, onClose: () => void)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 自定义Dialog组件基于element plus -->
|
<!-- 自定义Dialog组件基于element plus -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
title="自定义添加里程碑组件 - Element Plus"
|
title="自定义添加里程碑组件 - Element Plus"
|
||||||
v-model="showAddMilestoneDialog"
|
v-model="showAddMilestoneDialog"
|
||||||
width="400px"
|
width="400px"
|
||||||
@close="newTask = { name: '', startDate: '', endDate: '' }"
|
@close="newTask = { name: '', startDate: '', endDate: '' }"
|
||||||
>
|
>
|
||||||
@@ -370,11 +677,195 @@ const handleCustomMenuAction = (action: string, task: Task, onClose: () => void)
|
|||||||
<el-button @click="addMilestone">确定</el-button>
|
<el-button @click="addMilestone">确定</el-button>
|
||||||
<el-button @click="showAddMilestoneDialog = false">取消</el-button>
|
<el-button @click="showAddMilestoneDialog = false">取消</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* 工具设置面板 */
|
||||||
|
.tool-settings-panel {
|
||||||
|
background: #f5f7fa;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-settings-panel h3 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-settings-panel h4 {
|
||||||
|
margin: 15px 0 10px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态显示区域 */
|
||||||
|
.status-section {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
color: #909399;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value {
|
||||||
|
color: #606266;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value.active {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 控制模式区域 */
|
||||||
|
.control-mode-section {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-button {
|
||||||
|
padding: 8px 20px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
background: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-button:hover {
|
||||||
|
border-color: #409eff;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-button.active {
|
||||||
|
background: #409eff;
|
||||||
|
color: white;
|
||||||
|
border-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 控制区域 */
|
||||||
|
.control-section {
|
||||||
|
background: white;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 控制项流式布局容器 */
|
||||||
|
.controls-flow {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn {
|
||||||
|
padding: 6px 16px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
background: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn:hover {
|
||||||
|
border-color: #409eff;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn.primary {
|
||||||
|
background: #67c23a;
|
||||||
|
color: white;
|
||||||
|
border-color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prop-info {
|
||||||
|
margin: 5px 0 0 0;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #909399;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题 */
|
||||||
|
:global(html[data-theme='dark']) .tool-settings-panel {
|
||||||
|
background: #1e1e1e;
|
||||||
|
border-color: #3a3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .tool-settings-panel h3,
|
||||||
|
:global(html[data-theme='dark']) .tool-settings-panel h4 {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .status-section {
|
||||||
|
background: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .status-item {
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .control-section {
|
||||||
|
background: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .mode-button,
|
||||||
|
:global(html[data-theme='dark']) .control-btn {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-color: #3a3a3a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .mode-button:hover,
|
||||||
|
:global(html[data-theme='dark']) .control-btn:hover {
|
||||||
|
border-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .mode-button.active {
|
||||||
|
background: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
/* 抽屉遮罩层 */
|
/* 抽屉遮罩层 */
|
||||||
.drawer-overlay {
|
.drawer-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -600,4 +1091,4 @@ const handleCustomMenuAction = (action: string, task: Task, onClose: () => void)
|
|||||||
:global(html[data-theme='dark']) .custom-menu-divider {
|
:global(html[data-theme='dark']) .custom-menu-divider {
|
||||||
background: #444;
|
background: #444;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,9 +1,237 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div style="height: 600px;">
|
<!-- 工具设置面板 -->
|
||||||
|
<div class="tool-settings-panel">
|
||||||
|
<h3>🔧 External Control Demo</h3>
|
||||||
|
|
||||||
|
<!-- 当前状态显示 -->
|
||||||
|
<div class="status-section">
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">Fullscreen:</span>
|
||||||
|
<span :class="['status-value', { active: fullscreenStatus }]">
|
||||||
|
{{ fullscreenStatus ? '✓ Yes' : '✗ No' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">Expand All:</span>
|
||||||
|
<span :class="['status-value', { active: expandStatus }]">
|
||||||
|
{{ expandStatus ? '✓ Yes' : '✗ No' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">Locale:</span>
|
||||||
|
<span class="status-value active">{{ currentLocaleStatus }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">Time Scale:</span>
|
||||||
|
<span class="status-value active">{{ currentScaleStatus }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">Theme:</span>
|
||||||
|
<span class="status-value active">{{ currentThemeStatus }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">Control Mode:</span>
|
||||||
|
<span class="status-value active" :style="{ color: controlMode === 'props' ? '#67c23a' : '#409eff' }">
|
||||||
|
{{ controlMode === 'props' ? '📝 Props' : '⚡ Expose' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 控制模式切换 -->
|
||||||
|
<div class="control-mode-section">
|
||||||
|
<h4>🎛️ Control Mode</h4>
|
||||||
|
<div class="button-group">
|
||||||
|
<button
|
||||||
|
class="mode-button"
|
||||||
|
:class="{ active: controlMode === 'expose' }"
|
||||||
|
@click="controlMode = 'expose'"
|
||||||
|
>
|
||||||
|
⚡ Expose Methods
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="mode-button"
|
||||||
|
:class="{ active: controlMode === 'props' }"
|
||||||
|
@click="controlMode = 'props'"
|
||||||
|
>
|
||||||
|
📝 Props Control
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Expose 方法控制 -->
|
||||||
|
<div v-show="controlMode === 'expose'" class="control-section">
|
||||||
|
<h4>⚡ Expose Methods Control</h4>
|
||||||
|
|
||||||
|
<div class="controls-flow">
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Fullscreen:</label>
|
||||||
|
<button class="control-btn" @click="handleToggleFullscreen">Toggle Fullscreen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Expand All:</label>
|
||||||
|
<button class="control-btn" @click="handleToggleExpandAll">Toggle Expand All</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Locale:</label>
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="control-btn" @click="handleSetLocale('zh-CN')">中文</button>
|
||||||
|
<button class="control-btn" @click="handleSetLocale('en-US')">English</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Time Scale:</label>
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="control-btn" @click="handleSetTimeScale('day')">Day</button>
|
||||||
|
<button class="control-btn" @click="handleSetTimeScale('week')">Week</button>
|
||||||
|
<button class="control-btn" @click="handleSetTimeScale('month')">Month</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Theme:</label>
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="control-btn" @click="handleSetTheme('light')">☀️ Light</button>
|
||||||
|
<button class="control-btn" @click="handleSetTheme('dark')">🌙 Dark</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Props 控制 -->
|
||||||
|
<div v-show="controlMode === 'props'" class="control-section">
|
||||||
|
<h4>📝 Props Control</h4>
|
||||||
|
|
||||||
|
<div class="controls-flow">
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Locale Prop:</label>
|
||||||
|
<div class="button-group">
|
||||||
|
<button
|
||||||
|
class="control-btn"
|
||||||
|
:class="{ primary: propsLocale === 'zh-CN' }"
|
||||||
|
@click="propsLocale = 'zh-CN'"
|
||||||
|
>
|
||||||
|
中文
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="control-btn"
|
||||||
|
:class="{ primary: propsLocale === 'en-US' }"
|
||||||
|
@click="propsLocale = 'en-US'"
|
||||||
|
>
|
||||||
|
English
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="prop-info">:locale="{{ propsLocale }}"</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Theme Prop:</label>
|
||||||
|
<div class="button-group">
|
||||||
|
<button
|
||||||
|
class="control-btn"
|
||||||
|
:class="{ primary: propsTheme === 'light' }"
|
||||||
|
@click="propsTheme = 'light'"
|
||||||
|
>
|
||||||
|
☀️ Light
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="control-btn"
|
||||||
|
:class="{ primary: propsTheme === 'dark' }"
|
||||||
|
@click="propsTheme = 'dark'"
|
||||||
|
>
|
||||||
|
🌙 Dark
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="prop-info">:theme="{{ propsTheme }}"</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Time Scale Prop:</label>
|
||||||
|
<div class="button-group">
|
||||||
|
<button
|
||||||
|
class="control-btn"
|
||||||
|
:class="{ primary: propsTimeScale === 'day' }"
|
||||||
|
@click="propsTimeScale = 'day'"
|
||||||
|
>
|
||||||
|
Day
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="control-btn"
|
||||||
|
:class="{ primary: propsTimeScale === 'week' }"
|
||||||
|
@click="propsTimeScale = 'week'"
|
||||||
|
>
|
||||||
|
Week
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="control-btn"
|
||||||
|
:class="{ primary: propsTimeScale === 'month' }"
|
||||||
|
@click="propsTimeScale = 'month'"
|
||||||
|
>
|
||||||
|
Month
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="prop-info">:time-scale="{{ propsTimeScale }}"</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Fullscreen Prop:</label>
|
||||||
|
<div class="button-group">
|
||||||
|
<button
|
||||||
|
class="control-btn"
|
||||||
|
:class="{ primary: propsFullscreen }"
|
||||||
|
@click="propsFullscreen = true"
|
||||||
|
>
|
||||||
|
✓ True
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="control-btn"
|
||||||
|
:class="{ primary: !propsFullscreen }"
|
||||||
|
@click="propsFullscreen = false"
|
||||||
|
>
|
||||||
|
✗ False
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="prop-info">:fullscreen="{{ propsFullscreen }}"</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Expand All Prop:</label>
|
||||||
|
<div class="button-group">
|
||||||
|
<button
|
||||||
|
class="control-btn"
|
||||||
|
:class="{ primary: propsExpandAll }"
|
||||||
|
@click="propsExpandAll = true"
|
||||||
|
>
|
||||||
|
✓ True
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="control-btn"
|
||||||
|
:class="{ primary: !propsExpandAll }"
|
||||||
|
@click="propsExpandAll = false"
|
||||||
|
>
|
||||||
|
✗ False
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="prop-info">:expand-all="{{ propsExpandAll }}"</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gantt Chart -->
|
||||||
|
<div style="height: 600px; margin-top: 20px;">
|
||||||
<GanttChart
|
<GanttChart
|
||||||
|
ref="ganttRef"
|
||||||
:tasks="tasks"
|
:tasks="tasks"
|
||||||
:milestones="milestones"
|
:milestones="milestones"
|
||||||
|
:locale="controlMode === 'props' ? propsLocale : undefined"
|
||||||
|
:theme="controlMode === 'props' ? propsTheme : undefined"
|
||||||
|
:time-scale="controlMode === 'props' ? propsTimeScale : undefined"
|
||||||
|
:fullscreen="controlMode === 'props' ? propsFullscreen : undefined"
|
||||||
|
:expand-all="controlMode === 'props' ? propsExpandAll : undefined"
|
||||||
:task-list-config="taskListConfig"
|
:task-list-config="taskListConfig"
|
||||||
:toolbar-config="toolbarConfig"
|
:toolbar-config="toolbarConfig"
|
||||||
:use-default-drawer="false"
|
:use-default-drawer="false"
|
||||||
@@ -88,10 +316,89 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
import { GanttChart, Task } from 'jordium-gantt-vue3'
|
import { GanttChart, Task } from 'jordium-gantt-vue3'
|
||||||
import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
|
import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
|
||||||
|
|
||||||
|
// GanttChart ref
|
||||||
|
const ganttRef = ref(null)
|
||||||
|
|
||||||
|
// 控制模式:'expose' 使用expose方法,'props' 使用Props
|
||||||
|
const controlMode = ref<'expose' | 'props'>('expose')
|
||||||
|
|
||||||
|
// 状态变量
|
||||||
|
const fullscreenStatus = ref(false)
|
||||||
|
const expandStatus = ref(false)
|
||||||
|
const currentLocaleStatus = ref<'zh-CN' | 'en-US'>('zh-CN')
|
||||||
|
const currentScaleStatus = ref<'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'>('week')
|
||||||
|
const currentThemeStatus = ref<'light' | 'dark'>('light')
|
||||||
|
|
||||||
|
// Props控制变量
|
||||||
|
const propsLocale = ref<'zh-CN' | 'en-US'>('zh-CN')
|
||||||
|
const propsTheme = ref<'light' | 'dark'>('light')
|
||||||
|
const propsTimeScale = ref<'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'>('week')
|
||||||
|
const propsFullscreen = ref(false)
|
||||||
|
const propsExpandAll = ref(false)
|
||||||
|
|
||||||
|
// 监听 Props 变化,同步 status
|
||||||
|
watch(propsLocale, (newLocale) => {
|
||||||
|
currentLocaleStatus.value = newLocale
|
||||||
|
})
|
||||||
|
watch(propsTheme, (newTheme) => {
|
||||||
|
currentThemeStatus.value = newTheme
|
||||||
|
})
|
||||||
|
watch(propsTimeScale, (newScale) => {
|
||||||
|
currentScaleStatus.value = newScale
|
||||||
|
})
|
||||||
|
watch(propsFullscreen, (newFullscreen) => {
|
||||||
|
fullscreenStatus.value = newFullscreen
|
||||||
|
})
|
||||||
|
watch(propsExpandAll, (newExpandAll) => {
|
||||||
|
expandStatus.value = newExpandAll
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新状态函数
|
||||||
|
const updateStatus = () => {
|
||||||
|
if (ganttRef.value) {
|
||||||
|
fullscreenStatus.value = ganttRef.value.isFullscreen()
|
||||||
|
expandStatus.value = ganttRef.value.isExpandAll()
|
||||||
|
currentLocaleStatus.value = ganttRef.value.currentLocale()
|
||||||
|
currentScaleStatus.value = ganttRef.value.currentScale()
|
||||||
|
currentThemeStatus.value = ganttRef.value.currentTheme()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose 方法处理器
|
||||||
|
const handleToggleFullscreen = () => {
|
||||||
|
ganttRef.value?.toggleFullscreen()
|
||||||
|
updateStatus()
|
||||||
|
propsFullscreen.value = ganttRef.value?.isFullscreen() ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToggleExpandAll = () => {
|
||||||
|
ganttRef.value?.toggleExpandAll()
|
||||||
|
updateStatus()
|
||||||
|
propsExpandAll.value = ganttRef.value?.isExpandAll() ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSetLocale = (locale: 'zh-CN' | 'en-US') => {
|
||||||
|
ganttRef.value?.setLocale(locale)
|
||||||
|
currentLocaleStatus.value = locale
|
||||||
|
propsLocale.value = locale
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSetTimeScale = (scale: 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year') => {
|
||||||
|
ganttRef.value?.setTimeScale(scale)
|
||||||
|
updateStatus()
|
||||||
|
propsTimeScale.value = scale
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSetTheme = (mode: 'light' | 'dark') => {
|
||||||
|
ganttRef.value?.setTheme(mode)
|
||||||
|
updateStatus()
|
||||||
|
propsTheme.value = mode
|
||||||
|
}
|
||||||
|
|
||||||
// 定义类型接口
|
// 定义类型接口
|
||||||
interface TaskListColumnConfig {
|
interface TaskListColumnConfig {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -294,6 +601,190 @@ const onTaskAdded = (res) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* 工具设置面板 */
|
||||||
|
.tool-settings-panel {
|
||||||
|
background: #f5f7fa;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-settings-panel h3 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-settings-panel h4 {
|
||||||
|
margin: 15px 0 10px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态显示区域 */
|
||||||
|
.status-section {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
color: #909399;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value {
|
||||||
|
color: #606266;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value.active {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 控制模式区域 */
|
||||||
|
.control-mode-section {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-button {
|
||||||
|
padding: 8px 20px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
background: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-button:hover {
|
||||||
|
border-color: #409eff;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-button.active {
|
||||||
|
background: #409eff;
|
||||||
|
color: white;
|
||||||
|
border-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 控制区域 */
|
||||||
|
.control-section {
|
||||||
|
background: white;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 控制项流式布局容器 */
|
||||||
|
.controls-flow {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn {
|
||||||
|
padding: 6px 16px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
background: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn:hover {
|
||||||
|
border-color: #409eff;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn.primary {
|
||||||
|
background: #67c23a;
|
||||||
|
color: white;
|
||||||
|
border-color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prop-info {
|
||||||
|
margin: 5px 0 0 0;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #909399;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题 */
|
||||||
|
:global(html[data-theme='dark']) .tool-settings-panel {
|
||||||
|
background: #1e1e1e;
|
||||||
|
border-color: #3a3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .tool-settings-panel h3,
|
||||||
|
:global(html[data-theme='dark']) .tool-settings-panel h4 {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .status-section {
|
||||||
|
background: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .status-item {
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .control-section {
|
||||||
|
background: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .mode-button,
|
||||||
|
:global(html[data-theme='dark']) .control-btn {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-color: #3a3a3a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .mode-button:hover,
|
||||||
|
:global(html[data-theme='dark']) .control-btn:hover {
|
||||||
|
border-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .mode-button.active {
|
||||||
|
background: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
/* 抽屉遮罩层 */
|
/* 抽屉遮罩层 */
|
||||||
.drawer-overlay {
|
.drawer-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jordium-gantt-vue3",
|
"name": "jordium-gantt-vue3",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "npm-package/dist/jordium-gantt-vue3.cjs.js",
|
"main": "npm-package/dist/jordium-gantt-vue3.cjs.js",
|
||||||
"module": "npm-package/dist/jordium-gantt-vue3.es.js",
|
"module": "npm-package/dist/jordium-gantt-vue3.es.js",
|
||||||
|
|||||||
+383
-14
@@ -58,6 +58,11 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
taskListRowStyle: undefined,
|
taskListRowStyle: undefined,
|
||||||
enableTaskListContextMenu: true,
|
enableTaskListContextMenu: true,
|
||||||
enableTaskBarContextMenu: true,
|
enableTaskBarContextMenu: true,
|
||||||
|
fullscreen: false,
|
||||||
|
expandAll: true,
|
||||||
|
locale: 'zh-CN',
|
||||||
|
timeScale: 'week',
|
||||||
|
theme: 'light',
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
@@ -189,6 +194,16 @@ interface Props {
|
|||||||
// 当设置为 true 且声明了 TaskBarContextMenu 组件时,使用自定义菜单
|
// 当设置为 true 且声明了 TaskBarContextMenu 组件时,使用自定义菜单
|
||||||
// 当设置为 false 时,无论是否声明组件,TaskBar 右键菜单都失效
|
// 当设置为 false 时,无论是否声明组件,TaskBar 右键菜单都失效
|
||||||
enableTaskBarContextMenu?: boolean
|
enableTaskBarContextMenu?: boolean
|
||||||
|
// 全屏状态控制(响应式)
|
||||||
|
fullscreen?: boolean
|
||||||
|
// 展开/收起所有任务(响应式)
|
||||||
|
expandAll?: boolean
|
||||||
|
// 语言设置(响应式)
|
||||||
|
locale?: 'zh-CN' | 'en-US'
|
||||||
|
// 时间刻度(响应式)
|
||||||
|
timeScale?: TimelineScale
|
||||||
|
// 主题模式(响应式)
|
||||||
|
theme?: 'light' | 'dark'
|
||||||
}
|
}
|
||||||
|
|
||||||
// TaskList的固定总长度(所有列的最小宽度之和 + 边框等额外空间)
|
// TaskList的固定总长度(所有列的最小宽度之和 + 边框等额外空间)
|
||||||
@@ -694,7 +709,7 @@ const handleTaskRowMoved = (payload: {
|
|||||||
// 通知 TaskList 更新父级任务数据
|
// 通知 TaskList 更新父级任务数据
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
window.dispatchEvent(new CustomEvent('task-updated', {
|
window.dispatchEvent(new CustomEvent('task-updated', {
|
||||||
detail: result.movedTask
|
detail: result.movedTask,
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -731,6 +746,90 @@ const handleCollapseAll = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === 展开/收起相关方法 ===
|
||||||
|
/**
|
||||||
|
* 展开所有任务
|
||||||
|
*/
|
||||||
|
const expandAllTasks = () => {
|
||||||
|
handleExpandAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收起所有任务
|
||||||
|
*/
|
||||||
|
const collapseAllTasks = () => {
|
||||||
|
handleCollapseAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换展开/收起所有任务
|
||||||
|
*/
|
||||||
|
const toggleExpandAllTasks = () => {
|
||||||
|
// 检查是否所有有子任务的任务都已展开
|
||||||
|
const checkAllExpanded = (tasks: Task[]): boolean => {
|
||||||
|
for (const task of tasks) {
|
||||||
|
if (task.children && task.children.length > 0) {
|
||||||
|
if (task.collapsed) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!checkAllExpanded(task.children)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.tasks) {
|
||||||
|
const allExpanded = checkAllExpanded(props.tasks)
|
||||||
|
if (allExpanded) {
|
||||||
|
collapseAllTasks()
|
||||||
|
} else {
|
||||||
|
expandAllTasks()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前是否所有任务都已展开
|
||||||
|
*/
|
||||||
|
const getIsExpandAll = (): boolean => {
|
||||||
|
if (!props.tasks || props.tasks.length === 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkAllExpanded = (tasks: Task[]): boolean => {
|
||||||
|
for (const task of tasks) {
|
||||||
|
if (task.children && task.children.length > 0) {
|
||||||
|
if (task.collapsed) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!checkAllExpanded(task.children)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return checkAllExpanded(props.tasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 Props expandAll 变化
|
||||||
|
watch(
|
||||||
|
() => props.expandAll,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue !== undefined) {
|
||||||
|
if (newValue) {
|
||||||
|
expandAllTasks()
|
||||||
|
} else {
|
||||||
|
collapseAllTasks()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
// 处理TaskDrawer请求任务列表
|
// 处理TaskDrawer请求任务列表
|
||||||
const handleRequestTaskList = () => {
|
const handleRequestTaskList = () => {
|
||||||
// 创建扁平化的任务列表,包含所有任务和里程碑
|
// 创建扁平化的任务列表,包含所有任务和里程碑
|
||||||
@@ -817,11 +916,42 @@ onUnmounted(() => {
|
|||||||
window.removeEventListener('context-menu', handleTaskContextMenu as EventListener)
|
window.removeEventListener('context-menu', handleTaskContextMenu as EventListener)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 主题状态管理
|
||||||
|
const currentThemeMode = ref<'light' | 'dark'>('light')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置主题模式
|
||||||
|
* @param mode 主题模式,默认为 'dark'
|
||||||
|
*/
|
||||||
|
const setTheme = (mode?: 'light' | 'dark') => {
|
||||||
|
const targetMode = mode || 'dark'
|
||||||
|
currentThemeMode.value = targetMode
|
||||||
|
document.documentElement.setAttribute('data-theme', targetMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前主题模式
|
||||||
|
*/
|
||||||
|
const currentTheme = (): string => {
|
||||||
|
return currentThemeMode.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 Props theme 变化
|
||||||
|
watch(
|
||||||
|
() => props.theme,
|
||||||
|
(newTheme) => {
|
||||||
|
if (newTheme && newTheme !== currentThemeMode.value) {
|
||||||
|
setTheme(newTheme)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
// 全屏状态管理
|
// 全屏状态管理
|
||||||
const isFullscreen = ref(false)
|
const isFullscreen = ref(false)
|
||||||
|
|
||||||
// 多语言支持
|
// 多语言支持
|
||||||
const { t, locale } = useI18n()
|
const { t, locale: i18nLocale } = useI18n()
|
||||||
const collapseTaskListText = computed(() => t.value.collapseTaskList)
|
const collapseTaskListText = computed(() => t.value.collapseTaskList)
|
||||||
const expandTaskListText = computed(() => t.value.expandTaskList)
|
const expandTaskListText = computed(() => t.value.expandTaskList)
|
||||||
|
|
||||||
@@ -1443,6 +1573,63 @@ const handleTimelineScaleChanged = (scale: TimelineScale) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === 时间维度相关方法 ===
|
||||||
|
// 时间刻度顺序定义
|
||||||
|
const TIME_SCALE_ORDER: TimelineScale[] = ['hour', 'day', 'week', 'month', 'quarter', 'year']
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置时间刻度
|
||||||
|
* @param scale 时间刻度,默认为 'week'
|
||||||
|
*/
|
||||||
|
const setTimeScale = (scale?: TimelineScale) => {
|
||||||
|
const targetScale = scale || 'week'
|
||||||
|
if (TIME_SCALE_ORDER.includes(targetScale)) {
|
||||||
|
handleTimeScaleChange(targetScale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 放大时间刻度(显示更细粒度)
|
||||||
|
* year -> quarter -> month -> week -> day -> hour
|
||||||
|
*/
|
||||||
|
const zoomIn = () => {
|
||||||
|
const currentIndex = TIME_SCALE_ORDER.indexOf(currentTimeScale.value)
|
||||||
|
if (currentIndex > 0) {
|
||||||
|
const newScale = TIME_SCALE_ORDER[currentIndex - 1]
|
||||||
|
handleTimeScaleChange(newScale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缩小时间刻度(显示更粗粒度)
|
||||||
|
* hour -> day -> week -> month -> quarter -> year
|
||||||
|
*/
|
||||||
|
const zoomOut = () => {
|
||||||
|
const currentIndex = TIME_SCALE_ORDER.indexOf(currentTimeScale.value)
|
||||||
|
if (currentIndex < TIME_SCALE_ORDER.length - 1) {
|
||||||
|
const newScale = TIME_SCALE_ORDER[currentIndex + 1]
|
||||||
|
handleTimeScaleChange(newScale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前时间刻度
|
||||||
|
*/
|
||||||
|
const currentScale = (): string => {
|
||||||
|
return currentTimeScale.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 Props timeScale 变化
|
||||||
|
watch(
|
||||||
|
() => props.timeScale,
|
||||||
|
(newScale) => {
|
||||||
|
if (newScale && newScale !== currentTimeScale.value) {
|
||||||
|
handleTimeScaleChange(newScale)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
// 处理关闭高亮
|
// 处理关闭高亮
|
||||||
const handleClearHighlight = () => {
|
const handleClearHighlight = () => {
|
||||||
if (timelineRef.value?.clearHighlight) {
|
if (timelineRef.value?.clearHighlight) {
|
||||||
@@ -1672,7 +1859,7 @@ const pdfExportHandler = async () => {
|
|||||||
|
|
||||||
// 添加日期
|
// 添加日期
|
||||||
pdf.setFontSize(10)
|
pdf.setFontSize(10)
|
||||||
const currentDate = new Date().toLocaleDateString(locale.value)
|
const currentDate = new Date().toLocaleDateString(i18nLocale.value)
|
||||||
pdf.text(`${dateLabel}: ${currentDate}`, pdfWidth - 10, 10, { align: 'right' })
|
pdf.text(`${dateLabel}: ${currentDate}`, pdfWidth - 10, 10, { align: 'right' })
|
||||||
|
|
||||||
// 添加甘特图图片
|
// 添加甘特图图片
|
||||||
@@ -1730,6 +1917,78 @@ const handleFullscreenToggle = (event: CustomEvent) => {
|
|||||||
}, 500) // 比动画时间稍长一点,确保完全完成
|
}, 500) // 比动画时间稍长一点,确保完全完成
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === 全屏相关方法 ===
|
||||||
|
/**
|
||||||
|
* 进入全屏模式
|
||||||
|
*/
|
||||||
|
const enterFullscreen = () => {
|
||||||
|
if (!isFullscreen.value) {
|
||||||
|
isFullscreen.value = true
|
||||||
|
if (props.onFullscreenChange && typeof props.onFullscreenChange === 'function') {
|
||||||
|
props.onFullscreenChange(true)
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('timeline-container-resized', {
|
||||||
|
detail: { source: 'fullscreen-toggle' },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出全屏模式
|
||||||
|
*/
|
||||||
|
const exitFullscreen = () => {
|
||||||
|
if (isFullscreen.value) {
|
||||||
|
isFullscreen.value = false
|
||||||
|
if (props.onFullscreenChange && typeof props.onFullscreenChange === 'function') {
|
||||||
|
props.onFullscreenChange(false)
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('timeline-container-resized', {
|
||||||
|
detail: { source: 'fullscreen-toggle' },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换全屏模式
|
||||||
|
*/
|
||||||
|
const toggleFullscreen = () => {
|
||||||
|
if (isFullscreen.value) {
|
||||||
|
exitFullscreen()
|
||||||
|
} else {
|
||||||
|
enterFullscreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前是否全屏
|
||||||
|
*/
|
||||||
|
const getIsFullscreen = (): boolean => {
|
||||||
|
return isFullscreen.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 Props fullscreen 变化
|
||||||
|
watch(
|
||||||
|
() => props.fullscreen,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue !== undefined && newValue !== isFullscreen.value) {
|
||||||
|
if (newValue) {
|
||||||
|
enterFullscreen()
|
||||||
|
} else {
|
||||||
|
exitFullscreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
// 更新或添加里程碑到列表中
|
// 更新或添加里程碑到列表中
|
||||||
const updateOrAddMilestone = (milestones: Task[], milestone: Task): boolean => {
|
const updateOrAddMilestone = (milestones: Task[], milestone: Task): boolean => {
|
||||||
const existingIndex = milestones.findIndex(m => m.id === milestone.id)
|
const existingIndex = milestones.findIndex(m => m.id === milestone.id)
|
||||||
@@ -1898,22 +2157,94 @@ const defaultTodayLocate = () => {
|
|||||||
const offset = timelinePanelW ? timelinePanelW / 2 : 200 // 偏移量让今天居中显示
|
const offset = timelinePanelW ? timelinePanelW / 2 : 200 // 偏移量让今天居中显示
|
||||||
const scrollPosition = (totalDays - 1) * 30 - offset
|
const scrollPosition = (totalDays - 1) * 30 - offset
|
||||||
|
|
||||||
// 滚动到指定位置
|
if (timelinePanel) {
|
||||||
const timeline = document.querySelector('.timeline') as HTMLElement
|
timelinePanel.scrollLeft = Math.max(0, scrollPosition)
|
||||||
if (timeline) {
|
}
|
||||||
timeline.scrollLeft = Math.max(0, scrollPosition)
|
}
|
||||||
|
|
||||||
// 添加今日高亮效果到对应的日期列
|
// === 今日定位相关方法 ===
|
||||||
const todayColumn = timeline.querySelector('.day-column.today') as HTMLElement
|
/**
|
||||||
if (todayColumn) {
|
* 滚动到今日位置
|
||||||
todayColumn.classList.add('today-highlight')
|
*/
|
||||||
setTimeout(() => {
|
const scrollToToday = () => {
|
||||||
todayColumn.classList.remove('today-highlight')
|
if (timelineRef.value && typeof timelineRef.value.scrollToTodayCenter === 'function') {
|
||||||
}, 2000)
|
timelineRef.value.scrollToTodayCenter()
|
||||||
|
} else {
|
||||||
|
defaultTodayLocate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 滚动到指定任务
|
||||||
|
* @param taskId 任务ID
|
||||||
|
*/
|
||||||
|
const scrollToTask = (taskId: string | number) => {
|
||||||
|
// 查找任务并滚动到对应位置
|
||||||
|
const findTaskById = (tasks: Task[], id: string | number): Task | null => {
|
||||||
|
for (const task of tasks) {
|
||||||
|
if (task.id === id || String(task.id) === String(id)) {
|
||||||
|
return task
|
||||||
|
}
|
||||||
|
if (task.children && task.children.length > 0) {
|
||||||
|
const found = findTaskById(task.children, id)
|
||||||
|
if (found) return found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.tasks) {
|
||||||
|
const task = findTaskById(props.tasks, taskId)
|
||||||
|
if (task && task.startDate) {
|
||||||
|
// 使用Timeline的scrollToDate方法
|
||||||
|
if (timelineRef.value && typeof timelineRef.value.scrollToDate === 'function') {
|
||||||
|
timelineRef.value.scrollToDate(task.startDate)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 滚动到指定日期
|
||||||
|
* @param date 日期(Date对象或日期字符串)
|
||||||
|
*/
|
||||||
|
const scrollToDate = (date: Date | string) => {
|
||||||
|
// 使用Timeline的scrollToDate方法
|
||||||
|
if (timelineRef.value && typeof timelineRef.value.scrollToDate === 'function') {
|
||||||
|
timelineRef.value.scrollToDate(date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 语言切换相关方法 ===
|
||||||
|
/**
|
||||||
|
* 获取当前语言
|
||||||
|
*/
|
||||||
|
const currentLocale = (): string => {
|
||||||
|
return i18nLocale.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置语言
|
||||||
|
* @param locale 语言代码
|
||||||
|
*/
|
||||||
|
const setLocale = (locale: 'zh-CN' | 'en-US') => {
|
||||||
|
const { setLocale: setI18nLocale } = useI18n()
|
||||||
|
setI18nLocale(locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 Props locale 变化
|
||||||
|
watch(
|
||||||
|
() => props.locale,
|
||||||
|
(newLocale) => {
|
||||||
|
if (newLocale && newLocale !== i18nLocale.value) {
|
||||||
|
// 导入 setLocale 方法
|
||||||
|
const { setLocale } = useI18n()
|
||||||
|
setLocale(newLocale)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
// 窗口大小变化处理函数
|
// 窗口大小变化处理函数
|
||||||
const handleWindowResize = () => {
|
const handleWindowResize = () => {
|
||||||
// 直接检查当前宽度是否仍然有效,如果超出限制则调整
|
// 直接检查当前宽度是否仍然有效,如果超出限制则调整
|
||||||
@@ -2335,6 +2666,40 @@ function handleMilestoneDialogDelete(milestoneId: number) {
|
|||||||
// 4. 关闭对话框
|
// 4. 关闭对话框
|
||||||
handleMilestoneDialogClose()
|
handleMilestoneDialogClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 暴露方法供外部调用
|
||||||
|
defineExpose({
|
||||||
|
// 全屏相关
|
||||||
|
enterFullscreen,
|
||||||
|
exitFullscreen,
|
||||||
|
toggleFullscreen,
|
||||||
|
isFullscreen: getIsFullscreen,
|
||||||
|
|
||||||
|
// 展开/收起相关
|
||||||
|
expandAll: expandAllTasks,
|
||||||
|
collapseAll: collapseAllTasks,
|
||||||
|
toggleExpandAll: toggleExpandAllTasks,
|
||||||
|
isExpandAll: getIsExpandAll,
|
||||||
|
|
||||||
|
// 今日定位相关
|
||||||
|
scrollToToday,
|
||||||
|
scrollToTask,
|
||||||
|
scrollToDate,
|
||||||
|
|
||||||
|
// 语言切换相关
|
||||||
|
setLocale,
|
||||||
|
currentLocale,
|
||||||
|
|
||||||
|
// 时间维度相关
|
||||||
|
setTimeScale,
|
||||||
|
zoomIn,
|
||||||
|
zoomOut,
|
||||||
|
currentScale,
|
||||||
|
|
||||||
|
// 主题相关
|
||||||
|
setTheme,
|
||||||
|
currentTheme,
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -2347,6 +2712,10 @@ function handleMilestoneDialogDelete(milestoneId: number) {
|
|||||||
<GanttToolbar
|
<GanttToolbar
|
||||||
v-if="props.showToolbar"
|
v-if="props.showToolbar"
|
||||||
:config="props.toolbarConfig"
|
:config="props.toolbarConfig"
|
||||||
|
:time-scale="currentTimeScale"
|
||||||
|
:theme="currentThemeMode"
|
||||||
|
:fullscreen="isFullscreen"
|
||||||
|
:expand-all="getIsExpandAll()"
|
||||||
:on-today-locate="todayLocateHandler"
|
:on-today-locate="todayLocateHandler"
|
||||||
:on-export-csv="csvExportHandler"
|
:on-export-csv="csvExportHandler"
|
||||||
:on-export-pdf="pdfExportHandler"
|
:on-export-pdf="pdfExportHandler"
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ type Language = 'zh' | 'en'
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
config: () => ({}),
|
config: () => ({}),
|
||||||
|
timeScale: undefined,
|
||||||
|
theme: undefined,
|
||||||
|
fullscreen: undefined,
|
||||||
|
expandAll: undefined,
|
||||||
onAddTask: undefined,
|
onAddTask: undefined,
|
||||||
onAddMilestone: undefined,
|
onAddMilestone: undefined,
|
||||||
onTodayLocate: undefined,
|
onTodayLocate: undefined,
|
||||||
@@ -50,6 +54,10 @@ const localeMap: Record<Language, 'zh-CN' | 'en-US'> = {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
config?: ToolbarConfig
|
config?: ToolbarConfig
|
||||||
|
timeScale?: TimelineScale
|
||||||
|
theme?: 'light' | 'dark'
|
||||||
|
fullscreen?: boolean
|
||||||
|
expandAll?: boolean
|
||||||
// 自定义事件处理器
|
// 自定义事件处理器
|
||||||
onAddTask?: () => void
|
onAddTask?: () => void
|
||||||
onAddMilestone?: () => void
|
onAddMilestone?: () => void
|
||||||
@@ -90,6 +98,45 @@ const isFullscreen = ref(false)
|
|||||||
const showLanguageDropdown = ref(false)
|
const showLanguageDropdown = ref(false)
|
||||||
const currentTimeScale = ref<TimelineScale>(TimelineScale.DAY)
|
const currentTimeScale = ref<TimelineScale>(TimelineScale.DAY)
|
||||||
|
|
||||||
|
// 如果外部通过 Prop 传入 timeScale,则同步到本地状态
|
||||||
|
watch(
|
||||||
|
() => props.timeScale,
|
||||||
|
(newScale) => {
|
||||||
|
if (newScale && newScale !== currentTimeScale.value) {
|
||||||
|
currentTimeScale.value = newScale
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
// 监听 theme prop
|
||||||
|
watch(
|
||||||
|
() => props.theme,
|
||||||
|
(newTheme) => {
|
||||||
|
if (newTheme) {
|
||||||
|
const newMode = newTheme === 'dark'
|
||||||
|
if (isDarkMode.value !== newMode) {
|
||||||
|
isDarkMode.value = newMode
|
||||||
|
document.documentElement.setAttribute('data-theme', newTheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
// 监听 fullscreen prop
|
||||||
|
watch(
|
||||||
|
() => props.fullscreen,
|
||||||
|
(newFullscreen) => {
|
||||||
|
if (newFullscreen !== undefined && isFullscreen.value !== newFullscreen) {
|
||||||
|
isFullscreen.value = newFullscreen
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
// 监听 expandAll prop (注:这里不需要内部状态,只是用于 UI 显示)
|
||||||
|
|
||||||
// 翻译函数 - 使用 useI18n 提供的 getTranslation 函数
|
// 翻译函数 - 使用 useI18n 提供的 getTranslation 函数
|
||||||
const t = (key: string): string => {
|
const t = (key: string): string => {
|
||||||
return getTranslation(key)
|
return getTranslation(key)
|
||||||
|
|||||||
@@ -2504,6 +2504,154 @@ const scrollToToday = () => {
|
|||||||
}, 500) // 等待滚动完成后再添加高亮
|
}, 500) // 等待滚动完成后再添加高亮
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 滚动到指定日期(居中显示)
|
||||||
|
* @param date 日期(Date对象或日期字符串)
|
||||||
|
*/
|
||||||
|
const scrollToDate = (date: Date | string) => {
|
||||||
|
const targetDate = typeof date === 'string' ? new Date(date) : date
|
||||||
|
const timelineStart = timelineConfig.value.startDate
|
||||||
|
|
||||||
|
// 确保日期计算的精确性 - 使用年月日,忽略时分秒
|
||||||
|
const targetNormalized = new Date(
|
||||||
|
targetDate.getFullYear(),
|
||||||
|
targetDate.getMonth(),
|
||||||
|
targetDate.getDate(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 根据不同的时间刻度使用不同的起始日期
|
||||||
|
let startNormalized: Date
|
||||||
|
if (
|
||||||
|
currentTimeScale.value === TimelineScale.YEAR ||
|
||||||
|
currentTimeScale.value === TimelineScale.QUARTER
|
||||||
|
) {
|
||||||
|
const yearRange = getYearTimelineRange()
|
||||||
|
startNormalized = new Date(
|
||||||
|
yearRange.startDate.getFullYear(),
|
||||||
|
yearRange.startDate.getMonth(),
|
||||||
|
yearRange.startDate.getDate(),
|
||||||
|
)
|
||||||
|
} else if (currentTimeScale.value === TimelineScale.MONTH) {
|
||||||
|
const monthRange = getMonthTimelineRange()
|
||||||
|
startNormalized = new Date(
|
||||||
|
monthRange.startDate.getFullYear(),
|
||||||
|
monthRange.startDate.getMonth(),
|
||||||
|
monthRange.startDate.getDate(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
startNormalized = new Date(
|
||||||
|
timelineStart.getFullYear(),
|
||||||
|
timelineStart.getMonth(),
|
||||||
|
timelineStart.getDate(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算目标日期距离时间线开始日期的天数
|
||||||
|
const timeDiff = targetNormalized.getTime() - startNormalized.getTime()
|
||||||
|
const daysDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24))
|
||||||
|
|
||||||
|
// 计算目标日期在时间线中的像素位置(根据当前时间刻度)
|
||||||
|
let datePosition: number
|
||||||
|
|
||||||
|
if (currentTimeScale.value === TimelineScale.HOUR) {
|
||||||
|
// 小时视图:精确到小时的定位
|
||||||
|
const targetHour = targetDate.getHours()
|
||||||
|
const targetMinute = targetDate.getMinutes()
|
||||||
|
|
||||||
|
// 基础天数偏移(到目标日0点的位置)
|
||||||
|
const baseDayPosition = daysDiff * dayWidth.value
|
||||||
|
|
||||||
|
// 小时偏移:每小时40px
|
||||||
|
const hourOffset = targetHour * 40
|
||||||
|
|
||||||
|
// 分钟偏移:在当前小时内的精确位置
|
||||||
|
const minuteOffset = (targetMinute / 60) * 40
|
||||||
|
|
||||||
|
datePosition = baseDayPosition + hourOffset + minuteOffset
|
||||||
|
} else if (currentTimeScale.value === TimelineScale.QUARTER) {
|
||||||
|
// 季度视图:计算季度偏移
|
||||||
|
const targetYear = targetNormalized.getFullYear()
|
||||||
|
const baseYear = startNormalized.getFullYear()
|
||||||
|
|
||||||
|
const yearWidth = 240 // 每年4季度 * 60px
|
||||||
|
const quarterWidth = 60
|
||||||
|
|
||||||
|
// 计算年份偏移
|
||||||
|
const yearOffset = targetYear - baseYear
|
||||||
|
datePosition = yearOffset * yearWidth
|
||||||
|
|
||||||
|
// 计算季度内的偏移
|
||||||
|
const targetQuarter = Math.floor(targetNormalized.getMonth() / 3)
|
||||||
|
datePosition += targetQuarter * quarterWidth
|
||||||
|
|
||||||
|
// 计算季度内的天数偏移(季度内的细微定位)
|
||||||
|
const quarterStartMonth = targetQuarter * 3
|
||||||
|
const quarterStartDate = new Date(targetYear, quarterStartMonth, 1)
|
||||||
|
const daysIntoQuarter = Math.floor(
|
||||||
|
(targetNormalized.getTime() - quarterStartDate.getTime()) / (1000 * 60 * 60 * 24),
|
||||||
|
)
|
||||||
|
const avgDaysInQuarter = 91 // 平均每季度91天
|
||||||
|
datePosition += (daysIntoQuarter / avgDaysInQuarter) * quarterWidth
|
||||||
|
} else if (currentTimeScale.value === TimelineScale.YEAR) {
|
||||||
|
// 年视图:计算年内偏移
|
||||||
|
const targetYear = targetNormalized.getFullYear()
|
||||||
|
const baseYear = startNormalized.getFullYear()
|
||||||
|
|
||||||
|
const yearWidth = 360 // 每年360px
|
||||||
|
|
||||||
|
// 计算年份偏移
|
||||||
|
const yearOffset = targetYear - baseYear
|
||||||
|
datePosition = yearOffset * yearWidth
|
||||||
|
|
||||||
|
// 计算年内的天数偏移
|
||||||
|
const yearStartDate = new Date(targetYear, 0, 1)
|
||||||
|
const daysIntoYear = Math.floor(
|
||||||
|
(targetNormalized.getTime() - yearStartDate.getTime()) / (1000 * 60 * 60 * 24),
|
||||||
|
)
|
||||||
|
const daysInYear = 365 // 不考虑闰年的简化处理
|
||||||
|
datePosition += (daysIntoYear / daysInYear) * yearWidth
|
||||||
|
} else if (currentTimeScale.value === TimelineScale.MONTH) {
|
||||||
|
// 月视图:需要累计每个月的实际宽度
|
||||||
|
const targetYear = targetNormalized.getFullYear()
|
||||||
|
const targetMonth = targetNormalized.getMonth()
|
||||||
|
const baseYear = startNormalized.getFullYear()
|
||||||
|
const baseMonth = startNormalized.getMonth()
|
||||||
|
|
||||||
|
const monthWidth = 60 // 每月60px
|
||||||
|
|
||||||
|
// 计算跨越的月数
|
||||||
|
const monthsDiff = (targetYear - baseYear) * 12 + (targetMonth - baseMonth)
|
||||||
|
datePosition = monthsDiff * monthWidth
|
||||||
|
|
||||||
|
// 计算月内的天数偏移
|
||||||
|
const targetDay = targetNormalized.getDate()
|
||||||
|
const daysInMonth = new Date(targetYear, targetMonth + 1, 0).getDate()
|
||||||
|
datePosition += (targetDay / daysInMonth) * monthWidth
|
||||||
|
} else if (currentTimeScale.value === TimelineScale.WEEK) {
|
||||||
|
// 周视图:每周60px
|
||||||
|
const weekWidth = 60
|
||||||
|
datePosition = (daysDiff / 7) * weekWidth
|
||||||
|
} else {
|
||||||
|
// 日视图:每天30px
|
||||||
|
datePosition = daysDiff * dayWidth.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用缓存的容器元素
|
||||||
|
const timeline = timelineContainerElement.value
|
||||||
|
if (!timeline) return
|
||||||
|
|
||||||
|
const containerWidth = timeline.clientWidth
|
||||||
|
|
||||||
|
// 计算居中滚动位置
|
||||||
|
const centeredScrollPosition = datePosition - containerWidth / 2
|
||||||
|
|
||||||
|
// 滚动到指定位置,确保目标日期在中间
|
||||||
|
timeline.scrollTo({
|
||||||
|
left: Math.max(0, centeredScrollPosition),
|
||||||
|
behavior: 'smooth',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 更新任务
|
// 更新任务
|
||||||
const updateTask = (updatedTask: Task) => {
|
const updateTask = (updatedTask: Task) => {
|
||||||
// 不直接修改props数据,而是通过事件通知父组件
|
// 不直接修改props数据,而是通过事件通知父组件
|
||||||
@@ -3366,6 +3514,7 @@ defineExpose({
|
|||||||
scrollToTasks,
|
scrollToTasks,
|
||||||
scrollToToday,
|
scrollToToday,
|
||||||
scrollToTodayCenter,
|
scrollToTodayCenter,
|
||||||
|
scrollToDate,
|
||||||
// 时间线配置
|
// 时间线配置
|
||||||
timelineConfig,
|
timelineConfig,
|
||||||
// 时间刻度更新
|
// 时间刻度更新
|
||||||
|
|||||||
Reference in New Issue
Block a user