Jazz 插件
(function() {
'use strict';
const originalXHR = window.XMLHttpRequest;
let users = new Map();
let token;
let urlQuery;
let data;
let timeoutId;
window.XMLHttpRequest = function() {
const xhr = new originalXHR();
const originalOpen = xhr.open;
const originalSend = xhr.send;
const originalSetRequestHeader = xhr.setRequestHeader;
xhr.setRequestHeader = function(header, value) {
if ("Authorization" === header) token = value;
originalSetRequestHeader.apply(this, arguments);
};
xhr.open = function(method, url) {
this._method = method;
this._url = url;
return originalOpen.apply(this, arguments);
};
xhr.send = function(d) {
this._requestData = d;
const originalOnreadystatechange = this.onreadystatechange;
this.onreadystatechange = function() {
if (originalOnreadystatechange) {
return originalOnreadystatechange.apply(this, arguments);
}
if (this.readyState === 4) {
if (this._url.includes('queryDashboardPageList')) {
urlQuery = this._url;
console.log('拦截到请求: ', urlQuery);
data = JSON.parse(this.responseText);
run();
}
if (this._url.includes('/api/users/all')) {
console.log('拦截到请求: users');
const result = JSON.parse(this.responseText);
result.data.forEach(user => users.set(user.id, user));
}
}
};
return originalSend.apply(this, arguments);
};
return xhr;
};
function formatDate(utcDateString) {
const date = new Date(utcDateString);
const localDate = date.toLocaleDateString();
return localDate;
}
const pageDownEvent = new KeyboardEvent('keydown', {
key: 'PageDown',
code: 'PageDown',
which: 34,
keyCode: 34,
bubbles: true
});
function run() {
document.getElementById("root").style.opacity = "0";
document.getElementById("root").style.pointerEvents = "none";
document.getElementById("root").style.position = "absolute";
document.getElementById("root").style.zindex = "-1";
let overlayDiv = document.getElementById('overlayDiv');
if (!overlayDiv) {
overlayDiv = document.createElement("div");
overlayDiv.id = 'overlayDiv';
overlayDiv.innerHTML = `
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
line-height:1.2;
}
.card:focus {
outline: 2px solid black; /* 黑色边框 */
box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); /* 可选:添加阴影效果 */
}
.container {
display: flex;
flex-wrap: wrap; /* 允许换行 */
gap: 20px;
justify-content: left;
padding: 65px 10px 10px 10px;
}
.card {
width: 360px;
height: 423px;
background-color: #c6f4c6;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.card .header {
display: flex;
padding: 10px 10px 0 10px;
justify-content: space-between;
align-items: center;
font-weight: bold;
}
.card .header span {
font-size: 14px;
color: #777;
}
.card .details {
padding: 5px 10px 0 8px;
}
.card .details .name {
font-size: 16px;
font-weight: bold;
width:300px;
overflow: hidden;
}
.card .details .date {
color: #777;
}
.card .stats {
display: flex;
justify-content: space-between;
align-items: center;
}
.card .chart {
width: 100%;
height: 210px;
}
.chart img {
width: 360px;
height: 210px;
border-radius: 3px;
}
#dialogBody img {
width: 99%;
height: 99%;
border-radius: 3px;
}
.card .warning {
padding: 4px 10px 0px 10px;
font-size: 14px;
color: #333;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
line-clamp: 2;
height: 38px;
overflow: hidden;
margin-top:-209px;
}
.card .actionplan {
background-color: rgba(0, 204, 0, 0.3);
padding: 4px 10px 0px 10px;
font-size: 14px;
color: #333;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
line-clamp: 2;
height: 38px;
overflow: hidden;
border-radius: 0 0 8px 8px;
margin-top: 138px;
}
/* 保证小屏幕时卡片也会适配布局 */
@media (max-width: 1200px) {
.container {
justify-content: center;
}
}
/* 移动端适配,1行显示1个卡片 */
@media (max-width: 768px) {
.container {
justify-content: center;
}
.card {
width: 90%; /* 调整卡片宽度为90% */
}
}
.toolbar {
background-color: #000;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 1000;
}
.toolbar .search-box {
flex-grow: 1;
margin: 0 20px;
align-items: center;
display: flex;
}
.toolbar > div:last-child {
margin-right: 20px;
}
</style>
<div class="toolbar">
<div>
<img
style="
padding: 0;
margin: 0;
width: 50px;
height: 50px;
position: fixed;
top: -2px;
left: 10px;
cursor: pointer;
"
src="https://img.icons8.com/?size=100&id=7zme8ZmxBZhC&format=png&color=000000"
/>
</div>
<div style="margin-left: 40px">Jazz</div>
<div class="search-box">
<input
id="filter"
type="text"
placeholder="搜索..."
style="
height: 25px;
border-radius: 8px;
border: 1px solid #ccc;
box-shadow: none;
padding: 0 10px 0 10px;
width: 10%;
min-width: 100px;
"
/>
<div style="display: flex; align-items: center; gap: 3px;">
<select id="cbLocation" value="办公室列表" title="Location">
<option value="" disabled selected hidden>办公室列表</option>
<option value="">所有办公室</option>
</select>
<select id="cbPM" value="PM列表" title="Project Manager">
<option value="" disabled selected hidden>PM列表</option>
<option value="">所有PM</option>
</select>
<select id="cbSL" value="SL列表" title="System Leader">
<option value="" disabled selected hidden>SL列表</option>
<option value="">所有SL</option>
</select>
<select id="cbDL" value="DL列表" title="Developer Leader">
<option value="">所有DL</option>
<option value="" disabled selected hidden>DL列表</option>
</select>
<select id="cbProjectStatus" value="项目状态" title="Project Status">
<option value="" disabled selected hidden>项目状态</option>
<option value="Active">Active</option>
<option value="Finish">Finish</option>
<option value="">All</option>
</select>
<img src="https://img.icons8.com/?size=100&id=a2x1ggyePUME&format=png&color=FFFFFF" alt="上一页" width=24 height=24 id="searchBack" />
<img src="https://img.icons8.com/?size=100&id=a3XV2uYjYIEG&format=png&color=FFFFFF" alt="上一页" width=24 height=24 id="searchNext" />
<div id="pagingStatus"></div>
</div>
</div>
<div title="User profile menu">profile </div>
</div>
<div class="container" id="cards"></div>
<div id="dialog" tabindex="0" style="width: 90%; height: 90%; display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: white; padding: 20px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); z-index: 1000;">
<div style="display: flex; flex-direction: column; height: 100%;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<span style="font-size: 18px; font-weight: bold;">详情</span>
<button id="closeDialog" style="background: none; border: none; font-size: 20px; cursor: pointer;" onclick="document.getElementById('dialog').style.display='none'">×</button>
</div>
<div id="dialogBody" style="flex: 1; overflow-y: auto; border: 1px solid #ccc; box-sizing: border-box;"></div>
</div>
</div>
`;
document.body.appendChild(overlayDiv);
document.getElementById('searchBack').addEventListener('click', function(event) {
query(-1);
});
document.getElementById('searchNext').addEventListener('click', function(event) {
query(1);
});
document.getElementById('filter').addEventListener('input', function(event) {
clearTimeout(timeoutId);
timeoutId = setTimeout(function() {
const inputValue = event.target.value;
search();
}, 200);
});
document.getElementById('cbProjectStatus').addEventListener('change', (event) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", `/api/dashboard/queryDashboardPageList?page=1&pageSize=100&projectStatus=${event.target.value}&location=&dl=&pm=&projectType=`, true);
xhr.setRequestHeader('Authorization', token);
xhr.send();
});
}
parseUser();
init();
search();
}
window.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
document.getElementById('dialog').style.display = 'none';
}
});
function query(page) {
const xhr = new XMLHttpRequest;
let n = (+data.data.meta.page) + page;
if (n <= 1) {
alert("已经是第一页");
return;
}
if (n * data.data.meta.pageSize > data.data.meta.total) {
alert("已经是最后一页");
return;
}
const newUrl = urlQuery.replace(/([?&]page=)\d+/, `$1${n}`);
xhr.open("GET", newUrl, true);
xhr.setRequestHeader('Authorization', token);
xhr.send();
}
async function editProject(name, col) {
console.log('edit project: ', name, col);
const step = 5;
var node;
var index = 0;
const start = new Date();
while(true) {
node = document.evaluate(`//div[@role="cell" and @data-field="projectTeam"]/div[@class="MuiDataGrid-cellContent"][text()="${name}"]`, document).iterateNext();
if (node) break;
document.evaluate(`//*[@id="root"]/div/div/main/div[2]/div/div[5]/div/div[2]/div[2]/div/div/div/div[${index}]/div[1]`, document).iterateNext()?.dispatchEvent(pageDownEvent);
index = index > data.data.records.length ? step : index + step;
await new Promise(resolve => setTimeout(resolve, 1));
}
const end = new Date();
console.log(`定位节点耗时: ${end - start}ms => `, node);
if (col) {
node = node.parentElement.parentElement.querySelector(`div:nth-of-type(${col})`);
}
node?.click()
console.log("Index: ", index);
if (index > 0) {
node?.dispatchEvent(new KeyboardEvent('keydown', {
key: 'Home',
code: 'Home',
ctrlKey: true,
bubbles: true,
cancelable: true
}));
}
}
function initCb(property, cb) {
var select = document.getElementById(cb);
if (!select) return;
if (select.options.length == 2) {
select.addEventListener('change', () => {
console.log('Onchange: ', select);
selectChange(select, property);
});
}
select.options.length = 2;
const names = [...new Set(data.data.records.map(item =>item[property]))];
names.sort();
names.forEach(name => {
const option = document.createElement('option');
option.value = name;
option.textContent = name;
select.appendChild(option);
});
}
function parseUser() {
data.data.records.forEach(item => {
item.dl = item.dl ? (users.get(item.dl)?.name || "") + "(" + users.get(item.dl)?.cname + ")" : "-";
item.pm = item.pm ? (users.get(item.pm)?.name || "") + "(" + users.get(item.pm)?.cname + ")" : "-";
item.sl = item.sl ? (users.get(item.sl)?.name || "") + "(" + users.get(item.sl)?.cname + ")" : "-";
});
data.data.records.sort((a,b) => a.dl?.localeCompare(b.dl, undefined, { sensitivity: 'base' }));
}
function init() {
initCb('pm', 'cbPM');
initCb('location', 'cbLocation');
initCb('sl', 'cbSL');
initCb('dl', 'cbDL');
}
function selectChange(select, key) {
var list = [];
const value = select.value;
for (let item of data.data.records) {
if (item[key] === value || value === "") {
list.push(item);
}
}
render(list);
}
function search() {
var filter = document.getElementById("filter").value;
if (filter?.trim() === "") {
render(data.data.records);
return;
}
var list = [];
for (let item of data.data.records) {
if (JSON.stringify(item, (key, value) => {if (key==='burnDownChart' || value == null || value == "") return undefined; return value}, 2).indexOf(filter) > 0) {
list.push(item);
}
}
render(list);
}
function daydiff(t) {
return (((new Date()).getTime()) - (new Date(t)).getTime()) / 86400000;
}
function getRange(s1, s2) {
const [p1 = "?", p2 = "?"] = [s1?.substring(5, 10), s2?.substring(5, 10)];
return s1 === null && s2 === null ? "" : `(${p1} - ${p2})`;
}
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
const modelImg = {
"null": "https://img.icons8.com/?size=100&id=7744&format=png&color=000000",
"ModelA": "https://img.icons8.com/?size=100&id=WJhQO4L0uz5D&format=png&color=0000FF",
"ModelB": "https://img.icons8.com/?size=100&id=9542&format=png&color=000000",
"ModelC": "https://img.icons8.com/?size=100&id=yy5lqoytli5E&format=png&color=800080",
"ModelC(Without PM)": "https://img.icons8.com/?size=100&id=2NjzoyC1SSOE&format=png&color=800080"
}
function render(list) {
const pagingDiv = document.getElementById("pagingStatus");
pagingDiv.innerText = `第 ${data.data.meta.page} 页,总共 ${data.data.meta.total} 条`;
const cards = document.getElementById("cards");
cards.innerHTML = "";
for (let item of list) {
const risk = (item.risk || "暂无风险").toLowerCase();
const norisk = risk === "暂无风险" || risk === "暂无" || risk === "" || risk === "n/a" || risk === "no risk" || risk === "无";
const noactionplan = item.actionPlan === "暂无风险" || item.actionPlan === "暂无" || item.actionPlan == undefined || item.actionPlan === "" || item.actionPlan == null;
const updateTime = new Date(item.updateTime)
const days = daydiff(updateTime)
var card = document.createElement("div");
var springRange = getRange(item.sprintStartTime, item.sprintEndTime);
card.innerHTML = `
<div class="card" tabindex="0" style="background-color:${ norisk ? "#c6f4c6" : "#ffcc00" }" onclick="this.focus()">
<div class="header">
<div title="办公室" style="display:flex;algin-items:center"><img src="https://img.icons8.com/?size=100&id=kIuNfCeXdCvG&format=png&color=000000" width=18 height=18> ${item.location}</div><div title="项目编号 - 当前迭代周期" class="projectInfo" team="${item.projectTeam}" style="cursor:pointer">${item.srNumber} - <font color="blue">#${item.sprint}${springRange}</font></div>
</div>
<div class="projectTeam" team="${item.projectTeam}" title="团队名称(Project Team)" style="cursor:pointer;border-bottom: 1px solid #fff;padding: 0 10px 5px 10px; max-width:360px;white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"><font color="blue">${item.projectPhase || "N/A"}</font> - ${item.projectTeam}</div>
<div class="details" style="border-bottom: 1px solid #fff">
<table width='100%'><tbody>
<tr>
<td><b>PM</b>: ${item.pm}</td>
<td align="right">SR 时间范围</td>
</tr>
<tr>
<td><b>SL</b>: ${item.sl}</td>
<td align="right">${formatDate(item.srStartTime)}</td>
</tr>
<tr>
<td><b>DL</b>: ${item.dl}</td>
<td align="right"><span style="color:${daydiff(item.srEndTime) > -30 ? "red" : "default"}">${formatDate(item.srEndTime)}</span></td>
</tr>
</tbody></table>
</div>
<div class="stats" style="padding: 5px 10px 5px 8px">
<table><tbody>
<tr>
<td rowspan="3" align="center" class="projectBugs" team="${item.projectTeam}" style="cursor:pointer"><img src="https://img.icons8.com/?size=100&id=9227&format=png&color=000000" width=32 height=32 title="Bug 历史记录"><br>
<div title="DefectDensity,标准值: 0.264" style="color: ${item.totalDefectDensity >= 0.264 ? "Red": "Green"}">${item.totalDefectDensity}</div>
</td>
<td>Sprint #${item.bugRecords[0]?.sprint || '-'}: ${item.bugRecords[0]?.bugNumber || '-'}</td>
</tr>
<tr>
<td>Sprint #${item.bugRecords[1]?.sprint || '-'}: ${item.bugRecords[1]?.bugNumber || '-'}</td>
</tr>
<tr>
<td>Sprint #${item.bugRecords[2]?.sprint || '-'}: ${item.bugRecords[2]?.bugNumber || '-'}</td>
</tr>
</tbody></table>
<div class="team-size">
<table style="border-collapse: collapse;"><tbody>
<tr>
<td align="right" style="align: right"><img src="${modelImg[item.projectType]}" width=32 height=32 title="项目模式: ${item.projectType}"></td>
<td><span style="font-size:32px"> ${item.teamMember}</span></td>
</tr>
<tr>
<td colspan=2><div style="color: ${days > 7 ? "red" : "#000" }">更新于: ${updateTime.toLocaleDateString()}</div></td>
</tr>
</tbody></table>
</div>
</div>
<div class="chart" style="border-top:1px solid #fff;cursor:pointer">${item.burnDownChart?.replace("<p>", "").replace("</p>", "") || ""}</div>
<div class="warning projectRisk" title="${item.risk}" team="${item.projectTeam}" style="cursor:pointer; background-color: ${norisk ? 'rgba(0, 204, 0, 0.3)' : 'rgba(255, 204, 0, 0.3)'}">${item.risk}</div>
<div class="actionplan" title="${item.actionPlan}" style="display: ${noactionplan ? 'none' : 'block'}">${item.actionPlan}</div>
</div>
`;
cards.appendChild(card);
}
document.querySelectorAll('.projectTeam').forEach(item => {
const name = item.getAttribute('team');
item.addEventListener('click', function() {
editProject(name);
});
});
document.querySelectorAll('.projectBugs').forEach(item => {
const name = item.getAttribute('team');
item.addEventListener('click', function() {
editProject(name, 14);
});
});
document.querySelectorAll('.projectInfo').forEach(item => {
const name = item.getAttribute('team');
item.addEventListener('click', function() {
editProject(name, 9);
});
});
document.querySelectorAll('.projectRisk').forEach(item => {
const name = item.getAttribute('team');
item.addEventListener('click', function() {
editProject(name, 13);
});
});
const charts = document.querySelectorAll(".chart")
charts.forEach(chart => {
chart.addEventListener('click', function() {
const dialogBody = document.getElementById('dialogBody');
dialogBody.innerHTML = chart.innerHTML;
document.getElementById('dialog').style.display="block";
});
});
}
})();
queryDashbaord返回的数据结构:
{
"code": 200,
"message": "fetched successfully",
"data": {
"records": [
{
"id": 2,
"location": "US",
"projectTeam": "Team1",
"sl": "Bill Gates",
"dl": "Joe Biden",
"pm": "Donald Trump",
"sprint": 53,
"burnDownChart": "<p><img src=\"data:image/png;base64,iVBORw0AElFTkSuQmCC\"></p>",
"risk": "N/A",
"actionPlan": "N/A",
"riskLevel": "",
"owner": "",
"riskStatus": "",
"teamMember": 10,
"internalDemo": null,
"demoReviewForHA": null,
"technologyStack": null,
"srStartTime": "2024-12-31T16:00:00.000Z",
"srEndTime": "2025-05-30T16:00:00.000Z",
"renewalInfo": null,
"projectPhase": "Coding",
"remark": null,
"createdUserId": null,
"createdBy": null,
"createTime": null,
"updatedBy": "Bill Gates",
"updateTime": "2025-05-07T05:22:27.000Z",
"projectStatus": "Active",
"srNumber": "SR-1234",
"sprintStartTime": "2025-04-20T16:00:00.000Z",
"sprintEndTime": "2025-05-08T16:00:00.000Z",
"projectType": "ModelB",
"slName": "Bill Gates",
"pmName": "Joe Bidon",
"dlName": "Charles Hank",
"risks": [
{
"id": 335,
"location": "US",
"sprint": 55,
"projectTeam": "Team1",
"dl": 199,
"risk": "N/A",
"actionPlan": "Blabla",
"owner": "",
"riskStatus": "Closed",
"riskLevel": "",
"recordDate": "2025-04-24T07:57:49.000Z",
"completeDate": "2025-05-08T07:21:52.000Z",
"dashboardId": 8,
"createdUserId": 199,
"createdBy": null,
"createTime": "2025-05-08T07:21:52.000Z",
"updatedBy": null,
"updateTime": "2025-05-08T07:52:00.000Z"
}
],
"bugRecords": [
{
"dashboardProjectId": 2,
"sprint": 52,
"bugNumber": 3,
"actualEfforts": 119,
"defectDensity": 0.025
},
{
"dashboardProjectId": 2,
"sprint": 51,
"bugNumber": 3,
"actualEfforts": 99,
"defectDensity": 0.03
},
{
"dashboardProjectId": 2,
"sprint": 50,
"bugNumber": 3,
"actualEfforts": 117,
"defectDensity": 0.026
}
],
"totalDefectDensity": 0.028
}
],
"meta": {
"page": "1",
"pageSize": "100",
"total": 1
}
}
}
user 数据结构:
{
"code": 200,
"message": "UserList fetched successfully",
"data": [
{
"id": 1,
"name": "admin",
"cname": "管理员",
"email": "admin"
}
]
}
效果: