// ==UserScript== // @name 腾讯会议录制批量下载 // @namespace https://meeting.tencent.com/ // @version 1.0.0 // @icon https://meeting.tencent.com/favicon.ico // @description 悬浮更多 → 点击导出,批量处理勾选的录制 // @author MiMoCode // @match https://meeting.tencent.com/user-center/meeting-record* // @grant GM_notification // @grant GM_addStyle // @run-at document-idle // @license MIT // ==/UserScript== (function () { 'use strict'; let speed = 1500; GM_addStyle(` #tm-fab{position:fixed;bottom:28px;right:28px;z-index:99999;width:50px;height:50px;border-radius:50%;border:none;background:linear-gradient(135deg,#006eff,#0052cc);color:#fff;font-size:20px;cursor:pointer;box-shadow:0 4px 14px rgba(0,110,255,.4);display:flex;align-items:center;justify-content:center;transition:transform .2s} #tm-fab:hover{transform:scale(1.1)} #tm-fab .tip{position:absolute;right:58px;white-space:nowrap;background:rgba(0,0,0,.75);color:#fff;padding:4px 10px;border-radius:4px;font-size:12px;opacity:0;pointer-events:none;transition:opacity .2s} #tm-fab:hover .tip{opacity:1} #tm-panel{position:fixed;bottom:88px;right:28px;z-index:99999;width:380px;background:#fff;border-radius:10px;box-shadow:0 6px 30px rgba(0,0,0,.2);font-family:-apple-system,BlinkMacSystemFont,"Microsoft YaHei",sans-serif;display:none;flex-direction:column;overflow:hidden;max-height:520px} #tm-panel.show{display:flex} #tm-panel .hd{padding:12px 16px;background:linear-gradient(135deg,#006eff,#0052cc);color:#fff;display:flex;justify-content:space-between;align-items:center} #tm-panel .hd h4{margin:0;font-size:14px} #tm-panel .hd .close{cursor:pointer;font-size:20px;opacity:.8} #tm-panel .hd .close:hover{opacity:1} #tm-panel .bd{flex:1;overflow-y:auto;padding:10px 16px;font-size:13px} #tm-panel .ft{padding:10px 16px;border-top:1px solid #eee;display:flex;justify-content:space-between;align-items:center;background:#fafafa} #tm-panel .btn{padding:6px 14px;border:none;border-radius:5px;font-size:13px;cursor:pointer} #tm-panel .btn-go{background:#006eff;color:#fff} #tm-panel .btn-go:hover{background:#0058d4} #tm-panel .btn-go:disabled{background:#b0cef7;cursor:not-allowed} #tm-panel .btn-stop{background:#e34d59;color:#fff} #tm-panel .spd{display:flex;align-items:center;gap:6px;font-size:12px;color:#666} #tm-panel .spd select{padding:2px 4px;border:1px solid #ddd;border-radius:3px;font-size:12px} #tm-panel .log{padding:4px 0;border-bottom:1px solid #f5f5f5;display:flex;justify-content:space-between;align-items:center;font-size:12px} #tm-panel .log:last-child{border-bottom:none} #tm-panel .log-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} #tm-panel .log-st{margin-left:8px;white-space:nowrap;font-size:11px;padding:1px 6px;border-radius:3px;flex-shrink:0} .tm-ok{background:#e6ffed;color:#19a633} .tm-err{background:#fff0f0;color:#e34d59} .tm-dl{background:#e6f4ff;color:#006eff} .tm-hint{text-align:center;color:#999;padding:16px 0;font-size:13px;line-height:2} .tm-exported{background:#e6ffed!important;border-left:3px solid #19a633!important} `); const sleep = ms => new Promise(r => setTimeout(r, ms)); function closeAllMenus() { document.querySelectorAll('.met-tooltip__inner').forEach(el => { el.style.removeProperty('visibility'); el.style.removeProperty('opacity'); el.style.removeProperty('display'); const t = el.closest('.met-tooltip'); if (t) t.setAttribute('aria-hidden', 'true'); }); } function getCheckedBtns() { return Array.from(document.querySelectorAll('.me-icon.me-icon-more--outlined')).filter(btn => { const row = btn.closest('tr') || btn.closest('[class*="recordListRow"]'); if (!row) return false; const cb = row.querySelector('input[type="checkbox"], input.met-checkbox'); if (cb) return cb.checked; const wrap = row.querySelector('.met-checkbox, [class*="checkbox"]'); return wrap && (wrap.classList.contains('is-checked') || wrap.classList.contains('checked')); }); } async function exportOne(btn) { btn.scrollIntoView({ behavior: 'smooth', block: 'center' }); await sleep(400); closeAllMenus(); await sleep(200); const ev = { bubbles: true, cancelable: true }; btn.dispatchEvent(new MouseEvent('mouseover', ev)); btn.dispatchEvent(new MouseEvent('mouseenter', ev)); btn.dispatchEvent(new MouseEvent('pointerover', ev)); btn.dispatchEvent(new MouseEvent('pointerenter', ev)); await sleep(300); btn.dispatchEvent(new MouseEvent('mouseout', ev)); btn.dispatchEvent(new MouseEvent('mouseleave', ev)); await sleep(300); let item = null; const menus = document.querySelectorAll('.met-tooltip__inner'); for (const menu of menus) { for (const li of menu.querySelectorAll('li')) { if (li.textContent.trim() === '导出') { item = li; break; } } if (item) break; } if (!item) { menus.forEach(el => { el.style.visibility = 'visible'; el.style.opacity = '1'; el.style.display = 'block'; const t = el.closest('.met-tooltip'); if (t) t.setAttribute('aria-hidden', 'false'); }); await sleep(200); for (const li of document.querySelectorAll('.met-tooltip__inner li')) { if (li.textContent.trim() === '导出') { item = li; break; } } } if (!item) { closeAllMenus(); return false; } item.click(); await sleep(800); closeAllMenus(); return true; } function setupUI() { const fab = document.createElement('button'); fab.id = 'tm-fab'; fab.innerHTML = '\u2B07\uFE0F批量导出'; document.body.appendChild(fab); const panel = document.createElement('div'); panel.id = 'tm-panel'; panel.innerHTML = `

批量导出

×
勾选要导出的录制 → 点击开始
已导出的行会显示绿色高亮
间隔:
就绪
`; document.body.appendChild(panel); const $bd = panel.querySelector('#tm-bd'); const $info = panel.querySelector('#tm-info'); const $go = panel.querySelector('#tm-go'); const $spd = panel.querySelector('#tm-spd'); $spd.onchange = () => speed = +$spd.value; fab.onclick = () => panel.classList.toggle('show'); panel.querySelector('.close').onclick = () => panel.classList.remove('show'); let running = false, stop = false; $go.onclick = async () => { if (running) { stop = true; return; } const btns = getCheckedBtns(); if (!btns.length) { $info.textContent = '请先勾选录制'; return; } running = true; stop = false; $go.textContent = '停止'; $go.className = 'btn btn-stop'; $bd.innerHTML = ''; let ok = 0, fail = 0; for (let i = 0; i < btns.length; i++) { if (stop) break; const btn = btns[i]; const row = btn.closest('tr') || btn.closest('[class*="recordListRow"]'); const name = row?.querySelector('[class*="title"]')?.textContent?.trim() || `录制${i + 1}`; const log = document.createElement('div'); log.className = 'log'; log.innerHTML = `${name}导出中...`; $bd.appendChild(log); $bd.scrollTop = $bd.scrollHeight; const st = log.querySelector('.log-st'); $info.textContent = `${i + 1}/${btns.length}`; try { if (await exportOne(btn)) { st.className = 'log-st tm-ok'; st.textContent = '已导出'; if (row) row.classList.add('tm-exported'); ok++; } else { st.className = 'log-st tm-err'; st.textContent = '未找到导出'; fail++; } } catch (e) { st.className = 'log-st tm-err'; st.textContent = e.message; fail++; } if (i < btns.length - 1 && !stop) await sleep(speed); } $info.textContent = `完成 ${ok}/${ok + fail}`; $go.textContent = '开始'; $go.className = 'btn btn-go'; running = false; GM_notification?.({ title: '批量导出完成', text: `${ok} 成功 ${fail} 失败`, timeout: 5000 }); }; } setupUI(); })();