Claude Session Key 管理器

Claude Session Key 管理工具(移动端布局兼容),支持拖拽、测活、批量导入导出、WebDAV云备份等功能

// ==UserScript==
// @name         Claude Session Key 管理器
// @name:zh-CN   Claude Session Key 管理器
// @name:en      Claude Session Key Manager
// @version      1.1.1
// @description  Claude Session Key 管理工具(移动端布局兼容),支持拖拽、测活、导入导出、WebDAV云备份等功能
// @description:zh-CN  Claude Session Key 管理工具(移动端布局兼容),支持拖拽、测活、批量导入导出、WebDAV云备份等功能
// @description:en  Claude Session Key Manager with drag-and-drop, token validation, import/export, WebDAV backup and more
// @author       xiaoye6688
// @namespace    https://gf.qytechs.cn/users/1317128-xiaoye6688
// @homepage     https://gf.qytechs.cn/zh-CN/users/1317128-xiaoye6688
// @supportURL   https://gf.qytechs.cn/zh-CN/users/1317128-xiaoye6688
// @license      MIT
// @date         2025-03-09
// @modified     2025-03-09

// @match        https://claude.ai/*
// @match        https://claude.asia/*
// @match        https://demo.fuclaude.com/*
// @include      https://*fuclaude*/*
//
// @icon         https://claude.ai/favicon.ico
// @run-at       document-end

// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_info
// @grant        GM_cookie

// @connect      ipapi.co
// @connect      api.claude.ai
// @connect      *
// ==/UserScript==

(function () {
  "use strict";

  const config = {
    storageKey: "claudeTokens",
    ipApiUrl: "https://ipapi.co/country_code",
    defaultToken: {
      name: "Token00",
      key: "sk-key",
    },
    currentTokenKey: "currentClaudeToken",
    testResultsKey: "claudeTokenTestResults",
    testResultExpiry: 1800000, // 30分钟过期
  };

  const theme = {
    light: {
      bgColor: "#fcfaf5",
      textColor: "#333",
      borderColor: "#ccc",
      buttonBg: "#f5f1e9",
      buttonHoverBg: "#e5e1d9",
      modalBg: "rgba(0, 0, 0, 0.5)",
    },
    dark: {
      bgColor: "#2c2b28",
      textColor: "#f5f4ef",
      borderColor: "#3f3f3c",
      buttonBg: "#3f3f3c",
      buttonHoverBg: "#4a4a47",
      modalBg: "rgba(0, 0, 0, 0.7)",
    },
  };

  const getStyles = (isDarkMode) => `
  :root {
  --bg-color: ${isDarkMode ? theme.dark.bgColor : theme.light.bgColor};
  --text-color: ${isDarkMode ? theme.dark.textColor : theme.light.textColor};
  --border-color: ${
    isDarkMode ? theme.dark.borderColor : theme.light.borderColor
  };
  --button-bg: ${isDarkMode ? theme.dark.buttonBg : theme.light.buttonBg};
  --button-hover-bg: ${
    isDarkMode ? theme.dark.buttonHoverBg : theme.light.buttonHoverBg
  };
  --modal-bg: ${isDarkMode ? theme.dark.modalBg : theme.light.modalBg};
  }

  /* 浮动按钮样式 */
  #claude-toggle-button {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background-color: var(--bg-color);
  color: #b3462f;
  cursor: move;
  position: fixed;
  z-index: 10000;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
  transition: background-color 0.3s ease, transform 0.2s ease;
  outline: none;
  padding: 0;
  user-select: none;
  touch-action: none;
  border: 1px solid var(--border-color);
  font-size: 18px;
  }

  #claude-toggle-button:hover {
  transform: scale(1.1);
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
  }

  /* 下拉容器样式 */
  .claude-dropdown-container {
  position: fixed;
  background-color: var(--bg-color);
  padding: 20px;
  border-radius: 12px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
  display: none;
  flex-direction: column;
  gap: 0; /* 移除flex布局产生的空隙 */
  width: 600px;
  max-height: 80vh;
  overflow-y: auto;
  z-index: 9999;
  border: 1px solid var(--border-color);
  opacity: 0;
  transform: scale(0.95);
  transition: opacity 0.3s ease, transform 0.3s ease;
  scrollbar-gutter: stable; /* 保持滚动条空间稳定 */
  scrollbar-width: thin; /* Firefox滚动条样式 */
  scrollbar-color: ${
    isDarkMode
      ? "rgba(255, 255, 255, 0.2) transparent"
      : "rgba(0, 0, 0, 0.2) transparent"
  };
  }

  /* 标题容器 */
  .claude-title-container {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-bottom: 10px;
  }

  .claude-title-container h2 {
  margin: 0;
  color: var(--text-color);
  font-size: 18px;
  font-weight: 600;
  }

  .claude-ip-display {
  font-size: 14px;
  color: var(--text-color);
  padding: 4px 10px;
  background-color: var(--button-bg);
  border-radius: 12px;
  }

  /* Token 网格容器 */
  .claude-token-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
  max-height: calc(2 * (90px + 12px) + 24px); /* 两行token的高度加上间隙和padding */
  overflow-y: auto;
  padding: 12px 0 12px 12px;
  scrollbar-gutter: stable; /* 保持滚动条空间稳定,防止出现时推动内容 */
  border: 1px solid var(--border-color);
  border-radius: 8px;
  background-color: ${
    isDarkMode ? "rgba(255, 255, 255, 0.03)" : "rgba(0, 0, 0, 0.02)"
  };
  /* Firefox滚动条样式支持 */
  scrollbar-width: thin;
  scrollbar-color: ${
    isDarkMode
      ? "rgba(255, 255, 255, 0.2) transparent"
      : "rgba(0, 0, 0, 0.2) transparent"
  };
  }

  /* Token 卡片样式 */
  .claude-token-item {
  padding: 15px;
  border-radius: 8px;
  background-color: var(--bg-color);
  border: 1px solid var(--border-color);
  cursor: pointer;
  transition: all 0.3s ease;
  position: relative;
  height: 90px; /* 固定高度 */
  box-sizing: border-box; /* 确保padding不会增加总高度 */
  display: flex;
  flex-direction: column;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
  }

  .claude-token-item:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  border-color: ${
    isDarkMode ? "rgba(255, 255, 255, 0.3)" : "rgba(0, 0, 0, 0.2)"
  };
  }

  .claude-token-item.current-token {
  border: 2px solid #b3462f;
  background-color: ${
    isDarkMode ? "rgba(179, 70, 47, 0.1)" : "rgba(179, 70, 47, 0.05)"
  };
  position: relative;
  }

  .current-token-badge {
  position: absolute;
  top: -8px;
  left: 8px;
  background-color: #b3462f;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  display: flex;
  align-items: center;
  justify-content: center;
  }

  .current-token-badge::after {
  content: "";
  display: block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background-color: white;
  }

  /* Token 内容样式 */
  .token-info {
  display: flex;
  flex-direction: column;
  gap: 8px;
  flex: 1; /* 填充可用空间 */
  justify-content: space-between; /* 顶部行和底部行分别位于容器顶部和底部 */
  }

  .token-top-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  }

  .token-name-container {
  display: flex;
  align-items: center;
  gap: 8px;
  }

  .token-number {
  padding: 2px 8px;
  border-radius: 12px;
  font-size: 12px;
  background-color: var(--button-bg);
  }

  .token-name {
  font-weight: 500;
  font-size: 14px;
  }

  .token-actions {
  display: flex;
  gap: 8px;
  }

  .token-action-btn {
  background: transparent;
  border: none;
  cursor: pointer;
  padding: 4px;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-color);
  transition: all 0.2s ease;
  }

  .token-action-btn:hover {
  background-color: var(--button-hover-bg);
  transform: scale(1.1);
  }

  .token-action-btn.delete-btn {
  color: #e24a4a;
  }

  .token-bottom-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  }

  .token-status {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-left: auto;
  }

  .status-indicator {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background-color: #888;
  }

  .status-indicator.success {
  background-color: #48bb78;
  }

  .status-indicator.error {
  background-color: #e53e3e;
  }

  .status-indicator.loading {
  background-color: #888;
  animation: pulse 1.5s infinite;
  }

  @keyframes pulse {
  0% { opacity: 0.4; }
  50% { opacity: 1; }
  100% { opacity: 0.4; }
  }

  .token-time {
  font-size: 12px;
  color: var(--text-color);
  opacity: 0.7;
  }

  /* 按钮容器 */
  .claude-button-container {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
  padding-top: 12px;
  }

  .claude-button {
  padding: 8px 10px;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  transition: all 0.2s ease;
  font-size: 13px;
  font-weight: 500;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  position: relative;
  }

  .claude-button:hover {
  transform: translateY(-2px);
  }

  .claude-button.primary {
  background-color: #b3462f;
  color: white;
  }

  .claude-button.primary:hover {
  background-color: #a03d2a;
  }

  .claude-button.secondary {
  background-color: var(--button-bg);
  color: var(--text-color);
  }

  .claude-button.secondary:hover {
  background-color: var(--button-hover-bg);
  }

  /* 工具提示样式 */
  .claude-button[data-tooltip]:hover::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  background-color: rgba(0, 0, 0, 0.8);
  color: white;
  padding: 5px 10px;
  border-radius: 4px;
  font-size: 12px;
  white-space: nowrap;
  z-index: 10001;
  margin-bottom: 5px;
  pointer-events: none;
  opacity: 0;
  animation: tooltip-fade-in 0.2s ease forwards;
  }

  @keyframes tooltip-fade-in {
  from { opacity: 0; transform: translate(-50%, 5px); }
  to { opacity: 1; transform: translate(-50%, 0); }
  }

  /* 模态框样式 */
  .claude-modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: var(--modal-bg);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 10001;
  }

  .claude-modal-content {
  background-color: var(--bg-color);
  padding: 20px;
  padding-right: 14px; /* 右侧padding稍微增加,为滚动条预留空间但不过多 */
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  width: 500px;
  max-width: 90%;
  overflow-y: auto;
  position: relative;
  scrollbar-gutter: stable; /* 保持滚动条空间稳定 */
  scrollbar-width: thin; /* Firefox滚动条样式 */
  scrollbar-color: ${
    isDarkMode
      ? "rgba(255, 255, 255, 0.2) transparent"
      : "rgba(0, 0, 0, 0.2) transparent"
  };
  }

  .claude-modal-content.narrow-modal {
  width: 400px;
  max-width: 80%;
  }

  .claude-modal h2 {
  margin-top: 0;
  margin-bottom: 15px;
  color: var(--text-color);
  font-size: 18px;
  font-weight: 600;
  }

  .claude-modal input, .claude-modal textarea {
  width: 100%;
  padding: 10px;
  margin-bottom: 15px;
  border: 1px solid var(--border-color);
  border-radius: 4px;
  font-size: 14px;
  background-color: var(--bg-color);
  color: var(--text-color);
  }

  .claude-modal-buttons {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 15px;
  }

  .claude-close-button {
  position: absolute;
  top: 10px;
  right: 10px;
  background: none;
  border: none;
  font-size: 20px;
  cursor: pointer;
  color: var(--text-color);
  padding: 5px;
  line-height: 1;
  }

  /* 自定义滚动条样式 */
  .claude-token-grid::-webkit-scrollbar {
  width: 6px;
  /* 初始状态下滚动条透明 */
  background-color: transparent;
  }

  .claude-token-grid::-webkit-scrollbar-track {
  background: transparent;
  margin: 4px 0;
  }

  .claude-token-grid::-webkit-scrollbar-thumb {
  background-color: ${
    isDarkMode ? "rgba(255, 255, 255, 0.15)" : "rgba(0, 0, 0, 0.15)"
  };
  border-radius: 6px;
  transition: background-color 0.3s ease;
  /* 初始状态下滚动条半透明 */
  opacity: 0.6;
  }

  .claude-token-grid::-webkit-scrollbar-thumb:hover {
  background-color: ${
    isDarkMode ? "rgba(255, 255, 255, 0.3)" : "rgba(0, 0, 0, 0.3)"
  };
  opacity: 1;
  }

  .claude-token-grid:hover::-webkit-scrollbar-thumb {
  background-color: ${
    isDarkMode ? "rgba(255, 255, 255, 0.3)" : "rgba(0, 0, 0, 0.3)"
  };
  opacity: 1;
  }

  /* 滚动条样式 */
  .claude-dropdown-container::-webkit-scrollbar,
  .claude-modal-content::-webkit-scrollbar {
  width: 6px;
  background-color: transparent;
  }

  .claude-dropdown-container::-webkit-scrollbar-track,
  .claude-modal-content::-webkit-scrollbar-track {
  background: transparent;
  margin: 4px 0;
  }

  .claude-dropdown-container::-webkit-scrollbar-thumb,
  .claude-modal-content::-webkit-scrollbar-thumb {
  background-color: ${
    isDarkMode ? "rgba(255, 255, 255, 0.15)" : "rgba(0, 0, 0, 0.15)"
  };
  border-radius: 6px;
  transition: background-color 0.3s ease;
  opacity: 0.6;
  }

  .claude-dropdown-container::-webkit-scrollbar-thumb:hover,
  .claude-modal-content::-webkit-scrollbar-thumb:hover {
  background-color: ${
    isDarkMode ? "rgba(255, 255, 255, 0.3)" : "rgba(0, 0, 0, 0.3)"
  };
  opacity: 1;
  }

  .claude-dropdown-container:hover::-webkit-scrollbar-thumb,
  .claude-modal-content:hover::-webkit-scrollbar-thumb {
  opacity: 1;
  }

  /* 预览容器 */
  .claude-preview-container {
  margin-top: 15px;
  max-height: 200px;
  overflow-y: auto;
  border: 1px solid var(--border-color);
  border-radius: 8px;
  padding: 15px;
  }

  .claude-preview-title {
  font-size: 16px;
  margin-bottom: 10px;
  color: var(--text-color);
  border-bottom: 1px solid var(--border-color);
  padding-bottom: 5px;
  }

  .claude-preview-item {
  margin-bottom: 8px;
  font-size: 14px;
  padding: 8px;
  border-radius: 4px;
  background-color: var(--button-bg);
  }

  /* 滚动提示样式 */
  .scroll-indicator {
  grid-column: 1 / -1; /* 横跨所有列 */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 8px;
  margin-top: 5px;
  color: ${isDarkMode ? "rgba(255, 255, 255, 0.6)" : "rgba(0, 0, 0, 0.5)"};
  font-size: 12px;
  background-color: ${
    isDarkMode ? "rgba(255, 255, 255, 0.05)" : "rgba(0, 0, 0, 0.03)"
  };
  border-radius: 6px;
  gap: 8px;
  }

  .scroll-arrow {
  animation: bounce 1.5s infinite;
  }

  @keyframes bounce {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(3px); }
  }

  @media (max-width: 768px) {
  /* 浮动按钮移动端优化 */
  #claude-toggle-button {
  width: 36px;
  height: 36px;
  font-size: 10px;
  top: 10px;
  right: 10px;
  left: auto !important; /* 强制右下角显示 */
  }

  /* 下拉容器移动端适配 */
  .claude-dropdown-container {
  position: fixed;
  top: 10px !important;
  left: 10px !important;
  right: 10px !important;
  bottom: auto !important;
  width: auto !important;
  max-width: none;
  max-height: calc(100vh - 80px);
  padding: 15px;
  border-radius: 8px;
  }

  /* 标题容器移动端适配 */
  .claude-title-container {
  flex-direction: column;
  gap: 8px;
  align-items: flex-start;
  padding-bottom: 8px;
  }

  .claude-title-container h2 {
  font-size: 16px;
  }

  .claude-ip-display {
  font-size: 12px;
  padding: 3px 8px;
  }

  /* Token网格移动端单列布局 */
  .claude-token-grid {
  grid-template-columns: 1fr; /* 单列显示 */
  gap: 10px;
  max-height: calc(3 * (80px + 10px) + 20px); /* 三行token的高度 */
  padding: 10px;
  }

  /* Token卡片移动端优化 */
  .claude-token-item {
  height: 100px;
  padding: 12px;
  }

  .token-number {
  font-size: 11px;
  padding: 1px 6px;
  }

  .token-name {
  font-size: 13px;
  }

  .token-action-btn {
  padding: 3px;
  }

  .token-action-btn svg {
  width: 12px;
  height: 12px;
  }

  .token-time {
  font-size: 11px;
  }

  .status-indicator {
  width: 8px;
  height: 8px;
  }

  /* 按钮容器移动端适配 */
  .claude-button-container {
  grid-template-columns: repeat(2, 1fr); /* 改为2列布局 */
  gap: 6px;
  padding-top: 10px;
  }

  .claude-button {
  padding: 10px 8px;
  font-size: 12px;
  gap: 3px;
  }

  /* 模态框移动端适配 */
  .claude-modal {
  padding: 10px;
  }

  .claude-modal-content {
  width: 100%;
  max-width: calc(100vw - 20px);
  max-height: calc(100vh - 40px);
  padding: 15px;
  margin: 0;
  }

  .claude-modal-content.narrow-modal {
  width: 100%;
  max-width: calc(100vw - 20px);
  }

  .claude-modal h2 {
  font-size: 16px;
  margin-bottom: 12px;
  }

  .claude-modal input,
  .claude-modal textarea {
  padding: 12px;
  font-size: 16px; /* 防止iOS缩放 */
  border-radius: 6px;
  }

  .claude-modal-buttons {
  flex-direction: column-reverse;
  gap: 8px;
  }

  .claude-modal-buttons .claude-button {
  width: 100%;
  padding: 12px;
  font-size: 14px;
  }

  /* WebDAV表单移动端优化 */
  .input-group {
  flex-direction: column !important;
  gap: 5px !important;
  }

  .input-group label {
  min-width: auto !important;
  text-align: left !important;
  font-size: 14px;
  }

  .claude-webdav-actions {
  grid-template-columns: 1fr !important;
  gap: 8px;
  }

  /* 预览容器移动端优化 */
  .claude-preview-container {
  max-height: 150px;
  padding: 10px;
  }

  .claude-preview-title {
  font-size: 14px;
  }

  .claude-preview-item {
  font-size: 12px;
  padding: 6px;
  }

  /* 信息提示移动端优化 */
  .claude-info-section {
  font-size: 10px !important;
  padding: 6px !important;
  }

  /* 滚动提示移动端优化 */
  .scroll-indicator {
  padding: 6px;
  font-size: 11px;
  }

  /* 移动端触摸优化 */
  .claude-token-item,
  .claude-button,
  .token-action-btn {
  min-height: 44px; /* iOS推荐的最小触摸区域 */
  }

  /* 命名规则容器移动端适配 */
  .claude-naming-rule {
  display: grid;
  grid-template-columns: 1fr;
  gap: 8px;
  }

  .claude-naming-rule label {
  font-size: 14px;
  }

  .claude-naming-rule input {
  padding: 10px;
  font-size: 16px;
  }
  }

  /* 超小屏幕适配 (小于480px) */
  @media (max-width: 480px) {
  .claude-dropdown-container {
  top: 5px !important;
  left: 5px !important;
  right: 5px !important;
  padding: 12px;
  }

  .claude-button-container {
  grid-template-columns: 1fr; /* 超小屏幕单列 */
  }

  .claude-token-grid {
  max-height: calc(2 * (80px + 10px) + 20px); /* 两行token */
  }
  }
  `;

  const UI = {
    createElem(tag, className = "", styles = {}) {
      const elem = document.createElement(tag);
      if (className) elem.className = className;
      Object.assign(elem.style, styles);
      return elem;
    },

    createButton(text, className, icon = "") {
      const button = this.createElem("button", className);
      if (icon) {
        button.innerHTML = `${icon} ${text}`;
      } else {
        button.textContent = text;
      }
      return button;
    },

    createModal(title, content, includeCloseButton = true) {
      const modal = this.createElem("div", "claude-modal");
      modal.setAttribute("aria-modal", "true");
      modal.setAttribute("role", "dialog");

      const modalContent = this.createElem("div", "claude-modal-content");

      const titleElem = this.createElem("h2");
      titleElem.textContent = title;
      modalContent.appendChild(titleElem);

      if (includeCloseButton) {
        const closeButton = this.createElem("button", "claude-close-button");
        closeButton.textContent = "×";
        closeButton.addEventListener("click", () =>
          document.body.removeChild(modal)
        );
        modalContent.appendChild(closeButton);
      }

      modalContent.appendChild(content);

      const buttonContainer = this.createElem("div", "claude-modal-buttons");
      modalContent.appendChild(buttonContainer);

      modal.appendChild(modalContent);
      document.body.appendChild(modal);

      return {
        modal,
        buttonContainer,
        close: () => document.body.removeChild(modal),
      };
    },
  };

  const App = {
    init() {
      this.isDarkMode =
        document.documentElement.getAttribute("data-mode") === "dark";
      this.injectStyles();
      this.tokens = this.loadTokens();
      this.createUI();
      this.setupEventListeners();
      this.observeThemeChanges();

      // 获取保存的位置或使用默认值
      const savedPosition = {
        left: GM_getValue("buttonLeft", 10),
        bottom: GM_getValue("buttonBottom", 10),
      };

      // 设置按钮位置
      this.toggleButton.style.left = `${savedPosition.left}px`;
      this.toggleButton.style.bottom = `${savedPosition.bottom}px`;

      // 初始化拖拽状态
      this.isDragging = false;
      this.buttonLeft = savedPosition.left;
      this.buttonBottom = savedPosition.bottom;

      // 获取IP信息
      this.fetchIPCountryCode();

      // 添加窗口大小变化监听
      window.addEventListener("resize", () => {
        const isMobile = window.innerWidth <= 768;

        if (isMobile) {
          // 移动端时重置按钮位置
          this.toggleButton.style.right = "10px";
          this.toggleButton.style.top = "10px";
          this.toggleButton.style.left = "auto";

          // 如果下拉窗口可见,重新定位
          if (this.state.isDropdownVisible) {
            this.dropdownContainer.style.top = "10px";
            this.dropdownContainer.style.left = "10px";
            this.dropdownContainer.style.right = "10px";
            this.dropdownContainer.style.bottom = "auto";
            this.dropdownContainer.style.width = "auto";
          }
        } else {
          // 桌面端恢复保存的位置
          const savedPosition = {
            left: GM_getValue("buttonLeft", 10),
            bottom: GM_getValue("buttonBottom", 10),
          };
          this.toggleButton.style.left = `${savedPosition.left}px`;
          this.toggleButton.style.bottom = `${savedPosition.bottom}px`;
          this.toggleButton.style.right = "auto";

          // 如果下拉窗口可见,重新计算位置
          if (this.state.isDropdownVisible) {
            this.updateDropdownPosition();
          }
        }
      });
    },

    injectStyles() {
      this.styleElem = document.createElement("style");
      this.styleElem.textContent = getStyles(this.isDarkMode);
      document.head.appendChild(this.styleElem);
    },

    updateStyles() {
      this.styleElem.textContent = getStyles(this.isDarkMode);
    },

    loadTokens() {
      try {
        const savedTokens = GM_getValue(config.storageKey);
        let tokens =
          savedTokens && savedTokens.length > 0
            ? savedTokens
            : [config.defaultToken];

        // 为没有创建时间的token添加默认值
        tokens = tokens.map((token) => {
          if (!token.createdAt) {
            const now = new Date();
            return {
              ...token,
              createdAt: now.toLocaleString("zh-CN", {
                month: "2-digit",
                day: "2-digit",
                hour: "2-digit",
                minute: "2-digit",
              }),
              timestamp: now.getTime(),
            };
          }
          return token;
        });

        return tokens;
      } catch (error) {
        console.error("加载 tokens 失败:", error);
        return [config.defaultToken];
      }
    },

    saveTokens() {
      try {
        GM_setValue(config.storageKey, this.tokens);
      } catch (error) {
        console.error("保存 tokens 失败:", error);
        alert("保存 tokens 失败,请重试。");
      }
    },

    createUI() {
      // 创建浮动按钮
      this.toggleButton = UI.createElem("button", "claude-toggle-button");
      this.toggleButton.id = "claude-toggle-button";

      // 移动端检测并设置按钮位置
      const isMobile = window.innerWidth <= 768;
      if (isMobile) {
        // 移动端固定在右下角
        this.toggleButton.style.right = "20px";
        this.toggleButton.style.bottom = "20px";
        this.toggleButton.style.left = "auto";
      } else {
        // 桌面端使用保存的位置
        const savedPosition = {
          left: GM_getValue("buttonLeft", 10),
          bottom: GM_getValue("buttonBottom", 10),
        };
        this.toggleButton.style.left = `${savedPosition.left}px`;
        this.toggleButton.style.bottom = `${savedPosition.bottom}px`;
      }
      this.toggleButton.innerHTML = `
      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" pointer-events="none">
      <path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" pointer-events="none"></path>
      </svg>
      `;
      document.body.appendChild(this.toggleButton);

      // 创建下拉容器
      this.dropdownContainer = UI.createElem(
        "div",
        "claude-dropdown-container"
      );
      document.body.appendChild(this.dropdownContainer);

      // 创建标题容器
      const titleContainer = UI.createElem("div", "claude-title-container");

      const title = UI.createElem("h2");
      title.textContent = "Claude Session Key 管理器";

      this.ipDisplay = UI.createElem("div", "claude-ip-display");
      this.ipDisplay.textContent = "IP: 加载中...";

      titleContainer.appendChild(title);
      titleContainer.appendChild(this.ipDisplay);
      this.dropdownContainer.appendChild(titleContainer);

      // 创建 Token 网格
      this.tokenGrid = UI.createElem("div", "claude-token-grid");
      this.dropdownContainer.appendChild(this.tokenGrid);

      // 更新 Token 网格
      this.updateTokenGrid();

      // 创建按钮容器
      const buttonContainer = UI.createElem("div", "claude-button-container");

      // 测试所有按钮
      const testAllButton = UI.createButton(
        "测活",
        "claude-button primary",
        "🔍"
      );
      testAllButton.setAttribute("data-tooltip", "测试所有Token是否有效");
      testAllButton.addEventListener("click", () => this.testAllTokens());
      buttonContainer.appendChild(testAllButton);

      // 清理无效按钮
      const cleanInvalidButton = UI.createButton(
        "清理",
        "claude-button secondary",
        "🗑️"
      );
      cleanInvalidButton.setAttribute("data-tooltip", "清理所有无效的Token");
      cleanInvalidButton.addEventListener("click", () =>
        this.removeInvalidTokens()
      );
      buttonContainer.appendChild(cleanInvalidButton);

      // 添加 Token 按钮
      const addTokenButton = UI.createButton(
        "添加",
        "claude-button secondary",
        "➕"
      );
      addTokenButton.setAttribute("data-tooltip", "添加新的Token");
      addTokenButton.addEventListener("click", () => this.showAddTokenModal());
      buttonContainer.appendChild(addTokenButton);

      // 批量导入按钮
      const importButton = UI.createButton(
        "导入",
        "claude-button secondary",
        "📥"
      );
      importButton.setAttribute("data-tooltip", "批量导入多个Token");
      importButton.addEventListener("click", () => this.showBulkImportModal());
      buttonContainer.appendChild(importButton);

      // 批量导出按钮
      const exportButton = UI.createButton(
        "导出",
        "claude-button secondary",
        "📤"
      );
      exportButton.setAttribute("data-tooltip", "导出所有Token");
      exportButton.addEventListener("click", () => this.exportTokens());
      buttonContainer.appendChild(exportButton);

      // WebDAV备份按钮
      const webdavButton = UI.createButton(
        "云备份",
        "claude-button secondary",
        "☁️"
      );
      webdavButton.setAttribute("data-tooltip", "WebDAV云备份与恢复");
      webdavButton.addEventListener("click", () => this.showWebDAVModal());
      buttonContainer.appendChild(webdavButton);

      this.dropdownContainer.appendChild(buttonContainer);

      // 添加信息提示
      const infoSection = UI.createElem("div", "claude-info-section", {
        marginTop: "10px",
        padding: "8px",
        backgroundColor: "#f8f9fa",
        borderRadius: "6px",
        fontSize: "11px",
        color: "#666",
        textAlign: "center",
      });
      infoSection.innerHTML =
        "悬停显示面板 • 拖拽按钮调整位置 • 支持WebDAV云备份";
      this.dropdownContainer.appendChild(infoSection);
    },

    updateTokenGrid() {
      this.tokenGrid.innerHTML = "";

      // 获取当前使用的 token
      const currentTokenName = GM_getValue(config.currentTokenKey);

      // 加载测试结果
      const testResults = this.loadTestResults();

      this.tokens.forEach((token, index) => {
        const tokenItem = UI.createElem("div", "claude-token-item");
        if (token.name === currentTokenName) {
          tokenItem.classList.add("current-token");

          // 添加选中标记
          const currentBadge = UI.createElem("div", "current-token-badge");
          tokenItem.appendChild(currentBadge);
        }

        // Token 信息容器
        const tokenInfo = UI.createElem("div", "token-info");

        // 顶部行:名称和操作按钮
        const topRow = UI.createElem("div", "token-top-row");

        // 名称容器
        const nameContainer = UI.createElem("div", "token-name-container");

        const numberBadge = UI.createElem("span", "token-number");
        numberBadge.textContent = `#${(index + 1).toString().padStart(2, "0")}`;

        const nameSpan = UI.createElem("span", "token-name");
        nameSpan.textContent = token.name;

        nameContainer.appendChild(numberBadge);
        nameContainer.appendChild(nameSpan);

        // 操作按钮
        const actions = UI.createElem("div", "token-actions");

        // 编辑按钮
        const editButton = UI.createElem("button", "token-action-btn edit-btn");
        editButton.innerHTML = `
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path>
        </svg>
        `;
        editButton.dataset.index = index;
        editButton.addEventListener("click", (e) => {
          e.stopPropagation();
          this.showEditTokenModal(index);
        });

        // 删除按钮
        const deleteButton = UI.createElem(
          "button",
          "token-action-btn delete-btn"
        );
        deleteButton.innerHTML = `
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <path d="M3 6h18"></path>
        <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"></path>
        <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
        </svg>
        `;
        deleteButton.dataset.index = index;
        deleteButton.addEventListener("click", (e) => {
          e.stopPropagation();
          this.confirmDeleteToken(index);
        });

        actions.appendChild(editButton);
        actions.appendChild(deleteButton);

        topRow.appendChild(nameContainer);
        topRow.appendChild(actions);

        // 底部行:状态和时间
        const bottomRow = UI.createElem("div", "token-bottom-row");

        // 添加时间戳(使用token的创建时间)
        const timeSpan = UI.createElem("span", "token-time");
        timeSpan.textContent = token.createdAt || "";
        bottomRow.appendChild(timeSpan);

        // 状态指示器
        const status = UI.createElem("div", "token-status");
        const statusIndicator = UI.createElem("div", "status-indicator");

        // 检查缓存的测试结果
        const testResult = testResults[token.key];
        if (testResult) {
          statusIndicator.classList.add(testResult.status);
        }

        status.appendChild(statusIndicator);
        status.addEventListener("click", async (e) => {
          e.stopPropagation();
          await this.testSingleToken(token, statusIndicator, bottomRow);
        });

        bottomRow.appendChild(status);

        // 将行添加到信息容器
        tokenInfo.appendChild(topRow);
        tokenInfo.appendChild(bottomRow);

        // 将信息容器添加到 token 项
        tokenItem.appendChild(tokenInfo);

        // 点击切换 token
        tokenItem.addEventListener("click", () => this.switchToToken(token));

        // 将 token 项添加到网格
        this.tokenGrid.appendChild(tokenItem);
      });

      // 如果token数量超过4个(两行),添加滚动提示
      if (this.tokens.length > 4) {
        const scrollIndicator = UI.createElem("div", "scroll-indicator");
        scrollIndicator.innerHTML = `
        <div class="scroll-text">向下滚动查看更多 (${
          this.tokens.length - 4
        })</div>
        <div class="scroll-arrow">
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <path d="M7 13l5 5 5-5"></path>
        <path d="M7 6l5 5 5-5"></path>
        </svg>
        </div>
        `;
        this.tokenGrid.appendChild(scrollIndicator);
      }
    },

    async switchToToken(token) {
      // 检查是否有缓存的测试结果
      const cachedResult = this.getTestResult(token.key);

      // 如果有缓存的测试结果且为无效,提示用户并询问是否继续
      if (cachedResult && cachedResult.status === "error") {
        const confirmResult = await this.showConfirmDialog(
          "警告",
          `该 Token "${token.name}" 已被标记为无效,是否仍要切换到该 Token?`
        );

        if (!confirmResult) {
          return;
        }
      }

      // 应用 token
      this.applyToken(token.key);
      GM_setValue(config.currentTokenKey, token.name);

      // 隐藏下拉菜单
      this.hideDropdown();
    },

    applyToken(token) {
      const currentURL = window.location.href;

      if (currentURL.startsWith("https://claude.ai/")) {
        GM_cookie.set(
          {
            name: "sessionKey",
            value: token,
            domain: ".claude.ai",
            path: "/",
            secure: true,
            sameSite: "lax",
          },
          function () {
            window.location.reload();
          }
        );
      } else {
        let loginUrl;
        const hostname = new URL(currentURL).hostname;
        if (hostname !== "claude.ai") {
          loginUrl = `https://${hostname}/login_token?session_key=${token}`;
        }

        if (loginUrl) {
          window.location.href = loginUrl;
        }
      }
    },

    async testSingleToken(token, statusIndicator, bottomRow) {
      // 显示加载状态
      statusIndicator.className = "status-indicator loading";

      // 测试 token
      const result = await this.testToken(token.key);

      // 保存测试结果
      this.saveTestResult(token.key, result);

      // 更新状态指示器
      statusIndicator.className = `status-indicator ${result.status}`;

      // 不再更新时间戳,保持显示token的创建时间
    },

    async testAllTokens() {
      // 获取所有 token 项
      const tokenItems = this.tokenGrid.querySelectorAll(".claude-token-item");

      // 禁用测试按钮
      const testButton = this.dropdownContainer.querySelector(
        ".claude-button.primary"
      );
      testButton.disabled = true;
      testButton.textContent = "测试中...";

      // 清除所有缓存的测试结果
      GM_setValue(config.testResultsKey, {});

      const tokens = Array.from(tokenItems);

      // 按4个一组处理所有tokens
      for (let i = 0; i < tokens.length; i += 4) {
        // 取出当前4个(或更少)token
        const currentChunk = tokens.slice(i, Math.min(i + 4, tokens.length));

        // 并行处理这最多4个token
        await Promise.all(
          currentChunk.map(async (tokenItem) => {
            const index = Array.from(tokenItems).indexOf(tokenItem);
            const token = this.tokens[index];
            const statusIndicator =
              tokenItem.querySelector(".status-indicator");
            const bottomRow = tokenItem.querySelector(".token-bottom-row");

            await this.testSingleToken(token, statusIndicator, bottomRow);
          })
        );
      }

      // 恢复测试按钮
      testButton.disabled = false;
      testButton.innerHTML = "🔍 测活";
    },

    async testToken(key) {
      return new Promise((resolve) => {
        GM_xmlhttpRequest({
          method: "GET",
          url: "https://claude.ai/api/organizations",
          headers: {
            accept:
              "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
            "accept-language": "en-US,en;q=0.9",
            "cache-control": "max-age=0",
            cookie: `sessionKey=${key}`,
            "user-agent": "Mozilla/5.0 (X11; Linux x86_64)",
            "sec-fetch-mode": "navigate",
            "sec-fetch-site": "none",
          },
          onload: (response) => {
            try {
              if (response.status !== 200) {
                resolve({
                  status: "error",
                  message: "无效",
                });
                return;
              }

              const responseText = response.responseText;

              if (responseText.toLowerCase().includes("unauthorized")) {
                resolve({
                  status: "error",
                  message: "无效",
                });
                return;
              }

              if (responseText.trim() === "") {
                resolve({
                  status: "error",
                  message: "无响应",
                });
                return;
              }

              try {
                const objects = JSON.parse(responseText);
                if (objects && objects.length > 0) {
                  resolve({
                    status: "success",
                    message: "有效",
                  });
                  return;
                }
              } catch (e) {
                resolve({
                  status: "error",
                  message: "解析失败",
                });
                return;
              }

              resolve({
                status: "error",
                message: "无效数据",
              });
            } catch (error) {
              console.error("解析响应时发生错误:", error);
              resolve({
                status: "error",
                message: "测试失败",
              });
            }
          },
          onerror: (error) => {
            console.error("请求发生错误:", error);
            resolve({
              status: "error",
              message: "网络错误",
            });
          },
          ontimeout: () => {
            resolve({
              status: "error",
              message: "超时",
            });
          },
        });
      });
    },

    loadTestResults() {
      try {
        const cached = GM_getValue(config.testResultsKey, {});
        const now = Date.now();
        // 清理过期的测试结果
        const filtered = Object.entries(cached).reduce((acc, [key, value]) => {
          if (now - value.timestamp < config.testResultExpiry) {
            acc[key] = value;
          }
          return acc;
        }, {});
        return filtered;
      } catch (error) {
        console.error("加载测试结果缓存失败:", error);
        return {};
      }
    },

    saveTestResult(key, result) {
      try {
        const testResults = this.loadTestResults();
        const now = new Date();
        // 统一使用简短时间格式
        const formattedTime = now.toLocaleString("zh-CN", {
          month: "2-digit",
          day: "2-digit",
          hour: "2-digit",
          minute: "2-digit",
        });

        testResults[key] = {
          status: result.status,
          message: result.message,
          timestamp: now.getTime(),
          testTime: formattedTime, // 保存简短格式的时间
        };
        GM_setValue(config.testResultsKey, testResults);
      } catch (error) {
        console.error("保存测试结果失败:", error);
      }
    },

    getTestResult(key) {
      const testResults = this.loadTestResults();
      return testResults[key];
    },

    async removeInvalidTokens() {
      const confirmResult = await this.showConfirmDialog(
        "确认清理",
        "是否删除所有无效的 Tokens?此操作不可撤销。"
      );

      if (!confirmResult) return;

      const testResults = this.loadTestResults();
      const validTokens = this.tokens.filter((token) => {
        const result = testResults[token.key];
        return !result || result.status === "success";
      });

      if (validTokens.length === this.tokens.length) {
        alert("没有发现无效的 Tokens");
        return;
      }

      this.tokens = validTokens;
      this.saveTokens();
      this.updateTokenGrid();
    },

    showAddTokenModal() {
      const content = UI.createElem("div", "claude-add-token-form");

      const nameInput = UI.createElem("input");
      nameInput.placeholder = "Token 名称";
      nameInput.setAttribute("aria-label", "Token 名称");

      const keyInput = UI.createElem("input");
      keyInput.placeholder = "Token 密钥";
      keyInput.setAttribute("aria-label", "Token 密钥");

      content.appendChild(nameInput);
      content.appendChild(keyInput);

      const { modal, buttonContainer, close } = UI.createModal(
        "添加 Token",
        content
      );
      modal
        .querySelector(".claude-modal-content")
        .classList.add("narrow-modal");

      const addButton = UI.createButton("添加", "claude-button primary");
      addButton.addEventListener("click", () => {
        if (this.validateInput(nameInput.value, keyInput.value)) {
          // 获取当前时间并格式化
          const now = new Date();
          const formattedTime = now.toLocaleString("zh-CN", {
            month: "2-digit",
            day: "2-digit",
            hour: "2-digit",
            minute: "2-digit",
          });

          this.tokens.push({
            name: nameInput.value,
            key: keyInput.value,
            createdAt: formattedTime, // 添加创建时间
            timestamp: now.getTime(), // 添加时间戳用于排序
          });

          this.saveTokens();
          this.updateTokenGrid();
          close();
        }
      });

      const cancelButton = UI.createButton("取消", "claude-button secondary");
      cancelButton.addEventListener("click", close);

      buttonContainer.appendChild(cancelButton);
      buttonContainer.appendChild(addButton);
    },

    showEditTokenModal(index) {
      const token = this.tokens[index];
      const content = UI.createElem("div", "claude-edit-token-form");

      const nameInput = UI.createElem("input");
      nameInput.value = token.name;
      nameInput.placeholder = "Token 名称";

      const keyInput = UI.createElem("input");
      keyInput.value = token.key;
      keyInput.placeholder = "Token 密钥";

      content.appendChild(nameInput);
      content.appendChild(keyInput);

      const { modal, buttonContainer, close } = UI.createModal(
        "编辑 Token",
        content
      );
      modal
        .querySelector(".claude-modal-content")
        .classList.add("narrow-modal");

      const saveButton = UI.createButton("保存", "claude-button primary");
      saveButton.addEventListener("click", () => {
        if (this.validateInput(nameInput.value, keyInput.value)) {
          // 保留原有的创建时间和时间戳
          this.tokens[index] = {
            name: nameInput.value,
            key: keyInput.value,
            createdAt: token.createdAt || "",
            // 保留原有的创建时间
            timestamp: token.timestamp || Date.now(), // 保留原有的时间戳
          };

          this.saveTokens();
          this.updateTokenGrid();
          close();
        }
      });

      const cancelButton = UI.createButton("取消", "claude-button secondary");
      cancelButton.addEventListener("click", close);

      buttonContainer.appendChild(cancelButton);
      buttonContainer.appendChild(saveButton);
    },

    confirmDeleteToken(index) {
      const token = this.tokens[index];
      const content = UI.createElem("div", "claude-delete-confirm");

      content.innerHTML = `
      <div style="font-size: 48px; text-align: center; margin-bottom: 16px;">⚠️</div>
      <div style="font-size: 18px; font-weight: 600; text-align: center; margin-bottom: 12px;">删除确认</div>
      <div style="text-align: center; margin-bottom: 24px;">
      您确定要删除 Token "${token.name}" 吗?<br>
      此操作无法撤销。
      </div>
      `;

      const { modal, buttonContainer, close } = UI.createModal("", content);
      modal
        .querySelector(".claude-modal-content")
        .classList.add("narrow-modal");

      const deleteButton = UI.createButton("删除", "claude-button primary");
      deleteButton.style.backgroundColor = "#e53e3e";
      deleteButton.addEventListener("click", () => {
        this.deleteToken(index);
        close();
      });

      const cancelButton = UI.createButton("取消", "claude-button secondary");
      cancelButton.addEventListener("click", close);

      buttonContainer.appendChild(cancelButton);
      buttonContainer.appendChild(deleteButton);
    },

    deleteToken(index) {
      this.tokens.splice(index, 1);
      this.saveTokens();
      this.updateTokenGrid();
    },

    showBulkImportModal() {
      const content = UI.createElem("div", "claude-bulk-import-form");

      // 文本区域标签
      const textareaLabel = UI.createElem("label");
      textareaLabel.innerHTML =
        "<strong>1️⃣ Tokens 粘贴区:</strong><br>在这里粘贴您需要导入的 Tokens,每行一个!";
      content.appendChild(textareaLabel);

      // 文本区域
      const textarea = UI.createElem("textarea");
      textarea.rows = 10;
      content.appendChild(textarea);

      // 命名规则容器
      const namingRuleContainer = UI.createElem("div", "claude-naming-rule");

      // 命名规则标签
      const namingRuleLabel = UI.createElem("label");
      namingRuleLabel.innerHTML = "<strong>2️⃣ Tokens 命名规则:</strong>";
      namingRuleContainer.appendChild(namingRuleLabel);

      // 名称前缀
      const prefixLabel = UI.createElem("label");
      prefixLabel.textContent = "名称前缀:";
      namingRuleContainer.appendChild(prefixLabel);

      const prefixInput = UI.createElem("input");
      prefixInput.value = "token";
      namingRuleContainer.appendChild(prefixInput);

      // 起始编号
      const startNumberLabel = UI.createElem("label");
      startNumberLabel.textContent = "名称起始编号:";
      namingRuleContainer.appendChild(startNumberLabel);

      const startNumberInput = UI.createElem("input");
      startNumberInput.type = "number";
      startNumberInput.value = "1";
      namingRuleContainer.appendChild(startNumberInput);

      content.appendChild(namingRuleContainer);

      // 预览容器
      const previewLabel = UI.createElem("label");
      previewLabel.innerHTML = "<strong>3️⃣ Tokens 导入结果预览:</strong>";
      content.appendChild(previewLabel);

      const previewContainer = UI.createElem("div", "claude-preview-container");
      content.appendChild(previewContainer);

      const { modal, buttonContainer, close } = UI.createModal(
        "批量导入 Tokens",
        content
      );

      const importButton = UI.createButton("导入", "claude-button primary");
      importButton.addEventListener("click", () => {
        this.performBulkImport(
          textarea.value,
          prefixInput.value,
          startNumberInput.value
        );
        close();
      });

      const cancelButton = UI.createButton("取消", "claude-button secondary");
      cancelButton.addEventListener("click", close);

      buttonContainer.appendChild(cancelButton);
      buttonContainer.appendChild(importButton);

      // 更新预览
      const updatePreview = () => {
        this.previewBulkImport(
          textarea.value,
          prefixInput.value,
          startNumberInput.value,
          previewContainer
        );
      };

      [textarea, prefixInput, startNumberInput].forEach((elem) => {
        elem.addEventListener("input", updatePreview);
      });

      // 初始化预览
      updatePreview();
    },

    previewBulkImport(input, namePrefix, startNumber, previewContainer) {
      previewContainer.innerHTML = "";

      const tokens = this.parseTokens(input);
      const namedTokens = this.applyNamingRule(
        tokens,
        namePrefix,
        parseInt(startNumber)
      );

      const previewTitle = UI.createElem("div", "claude-preview-title");
      previewTitle.textContent = "请核对下方导入结果:";
      previewContainer.appendChild(previewTitle);

      if (namedTokens.length === 0) {
        const emptyMessage = UI.createElem("div", "claude-preview-item");
        emptyMessage.textContent = "等待输入...";
        previewContainer.appendChild(emptyMessage);
        return;
      }

      namedTokens.forEach((token) => {
        const previewItem = UI.createElem("div", "claude-preview-item");
        previewItem.innerHTML = `
        <strong>${token.name}:</strong>
        <span style="font-family: monospace; word-break: break-all;">${token.key}</span>
        `;
        previewContainer.appendChild(previewItem);
      });
    },

    performBulkImport(input, namePrefix, startNumber) {
      const tokens = this.parseTokens(input);
      const namedTokens = this.applyNamingRule(
        tokens,
        namePrefix,
        parseInt(startNumber)
      );

      if (namedTokens.length === 0) {
        alert("没有有效的 Tokens 可导入");
        return;
      }

      this.tokens = [...this.tokens, ...namedTokens];
      this.saveTokens();
      this.updateTokenGrid();
    },

    parseTokens(input) {
      return input
        .split("\n")
        .map((line) => line.trim())
        .filter((line) => this.validateTokenKey(line))
        .map((key) => ({
          key,
        }));
    },

    applyNamingRule(tokens, namePrefix, startNumber) {
      return tokens.map((token, index) => {
        const number = startNumber + index;
        const name = `${namePrefix}${number.toString().padStart(2, "0")}`;
        return {
          ...token,
          name,
        };
      });
    },

    exportTokens() {
      const testResults = this.loadTestResults();
      const exportData = this.tokens.map((token) => {
        const testResult = testResults[token.key] || {};
        return {
          name: token.name,
          sessionKey: token.key,
          isValid:
            testResult.status === "success"
              ? true
              : testResult.status === "error"
              ? false
              : null,
          testTime: testResult.testTime || null,
          testMessage: testResult.message || null,
        };
      });

      // 创建并下载 JSON 文件
      const blob = new Blob([JSON.stringify(exportData, null, 2)], {
        type: "application/json",
      });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = `claude_tokens_${
        new Date().toISOString().split("T")[0]
      }.json`;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
    },

    showWebDAVModal() {
      const content = UI.createElem("div", "claude-webdav-form");
      content.style.cssText = "width: 100%; max-width: 600px;";

      // 添加帮助信息
      const helpInfo = UI.createElem("div", "claude-webdav-help");
      helpInfo.style.cssText = `
      margin-bottom: 10px;
      padding: 12px;
      background-color: var(--bg-color);
      border: 1px solid var(--border-color);
      border-radius: 8px;
      font-size: 13px;
      color: var(--text-color);
      box-shadow: 0 2px 4px rgba(0,0,0,0.05);
      `;
      helpInfo.innerHTML = `
      <p style="margin: 0 0 10px 0; font-weight: 600; color: var(--text-color);">📝 WebDAV服务器设置说明:</p>
      <ul style="margin: 0; padding-left: 20px; line-height: 1.6;">
      <li>URL必须是完整的WebDAV路径,例如:https://dav.jianguoyun.com/dav/Claude/</li>
      <li>确保路径末尾有斜杠"/"</li>
      <li>如果遇到404错误,请确认路径是否存在</li>
      <li>坚果云用户请使用应用专用密码</li>
      </ul>
      `;
      content.appendChild(helpInfo);

      // 创建表单容器
      const formContainer = UI.createElem(
        "div",
        "claude-webdav-form-container"
      );
      formContainer.style.cssText = `
      display: grid;
      gap: 8px;
      margin-bottom: 15px;
      `;

      // WebDAV服务器URL输入
      const urlGroup = UI.createElem("div", "input-group");
      urlGroup.style.cssText = "display: flex; align-items: center; gap: 10px;";

      const urlLabel = UI.createElem("label");
      urlLabel.textContent = "WebDAV URL:";
      urlLabel.style.cssText =
        "font-weight: 500; color: var(--text-color); min-width: 120px; text-align: right;";

      const urlInput = UI.createElem("input");
      urlInput.type = "text";
      urlInput.placeholder = "https://dav.jianguoyun.com/dav/Claude/";
      urlInput.value = GM_getValue("webdav_url", "");
      urlInput.style.cssText = `
      flex: 1;
      padding: 10px;
      border: 1px solid var(--border-color);
      border-radius: 6px;
      font-size: 14px;
      background-color: var(--bg-color);
      color: var(--text-color);
      transition: all 0.3s ease;
      `;

      urlGroup.appendChild(urlLabel);
      urlGroup.appendChild(urlInput);
      formContainer.appendChild(urlGroup);

      // 用户名输入
      const usernameGroup = UI.createElem("div", "input-group");
      usernameGroup.style.cssText =
        "display: flex; align-items: center; gap: 10px;";

      const usernameLabel = UI.createElem("label");
      usernameLabel.textContent = "用户名:";
      usernameLabel.style.cssText =
        "font-weight: 500; color: var(--text-color); min-width: 120px; text-align: right;";

      const usernameInput = UI.createElem("input");
      usernameInput.type = "text";
      usernameInput.placeholder = "用户名";
      usernameInput.value = GM_getValue("webdav_username", "");
      usernameInput.style.cssText = `
      flex: 1;
      padding: 10px;
      border: 1px solid var(--border-color);
      border-radius: 6px;
      font-size: 14px;
      background-color: var(--bg-color);
      color: var(--text-color);
      transition: all 0.3s ease;
      `;

      usernameGroup.appendChild(usernameLabel);
      usernameGroup.appendChild(usernameInput);
      formContainer.appendChild(usernameGroup);

      // 密码输入
      const passwordGroup = UI.createElem("div", "input-group");
      passwordGroup.style.cssText =
        "display: flex; align-items: center; gap: 10px;";

      const passwordLabel = UI.createElem("label");
      passwordLabel.textContent = "密码:";
      passwordLabel.style.cssText =
        "font-weight: 500; color: var(--text-color); min-width: 120px; text-align: right;";

      const passwordInput = UI.createElem("input");
      passwordInput.type = "password";
      passwordInput.placeholder = "密码";
      passwordInput.value = GM_getValue("webdav_password", "");
      passwordInput.style.cssText = `
      flex: 1;
      padding: 10px;
      border: 1px solid var(--border-color);
      border-radius: 6px;
      font-size: 14px;
      background-color: var(--bg-color);
      color: var(--text-color);
      transition: all 0.3s ease;
      `;

      passwordGroup.appendChild(passwordLabel);
      passwordGroup.appendChild(passwordInput);
      formContainer.appendChild(passwordGroup);

      // 文件名输入
      const filenameGroup = UI.createElem("div", "input-group");
      filenameGroup.style.cssText =
        "display: flex; align-items: center; gap: 10px;";

      const filenameLabel = UI.createElem("label");
      filenameLabel.textContent = "文件名:";
      filenameLabel.style.cssText =
        "font-weight: 500; color: var(--text-color); min-width: 120px; text-align: right;";

      const filenameInput = UI.createElem("input");
      filenameInput.type = "text";
      filenameInput.placeholder = "claude_tokens.json";
      filenameInput.value = GM_getValue(
        "webdav_filename",
        "claude_tokens.json"
      );
      filenameInput.style.cssText = `
      flex: 1;
      padding: 10px;
      border: 1px solid var(--border-color);
      border-radius: 6px;
      font-size: 14px;
      background-color: var(--bg-color);
      color: var(--text-color);
      transition: all 0.3s ease;
      `;

      filenameGroup.appendChild(filenameLabel);
      filenameGroup.appendChild(filenameInput);
      formContainer.appendChild(filenameGroup);

      content.appendChild(formContainer);

      // 测试连接按钮
      const testConnectionButton = UI.createButton(
        "测试连接",
        "claude-button secondary"
      );
      testConnectionButton.style.cssText = `
      width: 100%;
      margin: 10px 0;
      padding: 10px;
      font-size: 14px;
      font-weight: 500;
      border-radius: 6px;
      background-color: var(--button-bg);
      color: var(--text-color);
      border: 1px solid var(--border-color);
      cursor: pointer;
      transition: all 0.3s ease;
      `;
      testConnectionButton.addEventListener("click", async () => {
        this.saveWebDAVSettings(
          urlInput.value,
          usernameInput.value,
          passwordInput.value,
          filenameInput.value
        );

        statusDisplay.style.display = "block";
        statusDisplay.textContent = "正在测试连接...";
        statusDisplay.style.backgroundColor = "var(--bg-color)";

        try {
          await this.checkWebDAVDirectory(
            urlInput.value,
            usernameInput.value,
            passwordInput.value
          );
          statusDisplay.textContent = "✅ 连接成功!目录存在且可访问。";
          statusDisplay.style.backgroundColor = "#d4edda";
        } catch (error) {
          statusDisplay.textContent = `❌ 连接失败: ${error.message}`;
          statusDisplay.style.backgroundColor = "#f8d7da";
        }
      });
      content.appendChild(testConnectionButton);

      // 状态显示
      const statusDisplay = UI.createElem("div", "claude-webdav-status");
      statusDisplay.style.cssText = `
      margin: 10px 0;
      padding: 10px;
      border-radius: 6px;
      font-size: 14px;
      text-align: center;
      display: none;
      transition: all 0.3s ease;
      `;
      content.appendChild(statusDisplay);

      // 操作按钮容器
      const actionsContainer = UI.createElem("div", "claude-webdav-actions");
      actionsContainer.style.cssText = `
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 10px;
      margin-top: 15px;
      `;

      // 备份按钮
      const backupButton = UI.createButton(
        "备份到WebDAV",
        "claude-button primary"
      );
      backupButton.style.cssText = `
      padding: 12px;
      font-size: 14px;
      font-weight: 500;
      border-radius: 6px;
      background-color: #b3462f;
      color: white;
      border: none;
      cursor: pointer;
      transition: all 0.3s ease;
      `;
      backupButton.addEventListener("click", async () => {
        this.saveWebDAVSettings(
          urlInput.value,
          usernameInput.value,
          passwordInput.value,
          filenameInput.value
        );

        statusDisplay.style.display = "block";
        statusDisplay.textContent = "正在备份...";
        statusDisplay.style.backgroundColor = "var(--bg-color)";

        try {
          await this.backupToWebDAV(
            urlInput.value,
            usernameInput.value,
            passwordInput.value,
            filenameInput.value
          );
          statusDisplay.textContent = "✅ 备份成功!";
          statusDisplay.style.backgroundColor = "#d4edda";
        } catch (error) {
          statusDisplay.textContent = `❌ 备份失败: ${error.message}`;
          statusDisplay.style.backgroundColor = "#f8d7da";
        }
      });

      // 恢复按钮
      const restoreButton = UI.createButton(
        "从WebDAV恢复",
        "claude-button secondary"
      );
      restoreButton.style.cssText = `
      padding: 12px;
      font-size: 14px;
      font-weight: 500;
      border-radius: 6px;
      background-color: var(--button-bg);
      color: var(--text-color);
      border: 1px solid var(--border-color);
      cursor: pointer;
      transition: all 0.3s ease;
      `;
      restoreButton.addEventListener("click", async () => {
        this.saveWebDAVSettings(
          urlInput.value,
          usernameInput.value,
          passwordInput.value,
          filenameInput.value
        );

        statusDisplay.style.display = "block";
        statusDisplay.textContent = "正在恢复...";
        statusDisplay.style.backgroundColor = "var(--bg-color)";

        try {
          await this.restoreFromWebDAV(
            urlInput.value,
            usernameInput.value,
            passwordInput.value,
            filenameInput.value
          );
          statusDisplay.textContent = "✅ 恢复成功!";
          statusDisplay.style.backgroundColor = "#d4edda";
          this.updateTokenGrid();
        } catch (error) {
          statusDisplay.textContent = `❌ 恢复失败: ${error.message}`;
          statusDisplay.style.backgroundColor = "#f8d7da";
        }
      });

      // 关闭按钮
      const closeButton = UI.createButton("关闭", "claude-button secondary");
      closeButton.style.cssText = `
      padding: 12px;
      font-size: 14px;
      font-weight: 500;
      border-radius: 6px;
      background-color: var(--button-bg);
      color: var(--text-color);
      border: 1px solid var(--border-color);
      cursor: pointer;
      transition: all 0.3s ease;
      `;
      closeButton.addEventListener("click", () => {
        modal.remove();
      });

      actionsContainer.appendChild(backupButton);
      actionsContainer.appendChild(restoreButton);
      actionsContainer.appendChild(closeButton);
      content.appendChild(actionsContainer);

      // 创建模态框
      const { modal, buttonContainer } = UI.createModal(
        "☁️ WebDAV备份与恢复",
        content,
        true
      );
      document.body.appendChild(modal);

      // 添加关闭按钮事件监听
      const closeBtn = modal.querySelector(".claude-close-button");
      if (closeBtn) {
        closeBtn.addEventListener("click", () => {
          document.body.removeChild(modal);
        });
      }
    },

    saveWebDAVSettings(url, username, password, filename) {
      GM_setValue("webdav_url", url);
      GM_setValue("webdav_username", username);
      GM_setValue("webdav_password", password);
      GM_setValue("webdav_filename", filename);
    },

    async backupToWebDAV(url, username, password, filename) {
      // 准备备份数据
      const testResults = this.loadTestResults();
      const exportData = this.tokens.map((token) => {
        const testResult = testResults[token.key] || {};
        return {
          name: token.name,
          sessionKey: token.key,
          isValid:
            testResult.status === "success"
              ? true
              : testResult.status === "error"
              ? false
              : null,
          testTime: testResult.testTime || null,
          testMessage: testResult.message || null,
        };
      });

      const jsonData = JSON.stringify(exportData, null, 2);

      // 确保URL以/结尾
      if (!url.endsWith("/")) {
        url += "/";
      }

      // 先检查目录是否存在
      try {
        await this.checkWebDAVDirectory(url, username, password);
      } catch (error) {
        // 如果是404错误,尝试创建目录
        if (error.message.includes("404")) {
          try {
            // 尝试创建父目录
            const parentUrl = url.substring(
              0,
              url.lastIndexOf("/", url.length - 2) + 1
            );
            if (parentUrl !== url) {
              await this.createWebDAVDirectory(
                parentUrl,
                username,
                password,
                url.substring(parentUrl.length, url.length - 1)
              );
            } else {
              throw new Error("无法确定父目录");
            }
          } catch (createError) {
            throw new Error(`目录不存在且无法创建: ${createError.message}`);
          }
        } else {
          throw error;
        }
      }

      // 发送到WebDAV服务器
      return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
          method: "PUT",
          url: url + filename,
          headers: {
            "Content-Type": "application/json",
            Authorization: "Basic " + btoa(username + ":" + password),
          },
          data: jsonData,
          onload: function (response) {
            if (response.status >= 200 && response.status < 300) {
              resolve();
            } else {
              console.error("WebDAV备份失败:", response);
              reject(
                new Error(
                  `HTTP错误: ${response.status} ${
                    response.statusText || ""
                  }\n响应: ${response.responseText || "无响应内容"}`
                )
              );
            }
          },
          onerror: function (error) {
            console.error("WebDAV备份网络错误:", error);
            reject(new Error(`网络错误: ${error.statusText || "连接失败"}`));
          },
        });
      });
    },

    // 检查WebDAV目录是否存在
    checkWebDAVDirectory(url, username, password) {
      return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
          method: "PROPFIND",
          url: url,
          headers: {
            Depth: "0",
            Authorization: "Basic " + btoa(username + ":" + password),
          },
          onload: function (response) {
            if (response.status >= 200 && response.status < 300) {
              resolve();
            } else {
              reject(
                new Error(
                  `HTTP错误: ${response.status} ${response.statusText || ""}`
                )
              );
            }
          },
          onerror: function (error) {
            reject(new Error(`网络错误: ${error.statusText || "连接失败"}`));
          },
        });
      });
    },

    // 创建WebDAV目录
    createWebDAVDirectory(parentUrl, username, password, dirName) {
      return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
          method: "MKCOL",
          url: parentUrl + dirName,
          headers: {
            Authorization: "Basic " + btoa(username + ":" + password),
          },
          onload: function (response) {
            if (response.status >= 200 && response.status < 300) {
              resolve();
            } else {
              reject(
                new Error(
                  `无法创建目录: HTTP错误 ${response.status} ${
                    response.statusText || ""
                  }`
                )
              );
            }
          },
          onerror: function (error) {
            reject(new Error(`网络错误: ${error.statusText || "连接失败"}`));
          },
        });
      });
    },

    async restoreFromWebDAV(url, username, password, filename) {
      // 确保URL以/结尾
      if (!url.endsWith("/")) {
        url += "/";
      }

      // 从WebDAV服务器获取数据
      return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
          method: "GET",
          url: url + filename,
          headers: {
            Authorization: "Basic " + btoa(username + ":" + password),
          },
          onload: (response) => {
            if (response.status >= 200 && response.status < 300) {
              try {
                const data = JSON.parse(response.responseText);

                // 转换数据格式
                const tokens = data.map((item) => ({
                  name: item.name,
                  key: item.sessionKey,
                  createdAt: new Date().toLocaleString("zh-CN", {
                    month: "2-digit",
                    day: "2-digit",
                    hour: "2-digit",
                    minute: "2-digit",
                  }),
                  timestamp: Date.now(),
                }));

                // 更新tokens
                this.tokens = tokens;
                this.saveTokens();

                resolve();
              } catch (error) {
                console.error(
                  "解析WebDAV数据失败:",
                  error,
                  response.responseText
                );
                reject(new Error(`解析数据失败: ${error.message}`));
              }
            } else {
              console.error("WebDAV恢复失败:", response);
              reject(
                new Error(
                  `HTTP错误: ${response.status} ${
                    response.statusText || ""
                  }\n响应: ${response.responseText || "无响应内容"}`
                )
              );
            }
          },
          onerror: function (error) {
            console.error("WebDAV恢复网络错误:", error);
            reject(new Error(`网络错误: ${error.statusText || "连接失败"}`));
          },
        });
      });
    },

    validateInput(name, key) {
      if (!name || !key) {
        alert("Token 名称和密钥都要填写!");
        return false;
      }
      // 移除对token名称的严格限制,允许更多字符,包括@和.
      // if (!/^[a-zA-Z0-9-_]+$/.test(name)) {
      //     alert("Token 名称只能包含字母、数字、下划线和连字符!");
      //     return false;
      // }
      if (!this.validateTokenKey(key)) {
        alert("无效的 Token 密钥格式!");
        return false;
      }
      return true;
    },

    validateTokenKey(key) {
      return /^sk-ant-sid\d{2}-[A-Za-z0-9_-]*$/.test(key);
    },

    showConfirmDialog(title, message) {
      return new Promise((resolve) => {
        const content = UI.createElem("div", "claude-confirm-dialog");

        content.innerHTML = `
        <div style="font-size: 48px; text-align: center; margin-bottom: 16px;">⚠️</div>
        <div style="font-size: 18px; font-weight: 600; text-align: center; margin-bottom: 12px;">${title}</div>
        <div style="text-align: center; margin-bottom: 24px;">${message}</div>
        `;

        const { modal, buttonContainer, close } = UI.createModal("", content);
        modal
          .querySelector(".claude-modal-content")
          .classList.add("narrow-modal");

        const confirmButton = UI.createButton("确认", "claude-button primary");
        confirmButton.addEventListener("click", () => {
          close();
          resolve(true);
        });

        const cancelButton = UI.createButton("取消", "claude-button secondary");
        cancelButton.addEventListener("click", () => {
          close();
          resolve(false);
        });

        buttonContainer.appendChild(cancelButton);
        buttonContainer.appendChild(confirmButton);
      });
    },

    fetchIPCountryCode() {
      this.ipDisplay.textContent = "IP: 加载中...";

      GM_xmlhttpRequest({
        method: "GET",
        url: config.ipApiUrl,
        onload: (response) => {
          if (response.status === 200) {
            this.ipDisplay.textContent = "IP: " + response.responseText.trim();
          } else {
            this.ipDisplay.textContent = "IP: 获取失败";
          }
        },
        onerror: () => {
          this.ipDisplay.textContent = "IP: 获取失败";
        },
      });
    },

    setupEventListeners() {
      // 拖拽相关事件
      this.toggleButton.addEventListener(
        "mousedown",
        this.onMouseDown.bind(this)
      );
      document.addEventListener("mousemove", this.onMouseMove.bind(this));
      document.addEventListener("mouseup", this.onMouseUp.bind(this));

      // 状态管理对象
      this.state = {
        isButtonHovered: false,
        isDropdownHovered: false,
        isDropdownVisible: false,
        isDragging: false,
        isProcessingClick: false, // 新增:处理点击状态
        isClosing: false, // 新增:窗口正在关闭的状态

        // 检查当前是否应该保持弹窗显示
        shouldKeepOpen() {
          // 修改逻辑,即使在拖动过程中,也要考虑鼠标悬停状态
          return this.isButtonHovered || this.isDropdownHovered;
        },
      };

      // 定时器
      this.hoverTimeout = null;
      this.closeTimeout = null;

      // 鼠标进入按钮
      this.toggleButton.addEventListener("mouseenter", (e) => {
        if (this.state.isProcessingClick) return; // 如果正在处理点击,忽略hover

        this.state.isButtonHovered = true;
        clearTimeout(this.closeTimeout);
        clearTimeout(this.hoverTimeout); // 确保清除之前的hover定时器

        // 如果下拉窗口未显示或正在关闭中,则显示窗口
        if (!this.state.isDropdownVisible || this.state.isClosing) {
          // 如果窗口正在关闭,立即显示
          if (this.state.isClosing) {
            this.state.isClosing = false;
            this.showDropdown();
          } else {
            this.hoverTimeout = setTimeout(() => {
              this.showDropdown();
            }, 300);
          }
        }
      });

      // 鼠标离开按钮
      this.toggleButton.addEventListener("mouseleave", (e) => {
        if (this.state.isProcessingClick) return; // 如果正在处理点击,忽略hover

        this.state.isButtonHovered = false;
        clearTimeout(this.hoverTimeout);

        // 检查是否应该关闭弹窗
        if (!this.state.shouldKeepOpen()) {
          this.scheduleHideDropdown();
        }
      });

      // 按钮点击事件
      this.toggleButton.addEventListener("click", (e) => {
        if (this.state.isDragging) return; // 如果正在拖拽,忽略点击

        this.state.isProcessingClick = true;
        clearTimeout(this.hoverTimeout);
        clearTimeout(this.closeTimeout);

        if (this.state.isDropdownVisible) {
          this.hideDropdown();
        } else {
          this.showDropdown();
        }

        setTimeout(() => {
          this.state.isProcessingClick = false;
        }, 100);
      });

      // 鼠标进入弹窗
      this.dropdownContainer.addEventListener("mouseenter", () => {
        if (this.state.isProcessingClick) return;

        this.state.isDropdownHovered = true;
        clearTimeout(this.closeTimeout);

        // 如果弹窗在淡出过程中,恢复显示
        if (
          this.state.isDropdownVisible &&
          this.dropdownContainer.style.opacity !== "1"
        ) {
          this.dropdownContainer.style.opacity = "1";
          this.dropdownContainer.style.transform = "scale(1)";
        }
      });

      // 鼠标离开弹窗
      this.dropdownContainer.addEventListener("mouseleave", () => {
        if (this.state.isProcessingClick) return;

        this.state.isDropdownHovered = false;

        // 检查是否应该关闭弹窗
        if (!this.state.shouldKeepOpen()) {
          this.scheduleHideDropdown();
        }
      });

      // 点击其他区域关闭下拉菜单
      document.addEventListener("click", (e) => {
        if (
          this.dropdownContainer.style.display === "flex" &&
          !this.dropdownContainer.contains(e.target) &&
          e.target !== this.toggleButton
        ) {
          this.state.isDropdownHovered = false;
          this.state.isButtonHovered = false;
          this.hideDropdown();
        }
      });

      // 添加触摸事件支持
      const isMobile = window.innerWidth <= 768;

      if (isMobile) {
        // 移动端触摸事件
        this.toggleButton.addEventListener("touchstart", (e) => {
          e.preventDefault();
          this.state.isProcessingClick = true;
          clearTimeout(this.hoverTimeout);
          clearTimeout(this.closeTimeout);

          if (this.state.isDropdownVisible) {
            this.hideDropdown();
          } else {
            this.showDropdown();
          }

          setTimeout(() => {
            this.state.isProcessingClick = false;
          }, 100);
        });

        // 点击其他区域关闭 - 触摸版本
        document.addEventListener("touchstart", (e) => {
          if (
            this.dropdownContainer.style.display === "flex" &&
            !this.dropdownContainer.contains(e.target) &&
            e.target !== this.toggleButton
          ) {
            this.state.isDropdownHovered = false;
            this.state.isButtonHovered = false;
            this.hideDropdown();
          }
        });
      }
    },

    onMouseDown(e) {
      const isMobile = window.innerWidth <= 768;
      if (isMobile) {
        // 移动端禁用拖拽功能
        return;
      }

      if (e.button !== 0) return; // 只处理左键点击

      this.isDragging = true;
      this.state.isDragging = true;
      this.startX = e.clientX;
      this.startY = e.clientY;

      const rect = this.toggleButton.getBoundingClientRect();
      this.offsetX = this.startX - rect.left;
      this.offsetY = this.startY - rect.top;

      this.toggleButton.style.cursor = "grabbing";

      // 不再清除悬停定时器,允许在拖动过程中显示窗口
      // clearTimeout(this.hoverTimeout);
      clearTimeout(this.closeTimeout);

      // 如果鼠标在按钮上,立即显示下拉窗口
      if (this.state.isButtonHovered && !this.state.isDropdownVisible) {
        this.showDropdown();
      }

      // 阻止默认行为和事件冒泡
      e.preventDefault();
      e.stopPropagation();
    },

    onMouseMove(e) {
      // 检查鼠标是否在按钮上,用于处理拖动过程中的悬停
      const buttonRect = this.toggleButton.getBoundingClientRect();
      const isOverButton =
        e.clientX >= buttonRect.left &&
        e.clientX <= buttonRect.right &&
        e.clientY >= buttonRect.top &&
        e.clientY <= buttonRect.bottom;

      // 更新悬停状态
      if (isOverButton && !this.state.isButtonHovered) {
        this.state.isButtonHovered = true;
        // 如果下拉窗口未显示或正在关闭中,则显示窗口
        if (!this.state.isDropdownVisible || this.state.isClosing) {
          clearTimeout(this.hoverTimeout);
          // 如果窗口正在关闭或正在拖动,立即显示
          if (this.state.isClosing || this.isDragging) {
            this.state.isClosing = false;
            this.showDropdown();
          } else {
            this.hoverTimeout = setTimeout(() => {
              this.showDropdown();
            }, 300);
          }
        }
      } else if (!isOverButton && this.state.isButtonHovered) {
        this.state.isButtonHovered = false;
        clearTimeout(this.hoverTimeout);
        if (!this.state.shouldKeepOpen()) {
          this.scheduleHideDropdown();
        }
      }

      if (!this.isDragging) return;

      const x = e.clientX - this.offsetX;
      const y = e.clientY - this.offsetY;

      // 计算底部位置
      const bottom = window.innerHeight - y - this.toggleButton.offsetHeight;

      // 确保按钮在窗口范围内
      const maxX = window.innerWidth - this.toggleButton.offsetWidth;
      const maxBottom = window.innerHeight - this.toggleButton.offsetHeight;

      this.buttonLeft = Math.max(0, Math.min(x, maxX));
      this.buttonBottom = Math.max(0, Math.min(bottom, maxBottom));

      // 更新按钮位置
      this.toggleButton.style.left = `${this.buttonLeft}px`;
      this.toggleButton.style.bottom = `${this.buttonBottom}px`;
      this.toggleButton.style.top = "auto";

      // 如果下拉窗口可见,更新其位置
      if (this.state.isDropdownVisible) {
        this.updateDropdownPosition();
      }

      e.preventDefault();
    },

    // 添加新方法用于更新下拉窗口位置
    updateDropdownPosition() {
      const buttonRect = this.toggleButton.getBoundingClientRect();
      const dropdownWidth = 600; // 下拉窗口宽度

      // 计算下拉窗口位置
      let left = buttonRect.right + 10;
      let top = buttonRect.top;

      // 检查是否超出右边界
      if (left + dropdownWidth > window.innerWidth) {
        // 如果右边放不下,则放到左边
        left = buttonRect.left - dropdownWidth - 10;
      }

      // 如果左边也放不下,则居中显示
      if (left < 0) {
        left = Math.max(0, (window.innerWidth - dropdownWidth) / 2);
      }

      // 检查是否超出底部边界
      const dropdownHeight = Math.min(
        this.dropdownContainer.scrollHeight,
        window.innerHeight * 0.8
      );
      if (top + dropdownHeight > window.innerHeight) {
        // 如果超出底部,则向上显示,确保完全可见
        top = Math.max(0, window.innerHeight - dropdownHeight - 10);
      }

      // 应用新位置
      this.dropdownContainer.style.left = `${left}px`;
      this.dropdownContainer.style.top = `${top}px`;
    },

    onMouseUp(e) {
      if (!this.isDragging) return;

      this.isDragging = false;
      this.state.isDragging = false;
      this.toggleButton.style.cursor = "move";

      // 保存位置
      GM_setValue("buttonLeft", this.buttonLeft);
      GM_setValue("buttonBottom", this.buttonBottom);

      // 检查鼠标是否在按钮上
      const buttonRect = this.toggleButton.getBoundingClientRect();
      const isOverButton =
        e.clientX >= buttonRect.left &&
        e.clientX <= buttonRect.right &&
        e.clientY >= buttonRect.top &&
        e.clientY <= buttonRect.bottom;

      // 更新悬停状态
      this.state.isButtonHovered = isOverButton;

      // 添加短暂延迟,避免与点击事件冲突
      setTimeout(() => {
        // 如果鼠标在按钮上且下拉窗口未显示,则显示下拉窗口
        if (isOverButton && !this.state.isDropdownVisible) {
          this.showDropdown();
        }
        // 否则检查是否应该关闭弹窗
        else if (!this.state.shouldKeepOpen()) {
          this.scheduleHideDropdown();
        }
      }, 100);

      e.preventDefault();
    },

    scheduleHideDropdown() {
      if (this.state.isProcessingClick) return;

      clearTimeout(this.closeTimeout);
      this.closeTimeout = setTimeout(() => {
        if (!this.state.shouldKeepOpen()) {
          this.hideDropdown();
        }
      }, 300);
    },

    showDropdown() {
      // 立即更新状态
      this.state.isDropdownVisible = true;
      this.state.isClosing = false;

      const isMobile = window.innerWidth <= 768;

      if (isMobile) {
        // 移动端固定全屏显示
        this.dropdownContainer.style.top = "10px";
        this.dropdownContainer.style.left = "10px";
        this.dropdownContainer.style.right = "10px";
        this.dropdownContainer.style.bottom = "auto";
        this.dropdownContainer.style.width = "auto";
      } else {
        // 桌面端计算位置
        this.updateDropdownPosition();
      }

      this.dropdownContainer.style.opacity = "0";
      this.dropdownContainer.style.display = "flex";

      setTimeout(() => {
        this.dropdownContainer.style.opacity = "1";
        this.dropdownContainer.style.transform = "scale(1)";
        this.toggleButton.style.transform = "scale(1.1)";
      }, 10);
    },

    hideDropdown() {
      // 设置正在关闭状态
      this.state.isClosing = true;

      // 添加动画
      this.dropdownContainer.style.opacity = "0";
      this.dropdownContainer.style.transform = "scale(0.95)";
      this.toggleButton.style.transform = "scale(1)";

      // 等待动画完成后隐藏
      this.closeTimeout = setTimeout(() => {
        if (!this.state.shouldKeepOpen()) {
          this.dropdownContainer.style.display = "none";
          this.state.isDropdownVisible = false;
          this.state.isClosing = false; // 重置关闭状态
        } else {
          // 如果此时应该保持打开,则恢复显示
          this.state.isClosing = false; // 重置关闭状态
          this.dropdownContainer.style.opacity = "1";
          this.dropdownContainer.style.transform = "scale(1)";
        }
      }, 300);
    },

    observeThemeChanges() {
      const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
          if (
            mutation.type === "attributes" &&
            mutation.attributeName === "data-mode"
          ) {
            this.isDarkMode =
              document.documentElement.getAttribute("data-mode") === "dark";
            this.updateStyles();
          }
        });
      });

      observer.observe(document.documentElement, {
        attributes: true,
        attributeFilter: ["data-mode"],
      });
    },
  };

  // 初始化应用
  App.init();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址