cuipengcheng / 云效助手

// ==UserScript==
// @name         云效助手
// @namespace    http://tampermonkey.net/
// @version      1.1.6
// @description  包含云效项目功能加强、自动登录、快速填写工时、工时提醒
// @author       温华
// @match        https://passport.aliyun.com/havanaone/login/login.htm*
// @match        https://devops.aliyun.com/*
// @match        https://codeup.aliyun.com/*
// @match        https://account.aliyun.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=aliyun.com
// @license      MIT
// @grant        GM_addStyle
// @grant        unsafeWindow
// @grant        GM_openInTab
// @grant        GM_setValue
// @grant        GM_getValue
// @downloadURL https://openuserjs.org/install/cuipengcheng/云效助手.user.js
// @updateURL https://openuserjs.org/meta/cuipengcheng/云效助手.meta.js
// @copyright 2025, cuipengcheng (https://openuserjs.org/users/cuipengcheng)
// ==/UserScript==

(async function () {
  "use strict";
  let userName = "";
  let password = "";
  var bugIconHtml = `<span class="CardTable--cardIconBox--M9k3BUL"><span class="workitemCategoryIconBox CardTable--cardIcon--39ZirqJ"><i class="teamix-icon teamix-icon-quexian-zhubug-line teamix-medium" style="color: var(--color-error-5, #e84738);"><svg viewBox="0 0 1024 1024"><use xlink:href="#yunxiao-quexian-zhubug-line"></use></svg></i></span></span>`;
  var reqIconHtml = `<span class="CardTable--cardIconBox--M9k3BUL"><span class="workitemCategoryIconBox CardTable--cardIcon--39ZirqJ"><i class="teamix-icon teamix-icon-assign-line teamix-medium" style="color: var(--color-success-5, #23b066);"><svg viewBox="0 0 1024 1024"><use xlink:href="#yunxiao-assign-line"></use></svg></i></span></span>`;
  var taskIconHtml = `<span class="CardTable--cardIconBox--M9k3BUL"><span class="workitemCategoryIconBox CardTable--cardIcon--39ZirqJ"><i class="teamix-icon teamix-icon-file-2-line teamix-medium" style="color: rgb(84, 64, 182);"><svg viewBox="0 0 1024 1024"><use xlink:href="#yunxiao-file-2-line"></use></svg></i></span></span>`;
  var setIconHtml = `<button type="button" class="next-btn next-small next-btn-normal next-btn-text isOnlyIcon isOnlyIcon aone-biz-today-work-setting-line is-yunxiao"><i class="teamix-icon teamix-icon-setting-line teamix-medium"><svg viewBox="0 0 1024 1024"><use xlink:href="#yunxiao-setting-line"></use></svg></i></button>`;

  /**
   * 自动登录
   */
  {
    userName = await GM_getValue("userName", "");
    password = await GM_getValue("password", "");

    if (!userName && location.host !== "passport.aliyun.com") {
      createGetLoginInfo();
    }
    const nameDom = document.querySelector("#fm-login-id");
    const passwordDom = document.querySelector("#fm-login-password");
    if (nameDom && passwordDom) {
      nameDom.value = userName;
      passwordDom.value = password;
      document.querySelector(".password-login").click();
    }
  }

  /**
   * 任务加强
   */
  if (location.host === "devops.aliyun.com") {
    let isLogin = false;
    const login = () => {
      if (isLogin) return;
      const loginDom = document.querySelector(
        ".next-overlay-wrapper .next-dialog-footer button"
      );
      if (loginDom && loginDom.textContent === "重新登录") {
        loginDom.click();
        isLogin = true;
      }
    };
    // 从其他tab切换回来时,需要重新登录
    window.addEventListener("visibilitychange", () => {
      if (document.visibilityState === "visible") {
        login();
      }
    });

    setInterval(() => {
      login();
      document.querySelectorAll(".next-btn").forEach((btn) => {
        const text = btn.querySelector(".teamix-title")?.innerText;
        if (!text) return;
        switch (text) {
          case "开发中":
            btn.style.cssText = "background-color: #09f !important";
            break;
          case "待修复":
            btn.style.cssText = "background-color: #2091ed !important";
            break;
          case "修复中":
            btn.style.cssText = "background-color: #f0812d !important";
            break;
          case "修复完成":
            btn.style.cssText =
              "background-color: rgb(117, 93, 255) !important";
            break;
          case "测试中":
            btn.style.cssText = "background-color: #2196F3 !important";
            break;
          case "测试完成":
            btn.style.cssText =
              "background-color: rgba(7, 175, 175, 0.65) !important";
            break;
          case "再次打开":
            btn.style.cssText = "background-color: red !important";
            break;
          case "推迟修复":
            btn.style.cssText = "background-color: #b2b8be !important";
            break;
        }

        if (testFilter) {
          filterDom(btn, text);
        }
      });
    }, 1000);

    const closest = (el, selector) => {
      let element = el;
      while (element) {
        if (element.matches(selector)) {
          break;
        }
        element = element.parentElement;
      }
      return element;
    };

    const contianer = document.createElement("div");
    contianer.style.position = "fixed";
    contianer.style.top = "28px";
    contianer.style.right = "6px";
    contianer.style.zIndex = "9999";
    document.body.appendChild(contianer);

    const filterBtn = document.createElement("button");
    setBtnStyle(filterBtn);
    filterBtn.style.backgroundColor = "#999";
    filterBtn.innerHTML = "过滤";
    contianer.appendChild(filterBtn);

    const setBtn = document.createElement("span");
    setBtn.innerHTML = setIconHtml;
    setBtn.onclick = () => {
      createGetLoginInfo();
    };
    contianer.appendChild(setBtn);

    const filterChilds = document.createElement("div");
    filterChilds.style.position = "absolute";
    filterChilds.style.top = "24px";
    filterChilds.style.right = "0";
    filterChilds.style.width = "300px";
    filterChilds.style.textAlign = "right";
    filterChilds.style.display = "none";

    contianer.appendChild(filterChilds);

    // 增加多项checkbox按钮
    const btns = [
      {
        text: "测试完成",
        selected: true,
      },
      {
        text: "已取消",
        selected: true,
      },
      {
        text: "已发布",
        selected: true,
      },
      {
        text: "已完成",
        selected: true,
      },
      {
        text: "走查完成",
        selected: true,
      },
    ];
    btns.forEach((item) => {
      const contianer = document.createElement("span");
      const checkbox = document.createElement("input");
      checkbox.type = "checkbox";
      checkbox.name = "test";
      checkbox.value = item.text;
      checkbox.checked = item.selected;
      checkbox.style.zoom = "0.8";
      checkbox.style.position = "relative";
      checkbox.style.top = "1px";
      checkbox.onchange = function () {
        item.selected = !item.selected;
      };
      contianer.appendChild(checkbox);
      const span = document.createElement("span");
      span.style.fontSize = "10px";
      span.innerHTML = item.text;
      contianer.appendChild(span);
      contianer.style.marginLeft = "4px";
      filterChilds.appendChild(contianer);
    });

    let testFilter = false;
    const filterDom = (btn, text = "") => {
      if (text === "") {
        text = btn.querySelector(".teamix-title")?.innerText;
      }
      if (!text) return;
      const filterItem = btns.find((item) => item.text === text);
      if (filterItem) {
        const tr = closest(btn, "tr");
        if (!testFilter) {
          tr.style.display = "";
        } else if (filterItem.selected) {
          tr.style.display = "none";
        } else {
          tr.style.display = "";
        }
      }
    };
    filterBtn.onclick = function () {
      testFilter = !testFilter;
      if (!testFilter) {
        filterBtn.style.backgroundColor = "#999";
        filterChilds.style.display = "none";
      } else {
        filterBtn.style.backgroundColor = "rgb(95, 206, 157)";
        filterChilds.style.display = "block";
      }
      document.querySelectorAll(".next-btn").forEach((btn) => {
        filterDom(btn);
      });
    };
  }

  /**
   * 工时管理
   */
  let recordedHours = -1;
  if (location.host === "devops.aliyun.com") {
    const getTaskList = async () => {
      const res = await fetchApi(
        "https://devops.aliyun.com/projex/api/workitem/workitem/list?_input_charset=utf-8",
        `{"spaceType":"User","spaceIdentifier":"${unsafeWindow.AONE_GLOBAL.user.identifier}","category":"","toPage":1,"pageSize":100,"conditions":"{\\"conditionGroups\\":[[{\\"fieldIdentifier\\":\\"statusStage\\",\\"operator\\":\\"CONTAINS\\",\\"value\\":[\\"1\\",\\"2\\",\\"6\\",\\"7\\",\\"11\\",\\"12\\",\\"13\\"],\\"toValue\\":null,\\"className\\":\\"statusStage\\",\\"format\\":\\"multiList\\"},{\\"fieldIdentifier\\":\\"assignedTo\\",\\"operator\\":\\"CONTAINS\\",\\"value\\":[\\"${unsafeWindow.AONE_GLOBAL.user.identifier}\\"],\\"toValue\\":null,\\"className\\":\\"user\\",\\"format\\":\\"list\\"}]]}","searchType":"LIST","orderBy":"{\\"fieldIdentifier\\":\\"workitemType\\",\\"format\\":\\"list\\",\\"order\\":\\"desc\\",\\"className\\":\\"workitemType\\"}","scope":"personal"}`,
        "POST"
      );
      return res.result;
    };
    const showTaskList = async () => {
      if (window.isShowTaskList) return;
      const list = await getTaskList();
      list.sort((a, b) => b.gmtModified - a.gmtModified);
      console.log(list);
      function createTable() {
        var contianer = document.createElement("div");
        contianer.id = "timeContianer";
        GM_addStyle(`
          #timeContianer {
            width: 950px;
            position: fixed;
            background: #fff;
            z-index: 99999;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            padding: 26px;
            box-shadow: 0 0px 0px 2000px rgba(0, 0, 0, 0.3);
            border-radius: 8px;
            overflow-y: auto;
            overflow-x: hidden;
            max-height: 600px;
          }
        `);

        const closeDow = document.createElement("div");
        closeDow.id = "closeDow";
        GM_addStyle(`
          #closeDow {
            position: absolute;
            top: 10px;
            right: 10px;
            color: #000;
            font-size: 20px;
            line-height: 17px;
            padding: 4px 2px;
            cursor: pointer;
            font-family: NextIcon;

            &:before {
              content: var(--icon-content-close, "\e626");
            }
          }
        `);
        closeDow.onclick = () => {
          window.isShowTaskList = false;
          contianer.remove();
        };
        contianer.appendChild(closeDow);

        const titleDom = document.createElement("div");
        const selectType = {
          value: localStorage.getItem("selectType") || "develop",
        };

        // 今日工时
        const timeDisplay = document.createElement("span");
        timeDisplay.id = "timeTitle2";
        timeDisplay.innerHTML = `今日工时:${recordedHours}小时 ${
          recordedHours >= 8 ? `<span style="color:red;">已满</span>` : ""
        }`;
        timeDisplay.style.marginRight = "12px";
        titleDom.appendChild(timeDisplay);

        createSelecter(titleDom, selectType);
        contianer.appendChild(titleDom);

        const table = document.createElement("table");
        table.id = "timeTable";
        GM_addStyle(`
          #timeTable {
            width: 100%;
          }

          .time-tr {
            height: 30px;
            background: #fff;
          }

          .time-tr:hover {
            background: #f2f5f7;
          }
        `);

        const thead = document.createElement("thead");
        const tbody = document.createElement("tbody");

        const headerRow = document.createElement("tr");
        const headerName = document.createElement("th");
        headerName.textContent = "名称";

        const todayWork = document.createElement("th");
        todayWork.textContent = "今日工时";

        const headerButtons = document.createElement("th");
        headerButtons.textContent = "添加工时";
        headerRow.appendChild(headerName);
        headerRow.appendChild(todayWork);
        headerRow.appendChild(headerButtons);
        thead.appendChild(headerRow);

        list.forEach((item) => {
          const name = item.subject;
          const row = document.createElement("tr");
          const nameCell = document.createElement("td");
          const title = document.createElement("div");
          row.className = "time-tr";
          title.style.width = "520px";
          title.style.whiteSpace = "nowrap";
          title.style.textOverflow = "ellipsis";
          title.style.overflow = "hidden";
          title.style.cursor = "pointer";

          title.onclick = () => {
            GM_openInTab(
              `https://devops.aliyun.com/projex/project/${item.spaceIdentifier}/req/${item.identifier}`,
              {
                active: true,
              }
            );
          };

          if (item.category.identifier === "Req") {
            const icon = document.createElement("span");
            icon.innerHTML = reqIconHtml;
            title.textContent = name;
            title.prepend(icon);
          } else if (item.category.identifier === "Task") {
            const icon = document.createElement("span");
            icon.innerHTML = taskIconHtml;
            title.textContent = name;
            title.prepend(icon);
          } else if (item.category.identifier === "Bug") {
            const icon = document.createElement("span");
            icon.innerHTML = bugIconHtml;
            title.textContent = name;
            title.prepend(icon);
          }
          nameCell.appendChild(title);

          const todayCell = document.createElement("td");
          todayCell.id = `td-${item.identifier}`;
          todayCell.innerHTML = `0`;

          const buttonCell = document.createElement("td");
          for (let i = 1; i <= 6; i++) {
            const button = document.createElement("button");
            button.textContent = `+${i}`;
            button.style.marginRight = "5px";
            button.style.cursor = "pointer";
            button.onclick = async function () {
              const t = recordedHours + i > 8 ? 8 - recordedHours : i;
              if (t <= 0) return;
              await addTime(item.identifier, t, selectType.value);
              const newTime = recordedHours + i > 8 ? 8 : recordedHours + i;
              recordedHours = newTime;
              setDayWorkitemTime(newTime);
              setTodayWorkItemTime();
            };
            buttonCell.appendChild(button);
          }

          row.appendChild(nameCell);
          row.appendChild(todayCell);
          row.appendChild(buttonCell);
          tbody.appendChild(row);
        });

        table.appendChild(thead);
        table.appendChild(tbody);
        contianer.appendChild(table);
        return contianer;
      }
      window.isShowTaskList = true;
      // 添加表格到页面
      document.body.appendChild(createTable());
      setTodayWorkItemTime();
    };

    const addTime = async (id, time, type) => {
      fetchApi(
        "https://devops.aliyun.com/projex/api/workitem/workitem/time?_input_charset=utf-8",
        `{"workitemIdentifier":"${id}","type":"${type}","actualTime":${time},"description":"","recordUserIdentifier":"${
          unsafeWindow.AONE_GLOBAL.user.identifier
        }","gmtStart":"${getLocalISOString()}","gmtEnd":"${getLocalISOString()}","containsRestDay":true}`,
        "POST"
      );
    };

    const setDayWorkitemTime = async (newTime = recordedHours) => {
      if (newTime < 0) {
        const res = await getDayWorkitemTime();
        newTime = res?.recordedHours || 0;
      }
      let timeTitle = document.querySelector("#timeTitle");
      let timeTitleMessage = document.querySelector("#timeTitleMessage");
      if (!timeTitle) {
        timeTitle = document.createElement("div");
        timeTitle.id = "timeTitle";
        GM_addStyle(`
          #timeTitle {
            color: #000;
            margin-left: 12px;
            font-weight: 900;
          }
        `);
        const dom = document.querySelector(".system-bar-left");
        dom.append(timeTitle);

        const btn = document.createElement("div");
        btn.id = "timeTitleBtn";
        GM_addStyle(`
          #timeTitleBtn {
            background: var(--color-brand1-6, #1b9aee);
            border-radius: 4px;
            cursor: pointer;
            mask: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxkZWZzPjxwYXRoIGQ9Ik0xMCAyMEM0LjQ3NyAyMCAwIDE1LjUyMyAwIDEwUzQuNDc3IDAgMTAgMHMxMCA0LjQ3NyAxMCAxMC00LjQ3NyAxMC0xMCAxMHptLjYxNS02LjU4NHYtMi44MDFoMi44MDFhLjYxNi42MTYgMCAwMDAtMS4yM2gtMi44MDFWNi41ODVhLjYxNS42MTUgMCAxMC0xLjIzIDB2Mi44bC0yLjguMDAyYS42MTQuNjE0IDAgMTAtLjAwMiAxLjIzaDIuODAydjIuOGEuNjE1LjYxNSAwIDAwMS4yMyAweiIgaWQ9ImEiLz48L2RlZnM+PHVzZSBmaWxsPSIjMDAwIiB4bGluazpocmVmPSIjYSIgZmlsbC1ydWxlPSJldmVub2RkIi8+PC9zdmc+) no-repeat center center;
            width: 20px;
            height: 20px;
            margin-left: 4px;
          }
        `);
        btn.onclick = showTaskList;
        dom.append(btn);

        timeTitleMessage = document.createElement("div");
        timeTitleMessage.id = "timeTitleMessage";
        GM_addStyle(`
          #timeTitleMessage {
            color: red;
            margin-left: 6px;
          }
        `);
        dom.append(timeTitleMessage);
      }
      timeTitle.innerHTML = `今日工时:${newTime}小时 ${
        newTime >= 8 ? `<span style="color:red;">已满</span>` : ""
      }`;
      let timeTitle2 = document.querySelector("#timeTitle2");
      if (timeTitle2)
        timeTitle2.innerHTML = `今日工时:${newTime}小时 ${
          newTime >= 8 ? `<span style="color:red;">已满</span>` : ""
        }`;

      const now = new Date();
      const hours = now.getHours();
      const minutes = now.getMinutes();
      if (newTime < 3.76 && (hours > 20 || (hours === 20 && minutes > 0))) {
        timeTitleMessage.innerText = "今日工时不足,请填写!";
      } else if (newTime > 8) {
        timeTitleMessage.innerText = "今日工时不得大于8小时!";
      } else {
        timeTitleMessage.innerText = "";
      }
    };

    const getDayWorkitemTime = async () => {
      const today = getFormattedDate();
      const { monday, friday } = getWeekStartAndEnd();
      const res = await fetchApi(
        `https://devops.aliyun.com/projex/api/workitem/workitem/time/stats/user/dayWorkitemTime?gmtStart=${monday}&gmtEnd=${friday}`
      );
      const dayWorkitemTime = res.result;
      const todayInfo = dayWorkitemTime.find((item) => item.date === today);
      recordedHours = todayInfo?.recordedHours || 0;
      return todayInfo;
    };

    const getTodayWorkitem = async () => {
      const today = getFormattedDate();
      const res = await fetchApi(
        `https://devops.aliyun.com/projex/api/workitem/workitem/workTable/workitemTime/list?day=${today}`
      );
      return res.result;
    };

    const getDayWorkitemTimeByWorkitems = async (ids) => {
      const today = getFormattedDate();
      const res = await fetchApi(
        `https://devops.aliyun.com/projex/api/workitem/workitem/time/stats/user/getDayWorkitemTimeByWorkitems?day=${today}&workitemIdentifiers=${ids.join()}`
      );
      return res.result;
    };

    const setTodayWorkItemTime = async () => {
      const list = await getTodayWorkitem();
      const ids = [];
      list.forEach((item) => {
        ids.push(item.identifier);
      });
      const timeList = await getDayWorkitemTimeByWorkitems(ids);
      timeList?.forEach((item) => {
        const td = document.querySelector(`#td-${item.workitemIdentifier}`);
        if (td) {
          td.innerHTML = `${item.recordedHours}小时`;
        }
      });
    };

    const removeDom = () => {
      document.querySelector("#tb-navigation-customOperation")?.remove();
    };
    if (location.host === "devops.aliyun.com") {
      setTimeout(async () => {
        removeDom();
        setDayWorkitemTime(-1);
      }, 1000);
    }
    setInterval(() => {
      setDayWorkitemTime(-1);
    }, 1000 * 60 * 10);
    window.addEventListener("visibilitychange", () => {
      setDayWorkitemTime(-1);
    });
  }

  /**
   * components
   */

  function createGetLoginInfo() {
    // 创建容器
    const container = document.createElement("div");
    container.className = "login-container";
    GM_addStyle(`
      .login-container {
        position: fixed;
        top: 40%;
        left: 50%;
        transform: translate(-50%, -50%);
        width: 400px;
        padding: 20px;
        border-radius: 8px;
        box-shadow: 0 0px 0px 2000px rgba(0, 0, 0, 0.3);
        background-color: #fff;
        z-index: 9999;
      }
      `);

    // 创建关闭按钮
    const closeButton = document.createElement("button");
    closeButton.textContent = "×"; // 使用 '×' 符号作为关闭按钮
    closeButton.style.position = "absolute";
    closeButton.style.top = "10px";
    closeButton.style.right = "10px";
    closeButton.style.fontSize = "20px";
    closeButton.style.backgroundColor = "transparent";
    closeButton.style.border = "none";
    closeButton.style.color = "#333";
    closeButton.style.cursor = "pointer";
    closeButton.style.padding = "0";
    closeButton.style.fontWeight = "bold";

    // 添加关闭按钮点击事件,隐藏容器
    closeButton.addEventListener("click", function () {
      container.remove();
    });

    // 创建标题
    const title = document.createElement("h2");
    title.textContent = "自动登录需设置账户密码";
    title.style.fontSize = "18px";
    title.style.fontWeight = "bold";
    title.style.marginBottom = "20px";
    title.style.color = "#333";
    title.style.textAlign = "center";

    // 创建说明文本
    const description = document.createElement("p");
    description.textContent = "提示:账号密码只存储到当前设备";
    description.style.fontSize = "12px";
    description.style.color = "#999";
    description.style.textAlign = "center";
    description.style.marginBottom = "30px";

    // 创建用户名输入框
    const usernameLabel = document.createElement("label");
    usernameLabel.textContent = "账号名";
    usernameLabel.style.fontSize = "14px";
    usernameLabel.style.color = "#333";
    usernameLabel.style.marginBottom = "5px";
    const usernameInput = document.createElement("input");
    usernameInput.placeholder = "请输入";
    usernameInput.style.width = "100%";
    usernameInput.style.padding = "12px";
    usernameInput.style.marginBottom = "15px";
    usernameInput.style.border = "1px solid #d9d9d9"; // 更浅的边框颜色
    usernameInput.style.borderRadius = "4px";
    usernameInput.style.fontSize = "14px";
    usernameInput.style.outline = "none";
    usernameInput.style.boxSizing = "border-box";
    usernameInput.style.color = "#333"; // 字体颜色调整

    // 创建密码输入框
    const passwordLabel = document.createElement("label");
    passwordLabel.textContent = "密码";
    passwordLabel.style.fontSize = "14px";
    passwordLabel.style.color = "#333";
    passwordLabel.style.marginBottom = "5px";
    const passwordInput = document.createElement("input");
    passwordInput.placeholder = "请输入";
    passwordInput.type = "password";
    passwordInput.style.width = "100%";
    passwordInput.style.padding = "12px";
    passwordInput.style.marginBottom = "20px";
    passwordInput.style.border = "1px solid #d9d9d9"; // 更浅的边框颜色
    passwordInput.style.borderRadius = "4px";
    passwordInput.style.fontSize = "14px";
    passwordInput.style.outline = "none";
    passwordInput.style.boxSizing = "border-box";
    passwordInput.style.color = "#333"; // 字体颜色调整

    // 创建登录按钮
    const loginButton = document.createElement("button");
    loginButton.textContent = "保存";
    loginButton.style.width = "100%";
    loginButton.style.padding = "12px";
    loginButton.style.backgroundColor = "#1890FF"; // 按钮颜色调整为蓝色
    loginButton.style.color = "#fff";
    loginButton.style.border = "none";
    loginButton.style.borderRadius = "4px";
    loginButton.style.cursor = "pointer";
    loginButton.style.fontSize = "16px";
    loginButton.style.transition = "background-color 0.3s";

    loginButton.addEventListener("click", async function () {
      userName = usernameInput.value;
      password = passwordInput.value;

      await GM_setValue("userName", userName);
      await GM_setValue("password", password);

      if (location.host === "account.aliyun.com") {
        location.href = location.href;
      } else {
        container.remove();
      }
    });

    // 添加按钮 hover 效果
    loginButton.addEventListener("mouseover", function () {
      loginButton.style.backgroundColor = "#0066cc"; // 悬停时的颜色
    });
    loginButton.addEventListener("mouseout", function () {
      loginButton.style.backgroundColor = "#1890FF"; // 悬停恢复颜色
    });

    // 将元素添加到容器中
    container.appendChild(closeButton);
    container.appendChild(title);
    container.appendChild(description);
    container.appendChild(usernameLabel);
    container.appendChild(usernameInput);
    container.appendChild(passwordLabel);
    container.appendChild(passwordInput);
    container.appendChild(loginButton);

    // 将容器添加到页面中
    unsafeWindow.document.body.appendChild(container);
  }

  /**
   * util
   */
  function setBtnStyle(btn) {
    btn.className = "next-btn next-medium next-btn-normal next-menu-btn";
    btn.style.backgroundColor = "rgb(95, 206, 157)";
    btn.style.color = "#fff";
    btn.style.padding = "0px 10px";
    btn.style.fontSize = "12px";
    btn.style.height = "24px";
    btn.style.width = "auto";
    btn.style.minWidth = "auto";
  }

  function getFormattedDate(dateInput = new Date()) {
    const date = new Date(dateInput); // 如果没有传参,则使用当前日期
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, "0"); // 月份从0开始,所以加1
    const day = String(date.getDate()).padStart(2, "0");
    return `${year}-${month}-${day}`;
  }

  function createSelecter(container, selectType) {
    const dropdownTitle = document.createElement("span");
    dropdownTitle.innerText = "工作类型:";
    container.appendChild(dropdownTitle);
    const dropdownContainer = document.createElement("div");
    dropdownContainer.classList.add("dropdown");

    const selectedOption = document.createElement("div");
    selectedOption.classList.add("selected");
    const options = [
      {
        name: "设计",
        type: "design",
      },
      {
        name: "研发",
        type: "develop",
      },
      {
        name: "测试",
        type: "test",
      },
      {
        name: "文档",
        type: "document",
      },
      {
        name: "其他",
        type: "others",
      },
    ];
    const name = options.find((item) => item.type === selectType.value).name;
    selectedOption.textContent = name; // Default selected option

    const optionsList = document.createElement("div");
    optionsList.style.display = "none";
    optionsList.classList.add("options");

    options.forEach((option) => {
      const optionItem = document.createElement("div");
      optionItem.classList.add("option");
      optionItem.textContent = option.name;
      optionItem.addEventListener("click", () => {
        selectedOption.textContent = option.name;
        selectType.value = option.type;
        localStorage.setItem("selectType", option.type);
        optionsList.style.display = "none"; // Hide the options after selection
      });
      optionsList.appendChild(optionItem);
    });

    dropdownContainer.appendChild(selectedOption);
    dropdownContainer.appendChild(optionsList);
    container.appendChild(dropdownContainer);

    selectedOption.addEventListener("click", () => {
      optionsList.style.display =
        optionsList.style.display === "none" ? "block" : "none";
    });

    const style = document.createElement("style");
    style.textContent = `
    .dropdown {
      position: relative;
      display: inline-block;
      font-family: Arial, sans-serif;
      width: 150px;
    }
    .selected {
      padding: 3px 10px;
      color: #2a7bf2;
      border: 1px solid #ccc;
      border-radius: 4px;
      cursor: pointer;
      font-size: 14px;
    }
    .selected:hover {
      background-color: #e3f2fd;
    }
    .options {
      display: none;
      position: absolute;
      top: 100%;
      left: 0;
      background-color: white;
      border: 1px solid #2a7bf2;
      box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
      width: 100%;
      border-radius: 4px;
      z-index: 1;
    }
    .option {
      padding: 10px;
      background-color: white;
      color: #333;
      cursor: pointer;
      font-size: 14px;
    }
    .option:hover {
      background-color: #f0f7ff;
    }
  `;
    container.appendChild(style);
  }

  function getWeekStartAndEnd(dateInput = new Date()) {
    const date = new Date(dateInput);
    const day = date.getDay();
    let diffToMonday = day === 0 ? -6 : 1 - day;
    diffToMonday = diffToMonday === 0 ? -1 : diffToMonday;
    const monday = new Date(date);
    monday.setDate(date.getDate() + diffToMonday);

    const friday = new Date(monday);
    friday.setDate(monday.getDate() + 6);

    const format = (d) => {
      const y = d.getFullYear();
      const m = String(d.getMonth() + 1).padStart(2, "0");
      const day = String(d.getDate()).padStart(2, "0");
      return `${y}-${m}-${day}`;
    };

    return {
      monday: format(monday),
      friday: format(friday),
    };
  }

  function getLocalISOString() {
    const date = new Date();

    let isoString = date.toISOString().split(".")[0];

    const timezoneOffset = date.getTimezoneOffset();
    const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60);
    const offsetMinutes = Math.abs(timezoneOffset) % 60;
    const sign = timezoneOffset > 0 ? "-" : "+";

    const formattedOffset = `${sign}${String(offsetHours).padStart(
      2,
      "0"
    )}:${String(offsetMinutes).padStart(2, "0")}`;

    const formattedDate = `${isoString}${formattedOffset}`;
    return formattedDate;
  }

  var headers = {
    accept: "application/json, text/plain, */*",
    "accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
    "cache-control": "no-cache",
    "content-type": "application/json",
    pragma: "no-cache",
    priority: "u=1, i",
    "sec-ch-ua":
      '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": '"macOS"',
    "sec-fetch-dest": "empty",
    "sec-fetch-mode": "cors",
    "sec-fetch-site": "same-origin",
    "x-csrf-token": "8rSfd6u3-6ucnCtsANW1H2wEUWab5kwxYL7E",
    "x-requested-with": "XMLHttpRequest",
  };

  function fetchApi(url, body = null, method = "GET") {
    return fetch(url, {
      headers: headers,
      referrerPolicy: "strict-origin-when-cross-origin",
      body: body,
      method: method,
      mode: "cors",
      credentials: "include",
    }).then((res) => res.json());
  }
})();