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/),
|
||||
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
|
||||
|
||||
### 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
|
||||
|
||||
<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">
|
||||
<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">
|
||||
@@ -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 |
|
||||
| `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 |
|
||||
| `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
|
||||
|
||||
@@ -537,6 +558,61 @@ const handleMilestoneSaved = milestone => {
|
||||
</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
|
||||
@@ -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
|
||||
> - 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 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
|
||||
|
||||
<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">
|
||||
<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">
|
||||
@@ -197,7 +213,12 @@ npm run dev
|
||||
| `enableTaskRowMove` | `boolean` | `false` | 是否允许拖拽和摆放TaskRow |
|
||||
| `enableTaskListContextMenu` | `boolean` | `true` | 是否启用 TaskList(TaskRow)右键菜单功能。为 `true` 时:未声明 `task-list-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 属性
|
||||
|
||||
@@ -535,6 +556,61 @@ const handleMilestoneSaved = milestone => {
|
||||
</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}]",
|
||||
"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}】",
|
||||
"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",
|
||||
"<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">
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { GanttChart, TaskListColumn, useI18n, TaskListContextMenu, TaskBarContextMenu } from 'jordium-gantt-vue3'
|
||||
import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
|
||||
|
||||
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([
|
||||
{
|
||||
id: 1,
|
||||
@@ -180,10 +259,10 @@ const handleTaskRowMoved = async (payload: {
|
||||
newParent: Task | null
|
||||
}) => {
|
||||
const { draggedTask, targetTask, position, oldParent, newParent } = payload
|
||||
|
||||
|
||||
// 组件已自动完成任务移动、parentId更新和TaskList/Timeline同步
|
||||
// 监听此事件为完全可选,仅用于:
|
||||
|
||||
|
||||
// 1. 调用后端 API 保存新的任务层级关系(示例)
|
||||
// 取消注释并替换为你的实际 API 调用
|
||||
/*
|
||||
@@ -205,7 +284,7 @@ const handleTaskRowMoved = async (payload: {
|
||||
alert('保存失败,请刷新页面')
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
// 3. 触发其他业务逻辑(如更新关联数据、记录操作日志等)
|
||||
// ...
|
||||
}
|
||||
@@ -216,7 +295,7 @@ const handleTaskRowMoved = async (payload: {
|
||||
const onTaskAdded = (res) => {
|
||||
// 组件已自动添加任务,这里只需要找到并更新额外字段
|
||||
const addedTask = tasks.value.find(t => t.id === res.task.id);
|
||||
|
||||
|
||||
if (addedTask && addedTask.assignee) {
|
||||
// 根据assignee值查找对应的label并赋值给assigneeName
|
||||
const assigneeOption = assigneeOptions.value.find(option => option.value === addedTask.assignee);
|
||||
@@ -224,7 +303,7 @@ const onTaskAdded = (res) => {
|
||||
addedTask.assigneeName = assigneeOption.label;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 不需要手动push,组件已处理
|
||||
};
|
||||
|
||||
@@ -236,10 +315,238 @@ const handleCustomMenuAction = (action: string, task: Task, onClose: () => void)
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div style="height: 600px;">
|
||||
<GanttChart
|
||||
:tasks="tasks"
|
||||
<!-- 工具设置面板 -->
|
||||
<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
|
||||
ref="ganttRef"
|
||||
:tasks="tasks"
|
||||
: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"
|
||||
:toolbar-config="toolbarConfig"
|
||||
:use-default-drawer="true"
|
||||
@@ -305,7 +612,7 @@ const handleCustomMenuAction = (action: string, task: Task, onClose: () => void)
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</TaskBarContextMenu>
|
||||
</TaskBarContextMenu>
|
||||
</GanttChart>
|
||||
</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="showTodayLocate = !showTodayLocate">开启/关闭今日按钮</button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 自定义抽屉组件 (原生HTML替代 el-drawer) -->
|
||||
<div v-if="showAddTaskDrawer" class="drawer-overlay" @click="showAddTaskDrawer = false">
|
||||
<div class="drawer-container" @click.stop>
|
||||
@@ -322,24 +629,24 @@ const handleCustomMenuAction = (action: string, task: Task, onClose: () => void)
|
||||
<h3>自定义添加任务组件</h3>
|
||||
<button class="close-btn" @click="showAddTaskDrawer = false">×</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="drawer-body">
|
||||
<div class="form-item">
|
||||
<label>任务名称:</label>
|
||||
<input v-model="newTask.name" type="text" placeholder="请输入任务名称" />
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-item">
|
||||
<label>开始日期:</label>
|
||||
<input v-model="newTask.startDate" type="date" />
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-item">
|
||||
<label>结束日期:</label>
|
||||
<input v-model="newTask.endDate" type="date" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="drawer-footer">
|
||||
<button class="btn btn-primary" @click="addTask">确定</button>
|
||||
<button class="btn btn-default" @click="showAddTaskDrawer = false">取消</button>
|
||||
@@ -348,9 +655,9 @@ const handleCustomMenuAction = (action: string, task: Task, onClose: () => void)
|
||||
</div>
|
||||
|
||||
<!-- 自定义Dialog组件基于element plus -->
|
||||
<el-dialog
|
||||
title="自定义添加里程碑组件 - Element Plus"
|
||||
v-model="showAddMilestoneDialog"
|
||||
<el-dialog
|
||||
title="自定义添加里程碑组件 - Element Plus"
|
||||
v-model="showAddMilestoneDialog"
|
||||
width="400px"
|
||||
@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="showAddMilestoneDialog = false">取消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
position: fixed;
|
||||
@@ -600,4 +1091,4 @@ const handleCustomMenuAction = (action: string, task: Task, onClose: () => void)
|
||||
:global(html[data-theme='dark']) .custom-menu-divider {
|
||||
background: #444;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,237 @@
|
||||
<template>
|
||||
<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
|
||||
ref="ganttRef"
|
||||
:tasks="tasks"
|
||||
: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"
|
||||
:toolbar-config="toolbarConfig"
|
||||
:use-default-drawer="false"
|
||||
@@ -88,10 +316,89 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { GanttChart, Task } from 'jordium-gantt-vue3'
|
||||
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 {
|
||||
key: string;
|
||||
@@ -294,6 +601,190 @@ const onTaskAdded = (res) => {
|
||||
</script>
|
||||
|
||||
<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 {
|
||||
position: fixed;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jordium-gantt-vue3",
|
||||
"version": "1.7.0",
|
||||
"version": "1.7.1",
|
||||
"type": "module",
|
||||
"main": "npm-package/dist/jordium-gantt-vue3.cjs.js",
|
||||
"module": "npm-package/dist/jordium-gantt-vue3.es.js",
|
||||
|
||||
+383
-14
@@ -58,6 +58,11 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
taskListRowStyle: undefined,
|
||||
enableTaskListContextMenu: true,
|
||||
enableTaskBarContextMenu: true,
|
||||
fullscreen: false,
|
||||
expandAll: true,
|
||||
locale: 'zh-CN',
|
||||
timeScale: 'week',
|
||||
theme: 'light',
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
@@ -189,6 +194,16 @@ interface Props {
|
||||
// 当设置为 true 且声明了 TaskBarContextMenu 组件时,使用自定义菜单
|
||||
// 当设置为 false 时,无论是否声明组件,TaskBar 右键菜单都失效
|
||||
enableTaskBarContextMenu?: boolean
|
||||
// 全屏状态控制(响应式)
|
||||
fullscreen?: boolean
|
||||
// 展开/收起所有任务(响应式)
|
||||
expandAll?: boolean
|
||||
// 语言设置(响应式)
|
||||
locale?: 'zh-CN' | 'en-US'
|
||||
// 时间刻度(响应式)
|
||||
timeScale?: TimelineScale
|
||||
// 主题模式(响应式)
|
||||
theme?: 'light' | 'dark'
|
||||
}
|
||||
|
||||
// TaskList的固定总长度(所有列的最小宽度之和 + 边框等额外空间)
|
||||
@@ -694,7 +709,7 @@ const handleTaskRowMoved = (payload: {
|
||||
// 通知 TaskList 更新父级任务数据
|
||||
nextTick(() => {
|
||||
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请求任务列表
|
||||
const handleRequestTaskList = () => {
|
||||
// 创建扁平化的任务列表,包含所有任务和里程碑
|
||||
@@ -817,11 +916,42 @@ onUnmounted(() => {
|
||||
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 { t, locale } = useI18n()
|
||||
const { t, locale: i18nLocale } = useI18n()
|
||||
const collapseTaskListText = computed(() => t.value.collapseTaskList)
|
||||
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 = () => {
|
||||
if (timelineRef.value?.clearHighlight) {
|
||||
@@ -1672,7 +1859,7 @@ const pdfExportHandler = async () => {
|
||||
|
||||
// 添加日期
|
||||
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' })
|
||||
|
||||
// 添加甘特图图片
|
||||
@@ -1730,6 +1917,78 @@ const handleFullscreenToggle = (event: CustomEvent) => {
|
||||
}, 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 existingIndex = milestones.findIndex(m => m.id === milestone.id)
|
||||
@@ -1898,22 +2157,94 @@ const defaultTodayLocate = () => {
|
||||
const offset = timelinePanelW ? timelinePanelW / 2 : 200 // 偏移量让今天居中显示
|
||||
const scrollPosition = (totalDays - 1) * 30 - offset
|
||||
|
||||
// 滚动到指定位置
|
||||
const timeline = document.querySelector('.timeline') as HTMLElement
|
||||
if (timeline) {
|
||||
timeline.scrollLeft = Math.max(0, scrollPosition)
|
||||
if (timelinePanel) {
|
||||
timelinePanel.scrollLeft = Math.max(0, scrollPosition)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加今日高亮效果到对应的日期列
|
||||
const todayColumn = timeline.querySelector('.day-column.today') as HTMLElement
|
||||
if (todayColumn) {
|
||||
todayColumn.classList.add('today-highlight')
|
||||
setTimeout(() => {
|
||||
todayColumn.classList.remove('today-highlight')
|
||||
}, 2000)
|
||||
// === 今日定位相关方法 ===
|
||||
/**
|
||||
* 滚动到今日位置
|
||||
*/
|
||||
const scrollToToday = () => {
|
||||
if (timelineRef.value && typeof timelineRef.value.scrollToTodayCenter === 'function') {
|
||||
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 = () => {
|
||||
// 直接检查当前宽度是否仍然有效,如果超出限制则调整
|
||||
@@ -2335,6 +2666,40 @@ function handleMilestoneDialogDelete(milestoneId: number) {
|
||||
// 4. 关闭对话框
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -2347,6 +2712,10 @@ function handleMilestoneDialogDelete(milestoneId: number) {
|
||||
<GanttToolbar
|
||||
v-if="props.showToolbar"
|
||||
:config="props.toolbarConfig"
|
||||
:time-scale="currentTimeScale"
|
||||
:theme="currentThemeMode"
|
||||
:fullscreen="isFullscreen"
|
||||
:expand-all="getIsExpandAll()"
|
||||
:on-today-locate="todayLocateHandler"
|
||||
:on-export-csv="csvExportHandler"
|
||||
:on-export-pdf="pdfExportHandler"
|
||||
|
||||
@@ -10,6 +10,10 @@ type Language = 'zh' | 'en'
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
config: () => ({}),
|
||||
timeScale: undefined,
|
||||
theme: undefined,
|
||||
fullscreen: undefined,
|
||||
expandAll: undefined,
|
||||
onAddTask: undefined,
|
||||
onAddMilestone: undefined,
|
||||
onTodayLocate: undefined,
|
||||
@@ -50,6 +54,10 @@ const localeMap: Record<Language, 'zh-CN' | 'en-US'> = {
|
||||
|
||||
interface Props {
|
||||
config?: ToolbarConfig
|
||||
timeScale?: TimelineScale
|
||||
theme?: 'light' | 'dark'
|
||||
fullscreen?: boolean
|
||||
expandAll?: boolean
|
||||
// 自定义事件处理器
|
||||
onAddTask?: () => void
|
||||
onAddMilestone?: () => void
|
||||
@@ -90,6 +98,45 @@ const isFullscreen = ref(false)
|
||||
const showLanguageDropdown = ref(false)
|
||||
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 函数
|
||||
const t = (key: string): string => {
|
||||
return getTranslation(key)
|
||||
|
||||
@@ -2504,6 +2504,154 @@ const scrollToToday = () => {
|
||||
}, 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) => {
|
||||
// 不直接修改props数据,而是通过事件通知父组件
|
||||
@@ -3366,6 +3514,7 @@ defineExpose({
|
||||
scrollToTasks,
|
||||
scrollToToday,
|
||||
scrollToTodayCenter,
|
||||
scrollToDate,
|
||||
// 时间线配置
|
||||
timelineConfig,
|
||||
// 时间刻度更新
|
||||
|
||||
Reference in New Issue
Block a user