OS-EASY 专属禅道标记助手

禅道助手: 工时统计(工时提醒/每日工时计算)、Bug管理(留存时间标记/一键复制/新标签页打开)、工作流优化(强制工时填写/解决方案提示)、悬浮球快捷工具

// ==UserScript==
// @name        OS-EASY 专属禅道标记助手
// @namespace   Violentmonkey Scripts
// @match       http*://172.16.203.14/*
// @require     https://unpkg.com/[email protected]/dist/jquery.min.js
// @require     https://unpkg.com/workday-cn/lib/workday-cn.umd.js
// @grant       GM_addStyle
// @grant       GM_setClipboard
// @version     2.0.3
// @author      LHQ & CHH & ZCX && zagger
// @license     GPLv3
// @description 禅道助手: 工时统计(工时提醒/每日工时计算)、Bug管理(留存时间标记/一键复制/新标签页打开)、工作流优化(强制工时填写/解决方案提示)、悬浮球快捷工具
// ==/UserScript==

(() => {
  $.noConflict(true)(document).ready(async ($) => {
      // 面板策略管理
      const panelStrategies = {
        strategies: {},
        currentStrategy: null,
        
        register(name, strategy) {
          if (!strategy.title || !strategy.render) {
            console.error('Strategy must have title and render function');
            return;
          }
          this.strategies[name] = strategy;
        },

        async switchStrategy(name, content) {
          // 取消之前策略的所有请求
          requestManager.clear();
          
          // 更新当前策略
          this.currentStrategy = name;
          
          // 清空内容并显示加载状态
          content.empty().html('<div class="zm-panel-item" style="text-align: center;">加载中...</div>');
          
          try {
            const strategy = this.strategies[name];
            const renderPromise = strategy.render(content);
            
            // 等待渲染完成
            await renderPromise;
            
            // 如果在渲染过程中切换了策略,则不显示结果
            if (this.currentStrategy !== name) {
              content.empty();
            }
          } catch (err) {
            if (err.name !== 'AbortError') {
              content.html(`<div class="zm-panel-item error">加载失败: ${err.message}</div>`);
            }
          }
        },

        getAll() {
          return this.strategies;
        },

        get(name) {
          return this.strategies[name];
        }
      };

      // 添加请求管理器
      const requestManager = {
        requests: new Map(),
        
        register(key, controller) {
          // 取消之前的请求
          this.abort(key);
          // 注册新请求
          this.requests.set(key, controller);
        },
        
        abort(key) {
          if (this.requests.has(key)) {
            this.requests.get(key).abort();
            this.requests.delete(key);
          }
        },
        
        clear() {
          // 取消所有请求
          this.requests.forEach(controller => controller.abort());
          this.requests.clear();
        }
      };

      // 初始化
      await initialize();

      // 定义颜色常量
      const colors = {
          green: '#82E0AA',
          yellow: '#F7DC6F',
          brown: '#FE9900',
          red: '#E74C3C'
      };

      // 公共任务类型配置
      const commonTaskTypes = {
          development: {
              title: '开发任务',
              color: '#007bff',
              items: [
                  {name: '【立项项目开发任务】', desc: '版本开发计划中包含的所有任务,有明确的禅道需求'},
                  {name: '【定制项目】', desc: '外部临时插入,以产品经理邮件为准,需要有禅道需求'},
                  {name: '【专项开发任务】', desc: '没有明确需求的隐形专项开发任务,不在版本开发计划中体现,包含预研/设计/性能/稳定性/代码优化等'}
              ]
          },
          management: {
              title: '管理任务',
              color: '#28a745',
              items: [
                  {name: '【培训任务】', desc: '参加内外部培训,技术纷享等'},
                  {name: '【学习任务】', desc: '新员工熟悉工作内容,开发人员学习新的工作方法等'},
                  {name: '【会议】', desc: '参加各种类型的会议,项目启动会、需求串讲会、计划评审会、项目周会、项目站会、项目复盘会、其他各类临时会议等'},
                  {name: '【管理任务】', desc: '项目管理类工作(制定项目计划、跟踪项目进展等)、人员管理类工作(人员招聘、绩效考核等)、编写管理规范类文档'}
              ]
          },
          support: {
              title: '支持任务',
              color: '#ffc107',
              items: [
                  {name: '【对外支持】', desc: '外部疑问解答,仅指导技术人员或排查疑问'},
                  {name: '【协助他人】', desc: '内部产品测试过程中外部原因导致的产品BUG(需关联到禅道问题单),协助测试人员排查环境问题,帮助其他开发搭建环境等'}
              ]
          }
      };

      // 公共函数:插入内容到页面
      function insertContentToPage(content) {
          const blankDiv = $('#mainContent > form > div')[0];
          if (blankDiv) {
              blankDiv.style.paddingTop = '0';
              blankDiv.innerHTML = content;
          } else {
              $(content).insertBefore('#mainContent > form > #objectTable');
          }
      }

      // 公共函数:添加视觉反馈
      function addVisualFeedback(element, color = '#d4edda') {
          element.css('background-color', color);
          setTimeout(() => element.css('background-color', 'white'), 300);
      }

      // 设置通用的点击事件监听器
      setBodyClickListener();
      // 根据当前路径进行不同的处理
      const path = document.location.pathname;
      switch (path) {
          case '/effort-calendar.html':
              handleEffortCalendar(colors);
              break;
          case '/my-work-bug.html':
              handleMyWorkBug(colors);
              break;
          default:
              handleDefaultPath(path);
              break;
      }

      // 初始化函数
      async function initialize() {
          const userName = localStorage.getItem('zm-username');
          if (!userName) {
              const name = prompt("看上去你是第一次使用,请输入禅道中的姓名:");
              if (name) localStorage.setItem('zm-username', name);
              else return;
          }
          $("td.text-left a").attr('target', '_blank');
          
          // 添加悬浮球和面板样式
          GM_addStyle(`
              .zm-float-ball {
                  position: fixed;
                  left: 105px;
                  top: 4px;
                  width: 36px;
                  height: 36px;
                  background: #1890ff;
                  border-radius: 50%;
                  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
                  display: flex;
                  align-items: center;
                  justify-content: center;
                  cursor: pointer;
                  z-index: 9999;
                  transition: all 0.3s;
              }
              .zm-float-ball::after {
                  content: '';
                  position: absolute;
                  width: 100%;
                  height: 100%;
                  border-radius: 50%;
                  border: 2px solid #1890ff;
                  animation: ripple 1.5s ease-out infinite;
              }
              @keyframes ripple {
                  0% {
                      transform: scale(1);
                      opacity: 0.8;
                  }
                  100% {
                      transform: scale(1.5);
                      opacity: 0;
                  }
              }
              .zm-float-ball:hover {
                  transform: scale(1.1);
              }
              .zm-float-ball i {
                  color: white;
                  font-size: 24px;
              }
              .zm-panel {
                  position: fixed;
                  right: 80px;
                  top: 50%;
                  transform: translateY(-50%);
                  width: 300px;
                  background: white;
                  border-radius: 8px;
                  box-shadow: 0 3px 6px -4px rgba(0,0,0,.12), 0 6px 16px 0 rgba(0,0,0,.08);
                  z-index: 9998;
                  display: none;
              }
              .zm-panel-header {
                  padding: 12px 16px;
                  border-bottom: 1px solid #f0f0f0;
                  font-weight: bold;
              }
              .zm-panel-content {
                  max-height: 400px;
                  overflow-y: auto;
                  scrollbar-width: thin;
                  scrollbar-color: rgba(0,0,0,.2) transparent;
              }
              .zm-panel-content::-webkit-scrollbar {
                  width: 6px;
              }
              .zm-panel-content::-webkit-scrollbar-track {
                  background: transparent;
              }
              .zm-panel-content::-webkit-scrollbar-thumb {
                  background-color: rgba(0,0,0,.2);
                  border-radius: 3px;
                  border: none;
              }
              .zm-panel-content::-webkit-scrollbar-thumb:hover {
                  background-color: rgba(0,0,0,.3);
              }
              .zm-panel-item {
                  padding: 12px 16px;
                  border-bottom: 1px solid #f0f0f0;
                  display: flex;
                  justify-content: space-between;
                  align-items: center;
              }
              .zm-panel-item:hover {
                  background: #f5f5f5;
              }
              .zm-hours {
                  color: #ff4d4f;
                  font-weight: bold;
              }
          `);

          // 创建悬浮球和面板
          createFloatBall();
      }

      // 设置通用的点击事件监听器
      function setBodyClickListener() {
          document.body.onclick = async function (e) {
              if (e instanceof PointerEvent) {
                  const aTag = getATag(e.target);
                  if (!aTag) return;
                  const aHref = $(aTag).attr('href');
                  if (aHref?.includes('bug-resolve')) {
                      await generatorResolveType();
                  }
              }
          };
      }

      // 获取点击的A标签
      function getATag(target) {
          if (target.tagName === 'A') return target;
          if (target.parentElement.tagName === 'A') return target.parentElement;
          return null;
      }

      // 处理 effort-calendar 页面
      function handleEffortCalendar(colors) {
          GM_addStyle(`
              span.zm-day { font-weight: bold; margin: 0 8px; }
              .warn { color: ${colors.brown}; }
              .fine { color: ${colors.green}; }
          `);
          waitForContentInContainer('#main', 'table').then(element => {
              const observer = new MutationObserver(() => markEffortCalendar(element, observer));
              observer.observe(element, { subtree: true, childList: true });
              markEffortCalendar(element, observer);
          });
      }

      // 标记 effort-calendar 页面的数据
      function markEffortCalendar(element, observer) {
          observer.disconnect();
          const days = element.querySelectorAll(".cell-day");
          days.forEach(dayElement => {
              const total = calculateTotalTime(dayElement);
              updateDayElement(dayElement, total);
          });
          observer.observe(element, { subtree: true, childList: true });
      }

      // 计算时间总和
      function calculateTotalTime(dayElement) {
          const timeEles = dayElement.querySelectorAll('.has-time .time');
          return Array.from(timeEles).reduce((total, time) => total + parseFloat(time.textContent), 0);
      }

      // 更新天数元素的显示
      function updateDayElement(dayElement, total) {
          $(dayElement).find('.zm-day').remove();
          $(dayElement).find('.copy-time').remove();
          if (total != 0) {
              const colorClass = total > 10 || total < 8 ? 'warn' : 'fine';
              $(dayElement).find('.heading').prepend(`<span class="zm-day ${colorClass}">【${total.toFixed(1)}小时】</span>`);
              $(dayElement).find('.heading').prepend(`<div class="copy-time btn-toolbar pull-left" style="margin-left:25px;display:flex;align-items:center;">复制</div>`);
              $(dayElement).find('.heading').find('.copy-time').on('click', async function (e) { copyTaskTime(e) })
          }
      }

      
      // 复制任务时间
      async function copyTaskTime(e) {
        e.stopPropagation()
        const targetEle = e.target
        const content = $(targetEle).parent('.heading').next('.content')
        function calculateTaskTimes(startTime, tasks) {
          let currentHour = parseInt(startTime.split(':')[0])
          let currentMinute = parseInt(startTime.split(':')[1])
          const results = []
          let startDate = new Date()
          startDate.setHours(currentHour)
          startDate.setMinutes(currentMinute)

          const middleStartDate = new Date()
          middleStartDate.setHours(12)
          middleStartDate.setMinutes(0)
          const middleEndDate = new Date()
          middleEndDate.setHours(14)
          middleEndDate.setMinutes(0)

          let endDate = null

          tasks.forEach((task) => {
            const hourStamp = 60 * 60 * 1000
            const timeParts = task.time.split('h')
            let hours = timeParts[0] * 1
            let startStamp = startDate.getTime()
            const middleStamp = middleStartDate.getTime()
            const middleEndStamp = middleEndDate.getTime()
            let endStamp = startStamp + hours * hourStamp

            if (startStamp <= middleStamp && endStamp > middleStamp) {
              endStamp = endStamp + 2 * hourStamp
            }
            const start = new Date(startStamp)
            const end = new Date(endStamp)
            const startTimeStr = `${String(start.getHours()).padStart(2, '0')}:${String(start.getMinutes()).padStart(2, '0')}`
            const endTimeStr = `${String(end.getHours()).padStart(2, '0')}:${String(end.getMinutes()).padStart(2, '0')}`
            startDate = new Date(endStamp)
            results.push({
              ...task,
              start: startTimeStr,
              end: endTimeStr
            })
          })

          return results
        }

        // 示例用法
        const start = '08:30'
        let tempTasks = Array.from(
          content
            .find('.events')
            .find('.event')
            .map(function () {
              const title = $(this).find('.title').text().trim()
              const time = $(this).find('.time').text().trim()
              const id = $(this).data('id')
              return {
                id,
                time,
                title
              }
            })
        )
        tempTasks = calculateTaskTimes(start, tempTasks)
        const parseTaskDoc = function (doc) {
          const objReg = new RegExp(`对象\n`)
          const id = $(doc).find('.main-header span.label').text()
          let item = {}
          $(doc)
            .find('table tbody tr')
            .each(function () {
              // console.log($(this).text())
              const text = $(this).text()
              if (objReg.test(text)) {
                item.obj = text.replace(objReg, '').replace('\n', '').trim()
                item.href = $(this).find('a').attr('href')
              }
            })
          return { ...item, id }
        }
        const fetchTaskData = async function () {
          const docs = await Promise.all(
            tempTasks.map(async function (t) {
              return fetchDocument(
                `/effort-view-${t.id}.html?onlybody=yes&tid=i2sh4q46`
              )
            })
          )
          return docs.map((d) => parseTaskDoc(d))
        }
        const taskObjData = await fetchTaskData()
        let tasks = tempTasks.map((t) => {
          const findOne = taskObjData.find(
            (task) => task.id * 1 === t.id * 1
          )
          return { ...t, ...findOne }
        })
        tasks = tasks
          .map((t) => {
            return `- [ ] ${t.start} - ${t.end} #工时 ${t.time}\t${t.title}\t ${t.obj && t.href ? `[${t.obj}](${location.origin + t.href})\t` : ''}\n`
          })
          .join('')
        GM_setClipboard(tasks)
      }

      // 设置 执行-版本-6.0.5-future-我解决的bug 页面功能
      function setupResolvedByMeBuildPage() {
        $(
          '<div class="btn btn-success" style="margin-right:10px;">勾选自己</div>'
        )
          .on('click', function () {
            const trList = $('#bugList tbody > tr')
            trList.each(function () {
              const tds = $(this).find('td')
              const name = $(tds[5]).text().trim()
              if (name.includes(localStorage.getItem('zm-username'))) {
                $(this).trigger('click')
              }
            })
          })
          .insertBefore('#bugs .actions a')
          
        $(
          '<div class="btn btn-success" style="margin-right:10px;">复制勾选</div>'
        )
          .on('click', function () {
            const bugs = $('tr.checked')
              .map(function () {
                const tds = $(this).find('td')
                const id = $(tds[0]).text().trim()
                const raw = $(tds[1]).text().trim()
                let range = raw.match(/【([^【】]+?\/.+?)】/)
                range = !range ? '' : range[1].replace(/(\d\.?|-){3}/, '') // 移除版本号
                const title = raw.slice(raw.lastIndexOf('】') + 1)
                return `${localStorage.getItem('zm-username')}\t\t${id} ${title}\t${range}\n`
              })
              .get()
              .join('')
            GM_setClipboard(bugs)
          })
          .insertBefore('#bugs .actions a')
          
      }
      
      // 处理 my-work-bug 页面
      function handleMyWorkBug(colors) {
          GM_addStyle(`
              td.text-left.nobr { white-space: normal; }
              span.zm-mark { padding: 2px; border-radius: 4px; border: 1px solid; font-size: .9em; }
          `);
          addBugFetchButton(colors);
      }

      // 添加获取bug时间按钮
      function addBugFetchButton(colors) {
          const btn = $(`<div class="btn-toolbar pull-right" style="display:flex;align-items:center;"><div class="btn btn-warning">获取bug时间</div><span style="color:${colors.red};">一页超过8个Bug时需要手动获取</span></div>`)
          .on('click', async function () {
              let bugData = await fetchBugData();
              bugData = bugData.map(({ start, hasReactive }) => ({ ...timeRangeStr(start), processed: hasReactive }))
              updateBugTimeCells(bugData, colors);
          }).appendTo('#mainMenu');

          // 自动点击按钮以加载数据
          if ($('tr').length < 9) btn.click();
      }

      // 获取Bug数据
      async function fetchBugData() {
          const bugUrls = $("tr td:nth-child(5) a").map((_, ele) => ele.href).get();
          const bugPages = await Promise.all(bugUrls.map(fetchDocument));
          return bugPages.map(parseBugPage);
      }

      // 更新Bug时间单元格
      function updateBugTimeCells(bugData, colors) {
          $("tr th:nth-child(9)").text('Bug 留存').removeClass('text-center');
          $("tr td:nth-child(9)").each((idx, ele) => {
              const cell = $(ele).empty().html(`<span class="zm-mark">${bugData[idx].str}</span>`);
              const { h, processed } = bugData[idx];
              updateCellColor(cell, h, processed, colors);
          });
      }

      // 更新单元格颜色
      function updateCellColor(cell, h, processed, colors) {
          if (h < 12) cell.css({ color: colors.green });
          else if (h < 24) cell.css({ color: !processed ? colors.yellow : colors.green });
          else if (h < 34) cell.css({ color: !processed ? colors.brown : colors.yellow });
          else if (h < 70) cell.css({ color: !processed ? colors.red : colors.brown });
          else cell.css({ color: colors.red });
      }

      // 处理默认路径
      function handleDefaultPath(path) {
          if (/bug-view-\d+\.html/.test(path)) {
              setupBugDetailPage();
          } else if (/story-view-\d+.html/.test(path)) {
              setupStoryDetailPage();
          } else if (/resolvedbyme/.test(path)) {
              setupResolvedByMePage();
          } else if (/build-view-\d+.*\.html/.test(path)) {
              setupVersionBugPage()
              setupResolvedByMeBuildPage()
          } else if (/effort-createForObject-bug-\d+.html/.test(path)) {
              setupBugEffortPage()
          } else if (/effort-createForObject-task-\d+.html/.test(path)) {
              setupTaskEffortPage()
          } else if (/my\//.test(path)) {
            setupMyPageWorkHoursReminder()
          } else if (/effort-batchCreate-\d+\.html/.test(path)) {
            setupBatchEffortPage()
          }
          setupLeftMenu()
      }

      // 设置my页面工时提醒
      async function setupMyPageWorkHoursReminder() {
        try {
          // 等待页面加载完成
          await waitForContentInContainer('body', '.col-side');
          
          // 检查是否已存在提醒
          if ($('.zm-work-hours-reminder').length > 0) {
            return;
          }
          
          // 获取本周工时数据
          const weeklyData = await dataStrategies.fetch('weeklyWorkHours');
          if (weeklyData && weeklyData.hasInsufficientHours) {
            // 创建提醒div
            const reminderHtml = `
              <div class="panel zm-work-hours-reminder" style="margin-bottom: 20px; border: 2px solid #ff4d4f; background: #fff2f0;">
                <div class="panel-heading" style="background: #ff4d4f; color: white; padding: 10px 15px; font-weight: bold;">
                  <i class="icon icon-exclamation-triangle"></i> 工时提醒
                </div>
                <div class="panel-body" style="padding: 15px;">
                  <p style="margin: 0 0 10px 0; color: #ff4d4f; font-weight: bold;">
                    ⚠️ 近7天工时未填满!检测时间范围:${weeklyData.weekRange.start} 至 ${weeklyData.weekRange.end}
                  </p>
                  <div style="margin-top: 10px;">
                    <strong>未填满工时的日期:</strong>
                    <ul style="margin: 5px 0; padding-left: 20px;">
                      ${weeklyData.insufficientDays.map(day => 
                        `<li data-date="${day.date}" style="color: #ff4d4f; display: flex;  align-items: center; margin-bottom: 5px;">
                          <span>${day.date} - 已填写 ${day.hours}小时 / 需要8小时</span>
                          <button class="btn btn-xs btn-default zm-remove-reminder" data-date="${day.date}" style="margin-left: 10px;" title="删除此提醒(如请假、调休等)">
                            <i class="icon icon-close"></i>
                          </button>
                        </li>`
                      ).join('')}
                    </ul>
                  </div>
                  <div style="margin-top: 15px;">
                    <a href="/effort-calendar.html" class="btn btn-primary btn-sm">
                      <i class="icon icon-edit"></i> 立即填写工时
                    </a>
                    <button class="btn btn-default btn-sm" onclick="$(this).closest('.zm-work-hours-reminder').fadeOut()" style="margin-left: 10px;">
                      <i class="icon icon-close"></i> 关闭提醒
                    </button>
                  </div>
                </div>
              </div>
            `;
            
            // 插入到col-side的开头
            $('.col-side').prepend(reminderHtml);
            
            // 绑定删除按钮的点击事件
            $('.zm-remove-reminder').on('click', function() {
              const date = $(this).data('date');
              removeWorkHoursReminder(date);
            });
            
            console.log('(zm) 已在my页面添加工时提醒');
          } else {
            console.log('(zm) 本周工时已填满,无需提醒');
          }
        } catch (err) {
          console.error('(zm) 设置my页面工时提醒失败:', err);
        }
      }

      // 删除单个工时提醒的函数
      function removeWorkHoursReminder(date) {
        try {
          // 从localStorage中获取已删除的日期列表
          let removedDates = JSON.parse(localStorage.getItem('zm-removed-work-hours-dates') || '[]');
          
          // 添加当前日期到删除列表
          if (!removedDates.includes(date)) {
            removedDates.push(date);
            localStorage.setItem('zm-removed-work-hours-dates', JSON.stringify(removedDates));
          }
          
          // 使用data-date属性查找对应的li元素
          $(`li[data-date="${date}"]`).fadeOut(300, function() {
            $(this).remove();
            
            // 检查是否还有其他未填满的日期
            const remainingItems = $('.zm-work-hours-reminder li').length;
            if (remainingItems === 0) {
              // 如果没有其他项目了,隐藏整个提醒面板
              $('.zm-work-hours-reminder').fadeOut(300, function() {
                $(this).remove();
              });
            }
          });
          
          console.log(`(zm) 已删除 ${date} 的工时提醒`);
        } catch (err) {
          console.error('(zm) 删除工时提醒失败:', err);
        }
      }

      async function setupLeftMenu() {
        const element = await waitForContentInContainer('body', '#menuMainNav')
        const myBug = $('<li><a href="/my-work-bug.html" class="show-in-app"><i class="icon icon-bug"></i><span class="text num">我的Bug</span></a></li>');
        const myTask = $('<li><a href="/my-work-task.html" class="show-in-app"><i class="icon icon-list-alt"></i><span class="text num">我的任务</span></a></li>');
        const zenGuard = $('<li><a class="show-in-app"><i class="icon icon-magic"></i><span class="text num">禅道卫士</span></a></li>');

        myBug.click(function () {
            window.location.href = '/my-work-bug.html';
        });
        myTask.click(function () {
            window.location.href = '/my-work-task.html';
        });
        zenGuard.click(function () {
            window.open('http://172.21.15.106:8090/')
        })

        $('#menuMainNav .divider').before(myBug, myTask, zenGuard);
    }

      // 设置Bug详情页功能
      function setupBugDetailPage() {
          $('.label.label-id').on('click', function () {
              GM_setClipboard(`🔨bug(${$(this).text().trim()}): ${$(this).next().text().trim().replace(/【.+】(【.+】)*(-)*/, '')}

禅道BUG链接: [【${$(this).text().trim()}】${$(this).next().text().trim()}](${location.href})`);
          }).attr('title', '点击复制Bug提交信息').css({cursor: 'pointer'});
          enforceEffortLogging();
      }

      // 设置需求详情页功能
      function setupStoryDetailPage() {
          $('.label.label-id').on('click', function () {
              GM_setClipboard(`🔥feat(${$(this).text().trim()}): ${$(this).next().text().trim().replace(/(【.+?】)(【.+?】)*(-)*(.+)/, '$1$2$4')}

需求链接: [【${$(this).text().trim()}】${$(this).next().text().trim()}](${location.href})`);
          }).attr('title', '点击需求提交信息').css({cursor: 'pointer'});
      }

      // 强制填写工时
      function enforceEffortLogging() {
          $('a').has('.icon-bug-resolve, .icon-bug-assignTo').each((_, e) => {
              e.addEventListener('click', async function (e) {
                  const targetEle = e.target;
                  const { needEffort } = parseBugPage();
                  if (needEffort) {
                      e.stopPropagation();
                      e.preventDefault();
                      $('a.effort').get(0).click();
                  }
              }, true);
          });
      }

      // 设置 "我解决的Bug" 页面功能
      function setupResolvedByMePage() {
          $('<div class="btn btn-success">复制勾选</div>').on('click', function () {
              const bugs = $('tr.checked').map(function () {
                  const tds = $(this).find("td");
                  const id = $(tds[0]).text().trim();
                  const raw = $(tds[4]).text().trim();
                  let range = raw.match(/【([^【】]+?\/.+?)】/);
                  range = !range ? '' : range[1].replace(/(\d\.?|-){3}/, ''); // 移除版本号
                  const title = raw.slice(raw.lastIndexOf('】') + 1);
                  return `${localStorage.getItem('zm-username')}\t\t${id} ${title}\t${range}\n`;
              }).get().join('');
              GM_setClipboard(bugs);
          }).insertBefore('.btn-group.dropdown');
      }

      // 迭代版本页面中,添加一键复制已勾选BUG的按钮
      function addCopyBtnOnVersionBugPage() {
        $('<div class="btn btn-success table-actions btn-toolbar">复制勾选</div>').on('click', function () {
                const bugs = $('tr.checked').map( function () {
                    const tds = $(this).find("td")
                    const id = $(tds[0]).text().trim()
                    const title = $(tds[1]).text().trim()
                    const resolver = $(tds[5]).text().trim()
                    return `${id} ${title}\t${resolver}\n`
            })
            GM_setClipboard(bugs.get().join(''))
        }).insertBefore('.table-statistic')
      }

      /**
       * 配置迭代版本BUG页面
       * 1. 添加一键复制已勾选BUG的按钮
       */
      function setupVersionBugPage() {
        addCopyBtnOnVersionBugPage()
      }



      /**
       * Bug填写工时窗口默认填充1h处理BUG
       */
      function setupBugEffortPage() {
          // 获取Bug ID
          const bug_id = $("#mainContent > div > h2 > span.label.label-id")[0].innerHTML;

          // Bug类型配置
          const bugTypes = {
              title: 'Bug处理类型',
              color: '#e74c3c',
              items: [
                  {name: '【解决内部BUG】处理BUG ' + bug_id, desc: '自身代码导致的BUG,有禅道BUG跟踪(需关联到禅道问题单)'},
                  {name: '【协助他人处理BUG】BUG归属人<实际归属人/已离职>,处理BUG ' + bug_id, desc: '协助解决或排查其他人的BUG,按照BUG归属人区分。内部测试提出的BUG,需关联到禅道问题单,描述清楚BUG归属人;外部反馈的BUG,需写清楚问题反馈人及BUG归属情况'},
                  {name: '【协助他人】处理外部原因导致的BUG ' + bug_id, desc: '外部人员反馈的BUG,排查后定位为产品质量问题'}
              ]
          };

          // 创建Bug卡片HTML
          const createBugCard = () => {
              const items = bugTypes.items.map(item =>
                  `<li style="margin-bottom: 8px; padding: 8px; background-color: white; border-radius: 4px; cursor: pointer; transition: background-color 0.2s;" 
             title="${item.desc}" data-content="${item.name}">
                ${item.name}
            </li>`
              ).join('');

              return `
        <div style="margin-bottom: 15px; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
            <div style="padding: 10px; background-color: ${bugTypes.color}; color: white; font-weight: bold;">
                ${bugTypes.title}
            </div>
            <ul style="list-style: none; margin: 0; padding: 10px; background-color: #f0f0f0;">
                ${items}
            </ul>
        </div>`;
          };

          // 插入卡片
          insertContentToPage(createBugCard());

          // 添加点击事件
          $("[data-content]").on('click', function () {
              const content = $(this).data('content');
              $(".form-control")[2].value = content;
              addVisualFeedback($(this), '#ffc7bf');
          });

          // 默认填写工时为1小时
          $(".form-control")[1].value = 1;
          $(".form-control")[2].value = "【解决内部BUG】处理BUG " + bug_id;
      }

      /**
       * 任务工时窗口默认填充1h完成任务
       */
      function setupTaskEffortPage() {
          const taskName = $("#mainContent > div > h2 > span:nth-child(2)")[0].innerHTML;

          // 生成任务卡片HTML
          const createTaskCard = (category) => {
              const items = category.items.map(item =>
                  `<li style="margin-bottom: 8px; padding: 8px; background-color: white; border-radius: 4px; cursor: pointer; transition: background-color 0.2s;" 
                 title="${item.desc}" data-task="${item.name}${taskName}">
                <div style="font-weight: bold; color: ${category.color};">${item.name}</div>
             </li>`
              ).join('');

              return `
            <div style="flex: 1; background-color: #f8f9fa; border-radius: 8px;">
                <div style="padding: 10px; background-color: ${category.color}; border-top-left-radius: 8px; border-top-right-radius: 8px; color: white; font-weight: bold;">
                    ${category.title}
                </div>
                <ul style="list-style: none; padding: 0; margin: 0;">${items}</ul>
            </div>`;
          };

          // 生成完整内容
          const content = `
        <div style="border-radius: 10px; background-color: #ccc; padding: 10px; margin-bottom: 10px">
            <p style="color: red; margin-bottom: 15px">* 点击下方文案,可自动填充至第一行</p>
            <div style="display: flex; gap: 15px;">
                ${Object.values(commonTaskTypes).map(createTaskCard).join('')}
            </div>
        </div>
        <style>
            .task-card li:hover {
                background-color: #e9ecef !important;
                transform: translateY(-1px);
                box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            }
        </style>`;

          // 插入内容
          insertContentToPage(content);

          // 添加点击事件
          $("div[style*='flex: 1'] li").on('click', function () {
              const fullText = $(this).attr('data-task');
              $(".form-control")[3].value = fullText;
              addVisualFeedback($(this));
          });
      }

      /**
       * 批量工时创建页面功能增强
       * 点击复制items里的name,处理路径/effort-batchCreate-\d+.html,拼接到id为effortBatchAddHeader的元素后面
       */
      function setupBatchEffortPage() {
          // 使用公共任务类型配置

          // 生成紧凑的任务类型选择器
          const createCompactSelector = () => {
              const categoryHtml = Object.values(commonTaskTypes).map(category => {
                  const itemsHtml = category.items.map(item => 
                      `<span class="zm-task-type-item" 
                           data-task-name="${item.name}" 
                           title="${item.desc}"
                           style="display: inline-block; margin: 2px 4px; padding: 4px 8px; background: white; border: 1px solid ${category.color}; border-radius: 4px; cursor: pointer; font-size: 12px; color: ${category.color}; transition: all 0.2s;">
                          ${item.name}
                      </span>`
                  ).join('');
                  
                  return `
                      <div class="zm-task-category" style="margin-bottom: 8px;">
                          <span class="zm-category-title" style="display: inline-block; margin-right: 8px; padding: 2px 8px; background: ${category.color}; color: white; border-radius: 3px; font-size: 11px; font-weight: bold;">
                              ${category.title}
                          </span>
                          <div class="zm-category-items" style="display: inline-block;">
                              ${itemsHtml}
                          </div>
                      </div>
                  `;
              }).join('');

              return `
                  <div class="zm-task-selector" style="margin-bottom: 15px; padding: 10px; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 6px; font-size: 12px;">
                      <div style="margin-bottom: 8px; color: #666; font-size: 11px;">
                          <i class="icon icon-info-circle"></i> 点击下方任务类型可复制到剪贴板
                      </div>
                      ${categoryHtml}
                  </div>
              `;
          };

          // 生成完整内容
          const content = createCompactSelector();

          // 查找目标容器
          const targetContainer = $('#effortBatchAddHeader');
          if (targetContainer.length > 0) {
              // 插入到目标容器后面
              targetContainer.after(content);
          } else {
              // 如果找不到目标容器,尝试插入到页面开头
              $('#mainContent').prepend(content);
          }

          // 添加点击事件 - 点击复制任务类型名称
          $('.zm-task-type-item').on('click', function () {
              const taskName = $(this).data('task-name');
              if (taskName) {
                  // 复制到剪贴板
                  GM_setClipboard(taskName);
                  
                  // 视觉反馈
                  const $this = $(this);
                  $this.css({
                      'background-color': '#d4edda',
                      'border-color': '#28a745',
                      'color': '#28a745',
                      'transform': 'scale(1.05)'
                  });
                  
                  setTimeout(() => {
                      $this.css({
                          'background-color': 'white',
                          'border-color': $this.css('border-color').replace('#28a745', $this.attr('style').match(/border: 1px solid ([^;]+)/)?.[1] || '#007bff'),
                          'color': $this.attr('style').match(/color: ([^;]+)/)?.[1] || '#007bff',
                          'transform': 'scale(1)'
                      });
                  }, 300);
                  
                  // 显示复制成功提示
                  const originalText = $this.text();
                  $this.text('✅ 已复制').css('color', '#28a745');
                  setTimeout(() => {
                      $this.text(originalText).css('color', $this.attr('style').match(/color: ([^;]+)/)?.[1] || '#007bff');
                  }, 1500);
                  
                  console.log('(zm) 已复制任务类型:', taskName);
              }
          });
          
          // 添加悬停效果
          $('.zm-task-type-item').hover(
              function() {
                  $(this).css({
                      'background-color': '#f8f9fa',
                      'transform': 'translateY(-1px)',
                      'box-shadow': '0 2px 4px rgba(0,0,0,0.1)'
                  });
              },
              function() {
                  $(this).css({
                      'background-color': 'white',
                      'transform': 'translateY(0)',
                      'box-shadow': 'none'
                  });
              }
          );
          
          console.log('(zm) 已设置批量工时创建页面功能');
      }

      // 根据时间范围生成字符串
      function timeRangeStr(start, end = Date.now()) {
          start = new Date(start);
          end = new Date(end);
          const msPerDay = 3.6e6 * 24;
          let ms = 0;

          while (start.getTime() < end) {
              if (workdayCn.isWorkday(start)) {
                  ms += msPerDay;
              }
              start.setDate(start.getDate() + 1);
          }

          ms += end - start;
          ms = Math.max(ms, 0);

          const rawh = ms / 3.6e6;
          const h = Math.trunc(rawh);
          const m = Math.trunc((rawh - h) * 60);
          return { str: `${h} 小时 ${m} 分钟`, h, m };
      }

      // 解析Bug页面
      function parseBugPage(document = window.document) {
          const userName = localStorage.getItem('zm-username');
          const processedRe = new RegExp(`由.${userName}.(指派|解决|确认|添加)`);
          const effortRe = new RegExp(`由.${userName}.记录工时`);
          const assignRe = new RegExp(`由.${userName}.指派`);
          const assignedRe = new RegExp(`指派给.${userName}`);
          const dateRe = /(\d{4}-.+:\d{2})/;

          let start, hasReactive = false, needEffort = true;
          const assignmens = [], reactives = [];

          const current = $('#legendBasicInfo th:contains(当前指派) ~ td').text().trim();

          $(document).find('#actionbox li').each(function () {
              const text = $(this).text().trim();
              if (processedRe.test(text)) {
                  hasReactive = true;
                  reactives.push({ time: new Date(text.match(dateRe)[1]), action: text });
              }
              if (effortRe.test(text)) {
                  needEffort = false;
              }
              if (/由.+创建/.test(text)) {
                  start = new Date(text.match(dateRe)[1]);
              }
              if (assignRe.test(text)) {
                  assignmens.push({ toMe: false, time: new Date(text.match(dateRe)[1]) });
              }
              if (assignedRe.test(text)) {
                  assignmens.push({ toMe: true, time: new Date(text.match(dateRe)[1]) });
                  if (assignmens.length && assignmens[0].toMe) {
                      start = assignmens[0].time;
                  }
                  needEffort = current.includes(userName);
              }
          });

          console.log('(zm)DEBUG: ', { start: new Date(start).toLocaleString(), reactives, assignmens, hasReactive, needEffort });
          return { start, reactives, assignmens, hasReactive, needEffort };
      }

      // 获取Owner信息
      function getOwner(type) {
          const data = {
              "已解决": "研发、产品经理",
              "设计如此": "产品经理",
              "设计缺陷": "项目经理",
              "不予解决": "产品经理",
              "外部原因": "研发",
              "提交错误": "研发",
              "重复Bug": "研发",
              "无法重现": "项目经理",
              "下个版本解决": "产品经理",
              "延期处理": "产品经理"
          };
          return data[type] ? `${type}<span style="color: #8e8e8e;">(填写人:${data[type]})</span>` : type;
      }

      // 设置Cookie 某些页面需要修改Cookie中的分页和页数才能查询生效
      function setCookie(name, value, options = { path: '/' }) {
        let cookie = `${name}=${encodeURIComponent(value)}`
        
        if (options.path) cookie += `; path=${options.path}`
        if (options.domain) cookie += `; domain=${options.domain}`
        if (options.expires) cookie += `; expires=${options.expires.toUTCString()}`
        if (options.maxAge) cookie += `; max-age=${options.maxAge}`
        if (options.secure) cookie += `; secure`
        if (options.sameSite) cookie += `; samesite=${options.sameSite}`
        
        document.cookie = cookie
      }

      // 生成处理类型选择器
      async function generatorResolveType() {
          const element = await waitForContentInContainer('body', '.modal-trigger.modal-scroll-inside .modal-dialog');
          const oIframe = element.querySelector('iframe');
          oIframe.addEventListener('load', () => {
              const content = oIframe.contentDocument;
              const body = content.querySelector('.m-bug-resolve');
              const oResolveType = body.querySelector('.chosen-container');
              oResolveType.addEventListener('click', () => {
                  const lis = oResolveType.querySelectorAll('li');
                  lis.forEach(node => {
                      const text = getOwner(node.textContent.trim());
                      node.innerHTML = text;
                      node.title = text.replace(/<span style="color: .*;">|<\/span>/g, '');
                  });
              });
          });
      }

      // 等待容器内内容加载
      async function waitForContentInContainer(containerSelector, targetSelector, timeout = 10000) {
          return new Promise((resolve, reject) => {
              let timer;
              const container = document.querySelector(containerSelector);

              if (!container) {
                  return reject(new Error(`Container ${containerSelector} not found`));
              }

              function checkElement() {
                  const element = container.querySelector(targetSelector);
                  if (element) {
                      if (timer) clearTimeout(timer);
                      observer.disconnect();
                      resolve(element);
                  }
              }

              const observer = new MutationObserver(checkElement);
              observer.observe(container, { childList: true, subtree: true });

              const iframes = container.querySelectorAll('iframe');
              let iframeLoadPromises = Array.from(iframes).map(iframe => new Promise(resolve => {
                  iframe.addEventListener('load', resolve);
                  if (iframe.contentDocument && iframe.contentDocument.readyState === 'complete') {
                      resolve();
                  }
              }));

              timer = setTimeout(() => {
                  observer.disconnect();
                  reject(new Error(`Timeout: Element ${targetSelector} not found within ${timeout}ms`));
              }, timeout);

              Promise.all(iframeLoadPromises).then(() => checkElement());
          });
      }

      // 获取网页文档
      async function fetchDocument(url) {
          const response = await fetch(url);
          const arrayBuffer = await response.arrayBuffer();
          const decoder = new TextDecoder(document.characterSet);
          return new DOMParser().parseFromString(decoder.decode(arrayBuffer), 'text/html');
      }

      // 修改面板创建代码
      async function createFloatBall() {
          // 检查是否在登录页面
          if (/user-login|file-read/.test(window.location.pathname)) {
              return;
          }

          // 检查是否在iframe中
          if (window.self !== window.top) {
              return;
          }

          // 检查是否已存在悬浮球
          if ($('.zm-float-ball').length > 0) {
              return;
          }

          // 添加拖动相关样式
          GM_addStyle(`
              .zm-float-ball {
                  user-select: none;
                  touch-action: none;
                  transition: all 0.3s;
                  z-index: 9999;
              }
              .zm-float-ball.dragging {
                  transition: none;
                  opacity: 0.8;
              }
              .zm-panel {
                  z-index: 9998;
              }
          `);

          const floatBall = $(`
              <div class="zm-float-ball">
                  <i class="icon icon-zentao"></i>
              </div>
          `).appendTo('body');

          // 创建带标签页的面板
          const panel = $(`
            <div class="zm-panel">
              <div class="zm-panel-header">
                <div class="zm-panel-tabs">
                  <span class="zm-panel-tab active" data-strategy="workHours">
                    <i class="icon icon-time"></i>工时提醒
                  </span>
                  <span class="zm-panel-tab" data-strategy="myBugs">
                    <i class="icon icon-bug"></i>Bug统计
                  </span>
                </div>
                <i class="icon icon-refresh" style="float: right; cursor: pointer; font-size: 14px;"></i>
              </div>
              <div class="zm-panel-content"></div>
            </div>
          `).appendTo('body');

          // 从 localStorage 获取上次选中的面板
          let currentStrategy = localStorage.getItem('zm-panel-active') || 'workHours';
          
          // 初始化激活状态
          panel.find('.zm-panel-tab').removeClass('active');
          panel.find(`[data-strategy="${currentStrategy}"]`).addClass('active');

          // 标签切换逻辑
          panel.find('.zm-panel-tab').click(async function() {
            const strategyName = $(this).data('strategy');
            // 避免重复加载相同策略
            if (strategyName === panelStrategies.currentStrategy) return;

            panel.find('.zm-panel-tab').removeClass('active');
            $(this).addClass('active');
            
            // 保存当前选中的面板到 localStorage
            localStorage.setItem('zm-panel-active', strategyName);
            currentStrategy = strategyName; // 更新当前策略

            // 使用新的切换方法
            await panelStrategies.switchStrategy(strategyName, panel.find('.zm-panel-content'));
          });

          // 初始加载内容
          await panelStrategies.switchStrategy(currentStrategy, panel.find('.zm-panel-content'));

          // 修改刷新按钮事件处理
          panel.find('.icon-refresh').click(
            debounce(async function(e) {
              e.stopPropagation();
              
              // 刷新时取消所有进行中的请求
              requestManager.clear();
              
              const refreshIcon = $(this);
              refreshIcon.css({
                'transform': 'rotate(360deg)',
                'transition': 'transform 0.5s'
              });
              
              const strategy = panelStrategies.get(currentStrategy);
              await strategy.render(panel.find('.zm-panel-content'));
              
              setTimeout(() => {
                refreshIcon.css({
                  'transform': 'rotate(0deg)',
                  'transition': 'none'
                });
              }, 500)
            }, 300) // 300ms 的防抖延迟
          );

          // 修改拖动相关变量
          let isDragging = false;
          let startX, startY;
          let initialLeft, initialTop;
          const margin = 20;
          let hasDragged = false;

          // 更新位置的动画函数
          function updatePosition(mouseX, mouseY) {
              if (!isDragging) return;

              // 计算位移
              const deltaX = mouseX - startX;
              const deltaY = mouseY - startY;
              
              // 计算新位置
              let left = initialLeft + deltaX;
              let top = initialTop + deltaY;
              
              // 边界限制
              const maxX = window.innerWidth - floatBall.outerWidth();
              const maxY = window.innerHeight - floatBall.outerHeight();
              
              left = Math.max(0, Math.min(left, maxX));
              top = Math.max(0, Math.min(top, maxY));
              
              floatBall.css({
                  left: left + 'px',
                  top: top + 'px',
                  right: 'auto'
              });

              // 实时更新面板位置
              if (panel.is(':visible')) {
                  updatePanelPosition();
              }
          }

          // 修改 pointer events 处理
          floatBall[0].addEventListener('pointerdown', function(e) {
              isDragging = true;
              hasDragged = false;
              floatBall.addClass('dragging');
              
              // 立即隐藏面板,不使用动画
              if (panel.is(':visible')) {
                  panel.hide();
                  $('.zm-panel-content').empty();
              }
              
              this.setPointerCapture(e.pointerId);
              
              // 记录初始位置
              startX = e.clientX;
              startY = e.clientY;
              const rect = floatBall[0].getBoundingClientRect();
              initialLeft = rect.left;
              initialTop = rect.top;
              
              e.preventDefault();
          });

          floatBall[0].addEventListener('pointermove', function(e) {
              if (isDragging) {
                  hasDragged = true;
                  updatePosition(e.clientX, e.clientY);
                  e.preventDefault();
              }
          });

          floatBall[0].addEventListener('pointerup', function(e) {
              if (isDragging) {
                  isDragging = false;
                  floatBall.removeClass('dragging');
                  this.releasePointerCapture(e.pointerId);
              }
          });

          // 防止文本选择和其他默认行为
          $(document).on('selectstart dragstart', function(e) {
              if (isDragging) {
                  e.preventDefault();
                  return false;
              }
          });

          // 添加一个变量来追踪面板状态
          let isPanelVisible = false;

          // 修改点击悬浮球显示面板的代码
          floatBall.click(async function(e) {
              if (hasDragged) return;
              
              e.stopPropagation();
              
              if (!isPanelVisible) {
                  // 显示面板时取消所有进行中的请求
                  requestManager.clear();
                  
                  panel.css('opacity', 0).show();
                  updatePanelPosition();
                  panel.animate({ opacity: 1 }, 200);
                  isPanelVisible = true;
                  
                  const strategy = panelStrategies.get(currentStrategy);
                  await strategy.render(panel.find('.zm-panel-content'));
              } else {
                  panel.fadeOut(200, function() {
                      $('.zm-panel-content').empty();
                      isPanelVisible = false;
                  });
              }
          });

          // 更新面板位置函数优化
          function updatePanelPosition() {
              const ballRect = floatBall[0].getBoundingClientRect();
              const panelWidth = panel.outerWidth();
              const panelHeight = panel.outerHeight();
              const windowWidth = window.innerWidth;
              const windowHeight = window.innerHeight;
              
              // 计算各个方向的可用空间
              const leftSpace = ballRect.left;
              const rightSpace = windowWidth - ballRect.right;
              const topSpace = ballRect.top;
              const bottomSpace = windowHeight - ballRect.bottom;
              
              // 水平位置计算
              let left;
              // 优先选择空间较大的左右侧
              if (leftSpace >= rightSpace && leftSpace >= panelWidth + 10) {
                  // 左侧空间足够
                  left = ballRect.left - panelWidth - 10;
              } else if (rightSpace >= panelWidth + 10) {
                  // 右侧空间足够
                  left = ballRect.right + 10;
              } else {
                  // 两侧空间都不够,强制靠左或靠右
                  left = leftSpace > rightSpace ? 10 : windowWidth - panelWidth - 10;
              }
              
              // 垂直位置计算
              let top;
              // 优先考虑上下空间是否足够显示完整面板
              if (bottomSpace >= panelHeight + 10) {
                  // 底部空间足够
                  top = Math.min(ballRect.top, windowHeight - panelHeight - 10);
              } else if (topSpace >= panelHeight + 10) {
                  // 顶部空间足够
                  top = Math.max(10, ballRect.bottom - panelHeight);
              } else {
                  // 上下空间都不够,强制靠上或靠下
                  top = topSpace > bottomSpace ? 10 : windowHeight - panelHeight - 10;
              }
              
              panel.css({
                  left: left + 'px',
                  top: top + 'px',
                  transform: 'none'
              });
          }

          // 监听悬浮球位置变化
          const observer = new MutationObserver(() => {
              if (panel.is(':visible')) {
                  updatePanelPosition();
              }
          });
          
          observer.observe(floatBall[0], {
              attributes: true,
              attributeFilter: ['style']
          });

          // 点击其他区域隐藏面板时也需要更新状态
          $(document).click(function(e) {
              if (!$(e.target).closest('.zm-panel').length) {
                  panel.fadeOut(200, function() {
                      $('.zm-panel-content').empty();
                      isPanelVisible = false;
                  });
              }
          });
      }

      // 数据获取策略
      const dataStrategies = {
        strategies: {},
        
        register(name, strategy) {
          if (!strategy.fetch) {
            console.error('Data strategy must have fetch function');
            return;
          }
          this.strategies[name] = strategy;
        },

        async fetch(name, ...args) {
          const strategy = this.strategies[name];
          if (!strategy) {
            console.error(`Data strategy ${name} not found`);
            return null;
          }
          return await strategy.fetch(...args);
        }
      };

      // 修改数据获取策略
      dataStrategies.register('workHours', {
        async fetch() {
          try {
            const controller = new AbortController();
            requestManager.register('workHours', controller);
            
            setCookie('pagerMyEffort', 100);
            
            const response = await fetch('/my-effort-all-date_desc-1000000-100-1.json', {
              signal: controller.signal
            });
            const text = await response.text(); // 先获取文本响应
            
            // 尝试解析 JSON
            let rawData;
            try {
              rawData = JSON.parse(text);
            } catch (e) {
              throw new Error('Invalid JSON response');
            }
            
            // 确保数据格式正确
            if (!rawData.data) {
              throw new Error('Invalid data format');
            }
            
            const data = JSON.parse(rawData.data);
            const efforts = data.efforts;
            
            // 获取日期范围
            const startDate = new Date(efforts[efforts.length - 1].date);
            const endDate = new Date(efforts[0].date);
            
            // 获取周期内的工作日
            const workdays = workdayCn.getWorkdaysBetween(startDate, endDate);
            
            // 计算每天的工时
            const dailyHours = new Map();
            efforts.forEach(effort => {
                const date = effort.date;
                const hours = parseFloat(effort.consumed);
                const currentHours = dailyHours.get(date) || 0;
                // 使用 toFixed(2) 确保精度,避免浮点数运算误差
                dailyHours.set(date, parseFloat((currentHours + hours).toFixed(2)));
            });
            
            // 获取用户已删除的日期列表
            const removedDates = JSON.parse(localStorage.getItem('zm-removed-work-hours-dates') || '[]');
            
            // 找出工时不足的日期并按时间逆序排序(排除用户已删除的日期)
            return workdays
              .map(date => date.toISOString().split('T')[0])
              .filter(date => {
                  const hours = dailyHours.get(date) || 0;
                  return hours < 8 && !removedDates.includes(date);
              })
              .map(date => {
                const hours = dailyHours.get(date);
                return {
                  date,
                  hours: hours ? Number(hours.toFixed(2)) : 0
                }
              })
              .sort((a, b) => new Date(b.date) - new Date(a.date));
          } catch (err) {
            if (err.name === 'AbortError') {
              console.log('Work hours request aborted');
              return [];
            }
            console.error('Error fetching work hours:', err);
            throw err;
          } finally {
            requestManager.requests.delete('workHours');
          }
        }
      });

      // 添加本周工时检测策略
      dataStrategies.register('weeklyWorkHours', {
        async fetch() {
          try {
            const controller = new AbortController();
            requestManager.register('weeklyWorkHours', controller);
            
            setCookie('pagerMyEffort', 1000);
            
            const response = await fetch('/my-effort-all-date_desc-1000000-1000-1.json', {
              signal: controller.signal
            });
            const text = await response.text();
            
            let rawData;
            try {
              rawData = JSON.parse(text);
            } catch (e) {
              throw new Error('Invalid JSON response');
            }
            
            if (!rawData.data) {
              throw new Error('Invalid data format');
            }
            
            const data = JSON.parse(rawData.data);
            const efforts = data.efforts;
            
            // 计算近7天工作日(包含当天)
            const today = new Date();
            const currentDay = today.getDay(); // 0=周日, 1=周一, ..., 6=周六
            
            // 获取近7天的工作日
            const workdays = [];
            let workdayCount = 0;
            
            // 从今天开始往前查找,直到找到7个工作日
            for (let i = 1; workdayCount < 7; i++) {
              const currentDate = new Date(today);
              currentDate.setDate(today.getDate() - i);
              
              if (workdayCn.isWorkday(currentDate)) {
                workdays.unshift(currentDate); // 添加到数组开头,保持时间顺序
                workdayCount++;
              }
            }
            
            // 获取开始和结束日期
            const startDate = workdays[0];
            const endDate = workdays[workdays.length - 1];
            
            // 调试信息
            console.log('(zm) 近7天工作日计算调试:', {
              today: today.toISOString().split('T')[0],
              currentDay: currentDay,
              startDate: startDate.toISOString().split('T')[0],
              endDate: endDate.toISOString().split('T')[0],
              workdays: workdays.map(d => d.toISOString().split('T')[0])
            });
            
            // 使用计算出的工作日范围
            const weekWorkdays = workdays;
            
            // 计算每天的工时
            const dailyHours = new Map();
            efforts.forEach(effort => {
                const date = effort.date;
                const hours = parseFloat(effort.consumed);
                const currentHours = dailyHours.get(date) || 0;
                // 使用 toFixed(2) 确保精度,避免浮点数运算误差
                dailyHours.set(date, parseFloat((currentHours + hours).toFixed(2)));
            });
            
            // 获取用户已删除的日期列表
            const removedDates = JSON.parse(localStorage.getItem('zm-removed-work-hours-dates') || '[]');
            
            // 检查本周是否有工时不足的日期(排除用户已删除的日期)
            const insufficientDays = weekWorkdays
              .map(date => date.toISOString().split('T')[0])
              .filter(date => {
                  const hours = dailyHours.get(date) || 0;
                  return hours < 8 && !removedDates.includes(date);
              });
            
            return {
              hasInsufficientHours: insufficientDays.length > 0,
              insufficientDays: insufficientDays.map(date => ({
                date,
                hours: dailyHours.get(date) || 0
              })),
              weekRange: {
                start: startDate.toISOString().split('T')[0],
                end: endDate.toISOString().split('T')[0]
              }
            };
          } catch (err) {
            if (err.name === 'AbortError') {
              console.log('Weekly work hours request aborted');
              return { hasInsufficientHours: false, insufficientDays: [], weekRange: null };
            }
            console.error('Error fetching weekly work hours:', err);
            throw err;
          } finally {
            requestManager.requests.delete('weeklyWorkHours');
          }
        }
      });

      // 修改Bug数据获取策略
      dataStrategies.register('bugs', {
        async fetch() {
          try {
            const controller = new AbortController();
            requestManager.register('bugs', controller);
            
            const userName = localStorage.getItem('zm-username');
            if (!userName) return [];

            const response = await fetch('/my-work-bug.html', {
              signal: controller.signal
            });
            const doc = new DOMParser().parseFromString(await response.text(), 'text/html');
            const bugs = Array.from(doc.querySelectorAll('tr')).slice(1);
            
            const bugDetails = (await Promise.all(
              bugs.map(async tr => {
                const id = tr.cells[0].textContent.trim();
                const title = tr.cells[4].textContent.trim();
                const status = tr.cells[6].textContent.trim();
                
                const detailResponse = await fetch(`/bug-view-${id}.json`, {
                  signal: controller.signal
                });
                const rawDetail = await detailResponse.json();
                const detail = JSON.parse(rawDetail.data);
                
                const users = detail.users || {};
                const { assignedDate, resolvedBy, assignedTo } = detail.bug;
                const actions = Object.values(detail.actions).sort((a, b) => 
                  new Date(a.date) - new Date(b.date)
                );
                // 确定开始时间
                let startDate = null;
                if (actions.length === 1) {
                  // 只有一条记录,且初始指派给自己
                  if (users[assignedTo] === userName) {
                    startDate = assignedDate;
                  }
                } else {
                  // 检查历史记录中是否存在从自己转出的情况
                  const hasAssignFromMe = actions.some(action => {
                    if (!action.history) return false;
                    return action.history.some(h => 
                      h.field === 'assignedTo' && 
                      users[h.old] === userName
                    );
                  });

                  if (hasAssignFromMe) {
                    // 历史记录中存在从自己转出的情况
                    // 使用第一条记录的时间
                    startDate = actions[0].date;
                  } else {
                    // 查找指派给自己的操作
                    const assignToMeAction = actions.find(a => users[a.extra] === userName);
                    if (assignToMeAction) {
                      startDate = assignToMeAction.date;
                    } else if (users[assignedTo] === userName) {
                      // 最后才考虑初始指派
                      startDate = actions[0].date;
                    }
                  }
                }
                // 如果没有找到开始时间,说明bug不属于当前用户
                if (!startDate) {
                  return null;
                }
                
                
                const start = new Date(startDate);
                const {str: timeStr, h: hours} = timeRangeStr(start);
                
                // 检查是否有自己的操作记录
                const hasMyAction = actions.some(action => 
                  users[action.actor] === userName
                );

                return {
                  id,
                  title,
                  status,
                  timeStr,
                  hours,
                  resolvedBy: users[resolvedBy] || resolvedBy,
                  confirmed: detail.bug.confirmed === '1',
                  hasMyAction,
                  assignedTo: users[assignedTo]
                };
              })
            )).filter(Boolean);

            // 对bugs进行分类
            return {
              new24h: bugDetails.filter(bug => 
                bug.hours <= 24  // 24小时内新增
              ).sort((a, b) => b.hours - a.hours),  // 按时长降序
              unconfirmed: bugDetails.filter(bug => 
                bug.assignedTo === userName && !bug.confirmed
              ).sort((a, b) => b.hours - a.hours),
              untreated36h: bugDetails.filter(bug => 
                bug.hours >= 36 && bug.hours < 72 && (!bug.confirmed || !bug.hasMyAction)
              ).sort((a, b) => b.hours - a.hours),
              unresolved72h: bugDetails.filter(bug => 
                bug.hours >= 72 && !bug.resolvedBy
              ).sort((a, b) => b.hours - a.hours),
              pendingResolve: bugDetails.filter(bug => 
                bug.confirmed && bug.hours > 24 && bug.hours < 72
              ).sort((a, b) => b.hours - a.hours)
            };
            
          } catch (err) {
            if (err.name === 'AbortError') {
              console.log('Bugs request aborted');
            }
            console.error('Error fetching bug details:', err);
            throw err;
          } finally {
            requestManager.requests.delete('bugs');
          }
        }
      });

       // 虚拟滚动组件
       class VirtualScroll {
         constructor(options) {
           const defaultOptions = {
             itemHeight: 32,
             visibleCount: 10,
             bufferSize: 5,
             container: null,
             data: [],
             renderItem: null,
             className: '',
             maxHeight: 360 // 添加最大高度限制
           };
           
           this.options = { ...defaultOptions, ...options };
           this.init();
         }

         init() {
           const { itemHeight, visibleCount, data, container, className, maxHeight } = this.options;
           
           // 计算实际需要的高度,不超过maxHeight
           const totalHeight = data.length * itemHeight;
           const actualHeight = Math.min(totalHeight, maxHeight);
           
           this.$container = $(`
             <div class="zm-virtual-list ${className}" style="height: ${actualHeight}px; overflow-y: auto;">
               <div class="zm-virtual-content" style="position: relative;"></div>
             </div>
           `);
           
           this.$virtualContent = this.$container.find('.zm-virtual-content');
           this.$virtualContent.css('height', `${totalHeight}px`);
           
           $(container).append(this.$container);
           
           this.$container.on('scroll', debounce(() => {
             requestAnimationFrame(() => {
               this.render();
             });
           }, 16));
           
           this.render();
         }

         render() {
           const { itemHeight, bufferSize, data, renderItem } = this.options;
           const scrollTop = this.$container.scrollTop();
           
           const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - bufferSize);
           const endIndex = Math.min(
             data.length,
             Math.ceil((scrollTop + this.$container.height()) / itemHeight) + bufferSize
           );
           
           this.$virtualContent.empty();
           
           for (let i = startIndex; i < endIndex; i++) {
             const itemContent = renderItem(data[i], i);
             const $item = $('<div>', {
               class: 'zm-panel-item',
               css: {
                 position: 'absolute',
                 top: `${i * itemHeight}px`,
                 width: '100%',
                 height: `${itemHeight}px`
               }
             }).html(itemContent);
             
             this.$virtualContent.append($item);
           }
         }

         updateData(newData) {
           this.options.data = newData;
           this.$virtualContent.css('height', `${newData.length * this.options.itemHeight}px`);
           this.render();
         }

         destroy() {
           this.$container.remove();
         }
       }

       // 工时提醒面板
       const workHoursPanel = {
         strategy: {
           title: '工时提醒',
           icon: 'icon-time',
           async render(content) {
             try {
               content.html('<div class="zm-panel-item" style="text-align: center;">加载中...</div>');
               
               const insufficientDays = await dataStrategies.fetch('workHours');
               
               content.empty();
               if (!insufficientDays || insufficientDays.length === 0) {
                 content.append('<div class="zm-panel-item">所有工作日工时已填写完整 👍</div>');
                 return;
               }

               new VirtualScroll({
                 container: content,
                 data: insufficientDays,
                 className: 'work-hours',
                 itemHeight: 48,
                 maxHeight: 360, // 限制最大高度
                 renderItem: (day) => 
                   $('<div>').append(
                     $('<span>').text(day.date),
                     $('<span>').addClass('zm-hours').text(`${day.hours}h / 8h`)
                   ).html()
               });
               
             } catch (err) {
               if (err.name === 'AbortError') {
                 content.html('<div class="zm-panel-item" style="text-align: center;">加载中...</div>');
                 return;
               }
               content.html(`<div class="zm-panel-item error">加载失败: ${err.message}</div>`);
             }
           }
         },
         
         style: `
           .zm-virtual-list.work-hours .zm-panel-item {
             line-height: 40px;
             padding: 4px 16px;
             display: flex;
             justify-content: space-between;
             align-items: center;
             border-bottom: 1px solid #f0f0f0;
             background-color: #fff;
           }
           
           .zm-virtual-list.work-hours .zm-hours {
             color: #ff4d4f;
             font-size: 12px;
           }
         `
       };

       // Bug统计面板
       const bugsPanel = {
         strategy: {
           title: 'Bug统计',
           icon: 'icon-bug',
           async render(content) {
             try {
               content.html('<div class="zm-panel-item" style="text-align: center;">加载中...</div>');
               
               const bugs = await dataStrategies.fetch('bugs');
               
               content.empty();
               if (!bugs || !Object.values(bugs).some(arr => arr.length > 0)) {
                 content.append('<div class="zm-panel-item">暂无Bug</div>');
                 return;
               }

               // 添加提示信息(如果是首次查看)
               if (!localStorage.getItem('zm-bug-tip-shown')) {
                 content.append(`
                   <div class="zm-bug-tip">
                     <span>💡 点击Bug ID可直接跳转到详情页</span>
                     <span class="close-tip">×</span>
                   </div>
                 `);
                 
                 content.find('.close-tip').click(function() {
                   $(this).parent().fadeOut(200);
                   localStorage.setItem('zm-bug-tip-shown', 'true');
                 });
               }

               // 更新分类配置和显示顺序
               const categories = [
                 { key: 'untreated36h', title: '36小时未处理', color: '#ff4d4f' },
                 { key: 'unresolved72h', title: '72小时未解决', color: '#f5222d' },
                 { key: 'pendingResolve', title: '待解决Bug', color: '#1890ff' },
                 { key: 'new24h', title: '24小时内新增', color: '#52c41a' }
               ];

               // 添加展开全部按钮
               content.append(`
                 <div class="zm-bug-expand-all">
                   <span class="expand-all-btn">展开全部</span>
                 </div>
               `);

               categories.forEach(({key, title, color}) => {
                 if (bugs[key].length > 0) {
                   const isExpanded = key === 'pendingResolve'; // 默认展开待解决bug
                   content.append(`
                     <div class="zm-bug-category">
                       <div class="zm-bug-category-title ${isExpanded ? 'expanded' : ''}" style="color: ${color}">
                         <i class="icon icon-chevron-right ${isExpanded ? 'icon-rotate-90' : ''}"></i>
                         ${title} (${bugs[key].length})
                       </div>
                       <div class="zm-bug-list-${key}" style="display: ${isExpanded ? 'block' : 'none'}"></div>
                     </div>
                   `);

                   new VirtualScroll({
                     container: content.find(`.zm-bug-list-${key}`),
                     data: bugs[key],
                     className: 'bugs',
                     itemHeight: 32,
                     visibleCount: Math.min(bugs[key].length, 5),
                     maxHeight: 200, // 限制每个分类的最大高度
                     renderItem: (bug) => {
                       // 根据时长确定颜色类
                       let colorClass = '';
                       if (bug.hours <= 24) {
                         colorClass = 'green';
                       } else if (bug.hours <= 34) {
                         colorClass = 'orange';
                       } else if (bug.hours <= 70) {
                         colorClass = 'yellow';
                       } else {
                         colorClass = 'red';
                       }
                       
                       return $('<div>')
                         .addClass('zm-bug-item')
                         .css('cursor', 'pointer')
                         .on('click', () => {
                           window.open(`/bug-view-${bug.id}.html`, '_blank');
                         })
                         .append(
                           $('<a>')
                             .addClass('zm-bug-id')
                             .attr('href', `/bug-view-${bug.id}.html`)
                             .attr('target', '_blank')
                             .text(`Bug: ${bug.id}`)
                             .attr('title', bug.title),
                           $('<span>')
                             .addClass(`zm-bug-hours ${colorClass}`)
                             .text(bug.timeStr)
                         ).html();
                     }
                   });
                 }
               });
               
               // 添加折叠/展开事件处理
               content.find('.zm-bug-category-title').click(function() {
                 $(this).toggleClass('expanded');
                 $(this).find('.icon').toggleClass('icon-rotate-90');
                 $(this).next('.zm-bug-list-' + $(this).parent().find('[class^="zm-bug-list-"]').attr('class').split('-')[3]).slideToggle(200);
                 
                 // 检查是否所有分类都已展开
                 const allExpanded = content.find('.zm-bug-category-title.expanded').length === content.find('.zm-bug-category-title').length;
                 content.find('.expand-all-btn').text(allExpanded ? '折叠全部' : '展开全部');
               });

               // 展开全部按钮事件处理
               content.find('.expand-all-btn').click(function() {
                 const isExpandAll = $(this).text() === '展开全部';
                 $(this).text(isExpandAll ? '折叠全部' : '展开全部');
                 content.find('.zm-bug-category-title').each(function() {
                   const $title = $(this);
                   const $list = $title.next('[class^="zm-bug-list-"]');
                   if (isExpandAll) {
                     $title.addClass('expanded');
                     $title.find('.icon').addClass('icon-rotate-90');
                     $list.slideDown(200);
                   } else {
                     $title.removeClass('expanded');
                     $title.find('.icon').removeClass('icon-rotate-90');
                     $list.slideUp(200);
                   }
                 });
               });
               
             } catch (err) {
               if (err.name === 'AbortError') {
                 content.html('<div class="zm-panel-item" style="text-align: center;">加载中...</div>');
                 return;
               }
               content.html(`<div class="zm-panel-item error">加载失败: ${err.message}</div>`);
             }
           }
         },
         
         style: `
           .zm-virtual-list.bugs .zm-panel-item {
             padding: 8px 12px;
           }
           
           .zm-bug-category {
             margin-bottom: 12px;
           }

           .zm-bug-category-title {
             padding: 8px 12px;
             font-weight: bold;
             background: #fafafa;
             cursor: pointer;
             user-select: none;
             display: flex;
             align-items: center;
           }
           
           .zm-bug-category-title:hover {
             background: #f0f0f0;
           }
           
           .zm-bug-category-title .icon {
             margin-right: 8px;
             transition: transform 0.2s;
           }
           
           .zm-bug-category-title .icon-rotate-90 {
             transform: rotate(90deg);
           }
           
           .zm-bug-item {
             display: flex;
             align-items: center;
             width: 100%;
             transition: background-color 0.2s;
           }
           
           .zm-bug-item:hover {
             background-color: rgba(24, 144, 255, 0.1);
           }
           
           .zm-bug-id {
             color: #666;
             margin-right: 8px;
             cursor: pointer;
             text-decoration: none;
             position: relative;
           }
           
           .zm-bug-id:hover {
             color: #1890ff;
             text-decoration: underline;
           }
           
           /* 添加鼠标悬停提示图标 */
           .zm-bug-id::before {
             content: '🔗';
             font-size: 12px;
             margin-right: 4px;
             opacity: 0;
             transition: opacity 0.2s;
           }
           
           .zm-bug-id:hover::before {
             opacity: 1;
           }
           
           /* 首次打开面板时的提示样式 */
           .zm-bug-tip {
             padding: 8px 12px;
             background: #e6f7ff;
             border: 1px solid #91d5ff;
             border-radius: 4px;
             margin-bottom: 8px;
             font-size: 12px;
             color: #1890ff;
             display: flex;
             align-items: center;
             justify-content: space-between;
           }
           
           .zm-bug-tip .close-tip {
             cursor: pointer;
             color: #1890ff;
             font-size: 14px;
           }
           
           .zm-bug-title {
             flex: 1;
             overflow: hidden;
             text-overflow: ellipsis;
             white-space: nowrap;
           }
           
           .zm-bug-hours {
             font-size: 12px;
             margin-left: 8px;
           }
           
           .zm-bug-hours.green { color: #52c41a; }
           .zm-bug-hours.yellow { color: #faad14; }
           .zm-bug-hours.orange { color: #fa8c16; }
           .zm-bug-hours.red { color: #ff4d4f; }
           
           .zm-bug-expand-all {
             padding: 8px 12px;
             border-bottom: 1px solid #f0f0f0;
           }

           .expand-all-btn {
             color: #1890ff;
             cursor: pointer;
             user-select: none;
           }

           .expand-all-btn:hover {
             color: #40a9ff;
           }
         `
       };

       // 注册面板
       panelStrategies.register('workHours', workHoursPanel.strategy);
       panelStrategies.register('myBugs', bugsPanel.strategy);

       // 添加样式
       GM_addStyle(`
         /* 通用虚拟列表样式 */
         .zm-panel-content {
           width: 100%;
         }

         .zm-virtual-list {
           position: relative;
           border-top: 1px solid #f0f0f0;
           width: 100%;
         }
         
         .zm-virtual-list .zm-virtual-content {
           width: 100%;
         }
         
         .zm-virtual-list .zm-panel-item {
           box-sizing: border-box;
           width: 100%;
         }
         
         .zm-virtual-list .zm-panel-item:hover {
           background-color: #f5f5f5;
         }
         
         /* 各面板特定样式 */
         ${workHoursPanel.style}
         ${bugsPanel.style}
       `);
  });
})();

// 添加面板样式
GM_addStyle(`
  .zm-panel {
    position: fixed;
    width: 300px;
    background: white;
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.15);
    display: none;
    z-index: 9999;
  }

  .zm-panel-header {
    padding: 12px;
    border-bottom: 1px solid #f0f0f0;
  }

  .zm-panel-tabs {
    display: flex;
    gap: 12px;
  }

  .zm-panel-tab {
    cursor: pointer;
    padding: 4px 8px;
    border-radius: 4px;
    transition: all 0.3s;
    user-select: none;
  }

  .zm-panel-tab:hover {
    background: rgba(0,0,0,0.05);
  }

  .zm-panel-tab.active {
    background: #1890ff;
    color: white;
  }

  .zm-panel-tab i {
    margin-right: 4px;
  }

  .zm-panel-content {
    padding: 12px;
    max-height: 400px;
    overflow-y: auto;
  }

  .zm-panel-item {
    padding: 8px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    border-bottom: 1px solid #f0f0f0;
  }

  .zm-panel-item:last-child {
    border-bottom: none;
  }
`);

// 简单的debounce实现
function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}