您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
优化对不同普通任务的适配
// ==UserScript== // @name 大管加资源占用视角任务甘特图显示 // @namespace http://tampermonkey.net/ // @version V1.4 // @description 优化对不同普通任务的适配 // @author 月夜箫声 // @match https://www.erplus.co/web/pc-link/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @connect www.erplus.co // @license MIT // @homepage https://gf.qytechs.cn/zh-CN/scripts/541426-大管加资源占用视角任务甘特图显示 // ==/UserScript== (function() { 'use strict'; // 添加全局样式 GM_addStyle(` #ganttContainer { position: fixed; top: 0px; right: 0px; width: 100%; height: 100vh; background: white; border: 1px solid #ddd; border-radius: 4px; padding: 10px; box-shadow: 0 4px 20px rgba(0,0,0,0.25); z-index: 9999; overflow: auto; font-family: Arial, sans-serif; display: none; font-size: 12px; } #ganttHeader { display: block; position: fixed; height: 40px; top: 0px; justify-content: space-between; align-items: center; margin-bottom: 5px; padding-bottom: 5px; padding-top: 10px; } #ganttTitle { font-size: 14px; font-weight: bold; color: #333; } .close-btn { position: fixed; top: 30px; right: 10px; padding: 8px 12px; background-color: #4CAF50; color: white; border: none; border-radius: 5px; cursor: pointer; z-index: 9999; font-size: 12px; box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); /* 基础阴影 */ } .close-btn:hover { color: #f44336; } #ganttControls { display: block; position: fixed; height: 25px; top: 40px; gap: 8px; margin-bottom: 8px; flex-wrap: wrap; } .control-btn { padding: 4px 8px; margin-right: 10px; background: #f5f5f5; border: 1px solid #ddd; border-radius: 3px; cursor: pointer; font-size: 12px; } .control-btn:hover { background: #e0e0e0; } .control-btn.active { background: #4CAF50; color: white; border-color: #388E3C; } .selectAssignee { background: #f5f5f5; padding: 5px 8px; margin-right: 10px; border: 1px solid #ddd; border-radius: 3px; cursor: pointer; font-size: 12px; } .dateSelect{ margin: 0px 10px; padding: 2px; border: 1px solid #ccc; borderRadius: 3px; width: 120px; } .fetchBtn{ padding: 5px 12px; background-color: #5cb3cc; color: white; border: none; border-radius: 4px; cursor: pointer; z-index: 9999; font-size: 12px; box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); /* 基础阴影 */ } .dateText{ font-size: 12px; } #ganttView { position: fixed; display: block; /*height: 25px;*/ top: 70px; width: 100%; height: calc(100vh - 70px); overflow: auto; } .gantt-table { border-collapse: collapse; max-width: 10000%; /*min-width: 800px;*/ background: white; /*table-layout: fixed;*/ } .gantt-table th { background: #f0f4f8; padding: 5px; text-align: center; border-bottom: 1px solid #ddd; position: sticky; top: 0; z-index: 10; font-weight: bold; /*overflow: hidden;*/ text-overflow: ellipsis; } .gantt-table td { padding: 5px; border-bottom: 1px solid #ccc; vertical-align: middle; height: 28px; /*position: relative;*/ } .task-bar { /*height: 20px;*/ border-radius: 3px; background: #64B5F6; display: flex; align-items: center; padding: 0 4px; color: white; font-size: 12px; box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); /* 基础阴影 */ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; position: absolute; top: 2px; bottom: 2px; z-index: 2; } .task-bar.priority-high { background: linear-gradient(90deg, #ad6598, #a35c8f); } .task-bar.priority-medium { background: linear-gradient(90deg, #FF5252, #FF1744); } .task-bar.priority-low { background: linear-gradient(90deg, #FFB74D, #FF9800); } .task-bar.completed { background: linear-gradient(90deg, #81C784, #4CAF50); } .assignee-cell { background: #e3f2fd; font-weight: bold; font-size:12px; text-align: center; position: sticky; left: 0; z-index: 12; /*border-right: 1px solid #ddd;*/ box-shadow: 1px 0 0 0 #ddd inset; width: 60px !important; min-width: 60px !important; } .project-cell { background: #e8f5e9; text-align: center; position: sticky; left: 60px; z-index: 11; /*border-right: 1px solid #ddd;*/ box-shadow: 1px 0 0 0 #ddd inset; width: 140px !important; min-width: 140px !important; line-height: 1.2; } .date-header- { width: 80px; text-align: center; /*border-right: 1px solid #ddd;*/ box-shadow: 1px 0 0 0 #ddd inset; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .date-header-weekend { width: 80px; background: #f0c9cf !important; text-align: center; /*border-right: 1px solid #e0e0e0;*/ box-shadow: 1px 0 0 0 #ddd inset; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .date-header-today { width: 80px; background: #f8d86a !important; text-align: center; /*border-right: 1px solid #e0e0e0;*/ box-shadow: 1px 0 0 0 #ddd inset; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .date-label-primary{ width: 70px; margin: 0 auto; } .date-label-secondary{ width: 70px; margin: 0 auto; } .timeline-cell { padding: 0 !important; overflow: hidden; /*border-right: 1px solid #f0f0f0;*/ box-shadow: 1px 0 0 0 #ddd inset; } .timeline-cell:last-child { border-right: none; } .timeline-container { position: relative; overflow: hidden; height: 100%; width: 100%; } .time-scale { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; } .time-segment { flex: 1; border-right: 1px dashed #e0e0e0; position: relative; } .time-label { position: absolute; top: -20px; font-size: 9px; color: #666; white-space: nowrap; } .month-header { text-align: center; font-weight: bold; background: #e8f5e9; font-size: 11px; } .today-marker { position: absolute; top: 0; bottom: 0; width: 2px; background: #F44336; z-index: 2; } .tooltip_cont { position: absolute; background: rgba(0, 0, 0, 0.85); color: white; padding: 8px; border-radius: 4px; font-size: 12px; z-index: 100; max-width: 300px; pointer-events: none; display: none; line-height: 1.4; } .no-tasks { padding: 10px; text-align: center; color: #757575; font-style: italic; font-size: 12px; } .compact-row { height: 15px !important; max-height: 15px !important; } .task-name { font-size: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .row-highlight { background-color: #f5f5f5 !important; } .error-message { color: red; padding: 10px; text-align: center; font-weight: bold; } @media (max-width: 1600px) { .date-header { min-width: 30px; } .task-bar { font-size: 12px; } } @media (max-width: 1400px) { .date-header { min-width: 25px; } } `); // 防抖函数 function debounce(func, wait) { let timeout; return function(...args) { clearTimeout(timeout); // 清除之前的定时器 timeout = setTimeout(() => { func.apply(this, args); // 执行函数 }, wait); // 等待一段时间后执行 }; } // 创建甘特图容器 const ganttContainer = document.createElement('div'); ganttContainer.id = 'ganttContainer'; // 创建甘特图头部 const ganttHeader = document.createElement('div'); ganttHeader.id = 'ganttHeader'; const ganttTitle = document.createElement('div'); ganttTitle.id = 'ganttTitle'; ganttTitle.textContent = '任务甘特图'; const closeBtn = document.createElement('div'); closeBtn.className = 'close-btn'; closeBtn.textContent = '关闭'; closeBtn.onclick = () => { ganttContainer.style.display = 'none'; }; ganttHeader.appendChild(ganttTitle); ganttHeader.appendChild(closeBtn); ganttContainer.appendChild(ganttHeader); // 创建控制面板 const controls = document.createElement('div'); controls.id = 'ganttControls'; const dayBtn = document.createElement('button'); dayBtn.className = 'control-btn active'; dayBtn.textContent = '日视图'; dayBtn.dataset.view = 'day'; const weekBtn = document.createElement('button'); weekBtn.className = 'control-btn'; weekBtn.textContent = '周视图'; weekBtn.dataset.view = 'week'; const monthBtn = document.createElement('button'); monthBtn.className = 'control-btn'; monthBtn.textContent = '月视图'; monthBtn.dataset.view = 'month'; controls.appendChild(dayBtn); controls.appendChild(weekBtn); controls.appendChild(monthBtn); ganttContainer.appendChild(controls); // 创建甘特图视图容器 const ganttView = document.createElement('div'); ganttView.id = 'ganttView'; ganttContainer.appendChild(ganttView); // 创建工具提示 const tooltip = document.createElement('div'); tooltip.className = 'tooltip_cont'; ganttContainer.appendChild(tooltip); // 添加到页面 document.body.appendChild(ganttContainer); // 创建小球容器 const ballContainer = document.createElement('div'); ballContainer.textContent = '展/收'; ballContainer.style.cssText = ` position: fixed; top: 50%; text-align: center; line-height: 40px; font-family: Arial, sans-serif; font-size: 13px; color: white; right: -40px; width: 50px; height: 40px; background: linear-gradient(90deg, #f8d86a, #ebb10d); box-shadow: 5px 5px 8px rgba(0, 0, 0, 0.3); /* 基础阴影 */ border-radius: 20px 0 0 20px; cursor: pointer; z-index: 9999; box-shadow: 0 1px 3px rgba(0,0,0,0.2); transition: right 0.3s; `; // 创建展开后的容器 const expandedContainer = document.createElement('div'); expandedContainer.style.cssText = ` position: fixed; top: 50%; right: -200px; // 默认收起 width: 200px; padding: 8px 12px; background: linear-gradient(90deg, #11659a, #144a74); box-shadow: 5px 5px 8px rgba(0, 0, 0, 0.3); /* 基础阴影 */ color: white; border-radius: 10px; z-index: 9999; font-size: 12px; transition: right 0.3s, visibility 0.3s, opacity 0.3s; visibility: hidden; // 默认隐藏 opacity: 0; // 默认透明 `; // 设置默认值为今日日期 let nowToday = new Date(); let year = nowToday.getFullYear(); let month = String(nowToday.getMonth() + 1).padStart(2, '0'); // 月份从0开始,所以要加1 let day = String(nowToday.getDate()).padStart(2, '0'); // 创建开始日期输入框 const startDateLabel = document.createElement('label'); startDateLabel.textContent = '开始日期: '; startDateLabel.className = 'dateText'; startDateLabel.htmlFor = 'startDate'; const startDateInput = document.createElement('input'); startDateInput.type = 'date'; startDateInput.id = 'startDate'; startDateInput.className = 'dateSelect'; startDateInput.value = `${year}-${month}-${day}`; nowToday.setDate(nowToday.getDate() + 30); year = nowToday.getFullYear(); month = String(nowToday.getMonth() + 1).padStart(2, '0'); // 月份从0开始,所以要加1 day = String(nowToday.getDate()).padStart(2, '0'); // 创建结束日期输入框 const endDateLabel = document.createElement('label'); endDateLabel.textContent = '结束日期: '; endDateLabel.className = 'dateText'; endDateLabel.htmlFor = 'endDate'; const endDateInput = document.createElement('input'); endDateInput.type = 'date'; endDateInput.id = 'endDate'; endDateInput.className = 'dateSelect'; endDateInput.value = `${year}-${month}-${day}`; // 创建获取数据按钮 const fetchBtn = document.createElement('button'); fetchBtn.id = 'fetchDataBtn'; fetchBtn.className = 'fetchBtn'; fetchBtn.textContent = '显示甘特图'; // 将元素添加到展开后的容器中 expandedContainer.appendChild(startDateLabel); expandedContainer.appendChild(startDateInput); expandedContainer.appendChild(endDateLabel); expandedContainer.appendChild(endDateInput); expandedContainer.appendChild(fetchBtn); // 将展开后的容器添加到网页的 body 中 document.body.appendChild(expandedContainer); // 将小球容器添加到网页的 body 中 document.body.appendChild(ballContainer); // 在创建元素后添加全局点击事件监听 document.addEventListener('click', function(event) { // 检查点击是否发生在展开容器或小球内部 const isClickInside = expandedContainer.contains(event.target) || ballContainer.contains(event.target); // 如果点击发生在外部且当前是展开状态,则收起容器 if (isExpanded && !isClickInside) { expandedContainer.style.right = '-500px'; expandedContainer.style.visibility = 'hidden'; expandedContainer.style.opacity = '0'; ballContainer.style.right = '-40px'; isExpanded = false; } }); // 使用防抖函数处理鼠标悬停事件 const handleMouseOver = debounce(() => { ballContainer.style.right = '0'; // 小球向左滑动,露出部分 }, 10); // 等待 100 毫秒后执行,避免频繁触发 const handleMouseOut = debounce(() => { ballContainer.style.right = '-40px'; // 小球回到原位 }, 10); // 等待 100 毫秒后执行,避免频繁触发 // 小球的鼠标悬停和离开事件 ballContainer.addEventListener('mouseover', handleMouseOver); // 使用 mouseover ballContainer.addEventListener('mouseout', handleMouseOut); // 使用 mouseout // 小球的点击事件:展开后的容器显示,小球隐藏 let isExpanded = false; // 用于跟踪展开状态 ballContainer.addEventListener('click', (e) => { e.stopPropagation(); // 阻止事件冒泡到document if (!isExpanded) { // 如果当前未展开 expandedContainer.style.right = '0'; // 展开容器 expandedContainer.style.visibility = 'visible'; // 显示容器 expandedContainer.style.opacity = '1'; // 不透明 ballContainer.style.right = '-40px'; // 小球继续向左滑动,完全隐藏 isExpanded = true; // 标记为已展开 } else { // 如果当前已展开,点击则收起 expandedContainer.style.right = '-500px'; // 收起容器 expandedContainer.style.visibility = 'hidden'; // 隐藏容器 expandedContainer.style.opacity = '0'; // 透明 ballContainer.style.right = '0'; // 小球回到原位 isExpanded = false; // 标记为未展开 } }); // 小球的拖拽事件 let isDragging = false; let offsetY = 0; ballContainer.addEventListener('mousedown', (e) => { isDragging = true; offsetY = e.clientY - ballContainer.offsetTop; // 计算偏移量 }); document.addEventListener('mousemove', (e) => { if (isDragging) { const newTop = e.clientY - offsetY; // 计算新的位置 ballContainer.style.top = newTop + 'px'; // 更新位置 expandedContainer.style.top = newTop + 'px'; // 同步展开后的容器位置 } }); document.addEventListener('mouseup', () => { isDragging = false; // 停止拖拽 }); // 全局变量 let tasksData = []; let contactsData = []; let projectsMap = new Map(); let currentView = 'day'; // 创建用户字典,id:用户信息完整字典 const contactMap = new Map(); // 设置天颗粒度表头宽度 let dayHeaderWidth = 80; // 设置周颗粒度表头宽度 let weekHeaderWidth = 200; // 设置月颗粒度表头宽度 let monthHeaderWidth = 400; var startDate,endDate var minDate,maxDate,MINDATE,MAXDATE // 计算传入两个日期的天数差 function daysBetween(date1, date2) { // 确保输入是Date对象 if (!(date1 instanceof Date) || !(date2 instanceof Date)) { throw new Error('Input must be Date objects'); } // 将两个日期的时间部分重置为0 const startDate = new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()); const endDate = new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()); // 计算两个日期之间的毫秒差值的绝对值 const diff = Math.abs(startDate.getTime() - endDate.getTime()); // 将毫秒差值转换为天数并四舍五入到最接近的整数 const daysDiff = Math.round(diff / (24 * 60 * 60 * 1000)); return daysDiff; } // 判断日期是否为今日 function isToday(someDate) { const today = new Date(); if (currentView === 'day') { return ( someDate.getFullYear() === today.getFullYear() && someDate.getMonth() === today.getMonth() && someDate.getDate() === today.getDate() ); } else if (currentView === 'week') { const day = today.getDay() const today_temp = today.getDate() - day + (day === 0 ? -6 : 1); today.setDate(today_temp) return ( someDate.getFullYear() === today.getFullYear() && someDate.getMonth() === today.getMonth() && someDate.getDate() === today.getDate() ); } else if (currentView === 'month') { return someDate.getMonth()===today.getMonth() } } // 从 cookie 中获取 token function getCookie(name) { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop().split(';').shift(); return null; } // 获取联系人数据(增强错误处理) function fetchContacts(token) { return new Promise((resolve, reject) => { const url = 'https://www.erplus.co/api/v1/contacts?hasDeleted=1'; GM_xmlhttpRequest({ method: 'GET', url: url, headers: { 'Authorization': `Bearer ${token}`, 'Accept': '*/*', 'DNT': '1', 'Referer': window.location.href }, onload: function(response) { if (response.status >= 200 && response.status < 300) { try { const contacts = JSON.parse(response.responseText); resolve(contacts); } catch (e) { reject('联系人数据解析失败: ' + e.message); } } else { reject(`联系人请求失败,状态码: ${response.status}`); } }, onerror: function(error) { reject('网络错误: ' + error); } }); }); } // 获取任务数据(增强错误处理) function fetchTasks(token,startDate,endDate) { return new Promise((resolve, reject) => { const url = `https://www.erplus.co/task/v1/programs/resources?type=0&state=2&startDate=${startDate}&endDate=${endDate}`; GM_xmlhttpRequest({ method: 'GET', url: url, headers: { 'Authorization': `Bearer ${token}`, 'Accept': '*/*', 'DNT': '1', 'Referer': window.location.href }, onload: function(response) { if (response.status >= 200 && response.status < 300) { try { const data = JSON.parse(response.responseText); if (data.erpCode === "000") { resolve(data.erpData); } else { reject(`任务请求失败: ${data.erpMsg}`); } } catch (e) { reject('任务数据解析失败: ' + e.message); } } else { reject(`任务请求失败,状态码: ${response.status}`); } }, onerror: function(error) { reject('网络错误: ' + error); } }); }); } // 获取元素引用 // 注意:这里需要再次获取,因为元素是在上面代码中创建并添加到DOM的 const scriptStartDateInput = document.getElementById('startDate'); const scriptEndDateInput = document.getElementById('endDate'); // 按钮点击事件 fetchBtn.addEventListener('click', async function() { startDate = scriptStartDateInput.value; endDate = scriptEndDateInput.value; // 检查用户是否选择了日期 if (!startDate) { alert('请选择开始日期!'); return; } if (!endDate) { alert('请选择结束日期!'); return; } // 验证日期范围是否有效 const startDateTime = new Date(startDate); const endDateTime = new Date(endDate); if (startDateTime > endDateTime) { alert('开始日期不能晚于结束日期!请重新选择。'); return; } minDate = startDateTime maxDate = endDateTime MINDATE = minDate MAXDATE = maxDate fetchBtn.textContent = '加载中...'; fetchBtn.disabled = true; // 清空前一次的渲染结果 ganttView.innerHTML = ''; const loadingText = document.createElement('div'); loadingText.textContent = '加载中,请稍候...'; loadingText.style.textAlign = 'center'; loadingText.style.padding = '20px'; ganttView.appendChild(loadingText); // 获取 token const token = getCookie('token'); if (!token) { alert('未找到 token,请先登录(不可用)'); resetButton(); ganttView.innerHTML = '<div class="error-message">未找到token,请先登录(不可用)</div>'; return; } try { // 并行获取联系人和任务数据 [contactsData, tasksData] = await Promise.all([ fetchContacts(token), fetchTasks(token,startDate,endDate) ]); // 处理任务数据 processTasks(); // 显示甘特图 renderGanttChart(); ganttContainer.style.display = 'block'; } catch (error) { console.error('加载失败:', error); ganttView.innerHTML = `<div class="error-message">加载失败: ${error}</div>`; } finally { resetButton(); // **关键部分**:数据加载完成后,隐藏展开后的容器并显示小球 expandedContainer.style.right = '-500px'; // 收起容器 expandedContainer.style.visibility = 'hidden'; // 隐藏容器 expandedContainer.style.opacity = '0'; // 透明 ballContainer.style.right = '-30px'; // 小球回到原位显示在侧边栏 isExpanded = false; // 标记为未展开状态 } }); // 重置按钮状态 function resetButton() { fetchBtn.textContent = '显示甘特图'; fetchBtn.disabled = false; } // 处理任务数据 function processTasks() { if (contactsData && Array.isArray(contactsData)) { contactsData.forEach(contact => { // 用户数据存在且有id值 if (contact && contact.id) { contactMap.set(contact.id, contact); } }); } // 创建项目映射表 projectsMap.clear(); // projectsMap{项目ID:项目名称} if (tasksData && Array.isArray(tasksData)) { tasksData.forEach(task => { if (task && task.programId && task.showPrefix) { projectsMap.set(task.programId, task.showPrefix); }else if (task && task.programId===0) { projectsMap.set(task.programId, '普通任务'); } // 添加负责人信息 if (task.assignerId) { task.assigner = contactMap.get(task.assignerId) || null; } // 创建任务开始日期对象(带校验) if (task.firstStartTime) { try { task.startDateObj = new Date(task.firstStartTime); if (isNaN(task.startDateObj.getTime())) { task.startDateObj = null; } } catch (e) { task.startDateObj = null; } } else { task.startDateObj = null; } // 创建任务截止日期对象(带校验) if (task.dueTime) { try { task.endDateObj = new Date(task.dueTime); if (isNaN(task.endDateObj.getTime())) { task.endDateObj = null; } } catch (e) { task.endDateObj = null; } } else { task.endDateObj = null; } }); } } // 优化时间轴标签格式 function formatDateLabel(date, viewType, isSecondary = false) { const year = date.getFullYear(); const month = date.getMonth() + 1; const day = date.getDate(); if (viewType === 'day') { if (isSecondary) { // 返回日期 return `${day}`; } else { // 返回年月 return `${year}年${month}月`; } } else if (viewType === 'week') { if (isSecondary) { // 返回周一到周日的日期范围 const weekEnd = new Date(date); weekEnd.setDate(date.getDate() + 6); return `${date.getDate()}-${weekEnd.getDate()}`; } else { // 返回年月 return `${year}年${month}月`; } } else if (viewType === 'month') { if (isSecondary) { // 返回月份 return `${month}月`; } else { // 返回年份 return `${year}年`; } } return ''; } // 生成时间轴表头 function generateDateHeaders(minDate, maxDate) { const headers = []; // 设定起始日期 let currentDate = new Date(minDate); // 重置为周一开始 if (currentView === 'week') { // 获取最小日期是星期几 const day = currentDate.getDay(); // 确保每周从周一开始 const diff = currentDate.getDate() - day + (day === 0 ? -6 : 1); currentDate.setDate(diff); minDate.setDate(diff); //console.log('周处理-开始日期改为:', minDate); }else if (currentView === 'month') { currentDate.setMonth(currentDate.getMonth(),1); minDate.setMonth(minDate.getMonth(),1); //console.log('月处理-开始日期改为:', minDate); } // 生成日期范围 while (currentDate <= maxDate) { const date = new Date(currentDate); if (currentView === 'day') { headers.push({ date: new Date(date), primaryLabel: formatDateLabel(date, 'day'), // 顶部显示年月 secondaryLabel: formatDateLabel(date, 'day', true),// 底部显示日期 isWeekend: date.getDay() === 0 || date.getDay() === 6, isToday:isToday(date), }); // 更新起始日期 currentDate.setDate(currentDate.getDate() + 1); } else if (currentView === 'week') { headers.push({ date: new Date(date), primaryLabel: formatDateLabel(date, 'week'), secondaryLabel: formatDateLabel(date, 'week', true), isWeekend: false, isToday:isToday(date), }); // 更新起始日期,同时更新 maxDate const nextWeek = new Date(currentDate); nextWeek.setDate(currentDate.getDate() + 7); if (nextWeek > maxDate) { // 如果下一周的第一天超过了 maxDate,更新 maxDate 为当前周的最后一天 //maxDate = new Date(currentDate); maxDate.setDate(currentDate.getDate() + 6); // 设置为当前周的最后一天 //console.log('周处理-结束日期改为:', maxDate); } currentDate.setDate(currentDate.getDate() + 7); } else if (currentView === 'month') { headers.push({ date: new Date(date), primaryLabel: formatDateLabel(date, 'month'), secondaryLabel: formatDateLabel(date, 'month', true), isWeekend: false, isToday:isToday(date), }); // 更新起始日期,同时更新 maxDate const nextMonth = new Date(currentDate); nextMonth.setMonth(currentDate.getMonth() + 1); if (nextMonth > maxDate) { // 如果下个月的第一天超过了 maxDate,更新 maxDate 为当前月的最后一天 maxDate.setMonth(currentDate.getMonth()+1, 0); // 设置为当前月的最后一天 //console.log('月处理-结束日期改为:', maxDate); } currentDate.setMonth(currentDate.getMonth() + 1); } } return headers; } var elementCreated = false; // 渲染甘特图 function renderGanttChart() { ganttView.innerHTML = ''; // 清除加载中提示 minDate = new Date(minDate); maxDate = new Date(maxDate); let totalDuration // 用于计算总时长 // 按负责人分组,将该负责人所有负责任务遍历出来放到tasks列表里,再与负责人名绑定,放到tasksByAssignee字典里{负责人ID:{assignee:负责人名,tasks:[任务列表]}} const tasksByAssignee = new Map(); const optionAssignee = new Array(); if (tasksData && Array.isArray(tasksData)) { tasksData.forEach(task => { if (task.assignerId) { const assigneeId = task.assignerId; if (!tasksByAssignee.has(assigneeId)) { optionAssignee.push({value:task.assigner.name,text:task.assigner.name}) tasksByAssignee.set(assigneeId, { assignee: task.assigner, tasks: [] }); } tasksByAssignee.get(assigneeId).tasks.push(task); } }); } // 如果没有任务 if (tasksByAssignee.size === 0) { const noTasks = document.createElement('div'); noTasks.className = 'no-tasks'; noTasks.textContent = '没有找到任务数据'; ganttView.appendChild(noTasks); return; } // 创建<select>元素 if (!elementCreated) { const selectElement = document.createElement('select'); selectElement.className = 'selectAssignee'; elementCreated = true; selectElement.setAttribute('name', 'exampleSelect'); // 设置名称属性(可选) // 添加change事件监听器 selectElement.addEventListener('change', function() { // 当用户改变选择时执行的代码 renderGanttChart(); }); // 遍历选项列表并创建<option>元素 optionAssignee.forEach(option => { const optionElement = document.createElement('option'); optionElement.setAttribute('value', option.value); // 设置值属性 optionElement.textContent = option.text; // 设置显示文本 selectElement.appendChild(optionElement); // 将<option>添加到<select>中 }); // 将生成的<select>元素添加到页面的指定位置(例如body中) controls.appendChild(selectElement); } // 创建甘特图表格 const table = document.createElement('table'); table.className = 'gantt-table'; // 创建表头行 // const headerRow = document.createElement('tr'); // 创建表头 - 第一行 (年月/年) const yearHeaderRow = document.createElement('tr'); // 负责人表头 const assigneeHeader = document.createElement('th'); assigneeHeader.className = 'assignee-cell'; assigneeHeader.textContent = '负责人'; assigneeHeader.style='z-index: 21;' yearHeaderRow.appendChild(assigneeHeader); // 项目表头 const projectHeader = document.createElement('th'); projectHeader.className = 'project-cell'; projectHeader.textContent = '项目'; projectHeader.style='z-index: 20;' yearHeaderRow.appendChild(projectHeader); // 获取日期表头数据列表,列表里罗列所有范围内的日期字典 // 字典格式{date: 日期,primaryLabel: 第一行年月,secondaryLabel: 第二行日期,isWeekend: 是否为周六日} const dateHeaders = generateDateHeaders(minDate, maxDate); // console.log('generateDateHeaders调用后开始日期:', minDate); // console.log('generateDateHeaders调用后结束日期:', maxDate); // 添加日期表头 dateHeaders.forEach(header => { const dateCell = document.createElement('th'); // 周六日、本日本周本月颜色特殊设定 dateCell.className = `date-header-${header.isToday ? 'today' : header.isWeekend ? 'weekend' : ''}`; if (currentView==='day'){ dateCell.style=`width:${dayHeaderWidth}px` } else if (currentView==='week'){ dateCell.style=`width:${weekHeaderWidth}px` } else if (currentView==='month'){ dateCell.style=`width:${monthHeaderWidth}px` } const primaryLabel = document.createElement('div'); primaryLabel.className = 'date-label-primary'; primaryLabel.textContent = header.primaryLabel; dateCell.appendChild(primaryLabel); const secondaryLabel = document.createElement('div'); secondaryLabel.className = 'date-label-secondary'; secondaryLabel.textContent = header.secondaryLabel; dateCell.appendChild(secondaryLabel); yearHeaderRow.appendChild(dateCell); }); table.appendChild(yearHeaderRow); // 添加任务行 let rowIndex = 0; // 遍历每个负责人,tasksByAssignee字典里{负责人ID:{assignee:负责人名,tasks:[任务列表]}} for (const [assigneeId, assigneeGroup] of tasksByAssignee) { const selectElement = document.querySelector('select[name="exampleSelect"]'); const selectedValue = selectElement.value; if (assigneeGroup.assignee.name!==selectedValue){ continue } // console.log('计算任务位置前开始日期:', minDate); // console.log('计算任务位置前结束日期:', maxDate); // 按项目分组任务 const tasksByProject = new Map(); // tasksByProject字典里{项目ID:{projectName:项目名,tasks:[任务列表]}} if (assigneeGroup.tasks && Array.isArray(assigneeGroup.tasks)) { assigneeGroup.tasks.forEach(task => { if (task) {//task.programId const projectId = task.programId; // projectsMap{项目ID:项目名称} const projectName = projectsMap.get(projectId) || '未知项目'; if (!tasksByProject.has(projectId)) { tasksByProject.set(projectId, { projectName: projectName, tasks: [] }); } tasksByProject.get(projectId).tasks.push(task); } }); } // 计算负责人总行数 const totalRowsForAssignee = Array.from(tasksByProject.values()).reduce( (total, project) => total + project.tasks.length, 0 ); // 创建负责人单元格(只创建一次) let assigneeCellCreated = false; // 遍历每个项目 for (const [projectId, projectGroup] of tasksByProject) { // 获取该项目的所有任务列表 const projectTasks = projectGroup.tasks; // 遍历项目中的每个任务 for (let taskIndex = 0; taskIndex < projectTasks.length; taskIndex++) { const task = projectTasks[taskIndex]; const taskRow = document.createElement('tr'); taskRow.className = 'compact-row'; if (rowIndex % 2 === 0) { taskRow.classList.add('row-highlight'); } // 负责人单元格 - 只在第一行创建 if (!assigneeCellCreated) { const assigneeCell = document.createElement('td'); assigneeCell.className = 'assignee-cell'; assigneeCell.rowSpan = totalRowsForAssignee; assigneeCell.textContent = assigneeGroup.assignee ? assigneeGroup.assignee.name : '未知'; if (assigneeGroup.assignee && assigneeGroup.assignee.departmentName) { assigneeCell.innerHTML += `<div style="font-size:10px;color:#666;">${assigneeGroup.assignee.departmentName}</div>`; } taskRow.appendChild(assigneeCell); assigneeCellCreated = true; } // 项目单元格 - 只在项目第一行创建 if (taskIndex === 0) { const projectCell = document.createElement('td'); projectCell.className = 'project-cell'; projectCell.rowSpan = projectTasks.length; projectCell.textContent = projectGroup.projectName; taskRow.appendChild(projectCell); } // 时间轴单元格 const timelineCell = document.createElement('td'); timelineCell.className = 'timeline-cell'; timelineCell.colSpan = dateHeaders.length; // 占据整个时间轴区域 // 创建时间轴容器 const taskTimelineContainer = document.createElement('div'); taskTimelineContainer.className = 'timeline-container'; taskTimelineContainer.style.position = 'relative'; taskTimelineContainer.style.height = '100%'; // 添加时间刻度(背景) const taskTimeScale = document.createElement('div'); taskTimeScale.className = 'time-scale'; dateHeaders.forEach(scale => { const segment = document.createElement('div'); segment.className = 'time-segment'; segment.style.width = `${scale.position - (taskTimeScale.children.length > 0 ? dateHeaders[taskTimeScale.children.length - 1].position : 0)}%`; segment.style.borderRight = '1px dashed #e0e0e0'; taskTimeScale.appendChild(segment); }); taskTimelineContainer.appendChild(taskTimeScale); // 添加任务条(带日期校验) if (task.startDateObj instanceof Date && task.endDateObj instanceof Date && !isNaN(task.startDateObj.getTime()) && !isNaN(task.endDateObj.getTime())) { // 计算任务位置 const taskStart = Math.max(task.startDateObj, minDate); const taskEnd = Math.min(task.endDateObj, maxDate); // console.log('计算任务位置后开始日期:', minDate); // console.log('计算任务位置后结束日期:', maxDate); const viewportWidth = document.documentElement.clientWidth; // console.log('视口宽度:', viewportWidth); var dateWidth = viewportWidth - 200 // console.log('时间范围初始宽度:', dateWidth); const dateNum = dateHeaders.length // console.log('时间格子数量:', dateNum); const minHeaderWidth = 80 if (currentView==='day'){ if (dateNum*dayHeaderWidth<=dateWidth){ dateWidth = dateNum*dayHeaderWidth }else{ dateWidth = dateNum*minHeaderWidth } } else if (currentView==='week'){ if (dateNum*weekHeaderWidth<=dateWidth){ dateWidth = dateNum*weekHeaderWidth }else if (dateWidth/dateNum<minHeaderWidth){ dateWidth = dateNum*minHeaderWidth } } else if (currentView==='month'){ if (dateNum*monthHeaderWidth<=dateWidth){ dateWidth = dateNum*monthHeaderWidth }else if (dateWidth/dateNum<minHeaderWidth){ dateWidth = dateNum*minHeaderWidth } } // console.log('任务条计算前的范围开始日期:', minDate); // console.log('任务条计算前的范围结束日期:', maxDate); totalDuration = daysBetween(maxDate,minDate); // 更新周或月视图调整后的总时长 const startPercent = daysBetween(new Date(taskStart),minDate) / (totalDuration+1); const widthPercent = (daysBetween(new Date(taskEnd),new Date(taskStart))+1) / (totalDuration+1); // console.log('范围格子:', totalDuration); // console.log('开始-最小:', daysBetween(new Date(taskStart),minDate)); // console.log('结束-最小:', daysBetween(new Date(taskEnd),minDate)); // console.log('长度格子:', widthPercent); if (widthPercent > 0) { const taskBar = document.createElement('div'); taskBar.className = 'task-bar'; taskBar.textContent = task.topic || '任务'; // console.log('任务名:', task.topic); // console.log('时间范围计算后宽度:', dateWidth); taskBar.style.left = `${startPercent*dateWidth}px`; // console.log('任务起始点位置:', startPercent*dateWidth); taskBar.style.width = `${widthPercent*dateWidth}px`; // console.log('任务宽度:', widthPercent*dateWidth); // 设置优先级样式 if (task.priority === 6) taskBar.classList.add('priority-high'); else if (task.priority === 5) taskBar.classList.add('priority-medium'); else if (task.priority === 4) taskBar.classList.add('priority-low'); // 设置完成状态 if (task.status === 3) taskBar.classList.add('completed'); // 添加悬停提示 taskBar.onmouseenter = (e) => { let taskStatus if (task.status === 1){ taskStatus = '进行中' } else if (task.status === 2){ taskStatus = '执行中' } else if (task.status === 3){ taskStatus = '已完成' } tooltip.innerHTML = ` <div><strong>${task.topic || '无任务名称'}</strong></div> <div>负责人: ${assigneeGroup.assignee ? assigneeGroup.assignee.name : '未知'}</div> <div>创建人: ${task.createId ? contactMap.get(task.createId).name : '未知'}</div> <div>项目: ${projectGroup.projectName}</div> <div>开始: ${task.firstStartTime || '未知'}</div> <div>结束: ${task.dueTime || '未知'}</div> <div>状态: ${taskStatus || '未知'}</div> <div>优先级: ${task.priority === 4 ? '重要' : task.priority === 5 ? '军令如山' : task.priority === 6 ? '覆水难收' : '普通'}</div> <div>进度: ${task.progress || 0}%</div> `; tooltip.style.display = 'block'; // tooltip.style.left = `${e.clientX + 10}px`; // tooltip.style.top = `${e.clientY + 10}px`; // 确保提示牌在视口范围内 updateTooltipPosition(e); }; taskBar.onmouseleave = () => { tooltip.style.display = 'none'; }; taskBar.onmousemove = (e) => { // tooltip.style.left = `${e.clientX + 10}px`; // tooltip.style.top = `${e.clientY + 10}px`; updateTooltipPosition(e); }; // 确保提示牌在视口范围内 function updateTooltipPosition(e) { // 获取提示牌的尺寸 const tooltipRect = tooltip.getBoundingClientRect(); const tooltipWidth = tooltipRect.width; const tooltipHeight = tooltipRect.height; // 获取视口尺寸 const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; // 计算默认位置(鼠标右下角) let left = e.clientX + 10; let top = e.clientY + 10; // 水平方向:如果超出右边界,向左调整 if (left + tooltipWidth > viewportWidth) { left = e.clientX - tooltipWidth - 10; } // 垂直方向:如果超出下边界,向上调整 if (top + tooltipHeight > viewportHeight) { top = e.clientY - tooltipHeight - 10; } // 确保不会超出左边界 if (left < 0) { left = 10; } // 确保不会超出上边界 if (top < 0) { top = 10; } // 应用位置 tooltip.style.left = `${left}px`; tooltip.style.top = `${top}px`; } taskTimelineContainer.appendChild(taskBar); } } timelineCell.appendChild(taskTimelineContainer); taskRow.appendChild(timelineCell); table.appendChild(taskRow); rowIndex++; } } ganttView.appendChild(table); } // // 添加今日标记 // const today = new Date(); // if (today >= minDate && today <= maxDate) { // const todayPercent = ((today - minDate) / totalDuration) * 100; // const todayMarker = document.createElement('div'); // todayMarker.className = 'today-marker'; // todayMarker.style.left = `${todayPercent}%`; // todayMarker.style.position = 'fixed'; // todayMarker.style.top = '60px'; // todayMarker.style.height = '100%'; // const timelineContainer = ganttContainer.querySelector('#ganttView'); //.timeline-container // if (timelineContainer) { // timelineContainer.appendChild(todayMarker); // // 添加今日标签 // const todayLabel = document.createElement('div'); // todayLabel.textContent = '今日'; // todayLabel.style.position = 'fixed'; // todayLabel.style.top = '40px'; // todayLabel.style.left = `${todayPercent}%`; // todayLabel.style.transform = 'translateX(-50%)'; // todayLabel.style.background = '#F44336'; // todayLabel.style.color = 'white'; // todayLabel.style.padding = '1px 6px'; // todayLabel.style.borderRadius = '8px'; // todayLabel.style.fontSize = '9px'; // timelineContainer.appendChild(todayLabel); // } // } // 添加缩放控制 // const zoomContainer = document.createElement('div'); // zoomContainer.style.marginTop = '10px'; // zoomContainer.style.display = 'flex'; // zoomContainer.style.alignItems = 'center'; // zoomContainer.style.gap = '10px'; // const zoomLabel = document.createElement('span'); // zoomLabel.textContent = '缩放:'; // zoomLabel.style.fontSize = '12px'; // zoomContainer.appendChild(zoomLabel); // const zoomSlider = document.createElement('input'); // zoomSlider.type = 'range'; // zoomSlider.min = '50'; // zoomSlider.max = '150'; // zoomSlider.value = '100'; // zoomSlider.style.width = '100px'; // zoomSlider.addEventListener('input', function() { // const scale = this.value / 100; // table.style.transform = `scale(${scale})`; // table.style.transformOrigin = 'left top'; // }); // zoomContainer.appendChild(zoomSlider); // ganttView.appendChild(zoomContainer); minDate = MINDATE maxDate = MAXDATE } // 视图切换事件 document.querySelectorAll('.control-btn').forEach(btn => { btn.addEventListener('click', function() { document.querySelectorAll('.control-btn').forEach(b => b.classList.remove('active')); this.classList.add('active'); currentView = this.dataset.view; renderGanttChart(); }); }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址