三叉戟-发版管理-表格增强

发版管理页面,相关功能的增强实现

当前为 2024-06-05 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         三叉戟-发版管理-表格增强
// @namespace    http://tampermonkey.net/
// @version      2024-04-03-1.0.6
// @description  发版管理页面,相关功能的增强实现
// @author       CloudS3n
// @match        https://poseidon.cisdigital.cn/app/devops*
// @icon         https://poseidon.cisdigital.cn/favicon.ico
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function () {
    "use strict";

    const releaseUrlPrefix =
        "https://poseidon.cisdigital.cn/app/devops/releases?";
    let isFirstLoad = true;
    let projectName;
    let productName;
    let productDisplayName;
    const observer = new MutationObserver((mutations, me) => {
        console.log("[三叉戟] 版本管理页面dom发生变化");
        if (isPageLoaded()) {
            console.log("[三叉戟] 页面加载完毕");
            observer.disconnect();
            console.log("[三叉戟] 停止监听版本管理页面");
            let projectDisplayName = document.querySelectorAll(
                "#base-app > div > div.header-container > div.header > div.left > div.base-selector.base-selector > span.selector-item-wrap.selector-project > div > div.el-select-dropdown.el-popper.base-selector-dropdown > div > div.el-select-dropdown__wrap.el-scrollbar__wrap > ul > ul.el-select-group__wrap.recent-group > li:nth-child(2) > ul > li.el-select-dropdown__item.selected"
            )[0].textContent;
            let defaultProductDisplayName = document.querySelectorAll(
                "#base-app > div > div.header-container > div.header > div.left > div.base-selector.base-selector > span.selector-item-wrap.selector-product > div > div.el-select-dropdown.el-popper.base-selector-dropdown > div > div.el-select-dropdown__wrap.el-scrollbar__wrap > ul > ul.el-select-group__wrap.recent-group > li:nth-child(2) > ul > li.el-select-dropdown__item.selected"
            )[0]?.textContent;
            let selectProductDisplayName = document.querySelectorAll(
                "body > div.el-select-dropdown.el-popper.base-selector-dropdown > div > div.el-select-dropdown__wrap.el-scrollbar__wrap > ul > ul:nth-child(2) > li:nth-child(2) > ul > li.el-select-dropdown__item.selected"
            )[0]?.textContent;
            productDisplayName = defaultProductDisplayName ? defaultProductDisplayName : selectProductDisplayName;
            if (!productDisplayName) {
                console.error(
                    `[三叉戟] 获取项目和产品信息失败,projectDisplayName=${productDisplayName}, productDisplayName=${productDisplayName}`
                );
                return;
            }
            fetchProjectList()
                .then((projectList) => {
                    if (projectList && projectList.length > 0) {
                        projectList.forEach((project) => {
                            if (project.display_name === projectDisplayName) {
                                projectName = project.name;
                                project.products.forEach((product) => {
                                    if (
                                        product.display_name === productDisplayName
                                    ) {
                                        productName = product.name;
                                    }
                                });
                            }
                        });
                    }
                    if (projectName && productName) {
                        console.log(`[三叉戟] project=${projectName}, product=${productName}`);
                        addIdColumn();
                        removeColumn('新建时间');
                        removeColumn('ID');
                        removeEditButtons();
                        addReleaseNoteButton();
                        fetchReleaseList()
                            .then((data) => {
                                updateTableWithRealIds(data);
                            })
                            .catch((error) => {
                                console.error(`[三叉戟] Error fetching data: ${error}`);
                            });
                    } else {
                        console.error(
                            `[三叉戟] 获取项目和产品信息失败,project=${projectName}, product=${productName}`
                        );
                    }
                })
                .catch((error) => {
                    console.error(`[三叉戟] Error fetching data: ${error}`);
                });
        }
    });

    initial();

    function initial() {
        // vue单页面框架,需要根据URL的变化执行相应的操作
        let lastUrl = location.href;
        new MutationObserver(() => {
            const url = location.href;
            if (isFirstLoad || url !== lastUrl) {
                lastUrl = url;
                onUrlChange();
                isFirstLoad = false;
            }
        }).observe(document, {subtree: true, childList: true});
    }

    function onUrlChange() {
        const currUrl = location.href;
        console.log("[三叉戟] URL changed!", currUrl);
        if (currUrl.startsWith(releaseUrlPrefix)) {
            enableTableObserver();
        }
    }

    function enableTableObserver() {
        console.log("[三叉戟] 开始监听版本管理页面");
        observer.observe(document.body, {
            childList: true,
            subtree: true,
        });
    }

    function isPageLoaded() {
        const headerRow = document.querySelector(
            ".el-table__header-wrapper .el-table__header thead tr"
        );
        const bodyRows = document.querySelectorAll(
            ".el-table__body-wrapper .el-table__body tbody tr"
        );
        const project = document.querySelectorAll(
            "#base-app > div > div.header-container > div.header > div.left > div.base-selector.base-selector > span.selector-item-wrap.selector-project > div > div.el-select-dropdown.el-popper.base-selector-dropdown > div > div.el-select-dropdown__wrap.el-scrollbar__wrap > ul > ul.el-select-group__wrap.recent-group > li:nth-child(2) > ul > li.el-select-dropdown__item.selected"
        )[0];
        const defaultProduct = document.querySelectorAll(
            "#base-app > div > div.header-container > div.header > div.left > div.base-selector.base-selector > span.selector-item-wrap.selector-product > div > div.el-select-dropdown.el-popper.base-selector-dropdown > div > div.el-select-dropdown__wrap.el-scrollbar__wrap > ul > ul.el-select-group__wrap.recent-group > li:nth-child(2) > ul > li.el-select-dropdown__item.selected"
        )[0];
        const product = document.querySelectorAll(
            "body > div.el-select-dropdown.el-popper.base-selector-dropdown > div > div.el-select-dropdown__wrap.el-scrollbar__wrap > ul > ul:nth-child(2) > li:nth-child(2) > ul > li.el-select-dropdown__item.selected"
        )[0];
        return (
            headerRow &&
            bodyRows &&
            bodyRows.length > 0 &&
            project &&
            (defaultProduct || product)
        );
    }

    function addReleaseNoteDom(releaseId, taskList) {
        addModalStyles();
        document.getElementById("release-log-modal")?.remove();
        const modalHtml = buildModalContent(releaseId, taskList);
        document.body.insertAdjacentHTML("beforeend", modalHtml);
        setupModal();
    }

    function getCookie(cookieName) {
        var strCookie = document.cookie;
        var arrCookie = strCookie.split("; ");
        for (var i = 0; i < arrCookie.length; i++) {
            var arr = arrCookie[i].split("=");
            if (cookieName == arr[0]) {
                return arr[1];
            }
        }
        return "";
    }

    function removeColumn(columnName) {
        for (let i = 0; i++; i < 2) {
            const headers = document.querySelectorAll(
                `.el-table__${
                    i == 0 ? "fixed-" : ""
                }header-wrapper .el-table__header thead th`
            );
            let columnIndex = -1;
            headers.forEach((th, index) => {
                if (th.textContent.trim() === columnName) {
                    columnIndex = index;
                    th.remove();
                }
            });

            if (columnIndex !== -1) {
                const cols = document.querySelector(
                    `.el-table__${
                        i == 0 ? "fixed-" : ""
                    }header-wrapper .el-table__header colgroup col`
                );
                if (cols[columnIndex]) {
                    cols[columnIndex].remove();
                }
                const bodyCols = document.querySelectorAll(
                    `.el-table__${
                        i == 0 ? "fixed-" : ""
                    }body-wrapper .el-table__body colgroup col`
                );
                if (bodyCols[columnIndex]) {
                    bodyCols[columnIndex].remove();
                }
            }

            const rows = document.querySelectorAll(
                `.el-table__${
                    i == 0 ? "fixed-" : ""
                }body-wrapper .el-table__body tbody tr`
            );
            rows.forEach((row) => {
                const cells = row.querySelectorAll("td");
                if (cells[columnIndex]) {
                    cells[columnIndex].remove();
                }
            });
        }
    }

    function removeEditButtons() {
        const actionGroups = document.querySelectorAll(".table-action-group");
        if (actionGroups && actionGroups.length > 0) {
            actionGroups.forEach((group) => {
                const actionItems = group.querySelectorAll(".table-action-item");
                actionItems.forEach((item) => {
                    const buttonText = Array.from(
                        item.querySelectorAll("button, button *")
                    )
                        .map((e) => e.textContent.trim())
                        .join("");
                    if (buttonText.includes("编辑")) {
                        item.remove();
                        console.log("删除编辑按钮");
                    }
                });
            });
        }
    }

    function addIdColumn() {
        console.log("[三叉戟] 添加ID列");
        const headerRow = document.querySelector(
            ".el-table__header-wrapper .el-table__header thead tr"
        );
        const colgroup = document.querySelector(
            ".el-table__header-wrapper .el-table__header colgroup"
        );
        const bodyColgroup = document.querySelector(
            ".el-table__body-wrapper .el-table__body colgroup"
        );
        const colgroupFixed = document.querySelector(
            ".el-table__fixed-header-wrapper .el-table__header colgroup"
        );
        const bodyColgroupFixed = document.querySelector(
            ".el-table__fixed-body-wrapper .el-table__body colgroup"
        );
        const newCol = document.createElement("col");
        newCol.style.width = "240px";
        if (colgroup.firstChild) {
            colgroup.insertBefore(newCol, colgroup.firstChild);
        } else {
            colgroup.appendChild(newCol);
        }
        if (bodyColgroup.firstChild) {
            bodyColgroup.insertBefore(
                newCol.cloneNode(true),
                bodyColgroup.firstChild
            );
        } else {
            bodyColgroup.appendChild(newCol.cloneNode(true));
        }

        if (colgroupFixed.firstChild) {
            colgroupFixed.insertBefore(
                newCol.cloneNode(true),
                colgroupFixed.firstChild
            );
        } else {
            colgroupFixed.appendChild(newCol.cloneNode(true));
        }
        if (bodyColgroupFixed.firstChild) {
            bodyColgroupFixed.insertBefore(
                newCol.cloneNode(true),
                bodyColgroupFixed.firstChild
            );
        } else {
            bodyColgroupFixed.appendChild(newCol.cloneNode(true));
        }
        const idHeader = document.createElement("th");
        idHeader.innerHTML = '<div class="cell">ID</div>';
        headerRow.insertBefore(idHeader, headerRow.firstChild);
        document
            .querySelectorAll(
                ".el-table__fixed-body-wrapper .el-table__body tbody tr"
            )
            .forEach((row) => {
                const idCell = document.createElement("td");
                idCell.innerHTML = '<div class="cell"></div>';
                row.insertBefore(idCell, row.firstChild);
            });
        document
            .querySelectorAll(".el-table__body-wrapper .el-table__body tbody tr")
            .forEach((row) => {
                const idCell = document.createElement("td");
                idCell.innerHTML = '<div class="cell"></div>';
                row.insertBefore(idCell, row.firstChild);
            });
    }

    function addReleaseNoteButton() {
        console.log("[三叉戟] 添加发版日志按钮");

        document
            .querySelectorAll(
                ".el-table__fixed-body-wrapper .el-table__body tbody tr"
            )
            .forEach((row) => {
                const actionGroup = row.querySelector(
                    "td:last-child .table-action-group"
                );
                if (actionGroup) {
                    const actionItem = document.createElement("div");
                    actionItem.className = "table-action-item";
                    const btn = document.createElement("button");
                    btn.setAttribute("type", "button");
                    btn.style.cssText =
                        "margin-top:-10px; vertical-align:middle; color:var(--cs-color_primary)";
                    btn.className =
                        "el-button table-action-button table-action-item el-button--text";
                    btn.innerHTML = "<span>发版日志</span>";
                    btn.addEventListener("click", function () {
                        let releaseId = row.querySelector("td:first-child .cell").innerText;
                        if (!releaseId) return;
                        let env = "dev";
                        fetchReleaseDetailData(releaseId)
                            .then((releaseDetial) => {
                                if (releaseDetial && releaseDetial.env) env = releaseDetial.env;
                                fetchTaskList(releaseId, env)
                                    .then((taskList) => {
                                        addReleaseNoteDom(releaseDetial, taskList);
                                        const modal = document.getElementById("release-log-modal");
                                        modal.style.display = "block";
                                    })
                                    .catch((error) => {
                                        console.error(`[三叉戟] Error fetching data: ${error}`);
                                    });
                            })
                            .catch((error) => {
                                console.error(`[三叉戟] Error fetching data: ${error}`);
                            });
                    });
                    actionItem.appendChild(btn);
                    actionGroup.appendChild(actionItem);
                }
            });
    }

    function fetchProjectList() {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: `https://poseidon.cisdigital.cn/api/projects/`,
                headers: {
                    accept: "application/json, text/plain, */*",
                    "accept-language": "zh-CN,zh;q=0.9",
                    authorization: `Bearer ${getCookie("poseidon_user_token")}`,
                    referer:
                        "https://poseidon.cisdigital.cn/app/devops/releases?project=ciip-private&product=datakits",
                },
                onload: function (response) {
                    const result = JSON.parse(response.responseText);
                    if (result.code === 0 && Array.isArray(result.data)) {
                        console.log(
                            `[三叉戟] 获取项目数据: ${JSON.stringify(result.data)}`
                        );
                        resolve(result.data);
                    } else {
                        reject(
                            new Error(
                                "[三叉戟] API call failed or returned an unexpected structure"
                            )
                        );
                    }
                },
                onerror: function (error) {
                    reject(new Error(`API call failed: ${error}`));
                },
            });
        });
    }

    function fetchReleaseList() {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: "https://poseidon.cisdigital.cn/devops/api/releases/?page=1&page_size=100&archived=0&search=",
                headers: {
                    accept: "application/json, text/plain, */*",
                    "accept-language": "zh-CN,zh;q=0.9",
                    authorization: `Bearer ${getCookie("poseidon_user_token")}`,
                    referer:
                        "https://poseidon.cisdigital.cn/app/devops/releases?project=ciip-private&product=datakits",
                    "x-poseidon-product": `${productName}`,
                    "x-poseidon-project": `${projectName}`,
                },
                onload: function (response) {
                    const result = JSON.parse(response.responseText);
                    if (result.code === 0 && Array.isArray(result.data)) {
                        console.log(
                            `[三叉戟] 获取版本管理数据: ${JSON.stringify(result.data)}`
                        );
                        resolve(result.data);
                    } else {
                        reject(
                            new Error(
                                "[三叉戟] API call failed or returned an unexpected structure"
                            )
                        );
                    }
                },
                onerror: function (error) {
                    reject(new Error(`API call failed: ${error}`));
                },
            });
        });
    }

    function fetchReleaseDetailData(releaseId) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: `https://poseidon.cisdigital.cn/devops/api/releases/${releaseId}/`,
                headers: {
                    accept: "application/json, text/plain, */*",
                    "accept-language": "zh-CN,zh;q=0.9",
                    authorization: `Bearer ${getCookie("poseidon_user_token")}`,
                    referer:
                        "https://poseidon.cisdigital.cn/app/devops/releases?project=ciip-private&product=datakits",
                    "x-poseidon-product": `${productName}`,
                    "x-poseidon-project": `${projectName}`,
                },
                onload: function (response) {
                    const result = JSON.parse(response.responseText);
                    if (result.code === 0 && result.data) {
                        console.log(
                            `[三叉戟] 获取版本详情数据: ${JSON.stringify(result.data)}`
                        );
                        resolve(result.data);
                    } else {
                        reject(
                            new Error(
                                "[三叉戟] API call failed or returned an unexpected structure"
                            )
                        );
                    }
                },
                onerror: function (error) {
                    reject(new Error(`API call failed: ${error}`));
                },
            });
        });
    }

    function fetchTaskList(releaseId, env) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: `https://poseidon.cisdigital.cn/devops/api/releases/${releaseId}/tasks/?env=${env}`,
                headers: {
                    accept: "application/json, text/plain, */*",
                    "accept-language": "zh-CN,zh;q=0.9",
                    authorization: `Bearer ${getCookie("poseidon_user_token")}`,
                    referer:
                        "https://poseidon.cisdigital.cn/app/devops/releases?project=ciip-private&product=datakits",
                    "x-poseidon-product": `${productName}`,
                    "x-poseidon-project": `${projectName}`,
                },
                onload: function (response) {
                    const result = JSON.parse(response.responseText);
                    if (result.code === 0 && Array.isArray(result.data)) {
                        console.log(
                            `[三叉戟] 获取版本任务数据: ${JSON.stringify(result.data)}`
                        );
                        resolve(result.data);
                    } else {
                        reject(
                            new Error(
                                "[三叉戟] API call failed or returned an unexpected structure"
                            )
                        );
                    }
                },
                onerror: function (error) {
                    reject(new Error(`API call failed: ${error}`));
                },
            });
        });
    }

    function updateTableWithRealIds(data) {
        const nameIdMap = data.reduce((acc, curr) => {
            acc[curr.name] = curr.id;
            return acc;
        }, {});

        const bodyRows = document.querySelectorAll(
            ".el-table__body-wrapper .el-table__body tbody tr"
        );
        const bodyRowsFixed = document.querySelectorAll(
            ".el-table__fixed-body-wrapper .el-table__body tbody tr"
        );
        console.log("[三叉戟] 填充ID列");
        bodyRows.forEach((row) => {
            const versionCell = row.querySelector("td:nth-child(2)");
            if (versionCell) {
                const versionText = versionCell.textContent.trim();
                const realId = nameIdMap[versionText];
                if (realId !== undefined) {
                    const idCell = row.querySelector("td:first-child .cell");
                    if (idCell) {
                        idCell.textContent = realId;
                    }
                }
            }
        });
        bodyRowsFixed.forEach((row) => {
            const versionCell = row.querySelector("td:nth-child(2)");
            if (versionCell) {
                const versionText = versionCell.textContent.trim();
                const realId = nameIdMap[versionText];
                if (realId !== undefined) {
                    const idCell = row.querySelector("td:first-child .cell");
                    if (idCell) {
                        idCell.textContent = realId;
                    }
                }
            }
        });
    }

    function addModalStyles() {
        GM_addStyle(`
        .modal {
            display: none;
            position: fixed;
            z-index: 10000;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            overflow: auto;
            background-color: rgb(0,0,0);
            background-color: rgba(0,0,0,0.4);
            padding-top: 60px;
        }

        .modal-content {
            background-color: #fefefe;
            margin: 5% auto;
            padding: 20px;
            border: 1px solid #888;
            width: 80%;
        }

        .close {
            color: #aaaaaa;
            float: right;
            font-size: 28px;
            font-weight: bold;
        }

        .close:hover,
        .close:focus {
            color: #000;
            text-decoration: none;
            cursor: pointer;
        }

        #release-note-table {
            width: 100%;
            border-collapse: collapse;
        }

        #release-note-table > thead > tr > th, td {
            border: 1px solid #ddd;
            text-align: left;
            padding: 8px;
        }

        #release-note-table > thead > tr > th {
            background-color: #f2f2f2;
            text-align: center;
            font-size: 20px;
            font-weight: bold;
        }

        #release-note-table > tbody > tr > td.sub-title {
          font-size: 18px;
          font-weight: bold;
          background-color: #f2f2f2;
        }

        #release-note-table > tbody > tr.third-title > td {
          font-size: 16px;
          font-weight: bold;
        }

        #release-note-table > tbody > tr.item-list:nth-child(odd) {
          background-color: #e0f2f1;
        }

        .multi-line-clamp {
          display: -webkit-box;
          -webkit-box-orient: vertical;
          -webkit-line-clamp: 500; /* 定义最大显示的行数 */
          overflow: hidden;
          text-overflow: ellipsis; /* 当内容溢出时显示... */
          word-break: break-word; /* 使得单词在必要时可以断行 */
          max-height: 20em; /* 根据行高和最大行数调整 */
          line-height: 1em; /* 设置行高 */
        }

        code {
          white-space: pre-wrap; /* 维持空格的格式,同时允许内容自动换行 */
          word-wrap: break-word; /* 在长单词或 URL 地址内部进行断行 */
        }
    `);
    }

    // 弹窗逻辑
    function setupModal() {
        const modal = document.getElementById("release-log-modal");
        const closeBtn = modal.querySelector(".close");
        const singleExportBtnList = document.querySelectorAll('#release-note-table > tbody > tr > td > button.singe-export-btn');

        // 点击关闭按钮隐藏弹窗
        closeBtn.onclick = function () {
            modal.style.display = "none";
        };

        // 点击窗口外部区域隐藏弹窗
        window.onclick = function (event) {
            if (event.target === modal) {
                modal.style.display = "none";
            }
        };

        // 单个导出按钮
        singleExportBtnList.forEach(button => {
            button.addEventListener('click', handleSingleExportButtonClick);
        });
    }

    function addElementToKey(myMap, key, element) {
        if (myMap.has(key)) {
            let currentList = myMap.get(key);
            currentList.push(element);
            myMap.set(key, currentList);
        } else {
            myMap.set(key, [element]);
        }
    }

    function buildModalContent(releaseDetail, taskList) {
        // 生成任务列表HTML
        const bodyMap = new Map();
        taskList
            ?.sort((a, b) => a.order - b.order)
            .forEach((task) => {
                switch (task.task_type) {
                    case 1: {
                        // 前端或后端服务
                        const serverHtml = `
            <tr class="item-list">
            <td colspan="1" width="20%">${task.task.app.name}</td>
            <td colspan="1" width="10%">${task.task.tag.replace('-release', '')}</td>
            <td colspan="1" width="20%">${task.task.digest}</td>
            <td colspan="1" width="45%">${task.task.description.replace(
                            /\n/g,
                            "<br>"
                        )}</td>
            <td colspan="1" width="5%"><button type="button" class="el-button table-action-button table-action-item el-button--text singe-export-btn" style="color:var(--cs-color_primary)">导出</button></td>
            </tr>
        `;
                        if (task.task.app.name.startsWith("fe-")) {
                            addElementToKey(bodyMap, "frontEndBody", serverHtml);
                        } else {
                            addElementToKey(bodyMap, "backEndBody", serverHtml);
                        }
                        break;
                    }
                    case 2: {
                        // 需要执行的sql
                        addElementToKey(
                            bodyMap,
                            "sqlBody",
                            `
              <tr class="item-list">
              <td colspan="1">${task.task.middleware}-${task.task.database}</td>
              <td colspan="1">${task.task.name}</td>
              <td colspan="3"><span style="width: 800px; display: block">${task.task.content.replace(
                                /\n/g,
                                "<br>"
                            )}</span></td>
              </tr>
        `
                        );
                        break;
                    }
                    case 3: {
                        // yaml配置
                        let yamlHtml = task.task
                            .map(
                                (t) => `
                <tr class="item-list">
                <td colspan="2">${t.name}</td>
                <td colspan="3">详情</td>
                </tr>
          `
                            )
                            .join("");
                        addElementToKey(bodyMap, "yamlBody", yamlHtml);
                        break;
                    }
                    case 4: {
                        // 需要人工确认的事项
                        addElementToKey(
                            bodyMap,
                            "manualBody",
                            `<tr class="item-list"><td colspan="5">${task.task.content.replace(
                                /\n/g,
                                "<br>"
                            )}</td></tr>`
                        );
                        break;
                    }
                    default:
                        return "";
                }
            });

        return `
    <div id="release-log-modal" class="modal">
        <div id="modal-content" class="modal-content">
            <span class="close">&times;</span>
            <table id="release-note-table">
              <thead>
                <tr>
                    <th colspan="5">${productDisplayName}-发版日志-<span id="product-version">${
            releaseDetail.name
        }</span></th>
                </tr>
              </thead>
              <tbody>
                <tr>
                  <td colspan="5" class="sub-title">1.需要人工确认的事项</td>
                </tr>
                ${
            bodyMap.get("manualBody")
                ? bodyMap.get("manualBody").join("")
                : ""
        }
                <tr>
                  <td colspan="5" class="sub-title">2.SQL变动</td>
                </tr>
                <tr class="third-title">
                  <td colspan="1">数据库</td>
                  <td colspan="1">描述</td>
                  <td colspan="3">执行sql</td>
                </tr>
                ${bodyMap.get("sqlBody") ? bodyMap.get("sqlBody").join("") : ""}
                <tr>
                  <td colspan="5" class="sub-title">3.YAML变动</td>
                </tr>
                <tr class="third-title">
                  <td colspan="2">YAML文件名</td>
                  <td colspan="3">修改详情</td>
                </tr>
                ${
            bodyMap.get("yamlBody")
                ? bodyMap.get("yamlBody").join("")
                : ""
        }
                <tr>
                  <td colspan="5" class="sub-title">4.前端服务</td>
                </tr>
                <tr class="third-title">
                    <td colspan="1">服务名</td>
                    <td colspan="1">镜像tag</td>
                    <td colspan="1">镜像摘要</td>
                    <td colspan="1">升级描述</td>
                    <td colspan="1">导出</td>
                </tr>
                ${
            bodyMap.get("frontEndBody")
                ? bodyMap.get("frontEndBody").join("")
                : ""
        }
                <tr>
                  <td colspan="5" class="sub-title">5.后端服务</td>
                </tr>
                <tr class="third-title">
                    <td colspan="1">服务名</td>
                    <td colspan="1">镜像tag</td>
                    <td colspan="1">镜像摘要</td>
                    <td colspan="1">升级描述</td>
                    <td colspan="1">导出</td>
                </tr>
                ${
            bodyMap.get("backEndBody")
                ? bodyMap.get("backEndBody").join("")
                : ""
        }
              </tbody>
          </table>
        </div>
    </div>
    `;
    }

    function handleSingleExportButtonClick(event) {
        const row = event.target.closest('tr.item-list');
        const appName = row.cells[0].textContent.trim();
        const tag = row.cells[1].textContent.trim();
        const digest = row.cells[2].textContent.trim();
        const token = getCookie("poseidon_user_token");

        const exportData = {
            spec: JSON.stringify({
                images: [{
                    app: appName,
                    images: [{tag, digest}]
                }]
            }),
            description: "导出镜像",
            meta_type: 2
        };

        GM_xmlhttpRequest({
            method: "POST",
            url: "https://poseidon.cisdigital.cn/devops/api/migration/exports/",
            headers: {
                "accept": "application/json, text/plain, */*",
                "accept-language": "zh-CN,zh;q=0.9",
                "authorization": `Bearer ${token}`,
                "content-type": "application/json",
                "origin": "https://poseidon.cisdigital.cn",
                "referer": "https://poseidon.cisdigital.cn/app/devops/import-export",
                "x-poseidon-product": `${productName}`,
                "x-poseidon-project": `${projectName}`,
            },
            data: JSON.stringify(exportData),
            onload: function (response) {
                const result = JSON.parse(response.responseText);
                if (result.code === 0 && result.data) {
                    console.log(
                        `[三叉戟] 成功触发导出单个镜像: ${appName}-${tag}-${digest}`
                    );
                    const url = `https://poseidon.cisdigital.cn/app/devops/import-export?project=${projectName}&product=${productName}`;
                    window.open(url, '_blank');
                } else {
                    console.error(
                        `[三叉戟] 触发导出单个镜像失败: ${JSON.stringify(result)}`
                    );
                }
            },
            onerror: function (error) {
                reject(new Error(`[三叉戟] 触发导出单个镜像API call failed: ${error}`));
            },
        });
    }
})();