首页  编辑  

油猴插件:Jazz 插件

Tags: /计算机文档/软件应用技巧/   Date Created:
Jazz 插件:

// ==UserScript==
// @name         Jazz
// @namespace    http://tampermonkey.net/
// @version      1.7
// @description  Jazz project viewer
// @author       Kingron
// @match        https://www.abc.com/contribute/
// @icon         https://img.icons8.com/?size=100&id=bDaPwYMHLojO&format=png&color=000000
// @run-at       document-start
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const originalXHR = window.XMLHttpRequest;
    let users = new Map();
    let token;
    let urlQuery;
    let data;
    let timeoutId; // 用于存储 setTimeout 的 ID

    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);

    // 替换XMLHttpRequest
    window.XMLHttpRequest = function() {
        const xhr = new originalXHR();

        // 保存原生open方法
        const originalOpen = xhr.open;

        // 保存原生send方法
        const originalSend = xhr.send;
        const originalSetRequestHeader = xhr.setRequestHeader;
        xhr.setRequestHeader = function(header, value) {
            if ("Authorization" === header) token = value;
            originalSetRequestHeader.apply(this, arguments);
        };

        // 重写open方法
        xhr.open = function(method, url) {
            this._method = method;
            this._url = url;
            return originalOpen.apply(this, arguments);
        };

        // 重写send方法
        xhr.send = function(d) {
            this._requestData = d;

            // 保存原生onreadystatechange
            const originalOnreadystatechange = this.onreadystatechange;

            // 重写onreadystatechange
            this.onreadystatechange = function() {
                // 调用原始回调函数
                if (originalOnreadystatechange) {
                    return originalOnreadystatechange.apply(this, arguments);
                }

                if (this.readyState === 4) {
                    // 拦截特定URL的请求
                    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;
    };

    // 将 UTC 时间转换为本地时间并提取日期部分
    function formatDate(utcDateString) {
        const date = new Date(utcDateString); // 将字符串转换为 Date 对象
        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;
          "
        />&nbsp;
        <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'">&times;</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;
        // /api/dashboard/queryDashboardPageList?page=1&pageSize=10&projectStatus=Active&location=&dl=&pm=&projectType=
        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, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;")
            .replace(/"/g, "&quot;")
            .replace(/'/g, "&#039;");
    }

    function convertToCSV(items) {
        if (items.length === 0) return '';

        // 获取表头
        const headers = Object.keys(items[0]);

        // 构建 CSV 内容
        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);
            //const raw = escapeHtml(JSON.stringify(item, (key, value) => {if (key==='burnDownChart' || value == null || value == "") return undefined; return value}, 2));'
            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>&nbsp;${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">&nbsp;${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";
    });
});
}
})();