(function() {
'use strict';
const originalXHR = window.XMLHttpRequest;
let users = new Map();
let token;
let urlQuery;
let data;
let timeoutId;
function MyTimer(callback, interval) {
let timerId = null;
let isActive = false;
return {
start() {
if (!isActive) {
isActive = true;
timerId = setInterval(callback, interval);
console.log("定时器已启动");
}
},
stop() {
if (isActive) {
isActive = false;
clearInterval(timerId);
console.log("定时器已停止");
}
},
isActive() {
return isActive;
}
};
}
const myTimer = MyTimer(() => {
console.log("定时任务执行", new Date().toLocaleTimeString());
document.getElementById('search')?.click();
}, 30000);
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();
document.getElementById("updateTime").innerText = "更新于" + new Date().toLocaleTimeString();
}
if (this._url.includes('/api/users/getUserByParam')) {
console.log('拦截到请求: users');
const result = JSON.parse(this.responseText);
result.res.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>
/*
[tooltip]:hover::before {
content: attr(tooltip);
position: absolute;
background-color: yellow;
font-size: 32px;
padding: 10px;
border-radius: 8px;
display: block;
white-space: pre-wrap;
}
*/
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;
}
.link {
color:blue;
cursor:pointer;
}
a {
text-decoration: none;
}
.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;
height: 30px;
overflow: hidden;
}
.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;
border-radius: 0 0 8px 8px;
}
.chart img {
width: 360px;
height: 210px;
}
#dialogBody img {
width: 99%;
height: 99%;
border-radius: 3px;
}
.card .warning {
padding: 0 10px 0 10px;
font-size: 14px;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
line-clamp: 2;
height: 38px;
overflow: hidden;
margin-top:-210px;
position: relative;
border-top: 1px solid #fff;
}
.card .actionplan {
background-color: rgba(0, 204, 0, 0.3);
padding: 0 10px 0 10px;
font-size: 14px;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
line-clamp: 2;
height: 38px;
overflow: hidden;
border-radius: 0 0 8px 8px;
position: relative;
top: 65px;
}
/* 保证小屏幕时卡片也会适配布局 */
@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;
}
.goal {
width: 100%;
height: 69px;
position: relative;
top: 65px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp:4;
padding: 5px;
overflow: hidden;
font-size: small;
}
.toolbar .search-box {
flex-grow: 1;
margin: 0 20px;
text-overflow: ellipsis;
align-items: center;
display: flex;
}
.twoLine {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow:hidden;
}
.toolbar > div:last-child {
margin-right: 0;
}
</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=bDaPwYMHLojO&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="cbModel" value="项目模式" title="Project Model">
<option value="" disabled selected hidden>项目模式</option>
<option value="">所有模式</option>
<option value="ModelA">Model A</option>
<option value="ModelB">Model B</option>
<option value="ModelC">Model C</option>
<option value="ModelC(Without PM)">Model C - 无PM</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="Active" title="项目状态" placeholder="项目状态">
<option value="" disabled>项目状态</option>
<option value="Active" selected>Active</option>
<option value="Close">Close</option>
<option value="">All</option>
</select>
<select id="cbSort" value="dl" title="排序依据">
<option value="" disabled>排序依据</option>
<option value="updateTime" selected>更新时间</option>
<option value="dlName">开发组长</option>
<option value="slName">系统组长</option>
<option value="pmName">项目经理</option>
<option value="srNumber">SR 编号</option>
<option value="srEndTime">SR 结束时间</option>
<option value="projectTeam">项目名称</option>
</select>
<select id="cbDirection" value="asc" title="排序方向">
<option value="" disabled>排序方向</option>
<option value="asc">升序</option>
<option value="desc" selected>降序</option>
</select>
<img src="https://img.icons8.com/?size=100&id=nWELeOk5IRzW&format=png&color=FFFFFF" title="向服务器查询" width=24 height=24" id="search" />
<img src="https://img.icons8.com/?size=100&id=a2x1ggyePUME&format=png&color=FFFFFF" title="上一页" width=24 height=24 id="searchBack" />
<img src="https://img.icons8.com/?size=100&id=a3XV2uYjYIEG&format=png&color=FFFFFF" title="下一页" width=24 height=24 id="searchNext" />
<img src="https://img.icons8.com/?size=100&id=v0jvWJ0ZJhAF&format=png&color=FFFFFF" title="导出CSV" width=24 height=24 id="export" />
<div id="pagingStatus"></div>
<label><input type="checkbox" name="color" value="red" id="refresh">自动刷新</label>
<div id="updateTime"></div>
</div>
</div>
<div style="display:flex;gap: 5px"><span id="user"></span><div style="cursor:pointer" onclick="document.evaluate(\`//button[normalize-space(text())='退出']\`, document).iterateNext()?.click();document.body.removeChild(document.getElementById('overlayDiv'));document.getElementById('root').style=''
">退出</div></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('user').innerText = document.evaluate('//*[@id="root"]/div/div/header/div/span', document).iterateNext().innerText;
document.getElementById('searchBack').addEventListener('click', function(event) {
query(-1);
});
document.getElementById('searchNext').addEventListener('click', function(event) {
query(1);
});
document.getElementById('refresh').addEventListener('click', function(event) {
if (event.target.checked) {
myTimer.start();
} else {
myTimer.stop();
}
});
document.getElementById('export').addEventListener('click', function(event) {
const csvData = convertToCSV(data.data.records);
downloadCSV(csvData);
});
document.getElementById('search').addEventListener('click', function(event) {
const xhr = new XMLHttpRequest();
const status = document.getElementById('cbProjectStatus').value;
const location = document.getElementById('cbLocation').value;
const dl = document.getElementById('cbDL').value.split('(')[0].replace(" ", "+");
const pm = document.getElementById('cbPM').value.split('(')[0].replace(" ", "+");
const sl = document.getElementById('cbSL').value.split('(')[0].replace(" ", "+");
const model = document.getElementById('cbModel').value;
xhr.open("GET", `/api/dashboard/queryDashboardPageList?page=1&pageSize=100&projectStatus=${status}&location=${location}&dl=${dl}&pm=${pm}&sl=${sl}&projectType=${model}`, true);
xhr.setRequestHeader('Authorization', token);
xhr.send();
});
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();
});
document.getElementById('cbSort').addEventListener('change', function(event) {
sort();
render(data.data.records);
});
document.getElementById('cbDirection').addEventListener('change', function(event) {
sort();
render(data.data.records);
});
document.getElementById('cbModel').addEventListener('change', (event) => {
selectChange(event.target, "projectType");
});
}
parseUser();
init();
search();
}
window.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
document.getElementById('dialog').style.display = 'none';
}
});
function sort() {
const dir = document.getElementById('cbDirection').value;
const sort = document.getElementById('cbSort').value;
if ("asc" === dir) {
data.data.records.sort((a,b) => a[sort]?.trim().localeCompare(b[sort]?.trim(), undefined, { sensitivity: 'base' }));
} else {
data.data.records.sort((a,b) => b[sort]?.trim().localeCompare(a[sort]?.trim(), undefined, { sensitivity: 'base' }));
}
}
function query(page) {
const xhr = new XMLHttpRequest;
let n = (+data.data.meta.page) + page;
if (n < 1 && page < 0) {
alert("已经是第一页");
return;
}
if (page > 0 && data.data.meta.page >= Math.ceil(data.data.meta.total / data.data.meta.pageSize)) {
alert("已经是最后一页");
return;
}
const newUrl = urlQuery.replace(/([?&]page=)\d+/, `$1${n}`);
xhr.open("GET", newUrl, true);
xhr.setRequestHeader('Authorization', token);
xhr.send();
}
function getGoalInfo(id) {
const xhr = new XMLHttpRequest;
const url = "/api/sprintInfoMg/querySprintInfoMgPageList?page=1&pageSize=10&dashboardProjectId=" + id;
xhr.open("GET", url, false);
xhr.setRequestHeader('Authorization', token);
try {
xhr.send();
if (xhr.status == 200) {
const response = JSON.parse(xhr.responseText);
return response.data?.records[0];
}
return {};
} catch (e) {
return e;
}
}
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);
});
}
const oldValue = select.value;
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);
});
select.value = oldValue;
}
function parseUser() {
data.data.records.forEach(item => {
item.dlFullName = item.dl ? (item.dlName) + "(" + users.get(item.dl)?.cname + ")" : "-";
item.pmFullName = item.pm ? (item.pmName) + "(" + users.get(item.pm)?.cname + ")" : "-";
item.slFullName = item.sl ? (item.slName) + "(" + users.get(item.sl)?.cname + ")" : "-";
});
sort();
}
function init() {
initCb('location', 'cbLocation');
initCb('pmFullName', 'cbPM');
initCb('slFullName', 'cbSL');
initCb('dlFullName', '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, "'");
}
function convertToCSV(items) {
if (items.length === 0) return '';
const headers = Object.keys(items[0]);
let csv = headers.join(',') + '\r\n';
items.forEach(item => {
const row = headers.map(header => {
let value = item[header] !== undefined ? item[header] : '';
if (value === null || value === undefined || header === 'burnDownChart') {
value = '';
} else if (typeof value === 'object') {
value = JSON.stringify(value, null, 2);
} else {
value = String(value);
}
if (value.includes(',') || value.includes('\n') || value.includes('\r') || value.includes('"')) {
value = value.replace(/"/g, '""');
value = `"${value}"`;
}
return value;
});
csv += row.join(',') + '\r\n';
});
return csv;
}
function downloadCSV(csvData, filename = 'data.csv') {
const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
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=800080",
"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"
}
const riskLevelImg = {
"": "https://img.icons8.com/?size=100&id=59757&format=png&color=000000",
"Mild": "https://img.icons8.com/?size=100&id=pDQPnWPHjQNM&format=png&color=000000",
"Medium": "https://img.icons8.com/?size=100&id=pDQPnWPHjQNM&format=png&color=FAB005",
"Serious": "https://img.icons8.com/?size=100&id=pDQPnWPHjQNM&format=png&color=FF0000",
}
const riskStatusImg = {
"": "https://img.icons8.com/?size=100&id=59757&format=png&color=000000",
"Monitoring": "https://img.icons8.com/?size=100&id=T8KCO0TdWmpo&format=png&color=ff0000",
"Closed": "https://img.icons8.com/?size=100&id=HBR0c_yZeXyu&format=png&color=40C057",
}
function render(list) {
const pagingDiv = document.getElementById("pagingStatus");
pagingDiv.innerText = `第 ${data.data.meta.page}/${Math.ceil(data.data.meta.total/data.data.meta.pageSize)} 页,共 ${data.data.meta.total} 条`;
const cards = document.getElementById("cards");
cards.innerHTML = "";
for (let item of list) {
const risk = (item.risk || "暂无风险").toLowerCase().trim();
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 || item.actionPlan == "N/A";
const updateTime = new Date(item.updateTime)
const days = daydiff(updateTime)
var card = document.createElement("div");
var springRange = getRange(item.sprintStartTime, item.sprintEndTime);
var goal = getGoalInfo(item.id);
card.innerHTML = `
<div class="card" tabindex="0" style="background-color:${ item.projectStatus === "Close" ? "#bbb" : (norisk ? "#c6f4c6" : "#ffcc00") }" onclick="this.focus()">
<div class="header">
<div title="办公室" style="display:flex;algin-items:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex-shrink:1"><img src="https://img.icons8.com/?size=100&id=kIuNfCeXdCvG&format=png&color=000000" width=18 height=18> ${item.location}</div>
<div title="项目编号 - 当前迭代周期(迭代时间范围)\n${item.srNumber || "?"} - #${item.sprint}${springRange}" class="projectInfo link" team="${item.projectTeam}" style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex-shrink:1;max-width:230px"><b>${item.srNumber || "N/A"} #${item.sprint}</b>${springRange}</div>
</div>
<div class="projectTeam link" team="${item.projectTeam}" title="团队名称(Project Team)\n${item.projectTeam}" style="border-bottom: 1px solid #fff;padding: 0 10px 5px 10px; max-width:360px;white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"><b>${item.projectPhase || "N/A"}</b> - ${item.projectTeam}</div>
<div class="details" style="border-bottom: 1px solid #fff">
<table width='100%'><tbody>
<tr>
<td><b>PM</b>: <a href="mailto:${users.get(item.pm)?.email}?cc=${users.get(item.dl)?.email + ";" + users.get(item.sl)?.email}&subject=Jazz: 关于项目 ${item.srNumber}(${item.projectTeam})">${item.pmFullName}</a></td>
<td align="right">SR 时间范围</td>
</tr>
<tr>
<td><b>SL</b>: <a href="mailto:${users.get(item.sl)?.email}?cc=${users.get(item.dl)?.email + ";" + users.get(item.pm)?.email}&subject=Jazz: 关于项目 ${item.srNumber}(${item.projectTeam})">${item.slFullName}</a></td>
<td align="right">${formatDate(item.srStartTime)}</td>
</tr>
<tr>
<td><b>DL</b>: <a href="mailto:${users.get(item.dl)?.email}?cc=${users.get(item.pm)?.email + ";" + users.get(item.sl)?.email}&subject=Jazz: 关于项目 ${item.srNumber}(${item.projectTeam})">${item.dlFullName}</a></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 link" team="${item.projectTeam}"><img src="https://img.icons8.com/?size=100&id=9227&format=png&color=0000ff" width=32 height=32 title="Bug 历史记录"><br>
<div title="DefectDensity,标准值: 0.238" style="color: ${item.totalDefectDensity >= 0.238 ? "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"><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 > 14 ? "red" : "#000" }" title="更新人: ${item.updatedBy || "?"}\n更新时间: ${updateTime.toLocaleString()}">更新于: ${updateTime.toLocaleDateString()}</div></td></tr>
</tbody></table></div></div>
<div style="width:100%;height:210px">
<div class="chart" style="cursor:pointer;overflow:hidden">${item.burnDownChart?.replace("<p>", "").replace("</p>", "") || ""}</div>
<div class="warning projectRisk link" tooltip="${item.risk}" title="${item.risk}" team="${item.projectTeam}" style="display:flex; align-items:center;gap:5px;background-color: ${norisk ? 'rgba(0, 204, 0, 0.3)' : 'rgba(255, 204, 0, 0.3)'}">
<img width=28 height=28 src="${riskLevelImg[item.riskLevel]}" tooltip="风险等级: ${item.riskLevel}" title="风险等级: ${item.riskLevel}"><span class="twoLine">${item.risk}</span>
</div>
<div class="goal" style="background-color: ${goal?.sprintNumber === item.sprint ? 'rgba(0,0,0,0.1)': 'none'}" title="${goal?.goal}">${goal?.sprintNumber === item.sprint ? goal.goal?.trim().replaceAll("\n", "<br/>") : ""}</div>
<div class="actionplan" tooltip="${item.actionPlan}" title="${item.actionPlan}" style="display:flex; align-items:center;gap:5px">
<img width=28 height=28 src="${riskStatusImg[item.riskStatus]}" title="风险状态: ${item.riskStatus}"><span class="twoLine">${item.actionPlan}</span>
</div>
</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";
});
});
}
})();