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

给发版管理的表格增加ID列、删除新建时间、新增发版日志按钮

目前為 2024-04-03 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         三叉戟-发版管理-表格增强
// @namespace    http://tampermonkey.net/
// @version      2024-04-03-1.0.1
// @description  给发版管理的表格增加ID列、删除新建时间、新增发版日志按钮
// @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;

  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)) {
      doTableObserver();
    }
  }

  function doTableObserver() {
    const observer = new MutationObserver((mutations, me) => {
      console.log("[三叉戟] 监听版本管理页面");
      if (isPageLoaded()) {
        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;
        let 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}`);
              observer.disconnect();
              addIdColumn();
              removeCreationTimeColumn();
              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}`);
          });
      }
    });

    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 removeCreationTimeColumn() {
    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() === "新建时间") {
          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 {
          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; /* 设置行高 */
        }
    `);
  }

  // 弹窗逻辑
  function setupModal() {
    const modal = document.getElementById("release-log-modal");
    const closeBtn = modal.querySelector(".close");

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

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

  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">${task.task.app.name}</td>
            <td colspan="1">${task.task.tag}</td>
            <td colspan="3">${task.task.digest}</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>${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}</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">数据集成与开发平台-发版日志-<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="3">镜像摘要</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="3">镜像摘要</td>
                </tr>
                ${
                  bodyMap.get("backEndBody")
                    ? bodyMap.get("backEndBody").join("")
                    : ""
                }
              </tbody>
          </table>
        </div>
    </div>
    `;
  }
})();