// ==UserScript== // @name 豆瓣电影排行榜 // @namespace https://scriptcat.org/zh-CN/ // @version 1.2.8 // @description 豆瓣电影分类排行榜移动端适配:28类无缝切换,无限滚动加载,自动续接下一页。左上搜索设置(6选项含默认跳转)、点海报按所选引擎搜索、右上搜索历史(≤30条自动清理)。https://movie.douban.com/chart可使用 // @author 失心向南 // @match *://movie.douban.com/chart* // @match *://movie.douban.com/typerank* // @match *://m.douban.com/movie/chart* // @grant GM_xmlhttpRequest // @connect movie.douban.com // @connect *.doubanio.com // @run-at document-start // @license MIT // ==/UserScript== (function () { 'use strict'; const vp = document.querySelector('meta[name="viewport"]'); if (vp) vp.setAttribute('content', 'width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no'); const CATEGORIES = [ ['11', '剧情'], ['24', '喜剧'], ['5', '动作'], ['13', '爱情'], ['17', '科幻'], ['25', '动画'], ['10', '悬疑'], ['19', '惊悚'], ['20', '恐怖'], ['1', '纪录片'], ['23', '短片'], ['6', '情色'], ['14', '音乐'], ['7', '歌舞'], ['28', '家庭'], ['8', '儿童'], ['2', '传记'], ['4', '历史'], ['22', '战争'], ['3', '犯罪'], ['27', '西部'], ['16', '奇幻'], ['15', '冒险'], ['12', '灾难'], ['29', '武侠'], ['30', '古装'], ['18', '运动'], ['31', '黑色电影'], ]; // ---------- 搜索引擎配置 ---------- const ENGINES = [ { key: 'ikan1', name: '爱看1', prefix: 'https://ikanbot.eu.org/search?q=' }, { key: 'ikan2', name: '爱看2', prefix: 'https://www1.ikanbot.com/search?q=' }, { key: 'quark', name: '夸克', prefix: 'https://m.quark.cn/s?q=' }, { key: 'bing', name: '必应', prefix: 'https://www.bing.com/search?q=' }, { key: 'sogou', name: '搜狗', prefix: 'https://www.sogou.com/web?query=' }, { key: 'default', name: '默认', prefix: '' }, ]; let engine = localStorage.getItem('db-rank-engine') || 'ikan1'; let state = { type: '11', start: 0, isLast: false, loading: false, }; const style = document.createElement('style'); style.textContent = ` html, body { margin: 0 !important; padding: 0 !important; background: #f5f5f7 !important; } body > *:not(#db-root):not(#db-engine-menu):not(#db-history-menu) { display: none !important; } #db-root { position: fixed; inset: 0; overflow-y: auto; -webkit-overflow-scrolling: touch; background: #f5f5f7; } #db-root * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; } #db-root .db-header { position: sticky; top: 0; z-index: 10; background: #fff; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; padding: max(12px, env(safe-area-inset-top)) 12px 12px; box-shadow: 0 1px 4px rgba(0,0,0,.06); } #db-root .db-icon-btn { width: 34px; height: 34px; border: 0; background: #f0f0f3; color: #333; border-radius: 17px; font-size: 18px; cursor: pointer; padding: 0; display: flex; align-items: center; justify-content: center; } #db-root .db-menu-btn { justify-self: start; } #db-root .db-history-btn { justify-self: end; } #db-root .db-title { font-size: 17px; font-weight: 700; color: #333; justify-self: center; line-height: 1; } #db-root .db-tabs { display: flex; gap: 8px; overflow-x: auto; padding: 10px 16px; background: #fff; scrollbar-width: none; border-bottom: 1px solid #f0f0f0; position: sticky; top: 58px; z-index: 9; } #db-root .db-tabs::-webkit-scrollbar { display: none; } #db-root .db-tabs button { flex: 0 0 auto; border: 0; background: #f0f0f3; padding: 7px 14px; border-radius: 18px; font-size: 13px; color: #666; cursor: pointer; white-space: nowrap; } #db-root .db-tabs button.active { background: #00b894; color: #fff; font-weight: 600; } #db-root .db-list { padding: 12px 12px 40px; } #db-root .db-movie { display: flex; gap: 12px; background: #fff; border-radius: 12px; padding: 12px; margin-bottom: 10px; cursor: pointer; } #db-root .db-movie img { width: 64px; height: 90px; border-radius: 6px; object-fit: cover; flex: 0 0 auto; background: #eee; } #db-root .db-info { flex: 1; min-width: 0; } #db-root .db-row { display: flex; align-items: center; gap: 8px; } #db-root .db-rank { font-size: 16px; font-weight: 800; color: #ccc; min-width: 24px; } #db-root .db-rank.r1 { color: #e74c3c; } #db-root .db-rank.r2 { color: #e67e22; } #db-root .db-rank.r3 { color: #f1c40f; } #db-root .db-name { font-size: 15px; font-weight: 700; color: #222; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #db-root .db-score { font-size: 15px; font-weight: 700; color: #00b894; flex: 0 0 auto; } #db-root .db-meta { font-size: 12px; color: #999; margin-top: 4px; } #db-root .db-actors { font-size: 12px; color: #aaa; margin-top: 4px; line-height: 1.5; } #db-root .db-sentinel { padding: 20px; text-align: center; color: #999; font-size: 13px; } #db-engine-menu, #db-history-menu { position: fixed; z-index: 9999; background: #fff; border-radius: 12px; box-shadow: 0 6px 24px rgba(0,0,0,.18); padding: 6px 0; display: none; } #db-engine-menu { top: 58px; left: 12px; min-width: 172px; } #db-history-menu { top: 58px; right: 12px; min-width: 210px; max-width: 260px; max-height: 62vh; overflow-y: auto; } #db-engine-menu.show, #db-history-menu.show { display: block; } #db-engine-menu h3, #db-history-menu h3 { font-size: 13px; color: #999; font-weight: 500; margin: 0; padding: 8px 16px; display: flex; align-items: center; justify-content: space-between; } #db-engine-menu button, #db-history-menu button { display: flex; align-items: center; width: 100%; border: 0; background: transparent; padding: 11px 16px; font-size: 15px; color: #333; cursor: pointer; text-align: left; } #db-engine-menu button .check { margin-left: auto; color: #00b894; font-weight: 700; } #db-engine-menu button.active { background: #e8f8f4; color: #00b894; } #db-history-menu button[data-htitle] { border-bottom: 1px solid #f5f5f7; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } #db-history-menu .db-history-empty { padding: 18px 16px; font-size: 13px; color: #999; text-align: center; } #db-history-menu #db-clear-history { width: auto; padding: 3px 10px; font-size: 12px; color: #666; background: #f0f0f3; border-radius: 10px; flex: 0 0 auto; } `; let root = document.createElement('div'); root.id = 'db-root'; let observer = null; let engineMenu = null; let historyMenu = null; // ---------- 搜索历史 ---------- function getHistory() { try { return JSON.parse(localStorage.getItem('db-rank-history') || '[]'); } catch (e) { return []; } } function saveHistory(arr) { localStorage.setItem('db-rank-history', JSON.stringify(arr)); } function pushHistory(title) { const h = getHistory().filter(x => x !== title); h.unshift(title); if (h.length > 30) h.length = 30; saveHistory(h); } function clearHistory() { saveHistory([]); } function escapeHtml(s) { return String(s).replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>'); } // 按当前引擎搜索片名:写入历史 + 打开搜索页(引擎为「默认」时回退第一个可搜索引擎) function searchTitle(title) { if (!title) return; pushHistory(title); renderHistoryMenu(); const en = ENGINES.find(e => e.key === engine && e.prefix) || ENGINES.find(e => e.prefix); window.open(en.prefix + encodeURIComponent(title), '_blank'); } // ---------- 下拉面板 ---------- function renderEngineMenu() { engineMenu.innerHTML = '

选择搜索引擎

' + ENGINES.map(en => { const act = en.key === engine; return ``; }).join(''); engineMenu.querySelectorAll('[data-engine]').forEach(b => b.onclick = (ev) => { ev.stopPropagation(); engine = b.dataset.engine; localStorage.setItem('db-rank-engine', engine); renderEngineMenu(); engineMenu.classList.remove('show'); }); } function renderHistoryMenu() { const h = getHistory(); const clearHtml = h.length ? '' : ''; historyMenu.innerHTML = `

搜索历史${clearHtml}

` + (h.length ? h.map(t => ``).join('') : '
暂无搜索记录
'); historyMenu.querySelectorAll('[data-htitle]').forEach(b => b.onclick = (ev) => { ev.stopPropagation(); searchTitle(b.dataset.htitle); renderHistoryMenu(); historyMenu.classList.remove('show'); }); const clearBtn = historyMenu.querySelector('#db-clear-history'); if (clearBtn) clearBtn.onclick = (ev) => { ev.stopPropagation(); clearHistory(); renderHistoryMenu(); }; } function injectPanels() { engineMenu = document.createElement('div'); engineMenu.id = 'db-engine-menu'; historyMenu = document.createElement('div'); historyMenu.id = 'db-history-menu'; document.body.appendChild(engineMenu); document.body.appendChild(historyMenu); renderEngineMenu(); renderHistoryMenu(); document.addEventListener('click', (ev) => { const inMenu = engineMenu.contains(ev.target) || historyMenu.contains(ev.target); const onBtn = ev.target.closest && ev.target.closest('#db-menu-btn, #db-history-btn'); if (!inMenu && !onBtn) { engineMenu.classList.remove('show'); historyMenu.classList.remove('show'); } }); } function doubanFetch(start) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: `https://movie.douban.com/j/chart/top_list?type=${state.type}&interval_id=100:90&action=&start=${start}&limit=20`, headers: { 'Referer': 'https://movie.douban.com/', 'Accept': 'application/json' }, onload: (r) => { try { resolve(JSON.parse(r.responseText)); } catch (e) { reject(e); } }, onerror: reject, }); }); } function renderMovies(movies) { return movies.map(m => `
${m.rank} ${m.title} ${m.score ? m.score + '分' : ''}
${(m.regions || []).join(' / ')} · ${(m.types || []).join(' / ')} · ${(m.release_date || '').slice(0, 4)}
${(m.actors || []).slice(0, 4).join(' / ')}
`).join(''); } function render() { root.innerHTML = `
豆瓣电影榜
${CATEGORIES.map(([id, name]) => `` ).join('')}
加载中…
`; // 确保有且仅有一个 active const buttons = root.querySelectorAll('[data-type]'); buttons.forEach(x => x.classList.toggle('active', x.dataset.type === state.type)); const activeBtn = root.querySelector('.db-tabs button.active'); if (activeBtn) activeBtn.scrollIntoView({ inline: 'center', block: 'nearest' }); root.querySelectorAll('[data-type]').forEach(b => b.onclick = () => { if (b.dataset.type === state.type) return; state.type = b.dataset.type; root.querySelectorAll('[data-type]').forEach(x => x.classList.toggle('active', x.dataset.type === state.type)); b.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }); load(true); }); const menuBtn = root.querySelector('#db-menu-btn'); if (menuBtn) menuBtn.onclick = (ev) => { ev.stopPropagation(); historyMenu.classList.remove('show'); engineMenu.classList.toggle('show'); }; const histBtn = root.querySelector('#db-history-btn'); if (histBtn) histBtn.onclick = (ev) => { ev.stopPropagation(); engineMenu.classList.remove('show'); renderHistoryMenu(); historyMenu.classList.toggle('show'); }; } async function load(reset) { if (reset) { state.start = 0; state.isLast = false; const list = root.querySelector('.db-list'); list.innerHTML = '
加载中…
'; } try { const data = await doubanFetch(state.start); state.isLast = data.length < 20; const list = root.querySelector('.db-list'); const sentinel = list.querySelector('.db-sentinel'); sentinel.insertAdjacentHTML('beforebegin', renderMovies(data)); if (reset) root.scrollTop = 0; sentinel.textContent = state.isLast ? '— 没有更多了 —' : ''; bindMovies(); if (reset) setupScroll(); } catch (e) { const list = root.querySelector('.db-list'); const sentinel = list.querySelector('.db-sentinel'); if (sentinel) { sentinel.style.color = '#e74c3c'; sentinel.textContent = '加载失败,点此重试'; sentinel.onclick = () => { sentinel.onclick = null; sentinel.style.color = ''; load(false); }; } } } function bindMovies() { root.querySelectorAll('.db-movie').forEach(el => el.onclick = () => { // 引擎为「默认」:恢复默认跳转(豆瓣详情页),不记搜索历史 if (engine === 'default') { const id = el.dataset.id; const isLandscape = window.innerWidth > window.innerHeight; location.href = isLandscape ? `https://movie.douban.com/subject/${id}/` : `https://m.douban.com/movie/subject/${id}/`; return; } const title = (el.dataset.title || '').trim(); if (title) searchTitle(title); }); } function setupScroll() { if (observer) observer.disconnect(); const list = root.querySelector('.db-list'); if (!list) return; const sentinel = list.querySelector('.db-sentinel'); if (!sentinel) return; observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && !state.loading && !state.isLast) { state.loading = true; state.start += 20; load(false).finally(() => { state.loading = false; }); } }, { rootMargin: '300px' }); observer.observe(sentinel); } function init() { document.head.appendChild(style); document.body.appendChild(root); injectPanels(); render(); load(true).then(() => setupScroll()); } if (document.body) init(); else window.addEventListener('DOMContentLoaded', init); })();