// ==UserScript== // @name 抖音私信内嵌浮窗 // @namespace https://github.com/douyin-chat-float-window // @version 2.2.1 // @description 自动在抖音 Web 页面内以内嵌浮窗打开官方私信页,支持拖动、缩放、最小化、关闭和状态记忆 // @author Ccc // @match https://www.douyin.com/* // @match https://*.douyin.com/* // @run-at document-start // @grant none // @noframes // ==/UserScript== (function () { 'use strict'; const SCRIPT_ID = 'douyin-chat-float-window'; const STORE_KEY = 'dy-chat-float-window-v1'; const DEFAULT_CHAT_URL = 'https://www.douyin.com/chat?isPopup=1'; const CHAT_URL_RE = /(?:^|[/?#&=._-])(im|message|chat|conversation|letter|webcast_im)(?:$|[/?#&=._-])/i; const MINIMIZED_WIDTH = 140; const MINIMIZED_HEIGHT = 36; const MINIMIZED_LEFT = 8; const MINIMIZED_BOTTOM = 42; const nativeOpen = window.open ? window.open.bind(window) : null; let floatWin = null; let iframe = null; let fallbackLink = null; let fallbackTimer = null; let currentUrl = ''; const state = Object.assign({ width: Math.round(window.innerWidth * 0.52), height: Math.round(window.innerHeight * 0.78), x: 0, y: 56, minimized: false, }, readState()); function readState() { try { return JSON.parse(localStorage.getItem(STORE_KEY)) || {}; } catch { return {}; } } function saveState() { try { localStorage.setItem(STORE_KEY, JSON.stringify(state)); } catch { // ignore } } function ready(fn) { if (document.body) { fn(); return; } document.addEventListener('DOMContentLoaded', fn, { once: true }); } function normalizeUrl(url) { if (!url) return null; try { return new URL(String(url), location.href); } catch { return null; } } function isDouyinHost(hostname) { return hostname === 'douyin.com' || hostname.endsWith('.douyin.com'); } function isChatWindowUrl(url) { const u = normalizeUrl(url); if (!u || !isDouyinHost(u.hostname)) return false; const text = `${u.pathname}${u.search}${u.hash}`; return CHAT_URL_RE.test(text); } function fakeOpenedWindow(url) { return { closed: false, focus() {}, close() { this.closed = true; }, location: { href: String(url || '') }, }; } function interceptWindowOpen() { if (!nativeOpen) return; window.open = function patchedWindowOpen(url, target, features) { if (isChatWindowUrl(url)) { openFloatingChat(url); return fakeOpenedWindow(url); } return nativeOpen(url, target, features); }; } function interceptAnchorClicks() { document.addEventListener('click', (event) => { const anchor = event.target && event.target.closest ? event.target.closest('a[href]') : null; if (!anchor || !isChatWindowUrl(anchor.href)) return; event.preventDefault(); event.stopPropagation(); openFloatingChat(anchor.href); }, true); } function injectStyle() { if (document.getElementById(SCRIPT_ID)) return; const style = document.createElement('style'); style.id = SCRIPT_ID; style.textContent = ` .dycf-window { position: fixed !important; left: 0; top: 0; width: 720px; height: 620px; min-width: 360px; min-height: 280px; max-width: calc(100vw - 16px); max-height: calc(100vh - 16px); background: #1f202b; border: 1px solid rgba(255,255,255,.12); border-radius: 12px; box-shadow: 0 18px 56px rgba(0,0,0,.38); overflow: hidden; z-index: 2147483646 !important; display: flex; flex-direction: column; color: #fff; } .dycf-window.dycf-minimized { min-width: ${MINIMIZED_WIDTH}px; min-height: ${MINIMIZED_HEIGHT}px !important; border-radius: 8px; box-shadow: 0 10px 28px rgba(0,0,0,.32); } .dycf-header { flex: 0 0 42px; height: 42px; display: flex; align-items: center; gap: 8px; padding: 0 8px 0 14px; background: #171821; border-bottom: 1px solid rgba(255,255,255,.08); cursor: move; user-select: none; } .dycf-title { flex: 1 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 14px; color: rgba(255,255,255,.92); } .dycf-actions { flex: 0 0 auto; display: flex; align-items: center; gap: 4px; } .dycf-btn { width: 28px; height: 28px; border: 0; border-radius: 7px; background: transparent; color: rgba(255,255,255,.78); cursor: pointer; font-size: 14px; line-height: 28px; text-align: center; } .dycf-btn:hover { background: rgba(255,255,255,.12); color: #fff; } .dycf-body { position: relative; flex: 1 1 auto; min-height: 0; background: #1f202b; } .dycf-window.dycf-minimized .dycf-body, .dycf-window.dycf-minimized .dycf-resize { display: none; } .dycf-window.dycf-minimized .dycf-header { height: ${MINIMIZED_HEIGHT}px; flex-basis: ${MINIMIZED_HEIGHT}px; padding: 0 8px 0 12px; cursor: pointer; } .dycf-window.dycf-minimized .dycf-title { font-size: 13px; } .dycf-window.dycf-minimized .dycf-btn[data-action="reload"], .dycf-window.dycf-minimized .dycf-btn[data-action="open"] { display: none; } .dycf-frame { display: block; width: 100%; height: 100%; border: 0; background: #1f202b; } .dycf-fallback { position: absolute; inset: 0; display: none; align-items: center; justify-content: center; padding: 24px; background: #1f202b; color: rgba(255,255,255,.78); text-align: center; font-size: 14px; line-height: 1.8; } .dycf-fallback.is-visible { display: flex; } .dycf-fallback a { color: #7aa7ff; text-decoration: none; } .dycf-resize { position: absolute; right: 0; bottom: 0; width: 18px; height: 18px; cursor: nwse-resize; z-index: 2; } .dycf-resize::after { content: ""; position: absolute; right: 5px; bottom: 5px; width: 8px; height: 8px; border-right: 2px solid rgba(255,255,255,.45); border-bottom: 2px solid rgba(255,255,255,.45); } `; (document.head || document.documentElement).appendChild(style); } function ensureDefaultGeometry() { const fallbackWidth = Math.round(window.innerWidth * 0.52); const fallbackHeight = Math.round(window.innerHeight * 0.78); state.width = clamp(state.width || fallbackWidth, 360, window.innerWidth - 16); state.height = clamp(state.height || fallbackHeight, 280, window.innerHeight - 16); if (!Number.isFinite(state.x) || state.x <= 0) { state.x = window.innerWidth - state.width - 24; } if (!Number.isFinite(state.y)) state.y = 56; clampToViewport(); } function clamp(value, min, max) { return Math.min(Math.max(value, min), Math.max(min, max)); } function clampToViewport() { state.width = clamp(state.width, 360, window.innerWidth - 16); state.height = clamp(state.height, 280, window.innerHeight - 16); state.x = clamp(state.x, 8, window.innerWidth - state.width - 8); state.y = clamp(state.y, 8, window.innerHeight - state.height - 8); } function applyGeometry() { if (!floatWin) return; clampToViewport(); const displayWidth = state.minimized ? MINIMIZED_WIDTH : state.width; const displayHeight = state.minimized ? MINIMIZED_HEIGHT : state.height; const displayX = state.minimized ? MINIMIZED_LEFT : state.x; const displayY = state.minimized ? clamp(window.innerHeight - MINIMIZED_BOTTOM - MINIMIZED_HEIGHT, 8, window.innerHeight - MINIMIZED_HEIGHT - 8) : state.y; floatWin.style.transform = `translate(${displayX}px, ${displayY}px)`; floatWin.style.width = `${displayWidth}px`; floatWin.style.height = `${displayHeight}px`; floatWin.classList.toggle('dycf-minimized', state.minimized); saveState(); } function createFloatingWindow() { injectStyle(); ensureDefaultGeometry(); const root = document.createElement('div'); root.className = 'dycf-window'; root.innerHTML = `
抖音私信浮窗
如果这里长时间空白,可能是抖音限制了页面内嵌。
在新标签页打开私信窗口
`; document.body.appendChild(root); floatWin = root; iframe = root.querySelector('.dycf-frame'); fallbackLink = root.querySelector('.dycf-fallback-link'); bindWindowEvents(root); applyGeometry(); updateMinimizeButton(); } function bindWindowEvents(root) { const header = root.querySelector('.dycf-header'); const resizeHandle = root.querySelector('.dycf-resize'); header.addEventListener('pointerdown', (event) => { if (event.target.closest('.dycf-btn')) return; if (state.minimized) { event.preventDefault(); state.minimized = false; applyGeometry(); updateMinimizeButton(); return; } startDrag(event); }); resizeHandle.addEventListener('pointerdown', startResize); root.addEventListener('click', (event) => { const button = event.target.closest('.dycf-btn'); if (!button) return; const action = button.dataset.action; if (action === 'close') { closeFloatingChat(); } else if (action === 'minimize') { state.minimized = !state.minimized; applyGeometry(); updateMinimizeButton(); } else if (action === 'reload' && iframe) { iframe.src = currentUrl; scheduleFallback(); } else if (action === 'open' && currentUrl && nativeOpen) { nativeOpen(currentUrl, '_blank', 'noopener,noreferrer'); } }); } function updateMinimizeButton() { if (!floatWin) return; const button = floatWin.querySelector('[data-action="minimize"]'); const title = floatWin.querySelector('.dycf-title'); if (button) { button.textContent = state.minimized ? '□' : '_'; button.title = state.minimized ? '还原' : '最小化'; } if (title) { title.textContent = state.minimized ? '抖音私信浮窗(点击还原)' : '抖音私信浮窗'; } } function startDrag(event) { event.preventDefault(); bringToFront(); const startX = event.clientX; const startY = event.clientY; const originX = state.x; const originY = state.y; setIframeInteractive(false); const move = (moveEvent) => { state.x = originX + moveEvent.clientX - startX; state.y = originY + moveEvent.clientY - startY; applyGeometry(); }; const up = () => { setIframeInteractive(true); window.removeEventListener('pointermove', move, true); window.removeEventListener('pointerup', up, true); }; window.addEventListener('pointermove', move, true); window.addEventListener('pointerup', up, true); } function startResize(event) { event.preventDefault(); bringToFront(); const startX = event.clientX; const startY = event.clientY; const originWidth = state.width; const originHeight = state.height; setIframeInteractive(false); const move = (moveEvent) => { state.width = originWidth + moveEvent.clientX - startX; state.height = originHeight + moveEvent.clientY - startY; state.minimized = false; applyGeometry(); updateMinimizeButton(); }; const up = () => { setIframeInteractive(true); window.removeEventListener('pointermove', move, true); window.removeEventListener('pointerup', up, true); }; window.addEventListener('pointermove', move, true); window.addEventListener('pointerup', up, true); } function setIframeInteractive(enabled) { if (iframe) iframe.style.pointerEvents = enabled ? '' : 'none'; } function bringToFront() { if (floatWin) floatWin.style.zIndex = '2147483646'; } function closeFloatingChat() { clearTimeout(fallbackTimer); fallbackTimer = null; if (floatWin) { floatWin.remove(); } floatWin = null; iframe = null; fallbackLink = null; currentUrl = ''; } function scheduleFallback() { if (!floatWin) return; const fallback = floatWin.querySelector('.dycf-fallback'); if (fallback) fallback.classList.remove('is-visible'); clearTimeout(fallbackTimer); fallbackTimer = setTimeout(() => { if (!floatWin) return; const nextFallback = floatWin.querySelector('.dycf-fallback'); if (nextFallback) nextFallback.classList.add('is-visible'); }, 6000); } function openFloatingChat(url) { const parsed = normalizeUrl(url); if (!parsed) return; ready(() => { if (!floatWin) createFloatingWindow(); currentUrl = parsed.href; state.minimized = false; applyGeometry(); updateMinimizeButton(); if (fallbackLink) fallbackLink.href = currentUrl; if (iframe && iframe.src !== currentUrl) { iframe.src = currentUrl; } if (iframe) { iframe.onload = () => { clearTimeout(fallbackTimer); const fallback = floatWin && floatWin.querySelector('.dycf-fallback'); if (fallback) fallback.classList.remove('is-visible'); }; } scheduleFallback(); }); } window.addEventListener('resize', () => { if (!floatWin) return; applyGeometry(); }); window.__dyChatFloatOpen = (url) => { openFloatingChat(url); return true; }; function autoOpenDefaultChat() { if (isChatWindowUrl(location.href)) return; openFloatingChat(DEFAULT_CHAT_URL); } interceptWindowOpen(); ready(interceptAnchorClicks); ready(autoOpenDefaultChat); console.log('[抖音私信内嵌浮窗] v2.2.1 已加载。页面打开后会自动内嵌官方私信页。'); })();