Merge branch 'develop' into pages

This commit is contained in:
LINING-PC\lining
2026-01-11 17:23:29 +08:00
13 changed files with 3272 additions and 50 deletions
+16
View File
@@ -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 & currentThemesetTimeScale & 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
View File
@@ -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
+239 -1
View File
@@ -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` | 是否启用 TaskListTaskRow)右键菜单功能。为 `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
View File
File diff suppressed because it is too large Load Diff
+87
View File
@@ -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"
}
}
}
}
+87
View File
@@ -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": "假"
}
}
}
}
+20
View File
@@ -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 & currentThemesetTimeScale & 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>"
]
}
]
+511 -20
View File
@@ -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
// parentIdTaskList/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) {
// assigneelabelassigneeName
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>
+493 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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) {
// 使TimelinescrollToDate
if (timelineRef.value && typeof timelineRef.value.scrollToDate === 'function') {
timelineRef.value.scrollToDate(task.startDate)
}
}
}
}
/**
* 滚动到指定日期
* @param date 日期Date对象或日期字符串
*/
const scrollToDate = (date: Date | string) => {
// 使TimelinescrollToDate
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"
+47
View File
@@ -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)
+149
View File
@@ -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,
//