UPhone 切号工具

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

// ==UserScript==
// @name         UPhone 切号工具
// @namespace    http://tampermonkey.net/
// @license      MIT
// @version      3.6
// @description  实现UPhone账号管理,实现多账号切换
// @author       kkkkkba
// @match        https://uphone.wo-adv.cn/cloudphone/*
// @grant        none
// ==/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: '250px',
      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: '14px', 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, head: accHead } = 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 = (txt, bg, color, border) => {
          const b = document.createElement('button');
          b.textContent = txt;
          Object.assign(b.style, {
            padding: '4px 8px', fontSize: '12px', borderRadius: '8px', cursor: 'pointer',
            background: bg, color, border: border || 'none'
          });
          return b;
        };

        const bSwitch = btn('切换', '#409EFF', '#fff');
        
        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>';

        
        //  bSwitch
        nm.onclick = async () => {
          if (!confirm(`确定切换到账号「${name}」吗?`)) return;
          const baseInfoStr = localStorage.getItem('baseInfo');
          if (!baseInfoStr) return alert('账号信息不存在');
          
          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: {
                  'Accept': '*/*',
                  'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
                  'Authorization': baseInfo.data.token,
                  'Connection': 'keep-alive',
                  'Referer': 'https://uphone.wo-adv.cn/cloudphone/',
                  'Sec-Fetch-Dest': 'empty',
                  'Sec-Fetch-Mode': 'cors',
                  'Sec-Fetch-Site': 'same-origin',
                  'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1 Edg/139.0.0.0',
                  'channel': 'bucp-master',
                  'channelCode': 'bucp-master',
                  'deviceId': 'c5c62d1dc02086a283f71c63197131da',
                  'os': 'H5',
                  'source': '4'
                }
              });
              
              if (response.ok) {
                const result = await response.json();
                if (result.code === 200 && result.data) {
                  baseInfo.data.userInfo = result.data;
                  showToast('用户信息获取成功', 1000);
                } else {
                  console.warn('获取用户信息失败:', result.message || '未知错误');
                  showToast('获取用户信息失败,使用空用户信息', 1500);
                }
              } else {
                throw new Error(`HTTP错误: ${response.status}`);
              }
            } catch (error) {
              console.error('请求用户信息失败:', error);
              showToast('获取用户信息失败,使用空用户信息', 1500);
            }
            
            // 保存更新后的baseInfo
            localStorage.setItem('baseInfo', JSON.stringify(baseInfo));
            showToast('切换中...', 800);
            
            setTimeout(() => location.reload(), 800);
            
          } catch (e) {
            console.error(e);
            alert('切换失败:baseInfo 解析错误');
          }
        };
        
        bDel.onclick = () => {
          if (!confirm(`确定删除账号「${name}」?此操作不可恢复!`)) return;
          const a = getAccounts();
          delete a[name];
          saveAccounts(a);
          refreshAccountList();
        };

        // actions.appendChild(bSwitch);
        actions.appendChild(bDel);

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

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

    const btnAdd = document.createElement('button');
    btnAdd.textContent = '添加账号';
    Object.assign(btnAdd.style, { flex: '1', padding: '8px 12px', fontSize: '13px', background: '#e6f0ff', color: '#2563eb', border: '1px solid #bfdbfe', borderRadius: '10px', cursor: 'pointer' });
    btnAdd.onclick = () => {
      const name = prompt('请输入账号名称:');
      if (!name) return;
      const token = prompt('请输入该账号的 token:');
      if (!token) return;
      const a = getAccounts();
      if (a[name] && !confirm(`账号「${name}」已存在,是否覆盖?`)) return;
      a[name] = token;
      saveAccounts(a);
      refreshAccountList();
    };

    const btnLogout = document.createElement('button');
    btnLogout.textContent = '退出登录';
    Object.assign(btnLogout.style, { flex: '1', padding: '8px 12px', fontSize: '13px', background: '#fff0f0', color: '#ef4444', border: '1px solid #fecaca', borderRadius: '10px', cursor: 'pointer' });
    btnLogout.onclick = () => {
      if (!confirm('确定要退出当前账号吗?')) return;
      const baseInfoStr = localStorage.getItem('baseInfo');
      if (!baseInfoStr) return alert('账号信息不存在');
      try {
        const baseInfo = JSON.parse(baseInfoStr);
        baseInfo.data.token = '';
        baseInfo.data.userInfo = {};
        localStorage.setItem('baseInfo', JSON.stringify(baseInfo));
        toast('退出中...');
        setTimeout(() => location.reload(), 800);
      } catch (e) {
        console.error(e); alert('退出失败:baseInfo 解析错误');
      }
    };

    controls.appendChild(btnAdd);
    controls.appendChild(btnLogout);

    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('mousedown', onMouseDown);
    // container.addEventListener('mousedown', (e) => { if (e.target === container) onMouseDown(e); });
    // window.addEventListener('mousemove', onMouseMove);
    // window.addEventListener('mouseup', onMouseUp);

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