| | |
| | | <div class="task-tree-container"> |
| | | <!-- 任务树操作按钮 --> |
| | | <div class="tree-actions mb10"> |
| | | <el-button type="primary" size="small" icon="Plus" @click="handleAddTask">添加任务</el-button> |
| | | <el-button type="success" size="small" icon="RefreshRight" @click="refreshTree">刷新</el-button> |
| | | <el-button type="info" size="small" icon="Filter" @click="toggleFilter"> |
| | | <el-button type="primary" |
| | | size="small" |
| | | icon="Plus" |
| | | @click="handleAddTask">添加任务</el-button> |
| | | <el-button type="success" |
| | | size="small" |
| | | icon="RefreshRight" |
| | | @click="refreshTree">刷新</el-button> |
| | | <el-button type="info" |
| | | size="small" |
| | | icon="Filter" |
| | | @click="toggleFilter"> |
| | | {{ showFilter ? '隐藏筛选' : '显示筛选' }} |
| | | </el-button> |
| | | </div> |
| | | |
| | | <!-- 筛选条件 --> |
| | | <div v-if="showFilter" class="filter-section mb10"> |
| | | <el-form :inline="true" :model="filterParams"> |
| | | <div v-if="showFilter" |
| | | class="filter-section mb10"> |
| | | <el-form :inline="true" |
| | | :model="filterParams"> |
| | | <el-form-item label="任务状态"> |
| | | <el-select v-model="filterParams.status" placeholder="全部" clearable style="width: 120px"> |
| | | <el-option label="未开始" value="notStarted" /> |
| | | <el-option label="进行中" value="inProgress" /> |
| | | <el-option label="已完成" value="completed" /> |
| | | <el-option label="已逾期" value="overdue" /> |
| | | <el-select v-model="filterParams.status" |
| | | placeholder="全部" |
| | | clearable |
| | | style="width: 120px"> |
| | | <el-option label="未开始" |
| | | value="notStarted" /> |
| | | <el-option label="进行中" |
| | | value="inProgress" /> |
| | | <el-option label="已完成" |
| | | value="completed" /> |
| | | <el-option label="已逾期" |
| | | value="overdue" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="负责人"> |
| | | <el-input v-model="filterParams.assignee" placeholder="输入负责人" clearable style="width: 150px" /> |
| | | <el-input v-model="filterParams.assignee" |
| | | placeholder="输入负责人" |
| | | clearable |
| | | style="width: 150px" /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" size="small" @click="filterTree">筛选</el-button> |
| | | <el-button size="small" @click="resetFilter">重置</el-button> |
| | | <el-button type="primary" |
| | | size="small" |
| | | @click="filterTree">筛选</el-button> |
| | | <el-button size="small" |
| | | @click="resetFilter">重置</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | |
| | | <!-- 任务树 --> |
| | | <div class="tree-content"> |
| | | <el-tree |
| | | v-loading="loading" |
| | | :data="taskTreeData" |
| | | :props="defaultProps" |
| | | :expand-on-click-node="false" |
| | | node-key="nodeId" |
| | | ref="treeRef" |
| | | @node-contextmenu="handleContextMenu" |
| | | @node-click="handleNodeClick" |
| | | > |
| | | <el-tree v-loading="loading" |
| | | :data="taskTreeData" |
| | | :props="defaultProps" |
| | | :expand-on-click-node="false" |
| | | node-key="nodeId" |
| | | ref="treeRef" |
| | | @node-contextmenu="handleContextMenu" |
| | | @node-click="handleNodeClick"> |
| | | <template #default="{ node, data }"> |
| | | <!-- 节点内容 --> |
| | | <div class="tree-node-content" :class="{ 'phase-node': data.type === 'phase', 'task-node': data.type === 'task' }"> |
| | | <div class="tree-node-content" |
| | | :class="{ 'phase-node': data.type === 'phase', 'task-node': data.type === 'task' }"> |
| | | <!-- 节点图标 --> |
| | | <div class="node-icon"> |
| | | <i v-if="data.type === 'phase'" class="el-icon-folder text-primary" /> |
| | | <i v-else-if="data.status === 'completed'" class="el-icon-circle-check text-success" /> |
| | | <i v-else-if="data.status === 'inProgress'" class="el-icon-circle-check text-primary" /> |
| | | <i v-else-if="data.status === 'overdue'" class="el-icon-alarm-clock text-danger" /> |
| | | <i v-else class="el-icon-circle-close text-gray-400" /> |
| | | <i v-if="data.type === 'phase'" |
| | | class="el-icon-folder text-primary" /> |
| | | <i v-else-if="data.status === 'completed'" |
| | | class="el-icon-circle-check text-success" /> |
| | | <i v-else-if="data.status === 'inProgress'" |
| | | class="el-icon-circle-check text-primary" /> |
| | | <i v-else-if="data.status === 'overdue'" |
| | | class="el-icon-alarm-clock text-danger" /> |
| | | <i v-else |
| | | class="el-icon-circle-close text-gray-400" /> |
| | | </div> |
| | | |
| | | <!-- 节点标题和描述 --> |
| | | <div class="node-info"> |
| | | <div class="node-title" :class="{ 'overdue-title': data.type === 'task' && data.status === 'overdue' }"> |
| | | <div class="node-title" |
| | | :class="{ 'overdue-title': data.type === 'task' && data.status === 'overdue' }"> |
| | | {{ node.label }} |
| | | <span v-if="data.type === 'task' && data.priority === 'high'" class="priority-tag">高优</span> |
| | | <span v-else-if="data.type === 'task' && data.priority === 'medium'" class="priority-tag medium">中优</span> |
| | | <span v-if="data.type === 'task' && data.priority === 'high'" |
| | | class="priority-tag">高优</span> |
| | | <span v-else-if="data.type === 'task' && data.priority === 'medium'" |
| | | class="priority-tag medium">中优</span> |
| | | </div> |
| | | <div v-if="data.description" class="node-description">{{ data.description }}</div> |
| | | |
| | | <div v-if="data.description" |
| | | class="node-description">{{ data.description }}</div> |
| | | <!-- 任务元信息 --> |
| | | <div v-if="data.type === 'task'" class="task-meta"> |
| | | <div v-if="data.type === 'task'" |
| | | class="task-meta"> |
| | | <span class="meta-item"> |
| | | <i class="el-icon-user"></i> |
| | | {{ data.assigneeName || '未分配' }} |
| | |
| | | </span> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 任务进度条 --> |
| | | <div v-if="data.type === 'task'" class="task-progress"> |
| | | <el-progress :percentage="data.progress || 0" :stroke-width="4" :show-text="false" /> |
| | | <div v-if="data.type === 'task'" |
| | | class="task-progress"> |
| | | <el-progress :percentage="data.progress || 0" |
| | | :stroke-width="4" |
| | | :show-text="false" /> |
| | | </div> |
| | | |
| | | <!-- 操作按钮 --> |
| | | <div class="node-actions"> |
| | | <el-button |
| | | v-if="data.type === 'task'" |
| | | type="text" |
| | | size="small" |
| | | icon="Edit" |
| | | @click.stop="handleEditTask(data)" |
| | | v-hasPermi="['oaSystem:task:edit']" |
| | | /> |
| | | <el-button |
| | | v-if="data.type === 'phase'" |
| | | type="text" |
| | | size="small" |
| | | icon="Plus" |
| | | @click.stop="handleAddTaskUnderPhase(data)" |
| | | v-hasPermi="['oaSystem:task:add']" |
| | | /> |
| | | <el-button |
| | | type="text" |
| | | size="small" |
| | | icon="Delete" |
| | | @click.stop="handleDeleteNode(data)" |
| | | v-hasPermi="['oaSystem:task:remove']" |
| | | /> |
| | | <el-button v-if="data.type === 'task'" |
| | | type="text" |
| | | size="small" |
| | | icon="Edit" |
| | | @click.stop="handleEditTask(data)" |
| | | v-hasPermi="['oaSystem:task:edit']" /> |
| | | <el-button v-if="data.type === 'phase'" |
| | | type="text" |
| | | size="small" |
| | | icon="Plus" |
| | | @click.stop="handleAddTaskUnderPhase(data)" |
| | | v-hasPermi="['oaSystem:task:add']" /> |
| | | <el-button type="text" |
| | | size="small" |
| | | icon="Delete" |
| | | @click.stop="handleDeleteNode(data)" |
| | | v-hasPermi="['oaSystem:task:remove']" /> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </el-tree> |
| | | </div> |
| | | |
| | | <!-- 右键菜单 --> |
| | | <div v-if="showContextMenu" :style="contextMenuStyle" class="context-menu"> |
| | | <div v-if="showContextMenu" |
| | | :style="contextMenuStyle" |
| | | class="context-menu"> |
| | | <el-menu @select="handleContextMenuSelect"> |
| | | <el-menu-item v-if="selectedNode.type === 'task'" index="edit">编辑任务</el-menu-item> |
| | | <el-menu-item v-if="selectedNode.type === 'phase'" index="addTask">添加子任务</el-menu-item> |
| | | <el-menu-item v-if="selectedNode.type === 'task'" |
| | | index="edit">编辑任务</el-menu-item> |
| | | <el-menu-item v-if="selectedNode.type === 'phase'" |
| | | index="addTask">添加子任务</el-menu-item> |
| | | <el-menu-item index="delete">删除</el-menu-item> |
| | | <el-menu-item index="expandAll">展开全部</el-menu-item> |
| | | <el-menu-item index="collapseAll">收起全部</el-menu-item> |
| | | </el-menu> |
| | | </div> |
| | | |
| | | <!-- 任务表单对话框 --> |
| | | <el-dialog :title="dialogTitle" v-model="dialogOpen" width="600px" append-to-body> |
| | | <el-form ref="taskFormRef" :model="taskForm" :rules="taskRules" label-width="80px"> |
| | | <el-form-item label="任务名称" prop="taskName"> |
| | | <el-input v-model="taskForm.taskName" placeholder="请输入任务名称" /> |
| | | <el-dialog :title="dialogTitle" |
| | | v-model="dialogOpen" |
| | | width="600px" |
| | | append-to-body> |
| | | <el-form ref="taskFormRef" |
| | | :model="taskForm" |
| | | :rules="taskRules" |
| | | label-width="80px"> |
| | | <el-form-item label="任务名称" |
| | | prop="taskName"> |
| | | <el-input v-model="taskForm.taskName" |
| | | placeholder="请输入任务名称" /> |
| | | </el-form-item> |
| | | <el-form-item label="负责人" prop="assigneeId"> |
| | | <el-input v-model="taskForm.assigneeId" placeholder="请输入负责人ID" /> |
| | | <el-form-item label="负责人" |
| | | prop="assigneeId"> |
| | | <el-input v-model="taskForm.assigneeId" |
| | | placeholder="请输入负责人ID" /> |
| | | </el-form-item> |
| | | <el-form-item label="开始日期" prop="startDate"> |
| | | <el-date-picker |
| | | v-model="taskForm.startDate" |
| | | type="date" |
| | | placeholder="选择开始日期" |
| | | style="width: 100%" |
| | | /> |
| | | <el-form-item label="开始日期" |
| | | prop="startDate"> |
| | | <el-date-picker v-model="taskForm.startDate" |
| | | type="date" |
| | | placeholder="选择开始日期" |
| | | style="width: 100%" /> |
| | | </el-form-item> |
| | | <el-form-item label="结束日期" prop="endDate"> |
| | | <el-date-picker |
| | | v-model="taskForm.endDate" |
| | | type="date" |
| | | placeholder="选择结束日期" |
| | | style="width: 100%" |
| | | /> |
| | | <el-form-item label="结束日期" |
| | | prop="endDate"> |
| | | <el-date-picker v-model="taskForm.endDate" |
| | | type="date" |
| | | placeholder="选择结束日期" |
| | | style="width: 100%" /> |
| | | </el-form-item> |
| | | <el-form-item label="优先级" prop="priority"> |
| | | <el-select v-model="taskForm.priority" placeholder="选择优先级"> |
| | | <el-option label="低" value="low" /> |
| | | <el-option label="中" value="medium" /> |
| | | <el-option label="高" value="high" /> |
| | | <el-form-item label="优先级" |
| | | prop="priority"> |
| | | <el-select v-model="taskForm.priority" |
| | | placeholder="选择优先级"> |
| | | <el-option label="低" |
| | | value="low" /> |
| | | <el-option label="中" |
| | | value="medium" /> |
| | | <el-option label="高" |
| | | value="high" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="进度" prop="progress"> |
| | | <el-input-number v-model="taskForm.progress" :min="0" :max="100" style="width: 100%" /> |
| | | <el-form-item label="进度" |
| | | prop="progress"> |
| | | <el-input-number v-model="taskForm.progress" |
| | | :min="0" |
| | | :max="100" |
| | | style="width: 100%" /> |
| | | </el-form-item> |
| | | <el-form-item label="描述" prop="description"> |
| | | <el-input v-model="taskForm.description" type="textarea" placeholder="请输入任务描述" /> |
| | | <el-form-item label="描述" |
| | | prop="description"> |
| | | <el-input v-model="taskForm.description" |
| | | type="textarea" |
| | | placeholder="请输入任务描述" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" |
| | | @click="submitTaskForm">确定</el-button> |
| | | <el-button @click="dialogOpen = false">取消</el-button> |
| | | <el-button type="primary" @click="submitTaskForm">确定</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, computed, watch, onMounted } from 'vue'; |
| | | import { ElMessage, ElMessageBox, ElMenu, ElMenuItem } from 'element-plus'; |
| | | // import { getProject, addTask, updateTask, deleteTask, deletePhase } from '@/api/oaSystem/projectManagement'; |
| | | import { ref, reactive, computed, watch, onMounted } from "vue"; |
| | | import { ElMessage, ElMessageBox, ElMenu, ElMenuItem } from "element-plus"; |
| | | // import { getProject, addTask, updateTask, deleteTask, deletePhase } from '@/api/oaSystem/projectManagement'; |
| | | |
| | | const props = defineProps({ |
| | | projectId: { |
| | | type: String, |
| | | required: true |
| | | } |
| | | }); |
| | | |
| | | const emit = defineEmits(['refresh']); |
| | | |
| | | // 组件状态 |
| | | const loading = ref(false); |
| | | const treeRef = ref(); |
| | | const showContextMenu = ref(false); |
| | | const contextMenuStyle = ref({}); |
| | | const selectedNode = ref({}); |
| | | const showFilter = ref(false); |
| | | const dialogOpen = ref(false); |
| | | const dialogTitle = ref(''); |
| | | const taskFormRef = ref(); |
| | | |
| | | // 筛选参数 |
| | | const filterParams = reactive({ |
| | | status: '', |
| | | assignee: '' |
| | | }); |
| | | |
| | | // 任务表单数据 |
| | | const taskForm = reactive({ |
| | | taskId: undefined, |
| | | taskName: '', |
| | | description: '', |
| | | startDate: '', |
| | | endDate: '', |
| | | assigneeId: '', |
| | | assigneeName: '', |
| | | status: 'notStarted', |
| | | progress: 0, |
| | | priority: 'medium', |
| | | phaseId: '', |
| | | projectId: props.projectId |
| | | }); |
| | | |
| | | // 表单验证规则 |
| | | const taskRules = { |
| | | taskName: [ |
| | | { required: true, message: '任务名称不能为空', trigger: 'blur' }, |
| | | { min: 2, max: 50, message: '任务名称长度在 2 到 50 个字符', trigger: 'blur' } |
| | | ], |
| | | startDate: [ |
| | | { required: true, message: '开始日期不能为空', trigger: 'change' } |
| | | ], |
| | | endDate: [ |
| | | { required: true, message: '结束日期不能为空', trigger: 'change' } |
| | | ], |
| | | assigneeId: [ |
| | | { required: true, message: '负责人不能为空', trigger: 'blur' } |
| | | ], |
| | | progress: [ |
| | | { required: true, message: '进度不能为空', trigger: 'blur' }, |
| | | { type: 'number', min: 0, max: 100, message: '进度必须在 0 到 100 之间', trigger: 'blur' } |
| | | ] |
| | | }; |
| | | |
| | | // 任务树数据 |
| | | const rawTaskTreeData = ref([]); |
| | | |
| | | // 模拟任务数据 |
| | | const mockTaskData = { |
| | | 'PRJ2023001': [ |
| | | { |
| | | phaseId: 'PHASE001', |
| | | phaseName: '需求分析', |
| | | startDate: '2023-11-01', |
| | | endDate: '2023-11-15', |
| | | status: 'completed', |
| | | tasks: [ |
| | | { |
| | | taskId: 'TASK001', |
| | | taskName: '需求调研', |
| | | description: '调研用户需求和业务流程', |
| | | startDate: '2023-11-01', |
| | | endDate: '2023-11-05', |
| | | assigneeId: 'USER001', |
| | | assigneeName: '张三', |
| | | status: 'completed', |
| | | progress: 100, |
| | | priority: 'medium' |
| | | }, |
| | | { |
| | | taskId: 'TASK002', |
| | | taskName: '需求文档编写', |
| | | description: '编写详细的需求规格说明书', |
| | | startDate: '2023-11-06', |
| | | endDate: '2023-11-15', |
| | | assigneeId: 'USER002', |
| | | assigneeName: '李四', |
| | | status: 'completed', |
| | | progress: 100, |
| | | priority: 'high' |
| | | } |
| | | ] |
| | | const props = defineProps({ |
| | | projectId: { |
| | | type: String, |
| | | required: true, |
| | | }, |
| | | { |
| | | phaseId: 'PHASE002', |
| | | phaseName: '系统设计', |
| | | startDate: '2023-11-16', |
| | | endDate: '2023-12-10', |
| | | status: 'completed', |
| | | tasks: [ |
| | | { |
| | | taskId: 'TASK003', |
| | | taskName: '系统架构设计', |
| | | description: '设计系统整体架构', |
| | | startDate: '2023-11-16', |
| | | endDate: '2023-11-25', |
| | | assigneeId: 'USER003', |
| | | assigneeName: '王五', |
| | | status: 'completed', |
| | | progress: 100, |
| | | priority: 'high' |
| | | }, |
| | | { |
| | | taskId: 'TASK004', |
| | | taskName: '数据库设计', |
| | | description: '设计数据库表结构和关系', |
| | | startDate: '2023-11-26', |
| | | endDate: '2023-12-10', |
| | | assigneeId: 'USER004', |
| | | assigneeName: '赵六', |
| | | status: 'completed', |
| | | progress: 100, |
| | | priority: 'medium' |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | phaseId: 'PHASE003', |
| | | phaseName: '开发实现', |
| | | startDate: '2023-12-11', |
| | | endDate: '2024-01-31', |
| | | status: 'inProgress', |
| | | tasks: [ |
| | | { |
| | | taskId: 'TASK005', |
| | | taskName: '前端开发', |
| | | description: '开发用户界面和交互逻辑', |
| | | startDate: '2023-12-11', |
| | | endDate: '2024-01-15', |
| | | assigneeId: 'USER005', |
| | | assigneeName: '钱七', |
| | | status: 'inProgress', |
| | | progress: 70, |
| | | priority: 'high' |
| | | }, |
| | | { |
| | | taskId: 'TASK006', |
| | | taskName: '后端开发', |
| | | description: '开发业务逻辑和API接口', |
| | | startDate: '2023-12-11', |
| | | endDate: '2024-01-20', |
| | | assigneeId: 'USER006', |
| | | assigneeName: '孙八', |
| | | status: 'inProgress', |
| | | progress: 60, |
| | | priority: 'high' |
| | | } |
| | | ] |
| | | } |
| | | ], |
| | | // 默认数据 |
| | | default: [ |
| | | { |
| | | phaseId: 'PHASE_DEFAULT1', |
| | | phaseName: '准备阶段', |
| | | startDate: '2023-01-01', |
| | | endDate: '2023-03-31', |
| | | status: 'completed', |
| | | tasks: [ |
| | | { |
| | | taskId: 'TASK_DEFAULT1', |
| | | taskName: '项目启动', |
| | | description: '召开项目启动会议', |
| | | startDate: '2023-01-01', |
| | | endDate: '2023-01-05', |
| | | assigneeId: 'USER_DEFAULT1', |
| | | assigneeName: '负责人A', |
| | | status: 'completed', |
| | | progress: 100, |
| | | priority: 'high' |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | phaseId: 'PHASE_DEFAULT2', |
| | | phaseName: '执行阶段', |
| | | startDate: '2023-04-01', |
| | | endDate: '2023-09-30', |
| | | status: 'inProgress', |
| | | tasks: [ |
| | | { |
| | | taskId: 'TASK_DEFAULT2', |
| | | taskName: '核心功能开发', |
| | | description: '开发系统核心功能模块', |
| | | startDate: '2023-04-01', |
| | | endDate: '2023-06-30', |
| | | assigneeId: 'USER_DEFAULT2', |
| | | assigneeName: '负责人B', |
| | | status: 'inProgress', |
| | | progress: 50, |
| | | priority: 'high' |
| | | } |
| | | ] |
| | | } |
| | | ] |
| | | }; |
| | | |
| | | const taskTreeData = computed(() => { |
| | | // 应用筛选条件 |
| | | if (!showFilter.value || (!filterParams.status && !filterParams.assignee)) { |
| | | return rawTaskTreeData.value; |
| | | } |
| | | |
| | | // 深拷贝原始数据以避免修改 |
| | | const filteredData = JSON.parse(JSON.stringify(rawTaskTreeData.value)); |
| | | |
| | | // 递归筛选节点 |
| | | const filterNodes = (nodes) => { |
| | | const result = []; |
| | | |
| | | nodes.forEach(node => { |
| | | // 对于阶段节点,检查其子任务是否符合筛选条件 |
| | | if (node.type === 'phase' && node.children) { |
| | | const filteredChildren = filterNodes(node.children); |
| | | if (filteredChildren.length > 0) { |
| | | // 保留至少有一个子任务符合条件的阶段 |
| | | node.children = filteredChildren; |
| | | result.push(node); |
| | | } |
| | | } |
| | | // 对于任务节点,直接应用筛选条件 |
| | | else if (node.type === 'task') { |
| | | const statusMatch = !filterParams.status || node.status === filterParams.status; |
| | | const assigneeMatch = !filterParams.assignee || |
| | | (node.assigneeName && node.assigneeName.includes(filterParams.assignee)); |
| | | |
| | | if (statusMatch && assigneeMatch) { |
| | | result.push(node); |
| | | } |
| | | } |
| | | }); |
| | | |
| | | return result; |
| | | }; |
| | | |
| | | return filterNodes(filteredData); |
| | | }); |
| | | |
| | | // 树节点配置 |
| | | const defaultProps = { |
| | | children: 'children', |
| | | label: (data) => { |
| | | if (data.type === 'phase') { |
| | | return `${data.phaseName}`; |
| | | } else { |
| | | return `${data.taskName}`; |
| | | } |
| | | } |
| | | }; |
| | | |
| | | // 加载任务树数据 |
| | | const loadTaskTree = async () => { |
| | | loading.value = true; |
| | | // try { |
| | | // const { data } = await getProject(props.projectId); |
| | | // rawTaskTreeData.value = buildTaskTree(data.phases || []); |
| | | // } catch (error) { |
| | | // ElMessage.error('加载任务树失败'); |
| | | // console.error('加载任务树失败:', error); |
| | | // } finally { |
| | | // loading.value = false; |
| | | // } |
| | | try { |
| | | // 模拟网络延迟 |
| | | await new Promise(resolve => setTimeout(resolve, 500)); |
| | | |
| | | // 使用模拟数据替代API请求 |
| | | const phases = mockTaskData[props.projectId] || mockTaskData.default; |
| | | rawTaskTreeData.value = buildTaskTree(phases); |
| | | } catch (error) { |
| | | ElMessage.error('加载任务树失败'); |
| | | console.error('加载任务树失败:', error); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | |
| | | // 构建任务树 |
| | | const buildTaskTree = (phases) => { |
| | | return phases.map(phase => ({ |
| | | nodeId: phase.phaseId, |
| | | phaseId: phase.phaseId, |
| | | phaseName: phase.phaseName, |
| | | type: 'phase', |
| | | children: (phase.tasks || []).map(task => ({ |
| | | nodeId: task.taskId, |
| | | taskId: task.taskId, |
| | | taskName: task.taskName, |
| | | description: task.description, |
| | | startDate: task.startDate, |
| | | endDate: task.endDate, |
| | | assigneeId: task.assigneeId, |
| | | assigneeName: task.assigneeName, |
| | | status: task.status, |
| | | progress: task.progress, |
| | | priority: task.priority, |
| | | phaseId: task.phaseId, |
| | | projectId: props.projectId, |
| | | type: 'task' |
| | | })) |
| | | })); |
| | | }; |
| | | |
| | | // 格式化日期范围 |
| | | const formatDateRange = (startDate, endDate) => { |
| | | if (!startDate || !endDate) return ''; |
| | | return `${startDate} - ${endDate}`; |
| | | }; |
| | | |
| | | // 刷新树 |
| | | const refreshTree = () => { |
| | | loadTaskTree(); |
| | | // 通知父组件刷新数据 |
| | | emit('refresh'); |
| | | }; |
| | | |
| | | // 切换筛选面板 |
| | | const toggleFilter = () => { |
| | | showFilter.value = !showFilter.value; |
| | | }; |
| | | |
| | | // 应用筛选 |
| | | const filterTree = () => { |
| | | // 筛选逻辑已经在computed中实现 |
| | | }; |
| | | |
| | | // 重置筛选 |
| | | const resetFilter = () => { |
| | | filterParams.status = ''; |
| | | filterParams.assignee = ''; |
| | | }; |
| | | |
| | | // 处理节点点击 |
| | | const handleNodeClick = (data, node) => { |
| | | // 切换展开/收起状态 |
| | | if (data.type === 'phase') { |
| | | node.expanded = !node.expanded; |
| | | } |
| | | }; |
| | | |
| | | // 处理右键菜单 |
| | | const handleContextMenu = (event, data) => { |
| | | event.preventDefault(); |
| | | selectedNode.value = data; |
| | | contextMenuStyle.value = { |
| | | position: 'fixed', |
| | | left: `${event.clientX}px`, |
| | | top: `${event.clientY}px`, |
| | | zIndex: 1000 |
| | | }; |
| | | showContextMenu.value = true; |
| | | }; |
| | | |
| | | // 处理右键菜单选择 |
| | | const handleContextMenuSelect = (index) => { |
| | | showContextMenu.value = false; |
| | | switch (index) { |
| | | case 'edit': |
| | | if (selectedNode.value.type === 'task') { |
| | | handleEditTask(selectedNode.value); |
| | | } |
| | | break; |
| | | case 'addTask': |
| | | if (selectedNode.value.type === 'phase') { |
| | | handleAddTaskUnderPhase(selectedNode.value); |
| | | } |
| | | break; |
| | | case 'delete': |
| | | handleDeleteNode(selectedNode.value); |
| | | break; |
| | | case 'expandAll': |
| | | treeRef.value?.expandAll(); |
| | | break; |
| | | case 'collapseAll': |
| | | treeRef.value?.collapseAll(); |
| | | break; |
| | | } |
| | | }; |
| | | |
| | | // 添加任务 |
| | | const handleAddTask = () => { |
| | | resetTaskForm(); |
| | | dialogTitle.value = '添加任务'; |
| | | dialogOpen.value = true; |
| | | }; |
| | | |
| | | // 在指定阶段下添加任务 |
| | | const handleAddTaskUnderPhase = (phase) => { |
| | | resetTaskForm(); |
| | | taskForm.phaseId = phase.phaseId; |
| | | dialogTitle.value = '添加子任务'; |
| | | dialogOpen.value = true; |
| | | }; |
| | | |
| | | // 编辑任务 |
| | | const handleEditTask = (task) => { |
| | | resetTaskForm(); |
| | | Object.assign(taskForm, { ...task }); |
| | | dialogTitle.value = '编辑任务'; |
| | | dialogOpen.value = true; |
| | | }; |
| | | |
| | | // 删除节点 |
| | | const handleDeleteNode = async (node) => { |
| | | const confirmMessage = node.type === 'phase' |
| | | ? `确定要删除阶段 "${node.phaseName}" 及其所有子任务吗?` |
| | | : `确定要删除任务 "${node.taskName}" 吗?`; |
| | | |
| | | await ElMessageBox.confirm(confirmMessage, '确认操作', { |
| | | confirmButtonText: '确定', |
| | | cancelButtonText: '取消', |
| | | type: 'warning' |
| | | }).catch(() => { |
| | | throw new Error('取消删除'); |
| | | }); |
| | | |
| | | try { |
| | | if (node.type === 'phase') { |
| | | await deletePhase(node.phaseId); |
| | | } else { |
| | | await deleteTask(node.taskId); |
| | | |
| | | const emit = defineEmits(["refresh"]); |
| | | |
| | | // 组件状态 |
| | | const loading = ref(false); |
| | | const treeRef = ref(); |
| | | const showContextMenu = ref(false); |
| | | const contextMenuStyle = ref({}); |
| | | const selectedNode = ref({}); |
| | | const showFilter = ref(false); |
| | | const dialogOpen = ref(false); |
| | | const dialogTitle = ref(""); |
| | | const taskFormRef = ref(); |
| | | |
| | | // 筛选参数 |
| | | const filterParams = reactive({ |
| | | status: "", |
| | | assignee: "", |
| | | }); |
| | | |
| | | // 任务表单数据 |
| | | const taskForm = reactive({ |
| | | taskId: undefined, |
| | | taskName: "", |
| | | description: "", |
| | | startDate: "", |
| | | endDate: "", |
| | | assigneeId: "", |
| | | assigneeName: "", |
| | | status: "notStarted", |
| | | progress: 0, |
| | | priority: "medium", |
| | | phaseId: "", |
| | | projectId: props.projectId, |
| | | }); |
| | | |
| | | // 表单验证规则 |
| | | const taskRules = { |
| | | taskName: [ |
| | | { required: true, message: "任务名称不能为空", trigger: "blur" }, |
| | | { |
| | | min: 2, |
| | | max: 50, |
| | | message: "任务名称长度在 2 到 50 个字符", |
| | | trigger: "blur", |
| | | }, |
| | | ], |
| | | startDate: [ |
| | | { required: true, message: "开始日期不能为空", trigger: "change" }, |
| | | ], |
| | | endDate: [{ required: true, message: "结束日期不能为空", trigger: "change" }], |
| | | assigneeId: [{ required: true, message: "负责人不能为空", trigger: "blur" }], |
| | | progress: [ |
| | | { required: true, message: "进度不能为空", trigger: "blur" }, |
| | | { |
| | | type: "number", |
| | | min: 0, |
| | | max: 100, |
| | | message: "进度必须在 0 到 100 之间", |
| | | trigger: "blur", |
| | | }, |
| | | ], |
| | | }; |
| | | |
| | | // 任务树数据 |
| | | const rawTaskTreeData = ref([]); |
| | | |
| | | // 模拟任务数据 |
| | | const mockTaskData = { |
| | | PRJ2023001: [ |
| | | { |
| | | phaseId: "PHASE001", |
| | | phaseName: "需求分析", |
| | | startDate: "2023-11-01", |
| | | endDate: "2023-11-15", |
| | | status: "completed", |
| | | tasks: [ |
| | | { |
| | | taskId: "TASK001", |
| | | taskName: "需求调研", |
| | | description: "调研用户需求和业务流程", |
| | | startDate: "2023-11-01", |
| | | endDate: "2023-11-05", |
| | | assigneeId: "USER001", |
| | | assigneeName: "张三", |
| | | status: "completed", |
| | | progress: 100, |
| | | priority: "medium", |
| | | }, |
| | | { |
| | | taskId: "TASK002", |
| | | taskName: "需求文档编写", |
| | | description: "编写详细的需求规格说明书", |
| | | startDate: "2023-11-06", |
| | | endDate: "2023-11-15", |
| | | assigneeId: "USER002", |
| | | assigneeName: "李四", |
| | | status: "completed", |
| | | progress: 100, |
| | | priority: "high", |
| | | }, |
| | | ], |
| | | }, |
| | | { |
| | | phaseId: "PHASE002", |
| | | phaseName: "系统设计", |
| | | startDate: "2023-11-16", |
| | | endDate: "2023-12-10", |
| | | status: "completed", |
| | | tasks: [ |
| | | { |
| | | taskId: "TASK003", |
| | | taskName: "系统架构设计", |
| | | description: "设计系统整体架构", |
| | | startDate: "2023-11-16", |
| | | endDate: "2023-11-25", |
| | | assigneeId: "USER003", |
| | | assigneeName: "王五", |
| | | status: "completed", |
| | | progress: 100, |
| | | priority: "high", |
| | | }, |
| | | { |
| | | taskId: "TASK004", |
| | | taskName: "数据库设计", |
| | | description: "设计数据库表结构和关系", |
| | | startDate: "2023-11-26", |
| | | endDate: "2023-12-10", |
| | | assigneeId: "USER004", |
| | | assigneeName: "赵六", |
| | | status: "completed", |
| | | progress: 100, |
| | | priority: "medium", |
| | | }, |
| | | ], |
| | | }, |
| | | { |
| | | phaseId: "PHASE003", |
| | | phaseName: "开发实现", |
| | | startDate: "2023-12-11", |
| | | endDate: "2024-01-31", |
| | | status: "inProgress", |
| | | tasks: [ |
| | | { |
| | | taskId: "TASK005", |
| | | taskName: "前端开发", |
| | | description: "开发用户界面和交互逻辑", |
| | | startDate: "2023-12-11", |
| | | endDate: "2024-01-15", |
| | | assigneeId: "USER005", |
| | | assigneeName: "钱七", |
| | | status: "inProgress", |
| | | progress: 70, |
| | | priority: "high", |
| | | }, |
| | | { |
| | | taskId: "TASK006", |
| | | taskName: "后端开发", |
| | | description: "开发业务逻辑和API接口", |
| | | startDate: "2023-12-11", |
| | | endDate: "2024-01-20", |
| | | assigneeId: "USER006", |
| | | assigneeName: "孙八", |
| | | status: "inProgress", |
| | | progress: 60, |
| | | priority: "high", |
| | | }, |
| | | ], |
| | | }, |
| | | ], |
| | | // 默认数据 |
| | | default: [ |
| | | { |
| | | phaseId: "PHASE_DEFAULT1", |
| | | phaseName: "准备阶段", |
| | | startDate: "2023-01-01", |
| | | endDate: "2023-03-31", |
| | | status: "completed", |
| | | tasks: [ |
| | | { |
| | | taskId: "TASK_DEFAULT1", |
| | | taskName: "项目启动", |
| | | description: "召开项目启动会议", |
| | | startDate: "2023-01-01", |
| | | endDate: "2023-01-05", |
| | | assigneeId: "USER_DEFAULT1", |
| | | assigneeName: "负责人A", |
| | | status: "completed", |
| | | progress: 100, |
| | | priority: "high", |
| | | }, |
| | | ], |
| | | }, |
| | | { |
| | | phaseId: "PHASE_DEFAULT2", |
| | | phaseName: "执行阶段", |
| | | startDate: "2023-04-01", |
| | | endDate: "2023-09-30", |
| | | status: "inProgress", |
| | | tasks: [ |
| | | { |
| | | taskId: "TASK_DEFAULT2", |
| | | taskName: "核心功能开发", |
| | | description: "开发系统核心功能模块", |
| | | startDate: "2023-04-01", |
| | | endDate: "2023-06-30", |
| | | assigneeId: "USER_DEFAULT2", |
| | | assigneeName: "负责人B", |
| | | status: "inProgress", |
| | | progress: 50, |
| | | priority: "high", |
| | | }, |
| | | ], |
| | | }, |
| | | ], |
| | | }; |
| | | |
| | | const taskTreeData = computed(() => { |
| | | // 应用筛选条件 |
| | | if (!showFilter.value || (!filterParams.status && !filterParams.assignee)) { |
| | | return rawTaskTreeData.value; |
| | | } |
| | | ElMessage.success('删除成功'); |
| | | refreshTree(); |
| | | } catch (error) { |
| | | if (error.message !== '取消删除') { |
| | | ElMessage.error('删除失败'); |
| | | console.error('删除失败:', error); |
| | | |
| | | // 深拷贝原始数据以避免修改 |
| | | const filteredData = JSON.parse(JSON.stringify(rawTaskTreeData.value)); |
| | | |
| | | // 递归筛选节点 |
| | | const filterNodes = nodes => { |
| | | const result = []; |
| | | |
| | | nodes.forEach(node => { |
| | | // 对于阶段节点,检查其子任务是否符合筛选条件 |
| | | if (node.type === "phase" && node.children) { |
| | | const filteredChildren = filterNodes(node.children); |
| | | if (filteredChildren.length > 0) { |
| | | // 保留至少有一个子任务符合条件的阶段 |
| | | node.children = filteredChildren; |
| | | result.push(node); |
| | | } |
| | | } |
| | | // 对于任务节点,直接应用筛选条件 |
| | | else if (node.type === "task") { |
| | | const statusMatch = |
| | | !filterParams.status || node.status === filterParams.status; |
| | | const assigneeMatch = |
| | | !filterParams.assignee || |
| | | (node.assigneeName && |
| | | node.assigneeName.includes(filterParams.assignee)); |
| | | |
| | | if (statusMatch && assigneeMatch) { |
| | | result.push(node); |
| | | } |
| | | } |
| | | }); |
| | | |
| | | return result; |
| | | }; |
| | | |
| | | return filterNodes(filteredData); |
| | | }); |
| | | |
| | | // 树节点配置 |
| | | const defaultProps = { |
| | | children: "children", |
| | | label: data => { |
| | | if (data.type === "phase") { |
| | | return `${data.phaseName}`; |
| | | } else { |
| | | return `${data.taskName}`; |
| | | } |
| | | }, |
| | | }; |
| | | |
| | | // 加载任务树数据 |
| | | const loadTaskTree = async () => { |
| | | loading.value = true; |
| | | // try { |
| | | // const { data } = await getProject(props.projectId); |
| | | // rawTaskTreeData.value = buildTaskTree(data.phases || []); |
| | | // } catch (error) { |
| | | // ElMessage.error('加载任务树失败'); |
| | | // console.error('加载任务树失败:', error); |
| | | // } finally { |
| | | // loading.value = false; |
| | | // } |
| | | try { |
| | | // 模拟网络延迟 |
| | | await new Promise(resolve => setTimeout(resolve, 500)); |
| | | |
| | | // 使用模拟数据替代API请求 |
| | | const phases = mockTaskData[props.projectId] || mockTaskData.default; |
| | | rawTaskTreeData.value = buildTaskTree(phases); |
| | | } catch (error) { |
| | | ElMessage.error("加载任务树失败"); |
| | | console.error("加载任务树失败:", error); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | }; |
| | | }; |
| | | |
| | | // 重置任务表单 |
| | | const resetTaskForm = () => { |
| | | taskForm.taskId = undefined; |
| | | taskForm.taskName = ''; |
| | | taskForm.description = ''; |
| | | taskForm.startDate = ''; |
| | | taskForm.endDate = ''; |
| | | taskForm.assigneeId = ''; |
| | | taskForm.assigneeName = ''; |
| | | taskForm.status = 'notStarted'; |
| | | taskForm.progress = 0; |
| | | taskForm.priority = 'medium'; |
| | | taskForm.phaseId = ''; |
| | | taskForm.projectId = props.projectId; |
| | | |
| | | if (taskFormRef.value) { |
| | | taskFormRef.value.resetFields(); |
| | | } |
| | | }; |
| | | // 构建任务树 |
| | | const buildTaskTree = phases => { |
| | | return phases.map(phase => ({ |
| | | nodeId: phase.phaseId, |
| | | phaseId: phase.phaseId, |
| | | phaseName: phase.phaseName, |
| | | type: "phase", |
| | | children: (phase.tasks || []).map(task => ({ |
| | | nodeId: task.taskId, |
| | | taskId: task.taskId, |
| | | taskName: task.taskName, |
| | | description: task.description, |
| | | startDate: task.startDate, |
| | | endDate: task.endDate, |
| | | assigneeId: task.assigneeId, |
| | | assigneeName: task.assigneeName, |
| | | status: task.status, |
| | | progress: task.progress, |
| | | priority: task.priority, |
| | | phaseId: task.phaseId, |
| | | projectId: props.projectId, |
| | | type: "task", |
| | | })), |
| | | })); |
| | | }; |
| | | |
| | | // 提交任务表单 |
| | | const submitTaskForm = async () => { |
| | | try { |
| | | await taskFormRef.value.validate(); |
| | | |
| | | if (taskForm.taskId) { |
| | | await updateTask(taskForm); |
| | | ElMessage.success('修改任务成功'); |
| | | } else { |
| | | await addTask(taskForm); |
| | | ElMessage.success('添加任务成功'); |
| | | } |
| | | |
| | | dialogOpen.value = false; |
| | | refreshTree(); |
| | | } catch (error) { |
| | | console.error('提交表单失败:', error); |
| | | } |
| | | }; |
| | | // 格式化日期范围 |
| | | const formatDateRange = (startDate, endDate) => { |
| | | if (!startDate || !endDate) return ""; |
| | | return `${startDate} - ${endDate}`; |
| | | }; |
| | | |
| | | // 点击其他区域关闭右键菜单 |
| | | document.addEventListener('click', () => { |
| | | if (showContextMenu.value) { |
| | | showContextMenu.value = false; |
| | | } |
| | | }); |
| | | |
| | | // 监听项目ID变化 |
| | | watch(() => props.projectId, (newProjectId) => { |
| | | if (newProjectId) { |
| | | // 刷新树 |
| | | const refreshTree = () => { |
| | | loadTaskTree(); |
| | | } |
| | | }); |
| | | // 通知父组件刷新数据 |
| | | emit("refresh"); |
| | | }; |
| | | |
| | | // 初始化 |
| | | onMounted(() => { |
| | | loadTaskTree(); |
| | | }); |
| | | // 切换筛选面板 |
| | | const toggleFilter = () => { |
| | | showFilter.value = !showFilter.value; |
| | | }; |
| | | |
| | | // 应用筛选 |
| | | const filterTree = () => { |
| | | // 筛选逻辑已经在computed中实现 |
| | | }; |
| | | |
| | | // 重置筛选 |
| | | const resetFilter = () => { |
| | | filterParams.status = ""; |
| | | filterParams.assignee = ""; |
| | | }; |
| | | |
| | | // 处理节点点击 |
| | | const handleNodeClick = (data, node) => { |
| | | // 切换展开/收起状态 |
| | | if (data.type === "phase") { |
| | | node.expanded = !node.expanded; |
| | | } |
| | | }; |
| | | |
| | | // 处理右键菜单 |
| | | const handleContextMenu = (event, data) => { |
| | | event.preventDefault(); |
| | | selectedNode.value = data; |
| | | contextMenuStyle.value = { |
| | | position: "fixed", |
| | | left: `${event.clientX}px`, |
| | | top: `${event.clientY}px`, |
| | | zIndex: 1000, |
| | | }; |
| | | showContextMenu.value = true; |
| | | }; |
| | | |
| | | // 处理右键菜单选择 |
| | | const handleContextMenuSelect = index => { |
| | | showContextMenu.value = false; |
| | | switch (index) { |
| | | case "edit": |
| | | if (selectedNode.value.type === "task") { |
| | | handleEditTask(selectedNode.value); |
| | | } |
| | | break; |
| | | case "addTask": |
| | | if (selectedNode.value.type === "phase") { |
| | | handleAddTaskUnderPhase(selectedNode.value); |
| | | } |
| | | break; |
| | | case "delete": |
| | | handleDeleteNode(selectedNode.value); |
| | | break; |
| | | case "expandAll": |
| | | treeRef.value?.expandAll(); |
| | | break; |
| | | case "collapseAll": |
| | | treeRef.value?.collapseAll(); |
| | | break; |
| | | } |
| | | }; |
| | | |
| | | // 添加任务 |
| | | const handleAddTask = () => { |
| | | resetTaskForm(); |
| | | dialogTitle.value = "添加任务"; |
| | | dialogOpen.value = true; |
| | | }; |
| | | |
| | | // 在指定阶段下添加任务 |
| | | const handleAddTaskUnderPhase = phase => { |
| | | resetTaskForm(); |
| | | taskForm.phaseId = phase.phaseId; |
| | | dialogTitle.value = "添加子任务"; |
| | | dialogOpen.value = true; |
| | | }; |
| | | |
| | | // 编辑任务 |
| | | const handleEditTask = task => { |
| | | resetTaskForm(); |
| | | Object.assign(taskForm, { ...task }); |
| | | dialogTitle.value = "编辑任务"; |
| | | dialogOpen.value = true; |
| | | }; |
| | | |
| | | // 删除节点 |
| | | const handleDeleteNode = async node => { |
| | | const confirmMessage = |
| | | node.type === "phase" |
| | | ? `确定要删除阶段 "${node.phaseName}" 及其所有子任务吗?` |
| | | : `确定要删除任务 "${node.taskName}" 吗?`; |
| | | |
| | | await ElMessageBox.confirm(confirmMessage, "确认操作", { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | | type: "warning", |
| | | }).catch(() => { |
| | | throw new Error("取消删除"); |
| | | }); |
| | | |
| | | try { |
| | | if (node.type === "phase") { |
| | | await deletePhase(node.phaseId); |
| | | } else { |
| | | await deleteTask(node.taskId); |
| | | } |
| | | ElMessage.success("删除成功"); |
| | | refreshTree(); |
| | | } catch (error) { |
| | | if (error.message !== "取消删除") { |
| | | ElMessage.error("删除失败"); |
| | | console.error("删除失败:", error); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | // 重置任务表单 |
| | | const resetTaskForm = () => { |
| | | taskForm.taskId = undefined; |
| | | taskForm.taskName = ""; |
| | | taskForm.description = ""; |
| | | taskForm.startDate = ""; |
| | | taskForm.endDate = ""; |
| | | taskForm.assigneeId = ""; |
| | | taskForm.assigneeName = ""; |
| | | taskForm.status = "notStarted"; |
| | | taskForm.progress = 0; |
| | | taskForm.priority = "medium"; |
| | | taskForm.phaseId = ""; |
| | | taskForm.projectId = props.projectId; |
| | | |
| | | if (taskFormRef.value) { |
| | | taskFormRef.value.resetFields(); |
| | | } |
| | | }; |
| | | |
| | | // 提交任务表单 |
| | | const submitTaskForm = async () => { |
| | | try { |
| | | await taskFormRef.value.validate(); |
| | | |
| | | if (taskForm.taskId) { |
| | | await updateTask(taskForm); |
| | | ElMessage.success("修改任务成功"); |
| | | } else { |
| | | await addTask(taskForm); |
| | | ElMessage.success("添加任务成功"); |
| | | } |
| | | |
| | | dialogOpen.value = false; |
| | | refreshTree(); |
| | | } catch (error) { |
| | | console.error("提交表单失败:", error); |
| | | } |
| | | }; |
| | | |
| | | // 点击其他区域关闭右键菜单 |
| | | document.addEventListener("click", () => { |
| | | if (showContextMenu.value) { |
| | | showContextMenu.value = false; |
| | | } |
| | | }); |
| | | |
| | | // 监听项目ID变化 |
| | | watch( |
| | | () => props.projectId, |
| | | newProjectId => { |
| | | if (newProjectId) { |
| | | loadTaskTree(); |
| | | } |
| | | } |
| | | ); |
| | | |
| | | // 初始化 |
| | | onMounted(() => { |
| | | loadTaskTree(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .task-tree-container { |
| | | padding: 10px; |
| | | } |
| | | .task-tree-container { |
| | | padding: 10px; |
| | | } |
| | | |
| | | .tree-actions { |
| | | display: flex; |
| | | gap: 10px; |
| | | align-items: center; |
| | | } |
| | | .tree-actions { |
| | | display: flex; |
| | | gap: 10px; |
| | | align-items: center; |
| | | } |
| | | |
| | | .filter-section { |
| | | background: #f5f7fa; |
| | | padding: 10px; |
| | | border-radius: 4px; |
| | | } |
| | | .filter-section { |
| | | background: #f5f7fa; |
| | | padding: 10px; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .tree-content { |
| | | background: #fff; |
| | | border: 1px solid #ebeef5; |
| | | border-radius: 4px; |
| | | padding: 10px; |
| | | max-height: 600px; |
| | | overflow-y: auto; |
| | | } |
| | | .tree-content { |
| | | background: #fff; |
| | | border: 1px solid #ebeef5; |
| | | border-radius: 4px; |
| | | padding: 10px; |
| | | max-height: 600px; |
| | | overflow-y: auto; |
| | | } |
| | | |
| | | .tree-node-content { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 5px 0; |
| | | min-height: 40px; |
| | | } |
| | | .tree-node-content { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 5px 0; |
| | | min-height: 40px; |
| | | } |
| | | |
| | | .phase-node { |
| | | font-weight: bold; |
| | | color: #409eff; |
| | | } |
| | | .phase-node { |
| | | font-weight: bold; |
| | | color: #409eff; |
| | | } |
| | | |
| | | .task-node { |
| | | color: #606266; |
| | | } |
| | | .task-node { |
| | | color: #606266; |
| | | } |
| | | |
| | | .node-icon { |
| | | display: flex; |
| | | align-items: center; |
| | | width: 20px; |
| | | } |
| | | .node-icon { |
| | | display: flex; |
| | | align-items: center; |
| | | width: 20px; |
| | | } |
| | | |
| | | .node-info { |
| | | flex: 1; |
| | | min-width: 0; |
| | | } |
| | | .node-info { |
| | | flex: 1; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .node-title { |
| | | font-weight: 500; |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | margin-bottom: 2px; |
| | | } |
| | | .node-title { |
| | | font-weight: 500; |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | margin-bottom: 2px; |
| | | } |
| | | |
| | | .overdue-title { |
| | | color: #f56c6c; |
| | | font-weight: bold; |
| | | } |
| | | .overdue-title { |
| | | color: #f56c6c; |
| | | font-weight: bold; |
| | | } |
| | | |
| | | .priority-tag { |
| | | background: #f56c6c; |
| | | color: white; |
| | | font-size: 10px; |
| | | padding: 1px 4px; |
| | | border-radius: 2px; |
| | | margin-left: 5px; |
| | | } |
| | | .priority-tag { |
| | | background: #f56c6c; |
| | | color: white; |
| | | font-size: 10px; |
| | | padding: 1px 4px; |
| | | border-radius: 2px; |
| | | margin-left: 5px; |
| | | } |
| | | |
| | | .priority-tag.medium { |
| | | background: #e6a23c; |
| | | } |
| | | .priority-tag.medium { |
| | | background: #e6a23c; |
| | | } |
| | | |
| | | .node-description { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | } |
| | | .node-description { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | } |
| | | |
| | | .task-meta { |
| | | display: flex; |
| | | gap: 15px; |
| | | font-size: 12px; |
| | | color: #909399; |
| | | margin-top: 2px; |
| | | } |
| | | .task-meta { |
| | | display: flex; |
| | | gap: 15px; |
| | | font-size: 12px; |
| | | color: #909399; |
| | | margin-top: 2px; |
| | | } |
| | | |
| | | .meta-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 3px; |
| | | } |
| | | .meta-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 3px; |
| | | } |
| | | |
| | | .task-progress { |
| | | width: 120px; |
| | | margin: 0 10px; |
| | | } |
| | | .task-progress { |
| | | width: 120px; |
| | | margin: 0 10px; |
| | | } |
| | | |
| | | .node-actions { |
| | | display: flex; |
| | | gap: 5px; |
| | | opacity: 0; |
| | | transition: opacity 0.3s; |
| | | } |
| | | .node-actions { |
| | | display: flex; |
| | | gap: 5px; |
| | | opacity: 0; |
| | | transition: opacity 0.3s; |
| | | } |
| | | |
| | | .tree-node-content:hover .node-actions { |
| | | opacity: 1; |
| | | } |
| | | .tree-node-content:hover .node-actions { |
| | | opacity: 1; |
| | | } |
| | | |
| | | .context-menu { |
| | | background: white; |
| | | border: 1px solid #ebeef5; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); |
| | | border-radius: 4px; |
| | | } |
| | | .context-menu { |
| | | background: white; |
| | | border: 1px solid #ebeef5; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .context-menu .el-menu { |
| | | min-width: 120px; |
| | | border: none; |
| | | } |
| | | .context-menu .el-menu { |
| | | min-width: 120px; |
| | | border: none; |
| | | } |
| | | |
| | | .context-menu .el-menu-item { |
| | | padding: 0 15px; |
| | | height: 36px; |
| | | line-height: 36px; |
| | | } |
| | | .context-menu .el-menu-item { |
| | | padding: 0 15px; |
| | | height: 36px; |
| | | line-height: 36px; |
| | | } |
| | | |
| | | .context-menu .el-menu-item:hover { |
| | | background-color: #f5f7fa; |
| | | } |
| | | .context-menu .el-menu-item:hover { |
| | | background-color: #f5f7fa; |
| | | } |
| | | |
| | | .text-gray-400 { |
| | | color: #c0c4cc; |
| | | } |
| | | .text-gray-400 { |
| | | color: #c0c4cc; |
| | | } |
| | | </style> |