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