Files
jordium-gantt-vue3/npm-demo/src/components/GanttTest.vue
T

1094 lines
29 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, 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,
name: '项目启动',
startDate: '2025-10-30',
endDate: '2025-11-5',
progress: 100,
department: '管理部',
departmentCode: 'D001',
type: 'task',
},
{
id: 2,
name: '项目启动2',
startDate: '2025-11-30',
endDate: '2025-12-5',
progress: 100,
department: '管理部',
departmentCode: 'D001',
type: 'task',
},
{
id: 3,
name: '项目启动3',
startDate: '2025-11-30',
endDate: '2025-12-5',
progress: 100,
department: '管理部',
departmentCode: 'D001',
type: 'task',
},
])
const milestones = ref([
{
id: 101,
name: '项目立项',
startDate: '2025-10-29',
type: 'milestone',
icon: 'diamond',
},
])
const customMessages = {
'zh-CN': {
department: '部门',
departmentCode: '部门编号',
// 其他文本
days: '天111',
hours: '小时111',
overtime: '超时111',
overdue: '逾期111',
gantt: {
planStartDate: '计划开始时间',
//planEndDate: '计划结束时间',
},
},
'en-US': {
department: 'Department',
departmentCode: 'Department Code',
// 其他文本
days: 'Day111',
hours: 'Hour111',
overtime: 'OverTime111',
overdue: 'OverDue111',
gantt: {
planStartDate: 'Plan Start Date',
planEndDate: 'Plan End Date',
},
},
}
// const tasks = ref([])
// const milestones = ref([])
const showAddTaskDrawer = ref(false)
const showAddMilestoneDialog = ref(false)
const showTodayLocate = ref(true)
// 定义可动态配置的列
const availableColumns = ref<TaskListColumnConfig[]>([
{ key: 'startDate', label: '开始日期', visible: true },
{ key: 'endDate', label: '结束日期', visible: true },
{ key: 'progress', label: '进度', visible: true },
{ key: 'department', label: '部门', visible: true, width: 200 },
{ key: 'departmentCode', label: '部门编号', visible: true },
{ key: 'assigneeName', label: '负责人', visible: true },
])
// 使用内置TaskDrawer组件的时候,可以传入负责人选项
// 请先设置GanttChart组件属性use-default-drawer="true"
const assigneeOptions = ref([
{ value: 'zhangsan', label: '张三' },
{ value: 'lisi', label: '李四' },
{ value: 'wangwu', label: '王五' },
])
// TaskList宽度配置示例
const taskListConfig = {
defaultWidth: '50%', // 默认展开宽度50%
minWidth: '300px', // 最小宽度300px(默认280px
maxWidth: '1200px', // 最大宽度1200px(默认1160px
columns: availableColumns.value,
}
// toolbar配置示例
const toolbarConfig: ToolbarConfig = {
showAddTask: true, // 显示添加任务按钮
showAddMilestone: true, // 显示添加里程碑按钮
showTodayLocate: true, // 显示定位到今天按钮
showExportCsv: true, // 显示导出CSV按钮
showExportPdf: true, // 显示导出PDF按钮
showLanguage: true, // 显示语言切换按钮
showTheme: true, // 显示主题切换按钮
showFullscreen: true, // 显示全屏按钮
showTimeScale: true, // 显示时间刻度按钮组
timeScaleDimensions: [ // 显示所有时间刻度维度
'hour', 'day', 'week', 'month', 'quarter', 'year',
],
defaultTimeScale: 'week', // 默认选中周视图
showExpandCollapse: false, // 显示展开/折叠按钮
}
const newTask = ref({
name: '',
startDate: '',
endDate: '',
})
const addTask = () => {
tasks.value.push({
id: tasks.value.length + 1,
name: newTask.value.name,
startDate: newTask.value.startDate,
endDate: newTask.value.endDate,
progress: 0,
})
newTask.value = { name: '', startDate: '', endDate: '' }
showAddTaskDrawer.value = false
}
const addMilestone = () => {
milestones.value.push({
id: milestones.value.length + 1,
name: newTask.value.name,
startDate: newTask.value.startDate,
progress: 0,
type: 'milestone',
icon: 'diamond',
})
console.log('milestones: ', milestones.value)
newTask.value = { name: '', startDate: '', endDate: '' }
showAddMilestoneDialog.value = false
}
const onTaskDblclick = (task) => {
alert(`双击任务: ${task.name}`)
}
const onTaskClick = (task) => {
alert(`单击任务: ${task.name}`)
}
const onMilestoneDblclick = (milestone) => {
alert(`双击里程碑: ${milestone.name}`)
}
// 任务行拖拽完成事件(可选)
const handleTaskRowMoved = async (payload: {
draggedTask: Task
targetTask: Task
position: 'after' | 'child'
oldParent: Task | null
newParent: Task | null
}) => {
const { draggedTask, targetTask, position, oldParent, newParent } = payload
// 组件已自动完成任务移动、parentId更新和TaskList/Timeline同步
// 监听此事件为完全可选,仅用于:
// 1. 调用后端 API 保存新的任务层级关系(示例)
// 取消注释并替换为你的实际 API 调用
/*
try {
await fetch('/api/tasks/update-hierarchy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
taskId: draggedTask.id,
targetTaskId: targetTask.id,
position: position,
oldParentId: oldParent?.id,
newParentId: newParent?.id,
})
})
console.log('任务层级保存成功')
} catch (error) {
console.error('保存任务层级失败:', error)
alert('保存失败,请刷新页面')
}
*/
// 3. 触发其他业务逻辑(如更新关联数据、记录操作日志等)
// ...
}
// 任务添加后回调
// 注意:当use-default-drawer="true"时,组件内部已经自动添加了任务到tasks数组
// 此回调仅用于补充业务逻辑,例如根据assignee填充assigneeName等
const onTaskAdded = (res) => {
// 组件已自动添加任务,这里只需要找到并更新额外字段
const addedTask = tasks.value.find(t => t.id === res.task.id)
if (addedTask && addedTask.assignee) {
// 根据assignee值查找对应的label并赋值给assigneeName
const assigneeOption = assigneeOptions.value.find(option => option.value === addedTask.assignee)
if (assigneeOption) {
addedTask.assigneeName = assigneeOption.label
}
}
// 不需要手动push,组件已处理
}
// 自定义右键菜单操作处理
const handleCustomMenuAction = (action: string, task: Task, onClose: () => void) => {
alert(`自定义操作: ${action} - 任务: ${task.name}`, 'info', { closable: true })
}
</script>
<template>
<div>
<!-- 工具设置面板 -->
<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"
:use-default-milestone-dialog="false"
:locale-messages="customMessages"
:allow-drag-and-resize="true"
:enable-task-row-move="true"
:assignee-options="assigneeOptions"
@add-milestone="showAddMilestoneDialog = true"
@task-double-click="onTaskDblclick"
@task-click="onTaskClick"
@milestone-double-click="onMilestoneDblclick"
@task-added="onTaskAdded"
>
<TaskListColumn prop="name" label="任务名称" width="300">
<template #header>
<strong style="color: #1890ff;">任务名称 (自定义Header)</strong>
</template>
<template #default="scope">
<span style="color: #52c41a;">{{ scope.row.name }}</span>
</template>
</TaskListColumn>
<TaskListColumn prop="startDate" label="开始时间" width="250">
<template #header>
<strong style="color: #1890ff;">{{ t.department }}</strong>
</template>
</TaskListColumn>
<TaskListColumn prop="endDate" :label="getTranslation('gantt.planEndDate', '结束的时间')" width="250" />
<!-- 使用声明式的 TaskListContextMenu 组件 - 推荐方式 -->
<TaskListContextMenu>
<template #default="scope">
<div class="custom-menu">
<div class="custom-menu-header">声明式 TaskList 菜单</div>
<div class="custom-menu-item" @click="handleCustomMenuAction('extend', scope.row)">
延长任务
</div>
<div class="custom-menu-item" @click="handleCustomMenuAction('move', scope.row)">
📅 移动任务
</div>
<div class="custom-menu-divider"></div>
<div class="custom-menu-item" @click="handleCustomMenuAction('copy', scope.row)">
📄 复制任务
</div>
</div>
</template>
</TaskListContextMenu>
<!-- 使用声明式的 TaskBarContextMenu 组件 - 推荐方式 -->
<TaskBarContextMenu>
<template #default="scope">
<div class="custom-menu">
<div class="custom-menu-header">声明式 TaskBar 菜单</div>
<div class="custom-menu-item" @click="handleCustomMenuAction('extend', scope.row)">
延长任务
</div>
<div class="custom-menu-item" @click="handleCustomMenuAction('move', scope.row)">
📅 移动任务
</div>
<div class="custom-menu-divider"></div>
<div class="custom-menu-item" @click="handleCustomMenuAction('copy', scope.row)">
📄 复制任务
</div>
</div>
</template>
</TaskBarContextMenu>
</GanttChart>
</div>
<!-- 自定义添加任务按钮 -->
<div>
<button class="btn btn-primary" @click="showAddTaskDrawer = true">添加任务</button>
<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>
<div class="drawer-header">
<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>
</div>
</div>
</div>
<!-- 自定义Dialog组件基于element plus -->
<el-dialog
v-model="showAddMilestoneDialog"
title="自定义添加里程碑组件 - Element Plus"
width="400px"
@close="newTask = { name: '', startDate: '', endDate: '' }"
>
<template #default>
<div class="form-item">
<label>任务名称:</label>
<el-input v-model="newTask.name" placeholder="请输入任务名称" />
</div>
<div class="form-item">
<label>日期:</label>
<el-date-picker v-model="newTask.startDate" type="date" value-format="YYYY-MM-DD" />
</div>
</template>
<template #footer>
<el-button @click="addMilestone">确定</el-button>
<el-button @click="showAddMilestoneDialog = false">取消</el-button>
</template>
</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;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
justify-content: flex-end;
animation: fadeIn 0.3s;
}
/* 抽屉容器 */
.drawer-container {
width: 400px;
max-width: 90%;
background: white;
height: 100vh;
display: flex;
flex-direction: column;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
animation: slideIn 0.3s;
}
/* 抽屉头部 */
.drawer-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #e8e8e8;
}
.drawer-header h3 {
margin: 0;
font-size: 16px;
font-weight: 500;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.close-btn:hover {
background: #f5f5f5;
color: #333;
}
/* 抽屉主体 */
.drawer-body {
flex: 1;
padding: 20px;
overflow-y: auto;
}
/* 表单项 */
.form-item {
margin-bottom: 20px;
}
.form-item label {
display: block;
margin-bottom: 8px;
font-size: 14px;
color: #333;
font-weight: 500;
}
.form-item input {
width: 100%;
padding: 8px 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
transition: border-color 0.3s;
}
.form-item input:focus {
outline: none;
border-color: #409eff;
}
/* 抽屉底部 */
.drawer-footer {
padding: 16px 20px;
border-top: 1px solid #e8e8e8;
display: flex;
gap: 10px;
justify-content: flex-end;
}
/* 按钮样式 */
.btn {
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
border: 1px solid #dcdfe6;
transition: all 0.3s;
}
.btn-primary {
background: #409eff;
color: white;
border-color: #409eff;
}
.btn-primary:hover {
background: #66b1ff;
border-color: #66b1ff;
}
.btn-default {
background: white;
color: #606266;
}
.btn-default:hover {
color: #409eff;
border-color: #409eff;
}
/* 动画 */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideIn {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
/* 自定义右键菜单样式 */
.custom-menu {
position: fixed;
z-index: 999999 !important;
background: white;
border: 1px solid #ddd;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 200px;
padding: 8px 0;
font-size: 14px;
}
.custom-menu-header {
padding: 10px 16px;
font-weight: bold;
color: #333;
background: #f8f8f8;
border-bottom: 1px solid #eee;
margin-bottom: 4px;
}
.custom-menu-item {
padding: 10px 16px;
cursor: pointer;
transition: background 0.2s;
user-select: none;
}
.custom-menu-item:hover {
background: #f0f0f0;
}
.custom-menu-item.danger {
color: #ff4d4f;
}
.custom-menu-item.danger:hover {
background: #fff1f0;
}
.custom-menu-divider {
height: 1px;
background: #eee;
margin: 4px 0;
}
/* 暗色主题下的自定义菜单 */
:global(html[data-theme='dark']) .custom-menu {
background: #2a2a2a;
border-color: #444;
}
:global(html[data-theme='dark']) .custom-menu-header {
background: #1e1e1e;
color: #e0e0e0;
border-bottom-color: #444;
}
:global(html[data-theme='dark']) .custom-menu-item {
color: #e0e0e0;
}
:global(html[data-theme='dark']) .custom-menu-item:hover {
background: #353535;
}
:global(html[data-theme='dark']) .custom-menu-item.danger {
color: #ff6b6b;
}
:global(html[data-theme='dark']) .custom-menu-item.danger:hover {
background: #3a2020;
}
:global(html[data-theme='dark']) .custom-menu-divider {
background: #444;
}
</style>