NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==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 = ' ';
listItem.appendChild(document.createTextNode('\n'));
listItem.appendChild(rightIconSpan);
}
// 插入预览按钮到容器开头( 之后)
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 初始化完成');
})();