UPhone Token Helper

实现UPhone账号管理,实现多账号切换

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         UPhone Token Helper
// @namespace    http://tampermonkey.net/
// @license      MIT
// @version      5.0
// @description  实现UPhone账号管理,实现多账号切换
// @author       kkkkkba
// @match        https://uphone.wo-adv.cn/cloudphone/*
// @match        https://uphone.wo-adv.cn/cloudphone/#/cloud-phone-list
// @match        https://uphone.wo-adv.cn/cloudphone/#/home
// @grant        GM_xmlhttpRequest
// @connect      member.zlhz.wostore.cn
// ==/UserScript==

(function () {
  'use strict';

  // —— 可配置常量 ——
  const STORAGE_ACCOUNTS = '__uphone_token_accounts__';
  const STORAGE_FLOATPOS = '__uphone_float_position__';

  const FLOAT_SIZE = 50;        // 浮球尺寸
  const PEEK_OFFSET = 25;       // 半隐藏露出像素
  const DRAG_THRESHOLD = 5;     // 判定拖动的最小移动距离(像素)

  const getAccounts = () => JSON.parse(localStorage.getItem(STORAGE_ACCOUNTS) || '{}');
  const saveAccounts = (obj) => localStorage.setItem(STORAGE_ACCOUNTS, JSON.stringify(obj));

  // 帮助:节流
  const raf = (fn) => requestAnimationFrame(fn);

  // —— 初始化 ——
  window.addEventListener('load', () => {
    // ===== 根容器(承载浮球与面板) =====
    const container = document.createElement('div');
    Object.assign(container.style, {
      position: 'fixed',
      top: '150px',
      right: '0',
      width: FLOAT_SIZE + 'px',
      height: FLOAT_SIZE + 'px',
      zIndex: '99999',
      userSelect: 'none',
      transition: 'transform .25s ease, right .25s ease, left .25s ease, top .25s ease',
      transform: 'translateX(' + PEEK_OFFSET + 'px)', // 初始半隐藏在右侧
    });

    // 恢复浮球停靠边与纵向位置
    const savedPos = (() => {
      try { return JSON.parse(localStorage.getItem(STORAGE_FLOATPOS) || '{}'); } catch (_) { return {}; }
    })();
    let dockEdge = savedPos.edge === 'left' ? 'left' : 'right'; // 默认右侧
    if (dockEdge === 'left') {
      container.style.left = '0px';
      container.style.right = 'auto';
      container.style.transform = 'translateX(-' + PEEK_OFFSET + 'px)';
    } else {
      container.style.right = '0px';
      container.style.left = 'auto';
      container.style.transform = 'translateX(' + PEEK_OFFSET + 'px)';
    }
    if (savedPos.top) container.style.top = savedPos.top;

    // ===== 浮动按钮(悬浮球) =====
    const floatBtn = document.createElement('div');
    Object.assign(floatBtn.style, {
      width: '100%',
      height: '100%',
      background: '#409EFF',
      color: '#fff',
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      borderRadius: '50%',
      boxShadow: '0 4px 12px rgba(64,158,255,.4)',
      border: '2px solid rgba(255,255,255,.35)',
      cursor: 'grab',
      transition: 'background .2s ease, opacity .2s ease, transform .2s ease',
      fontSize: '22px',
      lineHeight: 1,
      userSelect: 'none',
    });
    floatBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"></rect><line x1="12" y1="18" x2="12" y2="18"></line></svg>';

    // ===== 全局toast管理 =====
    let toastQueue = [];
    let isShowingToast = false;

    // 改进的toast函数
    function showToast(message, duration = 2000) {
      toastQueue.push({ message, duration });
      
      if (!isShowingToast) {
        processToastQueue();
      }
    }

    // 处理toast队列
    function processToastQueue() {
      if (toastQueue.length === 0) {
        isShowingToast = false;
        return;
      }
      
      isShowingToast = true;
      const { message, duration } = toastQueue.shift();
      
      // 显示toast(假设您已有toast函数)
      toast(message);
      
      setTimeout(() => {
        processToastQueue();
      }, duration);
    }    
    // ===== 控制面板 =====
    const panel = document.createElement('div');
    Object.assign(panel.style, {
      position: 'absolute',
      top: '0',
      right: dockEdge === 'right' ? (FLOAT_SIZE + 10) + 'px' : 'auto',
      left: dockEdge === 'left' ? (FLOAT_SIZE + 10) + 'px' : 'auto',
      width: '200px',
      background: '#fff',
      borderRadius: '12px',
      boxShadow: '0 8px 24px rgba(0,0,0,.15)',
      padding: '14px',
      fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, PingFang SC, Microsoft YaHei, sans-serif',
      opacity: '0',
      transform: 'translateX(12px)',
      pointerEvents: 'none',
      transition: 'opacity .2s ease, transform .2s ease',
    });

    const section = (titleText) => {
      const wrap = document.createElement('div');
      // wrap.style.marginBottom = '12px';
      const head = document.createElement('div');
      // Object.assign(head.style, {
      //   fontSize: '16px', fontWeight: '900', color: '#333', marginBottom: '8px',
      //   display: 'flex', alignItems: 'center', gap: '6px'
      // });
      // head.textContent = titleText;
      // wrap.appendChild(head);
      return { wrap, head };
    };


    // —— 账号管理 ——
    // —— 账号管理 —— 
    const divider = document.createElement('div');
    Object.assign(divider.style, { height: '1px', background: '#f1f5f9'});
    panel.appendChild(divider);

    const { wrap: accWrap } = section('账号管理');

    const list = document.createElement('div');
    Object.assign(list.style, { maxHeight: '260px', overflowY: 'auto', marginBottom: '10px' });

    const refreshAccountList = () => {
      list.innerHTML = '';
      const accounts = getAccounts();
      const names = Object.keys(accounts);
      if (!names.length) {
        const empty = document.createElement('div');
        empty.textContent = '暂无保存的账号';
        Object.assign(empty.style, { color: '#94a3b8', textAlign: 'center', padding: '12px', fontSize: '13px' });
        list.appendChild(empty);
        return;
      }

      names.forEach((name) => {
        const token = accounts[name];
        const item = document.createElement('div');
        Object.assign(item.style, {
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          background: '#EDF2FA', padding: '8px 10px', borderRadius: '10px', marginBottom: '8px'
        });

        const nm = document.createElement('div');
        nm.textContent = name;
        Object.assign(nm.style, {
          flex: '1', minWidth: 0,
          fontSize: '14px', fontWeight: '400',
          overflow: 'hidden', textOverflow: 'ellipsis',
          whiteSpace: 'nowrap'
        });

        const actions = document.createElement('div');
        Object.assign(actions.style, { display: 'flex', gap: '6px' });

        // 通用按钮样式
        const btn = (html, bg) => {
          const b = document.createElement('button');
          b.innerHTML = html;
          Object.assign(b.style, {
            padding: '4px 8px', fontSize: '12px',
            borderRadius: '8px', cursor: 'pointer',
            background: bg, color: '#fff', border: 'none'
          });
          return b;
        };

        // // ====== 新增:复制Token ======
        // const bCopy = btn(
        //   '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"></rect><path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg>',
        //   '#6366F1'
        // );
        // bCopy.title = '复制Token';
        // bCopy.onclick = () => {
        //   navigator.clipboard.writeText(token);
        //   showToast('Token已复制', 1200);
        // };

        // ===== 修改按钮 =====
        const bEdit = btn('', '#4AC960', '#fff');
        bEdit.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>';
        bEdit.title = '修改Token';
        bEdit.onclick = () => {
          const newToken = prompt(`请输入账号「${name}」的新Token:`, token);
          if (newToken === null) return;
          if (!newToken.trim()) return alert('Token不能为空!');

          const a = getAccounts();
          a[name] = newToken.trim();
          saveAccounts(a);
          refreshAccountList();
          showToast('Token已更新', 1500);
        };

        // ===== 删除按钮 =====
        const bDel = btn('', '#FC886F', '#fff');
        bDel.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>';
        bDel.title = '删除账号';
        bDel.onclick = () => {
          if (!confirm(`确定删除账号「${name}」?`)) return;
          const a = getAccounts();
          delete a[name];
          saveAccounts(a);
          refreshAccountList();
        };

        // ===== 原逻辑:点击名称来切换账号 =====
        nm.onclick = async () => {
          if (!confirm(`确定切换到账号「${name}」吗?`)) return;

          const baseInfoStr = localStorage.getItem('baseInfo');
          if (!baseInfoStr) return alert('baseInfo不存在');

          try {
            const baseInfo = JSON.parse(baseInfoStr);
            baseInfo.data.token = token;
            baseInfo.data.userInfo = {};

            // 请求用户信息(原逻辑不动)
            try {
              showToast('获取用户信息中...', 1500);
              const response = await fetch('https://uphone.wo-adv.cn/bucp/servers/system/user/getAppUserInfo', {
                method: 'GET',
                headers: {
                  'Authorization': baseInfo.data.token,
                  'User-Agent': 'Mozilla',
                  'channel': 'bucp-master',
                  'channelCode': 'bucp-master',
                  'os': 'H5',
                  'source': '4'
                }
              });
              const result = await response.json();

              if (result.code === 200 && result.data) {
                baseInfo.data.userInfo = result.data;
                showToast('用户信息获取成功', 1000);
              } else {
                showToast('获取用户信息失败:' + (result.msg || '未知错误'), 1500);
                return;
              }
            } catch (e) {
              showToast('获取用户信息失败', 1500);
              return;
            }

            localStorage.setItem('baseInfo', JSON.stringify(baseInfo));
            showToast('切换中...', 800);
            setTimeout(() => location.reload(), 1800);

          } catch (e) {
            alert('baseInfo 解析失败');
          }
        };

        // actions.appendChild(bCopy);
        actions.appendChild(bEdit);
        actions.appendChild(bDel);

        item.appendChild(nm);
        item.appendChild(actions);
        list.appendChild(item);
      });
    };

    // ===== 下方按钮区 ===================================================================================================================================
    //===================================================================================================================================
    // ===================== 控制按钮区域 =====================
    const controls = document.createElement('div');
    Object.assign(controls.style, {
      width: '100%',
      display: 'flex',
      flexDirection: 'column',
      gap: '10px',
      marginTop: '10px'
    });

    // 工厂: 创建按钮
    function createBtn(text, bgColor, bdColor, txtColor, onClick) {
      const btn = document.createElement('button');
      btn.textContent = text;
      Object.assign(btn.style, {
        padding: '8px 12px',
        fontSize: '13px',
        background: bgColor,
        color: txtColor,
        border: '1px solid ' + bdColor,
        borderRadius: '10px',
        cursor: 'pointer',
        flex: '1',
        whiteSpace: 'nowrap'
      });
      btn.onclick = onClick;
      return btn;
    }

    // ===================== 第一行按钮(固定显示) =====================
    const row1 = document.createElement('div');
    Object.assign(row1.style, {
      display: 'flex',
      gap: '10px'
    });

    // 添加账号
    const btnAdd = createBtn(
      '添加',
      '#e6f0ff', '#bfdbfe', '#2563eb',
      () => {
        const name = prompt('请输入账号名称:');
        if (!name) return;
        const token = prompt('请输入 token:');
        if (!token) return;
        const a = getAccounts();
        a[name] = token.trim();
        saveAccounts(a);
        refreshAccountList();
        showToast('账号已添加', 1200);
      }
    );

    // 导入
    const btnImport = createBtn(
      '导入',
      '#fff7ed', '#fed7aa', '#ea580c',
      () => {
        const txt = prompt('粘贴导入 JSON:');
        if (!txt) return;
        try {
          saveAccounts(JSON.parse(txt));
          refreshAccountList();
          showToast('导入成功', 1200);
        } catch {
          showToast('JSON 格式有误', 1500);
        }
      }
    );

    // 导出
    const btnExport = createBtn(
      '导出',
      '#eefce8', '#bbf7d0', '#16a34a',
      () => {
        navigator.clipboard.writeText(JSON.stringify(getAccounts(), null, 2));
        showToast('账号已复制', 1200);
      }
    );

    // 加入第一行
    [row1.appendChild(btnAdd), row1.appendChild(btnImport), row1.appendChild(btnExport)];
    controls.appendChild(row1);


    // ===================== 更多内容区(默认隐藏) =====================
    let expanded = false;

    const morePanel = document.createElement('div');
    Object.assign(morePanel.style, {
      display: 'none',            // 默认隐藏
      flexWrap: 'wrap',
      gap: '10px',
      marginTop: '5px'
    });

    // 恢复 TK
    const btnRestore = createBtn(
      '复活所有TK',
      '#f0f9ff', '#bae6fd', '#0284c7',
      () => {
        const accounts = getAccounts();
        const names = Object.keys(accounts);
        if (!names.length) return showToast('无可恢复账号', 1200);

        showToast('正在恢复...', 1200);

        names.forEach(name => {
          GM_xmlhttpRequest({
            method: "GET",
            url:
              "https://member.zlhz.wostore.cn/wcy_member/yunPhone/activity/coupon/list"
              + "?activityId=FREE_EQUITY_202504&token=" + encodeURIComponent(accounts[name]),
            headers: {
              "User-Agent": navigator.userAgent,
              "channel": "bucp-master",
              "channelCode": "bucp-master",
              "os": "H5",
              "source": "4"
            },
            onload: res => showToast(`账号 ${name} → ${res.status === 200 ? '成功' : '失败'}`, 1200),
            onerror: () => showToast(`账号 ${name} 异常`, 1200)
          });
        });
      }
    );

    // 复制当前 TK
    const btnCopyTK = createBtn(
      '复制当前TK',
      '#fef2f2', '#fecaca', '#dc2626',
      () => {
        try {
          const baseInfo = JSON.parse(localStorage.getItem('baseInfo') || '{}');
          const tk = baseInfo?.data?.token;
          if (!tk) return showToast('未找到 Token', 1300);
          navigator.clipboard.writeText(tk);
          showToast('当前 Token 已复制', 1200);
        } catch {
          showToast('复制失败', 1300);
        }
      }
    );


    // 将这些按钮加入更多区
    morePanel.appendChild(btnRestore);
    morePanel.appendChild(btnCopyTK);

    // 未来新增按钮 → 在这里 appendChild 即可自动换行

    controls.appendChild(morePanel);


    // ===================== “更多” / “收起”按钮 =====================
    const btnMore = createBtn(
      '更多',
      '#f3f4f6', '#e5e7eb', '#374151',
      () => {
        expanded = !expanded;
        morePanel.style.display = expanded ? 'flex' : 'none';
        btnMore.textContent = expanded ? '收起' : '更多';
      }
    );

    Object.assign(btnMore.style, { width: '100%' });
    controls.appendChild(btnMore);

    // 添加到面板
    accWrap.appendChild(controls);



    // 添加进面板
    accWrap.appendChild(list);
    accWrap.appendChild(controls);
    panel.appendChild(accWrap);


    // —— 轻提示 ——
    function toast(text) {
      const el = document.createElement('div');
      el.textContent = text;
      Object.assign(el.style, {
        position: 'fixed', left: '50%', top: '12%', transform: 'translateX(-50%)',
        background: 'rgba(0,0,0,.8)', color: '#fff', padding: '8px 12px', borderRadius: '999px',
        fontSize: '12px', zIndex: '100000', opacity: '0', transition: 'opacity .2s ease'
      });
      document.body.appendChild(el);
      requestAnimationFrame(() => el.style.opacity = '1');
      setTimeout(() => { el.style.opacity = '0'; setTimeout(() => el.remove(), 200); }, 1600);
    }

    // ===== 交互:展开/收起 & 半隐藏 =====
    let isExpanded = false;   // 面板是否展开
    let isHovering = false;   // 鼠标是否悬停容器
    let isDragging = false;   // 是否拖动中
    let dragStartX = 0, dragStartY = 0, moved = false;

    function applyPeekHidden(hidden) {
      if (isExpanded) { container.style.transform = 'translateX(0)'; return; }
      const tx = hidden ? (dockEdge === 'right' ? PEEK_OFFSET : -PEEK_OFFSET) : 0;
      container.style.transform = `translateX(${tx}px)`;
    }

    function togglePanel(force) {
      const willExpand = typeof force === 'boolean' ? force : !isExpanded;
      isExpanded = willExpand;
      if (willExpand) {
        refreshAccountList();
        // 面板在当前停靠边的反方向展开
        panel.style.left  = dockEdge === 'left'  ? (FLOAT_SIZE + 10) + 'px' : 'auto';
        panel.style.right = dockEdge === 'right' ? (FLOAT_SIZE + 10) + 'px' : 'auto';
        panel.style.opacity = '1';
        panel.style.transform = 'translateX(0)';
        panel.style.pointerEvents = 'auto';
        applyPeekHidden(false);
      } else {
        panel.style.opacity = '0';
        panel.style.transform = 'translateX(12px)';
        panel.style.pointerEvents = 'none';
        applyPeekHidden(true);
      }
    }

    container.addEventListener('mouseenter', () => { isHovering = true; applyPeekHidden(false); });
    container.addEventListener('mouseleave', () => {
      isHovering = false;
      setTimeout(() => { if (!isHovering && !isExpanded && !isDragging) applyPeekHidden(true); }, 10);
    });

    // 点击与拖动判定
    const onMouseDown = (e) => {
      isDragging = true; moved = false;
      dragStartX = e.clientX; dragStartY = e.clientY;
      floatBtn.style.cursor = 'grabbing';
      document.body.style.userSelect = 'none';
    };
    const onMouseMove = (e) => {
      if (!isDragging) return;
      const dx = e.clientX - dragStartX; const dy = e.clientY - dragStartY;
      if (!moved && (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD)) moved = true;
      // 临时跟随指针(解除停靠)
      container.style.left = (e.clientX - FLOAT_SIZE / 2) + 'px';
      container.style.top  = (e.clientY - FLOAT_SIZE / 2) + 'px';
      container.style.right = 'auto';
      applyPeekHidden(true); // 拖拽中保持完全显示
    };
    const onMouseUp = (e) => {
      if (!isDragging) return;
      isDragging = false; floatBtn.style.cursor = 'grab'; document.body.style.userSelect = '';

      // 是否当作点击?
      if (!moved) {
         togglePanel(); 
         return; 
      }

      // 结束时吸附到最近边 & 规范位置
      const rect = container.getBoundingClientRect();
      const centerX = rect.left + rect.width / 2;
      dockEdge = centerX < window.innerWidth / 2 ? 'left' : 'right';

      const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
      const topPx = clamp(rect.top, 0, window.innerHeight - FLOAT_SIZE);
      container.style.top = topPx + 'px';

      if (dockEdge === 'left') {
        container.style.left = '0px';
        container.style.right = 'auto';
      } else {
        container.style.right = '0px';
        container.style.left = 'auto';
      }

      // 记忆停靠边与纵向位置
      localStorage.setItem(STORAGE_FLOATPOS, JSON.stringify({ edge: dockEdge, top: container.style.top }));

      // 根据停靠边应用半隐藏
      raf(() => applyPeekHidden(true));
    };

    // 绑定鼠标事件(容器与按钮都能拖)
    floatBtn.addEventListener('pointerdown', onMouseDown);
    window.addEventListener('pointermove', onMouseMove);
    window.addEventListener('pointerup', onMouseUp);

    // 触摸支持
    floatBtn.addEventListener('touchstart', (e) => onMouseDown(e.touches[0]), { passive: true });
    window.addEventListener('touchmove', (e) => onMouseMove(e.touches[0]), { passive: true });
    window.addEventListener('touchend', (e) => onMouseUp(e.changedTouches?.[0] || e));

    // ESC 关闭面板
    window.addEventListener('keydown', (e) => { if (e.key === 'Escape' && isExpanded) togglePanel(false); });

    // 窗口尺寸变化,确保不超出屏幕
    window.addEventListener('resize', () => {
      const rect = container.getBoundingClientRect();
      const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
      container.style.top = clamp(rect.top, 0, window.innerHeight - FLOAT_SIZE) + 'px';
      // 重新应用停靠位置与半隐藏
      if (dockEdge === 'left') { container.style.left = '0px'; container.style.right = 'auto'; }
      else { container.style.right = '0px'; container.style.left = 'auto'; }
      applyPeekHidden(!isExpanded && !isHovering);
    });

    // 将元素添加到页面
    container.appendChild(floatBtn);
    container.appendChild(panel);
    document.body.appendChild(container);

    // 初次渲染:列表 & 初始半隐藏
    refreshAccountList();
    setTimeout(() => { if (!isExpanded) applyPeekHidden(true); }, 10);
  });
})();