抖音显示用户UID

// ==UserScript==
// @name 抖音显示用户UID
// @namespace https://xjrx.net
// @version 1.1.0
// @description 在抖音用户主页显示用户UID,点击即可复制(支持路由切换)
// @author YourName
// @license MIT
// @match *://*.douyin.com/*
// @match *://*.iesdouyin.com/*
// @grant none
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// ============ 配置 ============
const CONFIG = {
// 是否显示复制成功提示
showToast: true,
// Toast显示时间(毫秒)
toastDuration: 2000,
// 是否在控制台输出日志
debug: false
};
// ============ 工具函数 ============
function log(...args) {
if (CONFIG.debug) {
console.log('[UID]', ...args);
}
}
// ============ 样式 ============
const STYLE = `
.gm-user-uid-display {
color: var(--color-text-t3, #8a8a8a);
margin-right: 20px;
font-size: 12px;
line-height: 20px;
cursor: pointer;
user-select: all;
padding: 2px 8px;
border-radius: 4px;
transition: background-color 0.2s;
display: inline-block;
}
.gm-user-uid-display:hover {
background-color: var(--color-bg-b1, rgba(0,0,0,0.05));
}
.gm-user-uid-display .uid-label {
opacity: 0.6;
}
.gm-user-uid-display .uid-value {
font-weight: 600;
color: var(--color-text-t1, #1a1a1a);
}
.gm-user-uid-copied {
animation: uid-copied-flash 0.6s ease;
}
@keyframes uid-copied-flash {
0%, 100% { background-color: transparent; }
50% { background-color: #4caf50; color: white; }
}
`;
// ============ Toast ============
let toastTimer = null;
function showToast(message) {
if (!CONFIG.showToast) return;
const existing = document.querySelector('.gm-uid-toast');
if (existing) {
existing.remove();
if (toastTimer) {
clearTimeout(toastTimer);
toastTimer = null;
}
}
const toast = document.createElement('div');
toast.className = 'gm-uid-toast';
toast.textContent = message;
Object.assign(toast.style, {
position: 'fixed',
bottom: '80px',
left: '50%',
transform: 'translateX(-50%)',
backgroundColor: 'rgba(0,0,0,0.85)',
color: '#fff',
padding: '10px 24px',
borderRadius: '8px',
fontSize: '14px',
zIndex: '99999',
maxWidth: '90%',
textAlign: 'center',
animation: 'fadeInOut 2s ease forwards',
pointerEvents: 'none',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
});
if (!document.querySelector('#gm-uid-toast-style')) {
const style = document.createElement('style');
style.id = 'gm-uid-toast-style';
style.textContent = `
@keyframes fadeInOut {
0% { opacity: 0; transform: translateX(-50%) translateY(10px); }
15% { opacity: 1; transform: translateX(-50%) translateY(0); }
85% { opacity: 1; transform: translateX(-50%) translateY(0); }
100% { opacity: 0; transform: translateX(-50%) translateY(-10px); }
}
`;
document.head.appendChild(style);
}
document.body.appendChild(toast);
toastTimer = setTimeout(() => {
if (toast.parentNode) {
toast.remove();
}
toastTimer = null;
}, CONFIG.toastDuration);
}
// ============ 核心功能 ============
// 判断是否在用户主页
function isUserPage() {
const path = window.location.pathname;
return path.startsWith('/user/') && !path.includes('/search/');
}
// 从URL提取sec_uid
function getSecUidFromUrl() {
const match = window.location.pathname.match(/\/user\/([^/?]+)/);
return match ? match[1] : null;
}
// 通过多种方式获取UID
function getUidFromPage() {
// 方法1:从React Fiber中提取
const userInfoSelectors = [
'[data-e2e="user-detail"] [data-e2e="user-info"]',
'[data-e2e="user-info"]',
'.user-info-container'
];
for (const selector of userInfoSelectors) {
const el = document.querySelector(selector);
if (!el) continue;
try {
// 查找React属性
const reactKey = Object.keys(el).find(key =>
key.startsWith('__reactFiber$') ||
key.startsWith('__reactInternalInstance$')
);
if (reactKey) {
let fiber = el[reactKey];
let depth = 0;
while (fiber && depth < 30) {
const props = fiber.memoizedProps || fiber.pendingProps;
if (props) {
// 尝试多种可能的uid路径
const uidPaths = [
props.userInfo?.uid,
props.user?.uid,
props.data?.userInfo?.uid,
props.data?.user?.uid,
props.author?.uid,
props.userInfo?.userId,
props.user?.userId
];
for (const uid of uidPaths) {
if (uid && typeof uid === 'string' && uid.length > 5) {
log('从React获取到UID:', uid);
return uid;
}
}
}
if (fiber.return) {
fiber = fiber.return;
depth++;
} else {
break;
}
}
}
} catch (e) {
log('React解析失败:', e);
}
}
// 方法2:从页面文本中提取
const textNodes = document.querySelectorAll('[data-e2e="user-info"] *');
for (const node of textNodes) {
const text = node.textContent || '';
// 匹配 "抖音号:xxx" 或 "UID:xxx"
let match = text.match(/抖音号[::]\s*([a-zA-Z0-9_-]+)/);
if (match) {
log('从文本获取到抖音号:', match[1]);
return match[1];
}
match = text.match(/UID[::]\s*(\d+)/);
if (match) {
log('从文本获取到UID:', match[1]);
return match[1];
}
}
// 方法3:从页面全局数据中获取
try {
// 检查 __INITIAL_STATE__
if (window.__INITIAL_STATE__) {
const state = window.__INITIAL_STATE__;
const uidPaths = [
state.userInfo?.uid,
state.user?.uid,
state.userInfo?.userId,
state.user?.userId
];
for (const uid of uidPaths) {
if (uid && typeof uid === 'string' && uid.length > 5) {
log('从__INITIAL_STATE__获取到UID:', uid);
return uid;
}
}
}
} catch (e) {}
// 方法4:使用sec_uid作为备选
const secUid = getSecUidFromUrl();
if (secUid && secUid.length > 5) {
log('使用sec_uid作为UID:', secUid);
return secUid;
}
return null;
}
// 创建UID显示元素
function createUidElement(uid) {
const container = document.createElement('span');
container.className = 'gm-user-uid-display';
container.innerHTML = `
<span class="uid-label">UID:</span>
<span class="uid-value">${uid}</span>
`;
container.addEventListener('click', async function(e) {
e.stopPropagation();
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(uid);
} else {
// 降级方案
const textarea = document.createElement('textarea');
textarea.value = uid;
textarea.style.cssText = 'position:fixed;opacity:0;pointer-events:none;';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}
this.classList.add('gm-user-uid-copied');
setTimeout(() => this.classList.remove('gm-user-uid-copied'), 600);
showToast('✅ 已复制: ' + uid);
} catch (err) {
console.error('复制失败:', err);
showToast('❌ 复制失败,请手动复制');
}
});
return container;
}
// 注入UID到页面
function injectUid() {
// 只在用户主页执行
if (!isUserPage()) {
log('不在用户主页,跳过');
return false;
}
// 查找用户信息容器
const userInfoSelectors = [
'[data-e2e="user-detail"] [data-e2e="user-info"]',
'[data-e2e="user-info"]',
'.user-info-container',
'.user-detail .user-info'
];
let targetElement = null;
for (const selector of userInfoSelectors) {
const el = document.querySelector(selector);
if (el && el.offsetParent !== null) { // 确保元素可见
targetElement = el;
break;
}
}
if (!targetElement) {
log('未找到用户信息元素');
return false;
}
// 检查是否已经注入
if (targetElement.querySelector('.gm-user-uid-display')) {
log('UID已存在,跳过');
return true;
}
// 获取UID
const uid = getUidFromPage();
if (!uid) {
log('未获取到UID');
return false;
}
// 注入UID
const uidElement = createUidElement(uid);
// 找到合适的插入位置
const insertTarget = targetElement.querySelector('p:last-child') ||
targetElement.querySelector('.user-info-desc') ||
targetElement.querySelector('.user-info-stats') ||
targetElement.lastElementChild;
if (insertTarget && insertTarget !== targetElement) {
insertTarget.after(uidElement);
} else {
targetElement.appendChild(uidElement);
}
log('✅ UID注入成功:', uid);
return true;
}
// ============ 路由监听 ============
// 清理旧的UID元素
function cleanupUidElements() {
document.querySelectorAll('.gm-user-uid-display').forEach(el => el.remove());
}
// 防抖执行
function debounce(fn, delay = 300) {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
};
}
// 检查并注入(带防抖)
const debouncedInject = debounce(() => {
cleanupUidElements();
injectUid();
}, 300);
// 监听路由变化
function watchRouteChange() {
let lastUrl = window.location.href;
// 方法1:监听 popstate
window.addEventListener('popstate', () => {
const currentUrl = window.location.href;
if (currentUrl !== lastUrl) {
lastUrl = currentUrl;
log('路由变化(popstate):', currentUrl);
debouncedInject();
}
});
// 方法2:监听 hashchange
window.addEventListener('hashchange', () => {
const currentUrl = window.location.href;
if (currentUrl !== lastUrl) {
lastUrl = currentUrl;
log('路由变化(hashchange):', currentUrl);
debouncedInject();
}
});
// 方法3:监听 DOM 变化(SPA 路由)
let observerTimer = null;
const domObserver = new MutationObserver(() => {
// 检查URL是否变化
const currentUrl = window.location.href;
if (currentUrl !== lastUrl) {
lastUrl = currentUrl;
log('路由变化(DOM):', currentUrl);
// 清理旧的UID
cleanupUidElements();
// 延迟等待新页面渲染
if (observerTimer) clearTimeout(observerTimer);
observerTimer = setTimeout(() => {
injectUid();
observerTimer = null;
}, 500);
}
});
domObserver.observe(document, {
subtree: true,
childList: true,
attributes: true,
attributeFilter: ['href']
});
// 方法4:监听 history.pushState 和 replaceState
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function(...args) {
const result = originalPushState.apply(this, args);
const currentUrl = window.location.href;
if (currentUrl !== lastUrl) {
lastUrl = currentUrl;
log('路由变化(pushState):', currentUrl);
debouncedInject();
}
return result;
};
history.replaceState = function(...args) {
const result = originalReplaceState.apply(this, args);
const currentUrl = window.location.href;
if (currentUrl !== lastUrl) {
lastUrl = currentUrl;
log('路由变化(replaceState):', currentUrl);
debouncedInject();
}
return result;
};
}
// ============ 初始化 ============
function init() {
// 添加样式
const styleEl = document.createElement('style');
styleEl.textContent = STYLE;
document.head.appendChild(styleEl);
// 监听路由变化
watchRouteChange();
// 初始注入
setTimeout(() => {
if (isUserPage()) {
injectUid();
}
}, 500);
// 页面完全加载后再尝试一次
if (document.readyState === 'complete') {
setTimeout(() => {
if (isUserPage()) {
injectUid();
}
}, 1000);
} else {
window.addEventListener('load', () => {
setTimeout(() => {
if (isUserPage()) {
injectUid();
}
}, 1000);
});
}
log('🚀 抖音UID显示脚本已启动');
log('当前页面:', window.location.href);
log('是否用户主页:', isUserPage());
}
// 执行
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
发布时间: