Kakueeen / PMS增强工具

// ==UserScript==
// @name         PMS增强工具
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  统信PMS系统效率提升工具:一键指派Bug、智能自动填充、在线视频预览
// @author       Kakueeen
// @include      https://pms.uniontech.com/bug-view*
// @include      https://pms.uniontech.com/zentao/bug-view*
// @license      MIT
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  // ============================================================
  // 配置区域 - Configuration
  // ============================================================

  // LocalStorage配置键名
  const STORAGE_KEYS = {
    ASSIGN_TO_SELF: 'pms_enhanced_assign_to_self',
    AUTO_FILL_ASSIGNED_TO: 'pms_enhanced_auto_fill_assigned_to'
  };

  /**
   * 从LocalStorage读取配置
   * @param {string} key - 配置键名
   * @param {*} defaultValue - 默认值
   * @returns {*} 配置值
   */
  function getConfig(key, defaultValue = null) {
    try {
      const value = localStorage.getItem(key);
      return value !== null ? value : defaultValue;
    }
    catch (error) {
      console.warn(`[PMS增强] 读取配置失败 (${key}):`, error);
      return defaultValue;
    }
  }

  /**
   * 保存配置到LocalStorage
   * @param {string} key - 配置键名
   * @param {*} value - 配置值
   */
  function setConfig(key, value) {
    try {
      localStorage.setItem(key, value);
      console.log(`[PMS增强] ✓ 配置已保存 (${key}): ${value}`);
    }
    catch (error) {
      console.error(`[PMS增强] 保存配置失败 (${key}):`, error);
    }
  }

  // 指派给自己功能配置
  const ASSIGN_CONFIG = {
    get targetUser() {
      return getConfig(STORAGE_KEYS.ASSIGN_TO_SELF);
    },
    set targetUser(value) {
      setConfig(STORAGE_KEYS.ASSIGN_TO_SELF, value);
    }
  };

  // 自动填充功能配置
  const AUTO_FILL_CONFIG = {
    presetValues: {
      'exttype': '算法/方法', // 类型
      'resolution': 'fixed', // 集成状态
      'resolvedBuild': 'trunk', // 解决的版本
      'repair': '代码', // 修复点
      'symbol': '不涉及', // 限定符
      'age': '当前版本新增引入', // 引入阶段
      'source': '不涉及', // 来源
      get assignedTo() {
        return getConfig(STORAGE_KEYS.AUTO_FILL_ASSIGNED_TO);
      },
      set assignedTo(value) {
        setConfig(STORAGE_KEYS.AUTO_FILL_ASSIGNED_TO, value);
      },
      'comment': `【版本号】:
【自测环境镜像版本】:v25
【代码地址】:
【根因分析(Bug)】:
【影响范围】:
【自测结果截图/视频】:
【自测架构】:amd
【引入责任人】:无
【引入提交链接】:无
【维护线是否回合主线】:选填项,维护线修复后填写合入主线的结果以及原因,主线修复无需填写。
【测试点】:选填项
【前置条件】:选填项
【步骤】:选填项
【预期】:选填项
【自测结果】:通过` // 备注
    },
    // 字段配置映射
    fieldConfig: {
      resolution: {
        selector: '#resolution',
        label: '集成状态'
      },
      resolvedBuild: {
        selector: '#resolvedBuild',
        label: '解决版本'
      },
      repair: {
        selector: '#repair',
        label: '修复点'
      },
      exttype: {
        selector: '#exttype',
        label: '类型'
      },
      symbol: {
        selector: '#symbol',
        label: '限定符'
      },
      age: {
        selector: '#age',
        label: '引入阶段'
      },
      source: {
        selector: '#source',
        label: '来源'
      },
      assignedTo: {
        selector: '#assignedTo',
        label: '指派给'
      }
    }
  };

  // 视频预览功能配置
  const VIDEO_PREVIEW_CONFIG = {
    // 支持的视频格式(扩展名)
    videoExtensions: ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.mkv', '.flv', '.wmv', '.m4v'],
    // 弹窗样式配置
    modalStyle: {
      maxWidth: '90%',
      maxHeight: '90vh'
    }
  };

  // 视频内存缓存:避免重复下载同一视频
  // key: 下载URL, value: { blob, blobUrl, refCount }
  const VIDEO_CACHE = new Map();

  /**
   * 从缓存获取视频 Blob URL,或下载后缓存
   * @param {string} videoUrl - 视频下载URL
   * @param {HTMLElement} modal - 弹窗元素(用于显示进度)
   * @returns {Promise<string>} blob URL
   */
  async function getVideoBlobUrl(videoUrl, modal) {
    const cached = VIDEO_CACHE.get(videoUrl);
    if (cached) {
      cached.refCount++;
      console.log(`[PMS增强] 视频缓存命中: ${videoUrl}`);
      return cached.blobUrl;
    }

    console.log('[PMS增强] 正在下载视频到内存...');
    const response = await fetch(videoUrl);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);

    let blob;
    if (response.body) {
      // 流式下载,即使没有 Content-Length 也能显示已下载大小
      const reader = response.body.getReader();
      const chunks = [];
      let receivedBytes = 0;

      while (true) {
        const {
          done,
          value
        } = await reader.read();
        if (done) break;
        chunks.push(value);
        receivedBytes += value.length;
      }
      blob = new Blob(chunks, {
        type: 'video/mp4'
      });
      console.log(`[PMS增强] 视频下载完成: ${(receivedBytes / 1024 / 1024).toFixed(2)} MB`);
    }
    else {
      // 不支持 ReadableStream(极少见),降级为一次性获取
      blob = await response.blob();
    }

    const blobUrl = URL.createObjectURL(blob);
    VIDEO_CACHE.set(videoUrl, {
      blob,
      blobUrl,
      refCount: 1
    });
    return blobUrl;
  }

  /**
   * 释放视频缓存引用
   * @param {string} videoUrl - 视频下载URL
   */
  function releaseVideoCache(videoUrl) {
    const cached = VIDEO_CACHE.get(videoUrl);
    if (!cached) return;
    cached.refCount--;
    if (cached.refCount <= 0) {
      URL.revokeObjectURL(cached.blobUrl);
      VIDEO_CACHE.delete(videoUrl);
      console.log(`[PMS增强] 视频缓存已释放: ${videoUrl}`);
    }
  }

  // ============================================================
  // 功能1: 指派给自己
  // ============================================================

  /**
   * 获取当前Bug ID
   * @returns {string|null} Bug ID
   */
  function getBugId() {
    const match = window.location.pathname.match(/bug-view-(\d+)/);
    return match ? match[1] : null;
  }

  /**
   * 添加"指派给自己"按钮
   */
  function addAssignToSelfButton() {
    // 查找"指派"按钮
    const assignButton = document.querySelector('a[href*="bug-assignTo"]');
    if (!assignButton || document.getElementById('assignToSelfBtn')) {
      return;
    }

    // 创建"指派给自己"按钮
    const selfButton = createSelfAssignButton();

    // 在"指派"按钮后插入新按钮
    assignButton.parentNode.insertBefore(selfButton, assignButton.nextSibling);

    console.log('[PMS增强] 已添加"指派给自己"按钮');
  }

  /**
   * 创建"指派给自己"按钮元素
   * @returns {HTMLAnchorElement} 按钮元素
   */
  function createSelfAssignButton() {
    const button = document.createElement('a');
    button.id = 'assignToSelfBtn';
    button.href = 'javascript:void(0);';
    button.className = 'btn btn-link';
    button.innerHTML = '<i class="icon-hand-right"></i> <span class="text">指派给自己</span>';

    // 添加点击事件
    button.addEventListener('click', handleAssignToSelf);

    return button;
  }

  /**
   * 处理指派给自己的操作
   */
  async function handleAssignToSelf() {
    const bugId = getBugId();
    if (!bugId) {
      alert('无法获取Bug ID');
      return;
    }

    try {
      console.log('[PMS增强] 开始指派操作...');

      // 先触发点击打开弹窗
      const assignButton = document.querySelector('a[href*="bug-assignTo"]');
      if (!assignButton) {
        alert('未找到指派按钮');
        return;
      }

      assignButton.click();
      console.log('[PMS增强] 已触发指派弹窗');

      // 等待iframe加载并自动填充
      let attempts = 0;
      const maxAttempts = 20; // 最多尝试20次,共4秒

      const waitAndFill = setInterval(() => {
        attempts++;

        const iframe = document.getElementById('iframe-triggerModal');
        if (!iframe) {
          if (attempts >= maxAttempts) {
            clearInterval(waitAndFill);
            console.warn('[PMS增强] 未找到iframe元素');
          }
          return;
        }

        const iframeSrc = iframe.src || '';
        if (!iframeSrc.includes('bug-assignTo')) {
          return; // 不是指派窗口,继续等待
        }

        try {
          const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
          if (!iframeDoc) {
            if (attempts >= maxAttempts) {
              clearInterval(waitAndFill);
              console.warn('[PMS增强] 无法访问iframe文档,可能存在跨域限制');
            }
            return;
          }

          // 检查是否加载完成
          if (iframeDoc.readyState !== 'complete') {
            return; // 继续等待
          }

          // 检查关键元素是否存在
          const assignToSelect = iframeDoc.querySelector('#assignedTo');
          if (!assignToSelect) {
            if (attempts >= maxAttempts) {
              clearInterval(waitAndFill);
              console.warn('[PMS增强] 未找到指派选择框');
            }
            return;
          }

          // 找到了,停止轮询并执行填充
          clearInterval(waitAndFill);
          console.log('[PMS增强] 指派窗口已就绪,开始自动填充');

          // 再延迟一下确保Chosen插件初始化完成
          setTimeout(() => {
            fillAssignForm(iframeDoc, iframe.contentWindow);
          }, 300);

        }
        catch (err) {
          console.error('[PMS增强] 检查iframe时出错:', err);
          if (attempts >= maxAttempts) {
            clearInterval(waitAndFill);
          }
        }
      }, 200); // 每200ms检查一次

    }
    catch (error) {
      console.error('[PMS增强] 指派失败:', error);
      alert('指派操作失败,请手动指派');
    }
  }

  /**
   * 填充指派表单
   * @param {Document} iframeDoc - iframe的document对象
   * @param {Window} iframeWindow - iframe的window对象
   */
  function fillAssignForm(iframeDoc, iframeWindow) {
    try {
      const assignToSelect = iframeDoc.querySelector('#assignedTo');
      if (!assignToSelect) {
        console.warn('[PMS增强] 未找到指派对象选择框');
        return;
      }

      const savedUser = ASSIGN_CONFIG.targetUser;

      // 如果有保存的配置,自动填充
      if (savedUser) {
        assignToSelect.value = savedUser;
        assignToSelect.dispatchEvent(new Event('change', {
          bubbles: true
        }));

        if (iframeWindow && iframeWindow.$) {
          iframeWindow.$(assignToSelect).trigger('chosen:updated');
          iframeWindow.$(assignToSelect).trigger('change');
          console.log(`[PMS增强] ✓ 已自动设置指派给: ${savedUser}`);
        }
      }
      else {
        console.log('[PMS增强] 未设置默认指派对象,请手动选择后提交以保存配置');
      }

      // 监听提交按钮,保存用户选择的指派对象
      setupAssignFormSubmitListener(iframeDoc, assignToSelect);

    }
    catch (error) {
      console.error('[PMS增强] 填充指派表单失败:', error);
    }
  }

  /**
   * 设置指派表单提交监听器,保存配置
   * @param {Document} iframeDoc - iframe的document对象
   * @param {HTMLSelectElement} assignToSelect - 指派选择框
   */
  function setupAssignFormSubmitListener(iframeDoc, assignToSelect) {
    const submitBtn = iframeDoc.querySelector('#submit');
    if (!submitBtn || submitBtn.dataset.configSaveListenerAdded) {
      return;
    }

    submitBtn.addEventListener('click', function () {
      const selectedUser = assignToSelect.value;
      if (selectedUser) {
        ASSIGN_CONFIG.targetUser = selectedUser;
        console.log(`[PMS增强] ✓ 已保存指派配置: ${selectedUser}`);
      }
    });

    submitBtn.dataset.configSaveListenerAdded = 'true';
  }

  // ============================================================
  // 功能2: 视频预览
  // ============================================================

  /**
   * 检查文件是否为视频格式
   * @param {string} filename - 文件名或URL
   * @returns {boolean} 是否为视频文件
   */
  function isVideoFile(filename) {
    if (!filename) return false;
    const lowerFilename = filename.toLowerCase();
    return VIDEO_PREVIEW_CONFIG.videoExtensions.some(ext => lowerFilename.endsWith(ext));
  }

  /**
   * Debug function to inspect attachment structure
   * Call this in console: window.inspectPMSAttachments()
   */
  function inspectAttachments() {
    console.log('=== PMS Attachment Structure Inspector ===');

    // Try different selectors
    const selectors = [
      'div.files-list',
      '.files-list',
      'div.file',
      '.file',
      'a[href*="file-download"]'
    ];

    selectors.forEach(selector => {
      const elements = document.querySelectorAll(selector);
      if (elements.length > 0) {
        console.log(`\n[${selector}] Found ${elements.length} elements:`);
        elements.forEach((el, idx) => {
          console.log(`  ${idx + 1}.`, el);
          console.log(`     HTML:`, el.outerHTML.substring(0, 200));
        });
      }
    });

    console.log('\n=== Video files detection ===');
    const allLinks = document.querySelectorAll('a');
    allLinks.forEach(link => {
      const text = link.textContent.trim();
      if (text && isVideoFile(text)) {
        console.log('Found video:', text, link);
      }
    });
  }

  // Expose debug function to window
  window.inspectPMSAttachments = inspectAttachments;

  /**
   * 初始化视频预览功能
   * 遍历所有附件链接,为视频文件添加预览按钮
   */
  function initVideoPreview() {
    const attachmentLinks = document.querySelectorAll('ul.files-list li a[href*="file-download"]');

    if (attachmentLinks.length === 0) return;

    console.log(`[PMS增强] 发现 ${attachmentLinks.length} 个附件`);

    attachmentLinks.forEach(link => {
      // 避免重复添加
      if (link.dataset.videoPreviewAdded) return;

      const filename = extractFilename(link);
      if (!filename) return;

      console.log(`[PMS增强] 检查文件: ${filename}`);

      if (isVideoFile(filename)) {
        console.log(`[PMS增强] 识别到视频文件: ${filename}`);
        addVideoPreviewButton(link, filename);
        link.dataset.videoPreviewAdded = 'true';
      }
    });
  }

  /**
   * 从附件链接提取文件名
   * @param {HTMLAnchorElement} link - 附件链接元素
   * @returns {string} 文件名
   */
  function extractFilename(link) {
    // 优先从onclick属性提取
    // 示例: onclick="return downloadFile(1224031, 'mp4', 0, '录屏_选择区域_20260115162058.mp4')"
    const onclickAttr = link.getAttribute('onclick');
    if (onclickAttr) {
      const match = onclickAttr.match(/downloadFile\([^,]+,\s*'[^']+',\s*\d+,\s*'([^']+)'\)/);
      if (match?.[1]) return match[1];
    }

    // 降级方案:从链接文本提取(移除图标和大小信息)
    const textContent = link.textContent.trim();
    return textContent.replace(/^\s*\S+\s+/, '').replace(/\s*\([^)]+\)\s*$/, '').trim();
  }

  /**
   * 为视频附件添加预览按钮
   * @param {HTMLAnchorElement} attachmentLink - 附件链接元素
   * @param {string} filename - 视频文件名
   */
  function addVideoPreviewButton(attachmentLink, filename) {
    const previewBtn = createPreviewButton(attachmentLink.href, filename);
    const listItem = attachmentLink.closest('li');

    if (!listItem) {
      console.warn('[PMS增强] 未找到父级<li>元素');
      return;
    }

    insertPreviewButton(listItem, previewBtn, filename);
  }

  /**
   * 创建预览按钮元素
   * @param {string} videoUrl - 视频URL
   * @param {string} filename - 文件名
   * @returns {HTMLAnchorElement} 预览按钮元素
   */
  function createPreviewButton(videoUrl, filename) {
    const btn = document.createElement('a');
    btn.href = 'javascript:void(0);';
    btn.className = 'text-primary';
    btn.textContent = '预览';
    btn.title = `预览视频: ${filename}`;

    btn.addEventListener('click', (e) => {
      e.preventDefault();
      e.stopPropagation();

      // 提取文件ID并构建直接下载URL
      const fileId = videoUrl.match(/file-download-(\d+)/)?.[1];
      const directUrl = fileId ? `/zentao/file-download-${fileId}.html` : videoUrl;

      showVideoPreviewModal(directUrl, filename);
    });

    return btn;
  }

  /**
   * 将预览按钮插入到操作按钮容器中
   * @param {HTMLElement} listItem - 附件列表项
   * @param {HTMLAnchorElement} previewBtn - 预览按钮
   * @param {string} filename - 文件名
   */
  function insertPreviewButton(listItem, previewBtn, filename) {
    let rightIconSpan = listItem.querySelector('.right-icon');

    if (!rightIconSpan) {
      // 不存在操作按钮容器,创建一个
      rightIconSpan = document.createElement('span');
      rightIconSpan.className = 'right-icon';
      rightIconSpan.innerHTML = '&nbsp; ';
      listItem.appendChild(document.createTextNode('\n'));
      listItem.appendChild(rightIconSpan);
    }

    // 插入预览按钮到容器开头(&nbsp;之后)
    const firstChild = rightIconSpan.firstChild;
    if (firstChild?.nextSibling) {
      rightIconSpan.insertBefore(previewBtn, firstChild.nextSibling);
      rightIconSpan.insertBefore(document.createTextNode('\n'), previewBtn.nextSibling);
    }
    else {
      rightIconSpan.appendChild(previewBtn);
      rightIconSpan.appendChild(document.createTextNode('\n'));
    }

    console.log(`[PMS增强] ✓ 已添加视频预览按钮: ${filename}`);
  }

  /**
   * 显示视频预览弹窗
   * @param {string} videoUrl - 视频文件URL
   * @param {string} filename - 视频文件名
   */
  async function showVideoPreviewModal(videoUrl, filename) {
    console.log(`[PMS增强] 打开视频预览: ${filename}`);

    // 移除已存在的弹窗
    document.getElementById('videoPreviewModal')?.remove();

    // 创建并显示弹窗(先显示加载状态)
    const modal = createVideoPreviewModal(filename, null);
    modal.dataset.videoUrl = videoUrl;
    document.body.appendChild(modal);
    requestAnimationFrame(() => {
      modal.style.opacity = '1';
    });

    try {
      // 直接使用同源下载 URL,让 <video> 原生流式播放。
      // 不再走 fetch→blob 路径:本站 CSP 为 default-src 'self'(未声明 media-src),
      // blob: 会被拒绝并报 "Media load rejected by URL safety check",导致视频加载失败。
      setVideoPlayer(modal, videoUrl);
    }
    catch (error) {
      console.error(`[PMS增强] 视频加载失败:`, error);
      showErrorInModal(modal, '视频加载失败: ' + error.message);
    }
  }

  /**
   * 更新弹窗中的加载进度文本
   * @param {HTMLElement} modal - 弹窗元素
   * @param {string} blobUrl - Blob URL
   */
  function setVideoPlayer(modal, blobUrl) {
    const content = modal.querySelector('.pms-video-content');
    if (!content) return;

    // 移除加载状态元素
    const loadingEl = content.querySelector('.pms-video-loading');
    if (loadingEl) loadingEl.remove();

    // 创建视频播放器
    const video = createVideoPlayer(blobUrl);
    content.appendChild(video);
  }

  /**
   * 在弹窗中显示错误信息
   * @param {HTMLElement} modal - 弹窗元素
   * @param {string} message - 错误信息
   */
  function showErrorInModal(modal, message) {
    const content = modal.querySelector('.pms-video-content');
    if (!content) return;

    const loadingEl = content.querySelector('.pms-video-loading');
    if (loadingEl) loadingEl.remove();

    const errorDiv = document.createElement('div');
    errorDiv.style.cssText = 'color:#d9534f;padding:40px 20px;text-align:center;font-size:14px;';
    errorDiv.textContent = message;
    content.appendChild(errorDiv);
  }

  /**
   * 创建视频预览弹窗元素
   * @param {string} filename - 视频文件名
   * @param {string|null} blobUrl - Blob URL(传入时直接创建播放器,null 时显示加载状态)
   * @returns {HTMLDivElement} 弹窗元素
   */
  function createVideoPreviewModal(filename, blobUrl) {
    const modal = createModalOverlay();
    const content = createModalContent();
    content.classList.add('pms-video-content');
    const header = createModalHeader(filename, modal);

    content.appendChild(header);

    if (blobUrl) {
      const video = createVideoPlayer(blobUrl);
      content.appendChild(video);
    }
    else {
      // 显示加载状态
      const loadingDiv = document.createElement('div');
      loadingDiv.className = 'pms-video-loading';
      loadingDiv.style.cssText = 'text-align:center;padding:60px 20px;min-height:200px;display:flex;flex-direction:column;align-items:center;justify-content:center;';

      const loadingIcon = document.createElement('div');
      loadingIcon.innerHTML = '<i class="icon-play-circle" style="font-size:48px;color:#20a0ff;margin-bottom:20px;"></i>';

      const loadingLabel = document.createElement('div');
      loadingLabel.style.cssText = 'color:#333;font-size:15px;font-weight:bold;margin-bottom:20px;';
      loadingLabel.textContent = '正在加载视频...';

      const progressOuter = document.createElement('div');
      progressOuter.style.cssText = 'background:#e0e0e0;border-radius:6px;height:10px;width:300px;max-width:80%;overflow:hidden;';

      const progressBar = document.createElement('div');
      progressBar.id = 'pms-video-progress-bar';
      progressBar.style.cssText = 'background:linear-gradient(45deg,#1a8cff 25%,#20a0ff 25%,#20a0ff 50%,#1a8cff 50%,#1a8cff 75%,#20a0ff 75%);background-size:30px 30px;height:100%;width:100%;border-radius:6px;animation:pms-progress-stripe 1s linear infinite;';

      progressOuter.appendChild(progressBar);

      const progressText = document.createElement('div');
      progressText.id = 'pms-video-progress-text';
      progressText.style.cssText = 'color:#999;font-size:12px;margin-top:10px;';
      progressText.textContent = '准备中...';

      // 注入条纹动画 CSS(仅注入一次)
      if (!document.getElementById('pms-video-preview-style')) {
        const style = document.createElement('style');
        style.id = 'pms-video-preview-style';
        style.textContent = '@keyframes pms-progress-stripe{0%{background-position:0 0}100%{background-position:30px 0}}';
        document.head.appendChild(style);
      }

      loadingDiv.appendChild(loadingIcon);
      loadingDiv.appendChild(loadingLabel);
      loadingDiv.appendChild(progressOuter);
      loadingDiv.appendChild(progressText);
      content.appendChild(loadingDiv);
    }

    modal.appendChild(content);
    setupModalEventHandlers(modal);

    return modal;
  }

  /**
   * 创建弹窗遮罩层
   * @returns {HTMLDivElement} 遮罩层元素
   */
  function createModalOverlay() {
    const modal = document.createElement('div');
    modal.id = 'videoPreviewModal';

    Object.assign(modal.style, {
      position: 'fixed',
      top: '0',
      left: '0',
      width: '100%',
      height: '100%',
      backgroundColor: 'rgba(0, 0, 0, 0.85)',
      zIndex: '10000',
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      opacity: '0',
      transition: 'opacity 0.3s ease'
    });

    return modal;
  }

  /**
   * 创建弹窗内容容器
   * @returns {HTMLDivElement} 内容容器元素
   */
  function createModalContent() {
    const content = document.createElement('div');

    Object.assign(content.style, {
      backgroundColor: '#fff',
      borderRadius: '8px',
      padding: '20px',
      maxWidth: VIDEO_PREVIEW_CONFIG.modalStyle.maxWidth,
      maxHeight: VIDEO_PREVIEW_CONFIG.modalStyle.maxHeight,
      position: 'relative',
      boxShadow: '0 4px 20px rgba(0,0,0,0.3)',
      userSelect: 'none' // Prevent text selection interference during video scrubber drag
    });

    // Stop propagation of pointer/mouse events from the content area so that
    // page-level event handlers (e.g. from the PMS webapp) cannot interfere
    // with the native video controls, especially the progress-bar drag.
    ['mousedown', 'pointerdown', 'touchstart'].forEach(type => {
      content.addEventListener(type, (e) => e.stopPropagation());
    });

    return content;
  }

  /**
   * 创建弹窗头部(标题和关闭按钮)
   * @param {string} filename - 文件名
   * @param {HTMLElement} modal - 弹窗元素
   * @returns {HTMLDivElement} 头部元素
   */
  function createModalHeader(filename, modal) {
    const header = document.createElement('div');
    Object.assign(header.style, {
      display: 'flex',
      justifyContent: 'space-between',
      alignItems: 'center',
      marginBottom: '15px',
      paddingBottom: '10px',
      borderBottom: '1px solid #e0e0e0'
    });

    const title = document.createElement('h3');
    title.textContent = filename;
    Object.assign(title.style, {
      margin: '0',
      fontSize: '16px',
      fontWeight: 'bold',
      color: '#333'
    });

    const closeBtn = createCloseButton(modal);

    header.appendChild(title);
    header.appendChild(closeBtn);

    return header;
  }

  /**
   * 创建关闭按钮
   * @param {HTMLElement} modal - 弹窗元素
   * @returns {HTMLButtonElement} 关闭按钮元素
   */
  function createCloseButton(modal) {
    const btn = document.createElement('button');
    btn.innerHTML = '×';
    btn.title = '关闭';

    Object.assign(btn.style, {
      background: 'none',
      border: 'none',
      fontSize: '28px',
      fontWeight: 'bold',
      color: '#999',
      cursor: 'pointer',
      padding: '0',
      width: '30px',
      height: '30px',
      lineHeight: '30px',
      textAlign: 'center'
    });

    btn.addEventListener('click', () => closeVideoPreviewModal(modal));
    btn.addEventListener('mouseenter', () => btn.style.color = '#333');
    btn.addEventListener('mouseleave', () => btn.style.color = '#999');

    return btn;
  }

  /**
   * 创建视频播放器
   * @param {string} videoUrl - 视频URL
   * @returns {HTMLVideoElement} 视频元素
   */
  function createVideoPlayer(videoUrl) {
    const video = document.createElement('video');
    video.src = videoUrl;
    video.controls = true;
    video.autoplay = true;

    Object.assign(video.style, {
      width: '100%',
      maxHeight: 'calc(90vh - 100px)',
      borderRadius: '4px',
      backgroundColor: '#000'
    });

    // 错误处理
    video.addEventListener('error', () => {
      console.error(`[PMS增强] 视频加载失败: ${videoUrl}`);

      const errorMsg = document.createElement('div');
      errorMsg.textContent = '视频加载失败,请检查文件是否存在或网络连接。';
      Object.assign(errorMsg.style, {
        color: '#d9534f',
        padding: '20px',
        textAlign: 'center',
        fontSize: '14px'
      });

      video.replaceWith(errorMsg);
    });

    return video;
  }

  /**
   * 设置弹窗事件处理器
   * @param {HTMLElement} modal - 弹窗元素
   */
  function setupModalEventHandlers(modal) {
    // Track whether the pointer-down event originated on the backdrop itself.
    // When the user drags the video progress bar and releases the mouse outside
    // the video element, the resulting "click" has e.target === modal even though
    // the drag started inside the content area.  Without this guard the modal
    // closes unexpectedly, also aborting the seek operation.
    let pointerDownOnOverlay = false;

    modal.addEventListener('pointerdown', (e) => {
      pointerDownOnOverlay = (e.target === modal);
    });

    // Close the modal only when the user genuinely clicks on the backdrop
    // (both pointerdown and click originated on the overlay, not a drag-release).
    modal.addEventListener('click', (e) => {
      if (e.target === modal && pointerDownOnOverlay) {
        closeVideoPreviewModal(modal);
      }
      pointerDownOnOverlay = false;
    });

    // ESC键关闭
    const escHandler = (e) => {
      if (e.key === 'Escape') {
        closeVideoPreviewModal(modal);
        document.removeEventListener('keydown', escHandler);
      }
    };
    document.addEventListener('keydown', escHandler);
  }

  /**
   * 关闭视频预览弹窗
   * @param {HTMLElement} modal - 要关闭的弹窗元素
   */
  function closeVideoPreviewModal(modal) {
    // 暂停视频并释放缓存引用
    const video = modal.querySelector('video');
    if (video) video.pause();
    const videoUrl = modal.dataset.videoUrl;
    if (videoUrl) releaseVideoCache(videoUrl);

    modal.style.opacity = '0';
    setTimeout(() => {
      modal.remove();
      console.log('[PMS增强] 视频预览弹窗已关闭');
    }, 300);
  }

  // ============================================================
  // 功能3: 自动填充解决信息
  // ============================================================

  /**
   * 监听"解决"按钮的点击事件
   */
  function initResolveButtonListener() {
    const resolveButtons = document.querySelectorAll('a[href*="bug-resolve"]');

    resolveButtons.forEach(button => {
      if (!button.dataset.autoFillListenerAdded) {
        button.addEventListener('click', function (e) {
          console.log('[PMS增强] 检测到点击解决按钮,准备自动填充...');
          button.dataset.autoFillListenerAdded = 'true';

          setTimeout(() => {
            observeResolveIframe();
          }, 200);
        });

        button.dataset.autoFillListenerAdded = 'true';
      }
    });
  }

  /**
   * 监听解决窗口的 iframe 加载
   */
  function observeResolveIframe() {
    const iframe = document.getElementById('iframe-triggerModal');
    if (iframe) {
      const checkAndFill = () => {
        const iframeSrc = iframe.src || '';
        if (iframeSrc.includes('bug-resolve')) {
          const iframeDocument = iframe.contentDocument;
          if (iframeDocument) {
            console.log('[PMS增强] 解决窗口已加载,准备添加自动填充按钮');
            addAutoFillButton(iframeDocument);
          }
        }
      };

      iframe.addEventListener('load', checkAndFill);

      if (iframe.contentDocument && iframe.contentDocument.readyState === 'complete') {
        checkAndFill();
      }
    }
  }

  /**
   * 添加自动填充按钮到解决窗口
   * @param {Document} iframeDocument - iframe 的 document 对象
   */
  function addAutoFillButton(iframeDocument) {
    const header = iframeDocument.querySelector('.main-header h2');
    if (!header || iframeDocument.getElementById('autoFillBtn')) {
      return;
    }

    const button = createAutoFillButton();
    const iframeWindow = iframeDocument.defaultView;
    button.addEventListener('click', () => fillFormValues(iframeDocument, iframeWindow));
    header.insertBefore(button, header.firstChild);

    console.log('[PMS增强] 已添加自动填充按钮');
  }

  /**
   * 创建自动填充按钮元素
   * @returns {HTMLButtonElement} 按钮元素
   */
  function createAutoFillButton() {
    const button = document.createElement('button');
    button.id = 'autoFillBtn';
    button.textContent = '自动填充';

    Object.assign(button.style, {
      marginRight: '10px',
      padding: '5px 10px',
      height: '30px',
      lineHeight: '20px',
      backgroundColor: '#28a745',
      color: 'white',
      border: 'none',
      borderRadius: '3px',
      cursor: 'pointer',
      boxSizing: 'border-box'
    });

    return button;
  }

  /**
   * 填充表单值
   * @param {Document} iframeDocument - iframe 的 document 对象
   */
  async function fillFormValues(iframeDocument, iframeWindow) {
    try {
      console.log('[PMS增强] 开始自动填充表单...');

      const {
        presetValues,
        fieldConfig
      } = AUTO_FILL_CONFIG;
      const $ = iframeWindow?.$ || iframeWindow?.jQuery;
      if (!$) {
        console.error('[PMS增强] 未找到 jQuery,Chosen 插件无法正确同步');
      }

      // ============================================================
      // Hook loadRepairExttype: 在 AJAX 重建 exttype 后自动填充
      // repair 字段的 onchange 调用 loadRepairExttype(this.value),
      // 它通过 AJAX 重建 #exttypeBox 并重新 init Chosen,
      // 导致之前设置的 exttype 值被清空。
      // 修复:整体替换 loadRepairExttype,将赋值代码注入到
      // AJAX 回调内部(chosen() 之后),确保时序正确。
      // ============================================================
      let exttypeTargetValue = presetValues.exttype || '';
      let exttypeHooked = false;

      if (exttypeTargetValue && $) {
        const origLoadRepairExttype = iframeWindow.loadRepairExttype;
        if (origLoadRepairExttype) {
          iframeWindow.loadRepairExttype = function (repair) {
            const link = iframeWindow.createLink('bug', 'ajaxGetExtType', 'repair=' + repair);
            $('#exttypeBox').load(link, function () {
              $('#exttype').chosen();
              // chosen() 重初始化完成后,立即设置目标值
              const exttypeEl = iframeDocument.getElementById('exttype');
              if (exttypeEl && exttypeTargetValue) {
                const optionExists = Array.from(exttypeEl.options)
                  .some(o => o.value === exttypeTargetValue);
                if (optionExists) {
                  $(exttypeEl).val(exttypeTargetValue).trigger('chosen:updated').trigger('change');
                  console.log(`[PMS增强] Hook: 类型字段已通过 AJAX 回调填充: ${exttypeTargetValue}`);
                }
                else {
                  console.warn(`[PMS增强] Hook: 类型字段选项"${exttypeTargetValue}"不在当前列表中`);
                }
              }
            });
          };
          exttypeHooked = true;
          console.log('[PMS增强] 已 Hook loadRepairExttype');
        }
      }

      // ============================================================
      // 填充所有字段:先设值,再统一触发 jQuery 事件
      // 注意:repair 触发 change 后会调用被 hook 的 loadRepairExttype,
      // exttype 会在 AJAX 回调中被自动设置,所以这里跳过 exttype。
      // ============================================================
      const fieldOrder = Object.entries(fieldConfig);
      const skipExttype = exttypeHooked && exttypeTargetValue;

      for (const [key, config] of Object.entries(fieldConfig)) {
        // exttype 由 hook 在 AJAX 回调中处理
        if (skipExttype && key === 'exttype') {
          console.log('[PMS增强] 跳过类型字段(由 AJAX Hook 处理)');
          continue;
        }

        const value = presetValues[key];
        if (!value && key !== 'assignedTo') continue;

        const element = iframeDocument.querySelector(config.selector);
        if (!element) {
          console.warn(`[PMS增强] 未找到字段: ${config.label} (${config.selector})`);
          continue;
        }

        element.value = value;

        // 使用 jQuery 触发事件,确保 Chosen 插件正确同步
        if ($) {
          $(element).val(value).trigger('chosen:updated').trigger('change');
        }
        else {
          element.dispatchEvent(new Event('change', {
            bubbles: true
          }));
          console.warn(`[PMS增强] ${config.label}: 未找到 jQuery,使用原生事件(Chosen 可能不同步)`);
        }

        console.log(`[PMS增强] 已设置 ${config.label}: ${value}`);
      }

      // 填充富文本编辑器(备注)
      fillRichTextEditor(iframeDocument, '#comment', presetValues.comment);

      console.log('[PMS增强] 表单自动填充完成');
    }
    catch (error) {
      console.error('[PMS增强] 表单填充失败:', error);
    }
  }

  /**
   * 设置解决表单提交监听器,保存配置
   * @param {Document} iframeDocument - iframe的document对象
   * @param {HTMLSelectElement} assignToSelect - 指派选择框
   */
  function setupResolveFormSubmitListener(iframeDocument, assignToSelect) {
    const submitBtn = iframeDocument.querySelector('#submit');
    if (!submitBtn || submitBtn.dataset.resolveConfigSaveListenerAdded) {
      return;
    }

    submitBtn.addEventListener('click', function () {
      const selectedUser = assignToSelect.value;
      if (selectedUser) {
        AUTO_FILL_CONFIG.presetValues.assignedTo = selectedUser;
        console.log(`[PMS增强] ✓ 已保存自动填充指派配置: ${selectedUser}`);
      }
    });

    submitBtn.dataset.resolveConfigSaveListenerAdded = 'true';
  }

  /**
   * 填充富文本编辑器(KindEditor)
   * @param {Document} iframeDocument - iframe 的 document 对象
   * @param {string} selector - 编辑器选择器
   * @param {string} content - 要填充的内容
   */
  function fillRichTextEditor(iframeDocument, selector, content) {
    if (!content) return;

    try {
      const editorIframe = iframeDocument.querySelector('.ke-edit-iframe');
      if (!editorIframe?.contentWindow) {
        console.warn('[PMS增强] 未找到富文本编辑器 iframe');
        return;
      }

      const formattedContent = content.replace(/\n/g, '<br>');

      // 尝试使用 KindEditor API
      const editor = editorIframe.contentWindow.editor;
      if (editor?.html) {
        editor.html(formattedContent);
        console.log('[PMS增强] 已填充备注内容(使用 KindEditor API)');
      }
      else {
        // 降级方案:直接设置 iframe body 内容
        const editorDoc = editorIframe.contentWindow.document;
        if (editorDoc?.body) {
          editorDoc.body.innerHTML = formattedContent;
          const commentField = iframeDocument.querySelector(selector);
          if (commentField) {
            commentField.value = content;
            commentField.dispatchEvent(new Event('change', {
              bubbles: true
            }));
          }
          console.log('[PMS增强] 已填充备注内容(使用降级方案)');
        }
      }
    }
    catch (error) {
      console.error('[PMS增强] 填充备注失败:', error);
    }
  }

  /**
   * 延迟执行辅助函数
   * @param {number} ms - 延迟毫秒数
   * @returns {Promise} Promise 对象
   */
  function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // ============================================================
  // 初始化
  // ============================================================

  /**
   * 初始化所有功能
   */
  function init() {
    console.log('[PMS增强] 脚本已加载');

    // 初始化"指派给自己"功能
    addAssignToSelfButton();

    // 初始化"视频预览"功能
    initVideoPreview();

    // 初始化"自动填充"功能
    initResolveButtonListener();

    // 监听DOM变化,处理动态添加的按钮(带防抖)
    let observerTimer = null;
    const pageObserver = new MutationObserver(() => {
      if (observerTimer) clearTimeout(observerTimer);
      observerTimer = setTimeout(() => {
        // 检查并添加"指派给自己"按钮
        if (!document.getElementById('assignToSelfBtn')) {
          addAssignToSelfButton();
        }

        // 检查并初始化视频预览
        initVideoPreview();

        // 检查并初始化解决按钮监听
        initResolveButtonListener();
      }, 300);
    });

    pageObserver.observe(document.body, {
      childList: true,
      subtree: true
    });
  }

  // 启动脚本
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  }
  else {
    init();
  }

  console.log('[PMS增强] v1.3 初始化完成');
})();