Files
jordium-gantt-vue3/src/components/GanttToolbar.vue
T

1389 lines
35 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
import { useI18n } from '../composables/useI18n'
import type { ToolbarConfig } from '../models/configs/ToolbarConfig'
import { TimelineScale } from '../models/types/TimelineScale'
import '../styles/app.css'
// 语言定义 - 使用多语言系统的类型
type Language = 'zh' | 'en'
const props = withDefaults(defineProps<Props>(), {
config: () => ({}),
onAddTask: undefined,
onAddMilestone: undefined,
onTodayLocate: undefined,
onExportCsv: undefined,
onExportPdf: undefined,
onLanguageChange: undefined,
onThemeChange: undefined,
onFullscreenChange: undefined,
onSettingsConfirm: undefined,
onTimeScaleChange: undefined,
onExpandAll: undefined,
onCollapseAll: undefined,
})
const emit = defineEmits<{
'add-task': []
'add-milestone': []
'today-locate': []
'export-csv': []
'export-pdf': []
'language-change': [lang: 'zh-CN' | 'en-US']
'theme-change': [isDark: boolean]
'fullscreen-change': [isFullscreen: boolean]
'time-scale-change': [scale: TimelineScale]
'expand-all': []
'collapse-all': []
}>()
// LocalStorage keys
const THEME_STORAGE_KEY = 'gantt-theme'
const LANGUAGE_STORAGE_KEY = 'gantt-locale'
// 保留原始类型以兼容外部API
const localeMap: Record<Language, 'zh-CN' | 'en-US'> = {
zh: 'zh-CN',
en: 'en-US',
}
interface Props {
config?: ToolbarConfig
// 自定义事件处理器
onAddTask?: () => void
onAddMilestone?: () => void
onTodayLocate?: () => void
onExportCsv?: () => void
onExportPdf?: () => void
onLanguageChange?: (lang: 'zh-CN' | 'en-US') => void
onThemeChange?: (isDark: boolean) => void
onFullscreenChange?: (isFullscreen: boolean) => void
onTimeScaleChange?: (scale: TimelineScale) => void
onExpandAll?: () => void
onCollapseAll?: () => void
// 外部确认接口
onSettingsConfirm?: (
type: 'theme' | 'language',
value: string | boolean | Language
) => Promise<boolean> | boolean
}
// 从localStorage读取主题设置
const getInitialTheme = (): boolean => {
if (typeof window !== 'undefined') {
const stored = localStorage.getItem(THEME_STORAGE_KEY)
if (stored !== null) {
return stored === 'dark'
}
// 如果没有存储的设置,检查系统偏好
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
}
return false
}
// 状态管理 - 直接使用 useI18n 的响应式翻译
const { locale, setLocale, getTranslation } = useI18n()
const currentLanguage = ref<Language>('zh')
const isDarkMode = ref(getInitialTheme())
const isFullscreen = ref(false)
const showLanguageDropdown = ref(false)
const currentTimeScale = ref<TimelineScale>(TimelineScale.DAY)
// 翻译函数 - 使用 useI18n 提供的 getTranslation 函数
const t = (key: string): string => {
return getTranslation(key)
}
// 计算当前语言的显示名称
const currentLanguageDisplay = computed(() => {
return t('language')
})
// 确认对话框相关状态
const showConfirmDialog = ref(false)
const confirmAction = ref<'theme' | 'language' | null>(null)
const pendingValue = ref<string | boolean | Language | null>(null)
// 事件处理器
const handleAddTask = () => {
if (props.onAddTask && typeof props.onAddTask === 'function') {
props.onAddTask()
} else {
emit('add-task')
}
}
const handleAddMilestone = () => {
if (props.onAddMilestone && typeof props.onAddMilestone === 'function') {
props.onAddMilestone()
} else {
emit('add-milestone')
}
}
const handleTodayLocate = () => {
if (props.onTodayLocate && typeof props.onTodayLocate === 'function') {
props.onTodayLocate()
} else {
emit('today-locate')
}
}
const handleExportCsv = () => {
if (props.onExportCsv && typeof props.onExportCsv === 'function') {
props.onExportCsv()
} else {
emit('export-csv')
}
}
const handleExportPdf = () => {
if (props.onExportPdf && typeof props.onExportPdf === 'function') {
props.onExportPdf()
} else {
emit('export-pdf')
}
}
// 展开/折叠处理函数
const handleExpandAll = () => {
if (props.onExpandAll && typeof props.onExpandAll === 'function') {
props.onExpandAll()
} else {
emit('expand-all')
}
}
const handleCollapseAll = () => {
if (props.onCollapseAll && typeof props.onCollapseAll === 'function') {
props.onCollapseAll()
} else {
emit('collapse-all')
}
}
// 语言下拉菜单控制
const toggleLanguageDropdown = () => {
showLanguageDropdown.value = !showLanguageDropdown.value
}
const selectLanguage = (lang: Language) => {
// 立即应用语言变化
currentLanguage.value = lang
showLanguageDropdown.value = false
// 同步到多语言系统
const newLocale = localeMap[currentLanguage.value]
setLocale(newLocale)
// 立即触发语言变化事件
if (props.onLanguageChange && typeof props.onLanguageChange === 'function') {
props.onLanguageChange(newLocale)
} else {
emit('language-change', newLocale)
}
// 显示确认对话框询问是否保存设置
pendingValue.value = newLocale
confirmAction.value = 'language'
showConfirmDialog.value = true
}
// 确认保存设置到localStorage
const confirmSaveSettings = async () => {
// 检查confirmAction是否有效
if (!confirmAction.value || pendingValue.value === null) {
// 确认操作类型为空或待保存值为空
closeConfirmDialog()
return
}
// 如果有外部确认接口,先调用外部接口
if (props.onSettingsConfirm && typeof props.onSettingsConfirm === 'function') {
try {
const shouldSave = await props.onSettingsConfirm(confirmAction.value, pendingValue.value)
if (!shouldSave) {
closeConfirmDialog()
return
}
} catch {
// 外部确认接口调用失败 - 静默处理错误并关闭对话框
closeConfirmDialog()
return
}
}
// 保存到localStorage
if (confirmAction.value === 'theme') {
saveThemeToStorage()
} else if (confirmAction.value === 'language') {
saveLanguageToStorage()
}
closeConfirmDialog()
}
// 保存主题设置到localStorage
const saveThemeToStorage = () => {
if (typeof window !== 'undefined' && typeof pendingValue.value === 'boolean') {
localStorage.setItem(THEME_STORAGE_KEY, pendingValue.value ? 'dark' : 'light')
}
}
// 保存语言设置到localStorage
const saveLanguageToStorage = () => {
if (typeof window !== 'undefined' && typeof pendingValue.value === 'string') {
localStorage.setItem(LANGUAGE_STORAGE_KEY, pendingValue.value)
}
}
// 取消保存设置(不影响已生效的主题/语言,只是不保存到localStorage
const cancelSaveSettings = () => {
// 不恢复设置,主题/语言保持已切换的状态
// 只是不保存到localStorage
closeConfirmDialog()
}
// 关闭确认对话框
const closeConfirmDialog = () => {
showConfirmDialog.value = false
confirmAction.value = null
pendingValue.value = null
}
const handleThemeToggle = () => {
const newTheme = !isDarkMode.value
// 立即应用主题变化
isDarkMode.value = newTheme
document.documentElement.setAttribute('data-theme', newTheme ? 'dark' : 'light')
// 立即触发主题变化事件
if (props.onThemeChange && typeof props.onThemeChange === 'function') {
props.onThemeChange(newTheme)
} else {
emit('theme-change', newTheme)
}
// 显示确认对话框询问是否保存设置
pendingValue.value = newTheme
confirmAction.value = 'theme'
showConfirmDialog.value = true
}
const handleFullscreenToggle = () => {
isFullscreen.value = !isFullscreen.value
// 触发全局事件,通知GanttChart组件切换全屏状态
window.dispatchEvent(
new CustomEvent('fullscreen-toggle', {
detail: isFullscreen.value,
}),
)
if (props.onFullscreenChange && typeof props.onFullscreenChange === 'function') {
props.onFullscreenChange(isFullscreen.value)
} else {
emit('fullscreen-change', isFullscreen.value)
}
}
// 时间刻度配置映射
const timeScaleMap = {
hour: { value: TimelineScale.HOUR, label: () => t('timeScaleHour') },
day: { value: TimelineScale.DAY, label: () => t('timeScaleDay') },
week: { value: TimelineScale.WEEK, label: () => t('timeScaleWeek') },
month: { value: TimelineScale.MONTH, label: () => t('timeScaleMonth') },
quarter: { value: TimelineScale.QUARTER, label: () => t('timeScaleQuarter') },
year: { value: TimelineScale.YEAR, label: () => t('timeScaleYear') },
}
type TimeScaleKey = keyof typeof timeScaleMap
const defaultScaleKeys: TimeScaleKey[] = ['hour', 'day', 'week', 'month', 'year']
// 获取可用的时间刻度维度
const availableTimeScales = computed<TimeScaleKey[]>(() => {
const configured = props.config?.timeScaleDimensions as TimeScaleKey[] | undefined
const normalized = (configured ?? defaultScaleKeys).filter(
(scale): scale is TimeScaleKey => scale in timeScaleMap,
)
return normalized.length > 0 ? normalized : [...defaultScaleKeys]
})
// 根据配置解析默认时间刻度
const resolvedDefaultScaleKey = computed<TimeScaleKey>(() => {
const keys = availableTimeScales.value
const candidate = props.config?.defaultTimeScale as TimeScaleKey | undefined
if (candidate && keys.includes(candidate)) {
return candidate
}
if (keys.includes('day')) {
return 'day'
}
return keys[0] ?? 'day'
})
// 时间刻度切换处理
const handleTimeScaleChange = (scale: TimelineScale) => {
currentTimeScale.value = scale
if (props.onTimeScaleChange && typeof props.onTimeScaleChange === 'function') {
props.onTimeScaleChange(scale)
} else {
emit('time-scale-change', scale)
}
}
// 监听配置变化并应用默认时间刻度
let hasAppliedConfigScale = false
watch(
() => resolvedDefaultScaleKey.value,
newKey => {
const targetScale = timeScaleMap[newKey].value
if (currentTimeScale.value !== targetScale || !hasAppliedConfigScale) {
hasAppliedConfigScale = true
handleTimeScaleChange(targetScale)
}
},
{ immediate: true },
)
// 获取当前时间刻度对应的字符串键
const currentTimeScaleKey = computed<TimeScaleKey>(() => {
const matchedKey = availableTimeScales.value.find(
scaleKey => timeScaleMap[scaleKey].value === currentTimeScale.value,
)
return matchedKey ?? resolvedDefaultScaleKey.value
})
// 计算分段控制器滑块位置
const getThumbStyle = () => {
const currentIndex = availableTimeScales.value.findIndex(
scale => scale === currentTimeScaleKey.value,
)
const totalScales = availableTimeScales.value.length
if (totalScales === 0 || currentIndex < 0) {
return {
transform: 'translateX(0%)',
width: `${100 / totalScales || 25}%`,
}
}
// 每个按钮占据的百分比宽度
const itemWidth = 100 / totalScales
// 滑块的位置(左移的距离)
const translateX = currentIndex * 100
return {
transform: `translateX(${translateX}%)`,
width: `${itemWidth}%`,
}
}
// 点击外部关闭下拉菜单
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as HTMLElement
const dropdown = target.closest('.language-dropdown')
if (!dropdown && showLanguageDropdown.value) {
showLanguageDropdown.value = false
}
}
// 生命周期
onMounted(() => {
// 初始化语言状态,从多语言系统获取当前语言
const currentLocale = locale.value
currentLanguage.value = currentLocale === 'zh-CN' ? 'zh' : 'en'
// 初始化主题状态并应用到DOM
document.documentElement.setAttribute('data-theme', isDarkMode.value ? 'dark' : 'light')
// 添加点击外部关闭下拉菜单的监听
document.addEventListener('click', handleClickOutside)
// 监听系统主题变化(仅在用户未手动设置时)
if (
window.matchMedia &&
typeof window !== 'undefined' &&
!localStorage.getItem(THEME_STORAGE_KEY)
) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
isDarkMode.value = e.matches
document.documentElement.setAttribute('data-theme', e.matches ? 'dark' : 'light')
})
}
})
onUnmounted(() => {
// 清理事件监听
document.removeEventListener('click', handleClickOutside)
})
</script>
<template>
<div class="gantt-toolbar">
<!-- 左侧操作区 -->
<div class="toolbar-left">
<!-- 新增按钮组 -->
<div
v-if="config.showAddTask !== false || config.showAddMilestone !== false"
class="btn-group add-btn-group"
>
<button
v-if="config.showAddTask !== false"
class="btn-group-item"
:title="t('addTask')"
@click="handleAddTask"
>
<svg
class="btn-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
{{ t('addTask') }}
</button>
<button
v-if="config.showAddMilestone !== false"
class="btn-group-item"
:title="t('addMilestone')"
@click="handleAddMilestone"
>
<svg
class="btn-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<g transform="rotate(45 12 12)">
<rect
x="4"
y="4"
width="16"
height="16"
rx="4"
ry="4"
fill="none"
stroke="currentColor"
/>
</g>
<line x1="12" y1="8" x2="12" y2="16"></line>
<line x1="8" y1="12" x2="16" y2="12"></line>
</svg>
{{ t('addMilestone') }}
</button>
</div>
<!-- 展开/折叠按钮组 -->
<div
v-if="config.showExpandCollapse !== false"
class="btn-group expand-collapse-btn-group"
>
<button
class="btn-group-item"
:title="t('expandAll')"
@click="handleExpandAll"
>
<svg
class="btn-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
{{ t('expandAll') }}
</button>
<button
class="btn-group-item"
:title="t('collapseAll')"
@click="handleCollapseAll"
>
<svg
class="btn-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="18 15 12 9 6 15"></polyline>
</svg>
{{ t('collapseAll') }}
</button>
</div>
<!-- 导出按钮组 -->
<div
v-if="config.showExportCsv !== false || config.showExportPdf !== false"
class="btn-group"
>
<button
v-if="config.showExportCsv !== false"
class="btn-group-item"
:title="t('exportCsv')"
@click="handleExportCsv"
>
<svg
class="btn-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"></path>
<polyline points="14,2 14,8 20,8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10,9 9,9 8,9"></polyline>
</svg>
{{ t('exportCsv') }}
</button>
<button
v-if="config.showExportPdf !== false"
class="btn-group-item"
:title="t('exportPdf')"
@click="handleExportPdf"
>
<svg
class="btn-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"></path>
<polyline points="14,2 14,8 20,8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<line x1="12" y1="13" x2="12" y2="17"></line>
</svg>
{{ t('exportPdf') }}
</button>
</div>
</div>
<!-- 右侧设置区 -->
<div class="toolbar-right">
<!-- 时间刻度分段控制器 (Segmented) -->
<div v-if="config.showTimeScale !== false" class="segmented-control time-scale-segmented">
<div class="segmented-track">
<div class="segmented-thumb" :style="getThumbStyle()"></div>
</div>
<button
v-for="scale in availableTimeScales"
:key="scale"
class="segmented-item"
:class="{ active: currentTimeScaleKey === scale }"
:title="t('timeScaleTooltip')"
@click="handleTimeScaleChange(timeScaleMap[scale].value)"
>
{{ timeScaleMap[scale].label() }}
</button>
</div>
<!-- 语言选择下拉菜单 -->
<div v-if="config.showLanguage !== false" class="language-dropdown">
<button
class="toolbar-lang-btn"
:title="t('languageTooltip')"
@click="toggleLanguageDropdown"
>
<svg
class="btn-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10"></circle>
<line x1="2" y1="12" x2="22" y2="12"></line>
<path
d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"
></path>
</svg>
<span class="lang-text">{{ currentLanguageDisplay }}</span>
<svg
class="dropdown-arrow"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</button>
<!-- 下拉菜单 -->
<div v-if="showLanguageDropdown" class="language-menu">
<div
v-for="lang in ['zh', 'en']"
:key="lang"
class="language-option"
:class="{ active: currentLanguage === lang }"
@click="selectLanguage(lang as Language)"
>
<span class="option-text">{{ lang === 'zh' ? '中文' : 'English' }}</span>
<svg
v-if="currentLanguage === lang"
class="check-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="20,6 9,17 4,12"></polyline>
</svg>
</div>
</div>
</div>
<!-- 今日定位按钮 -->
<button
v-if="config.showTodayLocate !== false"
class="toolbar-icon-btn today-locate-btn"
:title="t('todayLocateTooltip')"
@click="handleTodayLocate"
>
<svg
class="btn-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
<line x1="16" y1="2" x2="16" y2="6"></line>
<line x1="8" y1="2" x2="8" y2="6"></line>
<line x1="3" y1="10" x2="21" y2="10"></line>
<circle cx="12" cy="16" r="1"></circle>
</svg>
</button>
<button
v-if="config.showTheme !== false"
class="toolbar-icon-btn"
:title="t(isDarkMode ? 'lightMode' : 'darkMode')"
@click="handleThemeToggle"
>
<svg
v-if="isDarkMode"
class="btn-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
<svg
v-else
class="btn-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
<!-- 备用表情符号 -->
<span class="icon-fallback">{{ isDarkMode ? '☀️' : '🌙' }}</span>
</button>
<button
v-if="config.showFullscreen !== false"
class="toolbar-icon-btn"
:title="t(isFullscreen ? 'exitFullscreen' : 'fullscreen')"
@click="handleFullscreenToggle"
>
<svg
v-if="isFullscreen"
class="btn-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"
></path>
</svg>
<svg
v-else
class="btn-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3"
></path>
</svg>
<!-- 备用表情符号 -->
<span class="icon-fallback">{{ isFullscreen ? '⇲' : '⇱' }}</span>
</button>
</div>
<!-- 确认对话框 -->
<div v-if="showConfirmDialog" class="confirm-dialog">
<div class="dialog-content">
<p class="dialog-message">{{ t('confirmDialogMessage') }}</p>
<div class="dialog-actions">
<button class="btn btn-default" @click="cancelSaveSettings">{{ t('cancel') }}</button>
<button class="btn btn-primary" @click="confirmSaveSettings">{{ t('confirm') }}</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
@import '../styles/theme-variables.css';
.gantt-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: var(--gantt-bg-toolbar, #f8f9fa);
border-bottom: 1px solid var(--gantt-border-color, #ebeef5);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.toolbar-left {
display: flex;
align-items: center;
gap: 12px;
}
.toolbar-right {
display: flex;
align-items: center;
gap: 8px;
}
/* 图标按钮样式 */
.toolbar-icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
margin-right: 8px;
padding: 0;
border: none;
border-radius: 4px;
background: transparent;
color: var(--gantt-text-primary, #606266);
cursor: pointer;
outline: none;
}
.toolbar-icon-btn:hover {
background: var(--gantt-bg-hover, rgba(0, 0, 0, 0.2));
color: var(--gantt-primary, #409eff);
}
.toolbar-icon-btn:focus {
outline: none;
background: var(--gantt-bg-hover, rgba(0, 0, 0, 0.2));
color: var(--gantt-primary, #409eff);
}
.toolbar-icon-btn:active {
background: var(--gantt-bg-active, rgba(0, 0, 0, 0.3));
}
.toolbar-icon-btn .btn-icon {
width: 18px;
height: 18px;
}
/* 图标样式 */
.btn-icon {
width: 16px;
height: 16px;
stroke-width: 2;
fill: none;
stroke: currentColor;
display: block;
flex-shrink: 0;
}
/* 备用图标样式 */
.icon-fallback {
position: absolute;
font-size: 16px;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
user-select: none;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* 语言下拉菜单样式 */
.language-dropdown {
position: relative;
display: inline-block;
}
.toolbar-lang-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
height: 36px;
padding: 0 12px;
border-radius: 4px;
background: transparent;
border: 1px solid var(--gantt-border-color, #dcdfe6);
color: var(--gantt-text-primary, #606266);
cursor: pointer;
outline: none;
font-size: 14px;
white-space: nowrap;
}
.toolbar-lang-btn:hover {
background: var(--gantt-bg-hover, #f5f7fa);
border-color: var(--gantt-primary-color, #409eff);
color: var(--gantt-primary-color, #409eff);
}
.toolbar-lang-btn:focus {
outline: none;
background: var(--gantt-bg-hover, #f5f7fa);
border-color: var(--gantt-primary-color, #409eff);
color: var(--gantt-primary-color, #409eff);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
.toolbar-lang-btn:active {
background: var(--gantt-bg-active, rgba(64, 158, 255, 0.1));
}
.toolbar-lang-btn .btn-icon {
width: 16px;
height: 16px;
stroke-width: 2;
}
.lang-text {
font-size: 14px;
font-weight: 500;
}
.dropdown-arrow {
width: 12px;
height: 12px;
stroke-width: 2;
transition: transform 0.2s ease;
}
.language-dropdown[aria-expanded='true'] .dropdown-arrow,
.language-dropdown:has(.language-menu) .dropdown-arrow {
transform: rotate(180deg);
}
.language-menu {
position: absolute;
top: 100%;
right: 0;
margin-top: 4px;
min-width: 120px;
background: var(--gantt-bg-primary, #ffffff);
border: 1px solid var(--gantt-border-color, #dcdfe6);
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 1000;
overflow: hidden;
animation: dropdown-appear 0.2s ease;
}
@keyframes dropdown-appear {
from {
opacity: 0;
transform: translateY(-8px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.language-option {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
cursor: pointer;
color: var(--gantt-text-primary, #606266);
transition: all 0.2s ease;
border-bottom: 1px solid var(--gantt-border-color, #f0f0f0);
}
.language-option:last-child {
border-bottom: none;
}
.language-option:hover {
background: var(--gantt-bg-hover, #f5f7fa);
color: var(--gantt-primary-color, #409eff);
}
.language-option.active {
background: var(--gantt-primary-color, #409eff);
color: #ffffff;
}
.language-option.active:hover {
background: var(--gantt-primary-hover, #66b1ff);
}
.check-icon {
width: 14px;
height: 14px;
stroke-width: 2.5;
opacity: 0.9;
flex-shrink: 0;
}
/* 确认对话框样式 */
.confirm-dialog {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
animation: dialog-fade-in 0.2s ease-out;
}
.dialog-content {
background: var(--gantt-bg-primary, #ffffff);
border-radius: 8px;
padding: 24px;
min-width: 320px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
animation: dialog-slide-up 0.2s ease-out;
}
.dialog-message {
font-size: 16px;
color: var(--gantt-text-primary, #303133);
margin: 0 0 20px 0;
text-align: center;
line-height: 1.5;
}
.dialog-actions {
display: flex;
justify-content: center;
}
.dialog-actions .btn + .btn {
margin-left: 12px;
}
/* 暗黑模式样式 */
:global(html[data-theme='dark']) .toolbar-icon-btn:hover {
background: var(--gantt-bg-hover, rgba(255, 255, 255, 0.08));
color: var(--gantt-primary, #66b1ff);
}
:global(html[data-theme='dark']) .toolbar-icon-btn:focus {
background: var(--gantt-bg-hover, rgba(255, 255, 255, 0.1));
color: var(--gantt-primary, #66b1ff);
}
:global(html[data-theme='dark']) .toolbar-lang-btn:hover {
background: var(--gantt-bg-hover, rgba(255, 255, 255, 0.06));
border-color: var(--gantt-primary, #66b1ff);
color: var(--gantt-primary, #66b1ff);
}
:global(html[data-theme='dark']) .toolbar-lang-btn:focus {
background: var(--gantt-bg-hover, rgba(255, 255, 255, 0.08));
border-color: var(--gantt-primary, #66b1ff);
color: var(--gantt-primary, #66b1ff);
box-shadow: 0 0 0 2px rgba(102, 177, 255, 0.4);
}
:global(html[data-theme='dark']) .dialog-content {
background: var(--gantt-bg-secondary, #2c2c2c);
}
:global(html[data-theme='dark']) .dialog-message {
color: var(--gantt-text-primary, #e5eaf3);
}
@keyframes dialog-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes dialog-slide-up {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.toolbar-lang-btn {
padding: 0 8px;
gap: 4px;
}
.lang-text {
display: none; /* 移动端隐藏文字,只显示图标 */
}
.language-menu {
right: 0;
min-width: 100px;
}
.language-option {
padding: 10px 12px; /* 移动端增加触摸区域 */
}
.dialog-content {
padding: 16px;
}
.dialog-message {
font-size: 14px;
}
.btn {
padding: 8px 12px;
font-size: 13px;
}
}
/* 按钮组样式 - Element Plus primary button group 风格 */
.btn-group {
display: inline-flex;
margin-right: 8px;
overflow: hidden;
transition: all 0.2s ease;
}
.btn-group:hover,
.btn-group:focus-within {
transform: translateY(-1px);
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.15);
}
.btn-group-item {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 10px 16px;
border: 1px solid;
background: #ffffff;
color: #606266;
font-size: 14px;
font-weight: 500;
cursor: pointer;
outline: none;
line-height: 1;
white-space: nowrap;
position: relative;
border-radius: 0;
margin: 0;
border-left: none;
transition: all 0.2s ease;
}
.btn-group-item:first-child {
border-left: 1px solid;
border-right: none;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.btn-group-item:last-child {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
.btn-group-item:not(:last-child)::after {
content: '';
position: absolute;
right: 0;
top: 25%;
height: 50%;
width: 1px;
background: var(--gantt-border-color, #dcdfe6);
transition: opacity 0.2s ease;
}
/* Primary 按钮组样式 */
.add-btn-group .btn-group-item {
background: #409eff;
border-color: #409eff;
color: #ffffff;
}
.add-btn-group .btn-group-item::after {
background: rgba(255, 255, 255, 0.3);
}
.add-btn-group .btn-group-item:hover {
background: #66b1ff;
border-color: #66b1ff;
z-index: 1;
}
.add-btn-group .btn-group-item:focus {
background: #3a8ee6;
border-color: #3a8ee6;
z-index: 1;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.3);
}
.add-btn-group .btn-group-item:active {
background: #337ecc;
border-color: #337ecc;
}
/* 悬浮和焦点状态下隐藏分割线 */
.btn-group:hover .btn-group-item::after,
.btn-group:focus-within .btn-group-item::after {
opacity: 0;
}
/* 普通按钮组样式 */
.btn-group:not(.add-btn-group) .btn-group-item {
border-color: #dcdfe6;
}
.btn-group:not(.add-btn-group) .btn-group-item:hover {
background: #ecf5ff;
border-color: #b3d8ff;
color: #409eff;
z-index: 1;
}
.btn-group:not(.add-btn-group) .btn-group-item:focus {
background: #ecf5ff;
border-color: #409eff;
color: #409eff;
z-index: 1;
box-shadow: inset 0 0 0 1px #409eff;
}
.btn-group:not(.add-btn-group) .btn-group-item:active {
background: #d9ecff;
border-color: #409eff;
color: #409eff;
}
/* 按钮组图标样式 */
.btn-group-item .btn-icon {
width: 16px;
height: 16px;
stroke-width: 2;
}
/* 暗黑模式下的按钮组样式 */
:global(html[data-theme='dark']) .btn-group {
box-shadow:
0 1px 3px 0 rgba(0, 0, 0, 0.3),
0 1px 2px -1px rgba(0, 0, 0, 0.3);
}
:global(html[data-theme='dark']) .btn-group:hover,
:global(html[data-theme='dark']) .btn-group:focus-within {
box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.5);
}
:global(html[data-theme='dark']) .add-btn-group .btn-group-item {
background: #337ecc;
border-color: #337ecc;
color: #ffffff;
}
:global(html[data-theme='dark']) .add-btn-group .btn-group-item:hover {
background: #4d94d4;
border-color: #4d94d4;
}
:global(html[data-theme='dark']) .add-btn-group .btn-group-item:focus {
background: #2c5aa0;
border-color: #2c5aa0;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.3);
}
:global(html[data-theme='dark']) .add-btn-group .btn-group-item:active {
background: #1f4872;
border-color: #1f4872;
}
:global(html[data-theme='dark']) .btn-group:not(.add-btn-group) .btn-group-item {
background: #2c2c2c;
border-color: #4c4c4c;
color: #e5e5e5;
}
/* 分段控制器样式 - Element Plus Segmented 风格 */
.segmented-control {
position: relative;
display: inline-flex;
background: var(--gantt-bg-primary, #ffffff);
border: 1px solid var(--gantt-border-color, #dcdfe6);
border-radius: 6px;
padding: 1px;
margin-right: 8px;
overflow: hidden;
transition: border-color 0.2s ease;
height: 36px;
}
.segmented-control:hover {
border-color: var(--gantt-primary-light, #79bbff);
}
.segmented-track {
position: absolute;
top: 1px;
left: 1px;
right: 1px;
bottom: 1px;
pointer-events: none;
}
.segmented-thumb {
position: absolute;
top: 0;
left: 0;
width: 25%; /* 默认宽度,将通过内联样式动态设置 */
height: 100%;
background: var(--gantt-primary, #409eff);
border-radius: 5px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.1),
0 1px 6px -1px rgba(0, 0, 0, 0.1);
}
.segmented-item {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 1;
height: 34px;
padding: 0 12px;
border: none;
background: transparent;
font-size: 14px;
font-weight: 500;
cursor: pointer;
outline: none;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
min-width: 40px;
z-index: 1;
border-radius: 5px;
user-select: none;
}
.segmented-item:hover:not(.active) {
color: var(--gantt-primary, #409eff);
background: var(--gantt-bg-hover, rgba(64, 158, 255, 0.06));
}
.segmented-item:active:not(.active) {
background: var(--gantt-bg-active, rgba(64, 158, 255, 0.12));
}
.segmented-item.active {
color: #ffffff;
font-weight: 600;
}
.time-scale-segmented {
height: 36px;
}
.time-scale-segmented .segmented-item {
height: 34px;
font-size: 13px;
min-width: 36px;
}
/* 暗黑模式下的分段控制器样式 */
:global(html[data-theme='dark']) .segmented-control {
background: var(--gantt-bg-secondary, #4b4b4b);
border-color: var(--gantt-border-color, #808080);
}
:global(html[data-theme='dark']) .segmented-control:hover {
border-color: var(--gantt-primary, #3399ff);
}
:global(html[data-theme='dark']) .segmented-thumb {
background: var(--gantt-primary, #3399ff);
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.3),
0 1px 6px -1px rgba(0, 0, 0, 0.3);
}
:global(html[data-theme='dark']) .segmented-item {
color: #ffffff !important; /* 强制使用纯白色,更加突出 */
}
/* 特别针对时间刻度分段控制器的暗黑模式样式 */
:global(html[data-theme='dark']) .time-scale-segmented .segmented-item {
color: #ffffff !important; /* 确保时间刻度按钮也使用纯白色 */
}
:global(html[data-theme='dark']) .segmented-item:hover:not(.active) {
color: var(--gantt-primary, #3399ff); /* 使用主色调,更加鲜艳 */
background: rgba(51, 153, 255, 0.12); /* 调整背景透明度,与主色调匹配 */
}
:global(html[data-theme='dark']) .segmented-item:active:not(.active) {
background: rgba(51, 153, 255, 0.2); /* 调整背景透明度,与主色调匹配 */
}
:global(html[data-theme='dark']) .segmented-item.active {
color: #ffffff;
}
</style>