// ==UserScript== // @name 腾讯会议智能操作 & 自动关闭 // @namespace https://meeting.tencent.com/ // @version 1.1.0 // @icon https://meeting.tencent.com/favicon.ico // @description 打开腾讯会议链接后:进行中→加入会议;未开始→添加到会议列表;操作后倒计时关闭页面(可取消) // @author bo Sea // @match https://meeting.tencent.com/dm/* // @match https://meeting.weixin.qq.com/dm/* // @grant window.close // @run-at document-idle // @license MIT // ==/UserScript== (function () { "use strict"; /* ================= 常量与状态 ================= */ const THRESHOLD = 20 * 60; const CLOSE_SEC = 8; const COLOR = { green: "#0f9d58", amber: "#f9a825", orange: "#f57c00", gray: "#9e9e9e", }; let cancelFlag = false; let closeTimer = null; /* ================= 样式 ================= */ function injectStyle() { if (document.getElementById("tm-style")) return; const s = document.createElement("style"); s.id = "tm-style"; s.textContent = ` @keyframes tm-in { from { transform: translateX(120%); opacity: 0 } to { transform: translateX(0); opacity: 1 } } #tm-box { position: fixed; top: 16px; right: 16px; z-index: 999999; width: 310px; border-radius: 12px; overflow: hidden; box-shadow: 0 8px 32px rgba(0,0,0,.2); font-family: system-ui, -apple-system, sans-serif; animation: tm-in .3s ease-out; } .tm-header { padding: 14px 18px; color: #fff; display: flex; align-items: center; gap: 10px; } .tm-header .tm-icon { font-size: 20px } .tm-header .tm-title { font-weight: 600; font-size: 15px } .tm-body { padding: 14px 18px; background: #fff; font-size: 13px; line-height: 1.8; color: #333; } .tm-subject { font-weight: 600 } .tm-code { color: #999; font-size: 12px } .tm-hint { color: #999; font-size: 12px; margin-top: 4px } .tm-status { margin-top: 6px } .tm-countdown { font-size: 20px; font-weight: 700; margin-top: 8px } .tm-bar { margin-top: 12px; padding: 8px 12px; background: #f5f5f5; border-radius: 6px; display: flex; align-items: center; justify-content: space-between; } .tm-bar .tm-sec { color: #e53935; font-weight: bold } .tm-cancel-btn { padding: 4px 14px; font-size: 12px; border: 1px solid #e53935; color: #e53935; border-radius: 4px; background: #fff; cursor: pointer; } .tm-cancel-btn:hover { background: #fce4ec } .tm-muted { color: #999 } .tm-green { color: #0f9d58 } .tm-amber { color: #f9a825 } `; document.head.appendChild(s); } /* ================= 公共方法:页面数据 ================= */ function getData() { const el = (id) => document.getElementById(id); const txt = (id) => (el(id)?.textContent || "").trim(); let start = null; const dm = txt("tm-meeting-start-date").match( /(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日/, ); const tm = txt("tm-meeting-start-time").match(/(\d{1,2}):(\d{2})/); if (dm && tm) start = new Date(+dm[1], dm[2] - 1, +dm[3], +tm[1], +tm[2]); return { state: txt("tm-meeting-state"), subject: txt("tm-meeting-subject") || "腾讯会议", code: txt("tm-meeting-code"), duration: txt("tm-meeting-duration"), dateStr: txt("tm-meeting-start-date"), timeStr: txt("tm-meeting-start-time"), start, }; } function getDiffSec(start) { if (!start) return null; return Math.floor((start.getTime() - Date.now()) / 1000); } function fmtDuration(ms) { const h = Math.floor(ms / 3600000); const m = Math.floor((ms % 3600000) / 60000); return h > 0 ? `${h}小时${m}分钟` : `${m}分钟`; } /* ================= 公共方法:定时器 ================= */ function everySecond(fn) { return setInterval(fn, 1000); } /* ================= 公共 UI 片段 ================= */ function subjectHTML(d) { return `
${d.subject}
`; } function codeHTML(code) { return code ? `
会议号 ${code}
` : ""; } function statusHTML(ok, okMsg, failMsg) { return `
${ok ? "✅ " + okMsg : failMsg}
`; } function closeBarHTML() { return `
⏱ ${CLOSE_SEC} 秒后自动关闭
`; } // 拼接弹层正文,自动跳过空片段 function buildBody(...sections) { return sections.filter(Boolean).join("\n"); } /* ================= 弹层 ================= */ function toast(bgColor, icon, title, bodyHTML) { injectStyle(); let box = document.getElementById("tm-box"); if (!box) { box = document.createElement("div"); box.id = "tm-box"; document.body.appendChild(box); } box.innerHTML = `
${icon} ${title}
${bodyHTML}
`; } // 根据操作成败显示弹层(成功/失败两套配色文案统一在此分支) function resultToast(ok, opts) { const { okColor, failColor = COLOR.orange, okIcon, failIcon = "⚠️", okTitle, failTitle, body, } = opts; toast( ok ? okColor : failColor, ok ? okIcon : failIcon, ok ? okTitle : failTitle, body, ); } /* ================= 自动关闭 ================= */ function doCancel() { if (cancelFlag) return; cancelFlag = true; clearInterval(closeTimer); const bar = document.getElementById("tm-bar"); if (bar) bar.innerHTML = '已取消自动关闭'; } function startClose() { cancelFlag = false; let n = CLOSE_SEC; document.getElementById("tm-cancel")?.addEventListener("click", doCancel); document.addEventListener( "keydown", (e) => { if (e.key === "Escape") doCancel(); }, { once: true }, ); closeTimer = everySecond(() => { if (cancelFlag) return clearInterval(closeTimer); document.getElementById("tm-sec").textContent = --n; if (n <= 0) { clearInterval(closeTimer); doClose(); } }); } function doClose() { window.close(); setTimeout(() => { if (!document.hidden) history.back(); }, 300); } document.addEventListener("visibilitychange", () => { if (document.hidden && !cancelFlag) { clearInterval(closeTimer); doClose(); } }); /* ================= 页面动作 ================= */ function doJoin() { const btns = document.querySelectorAll('[id="mpJoinBtnCtrl"]'); const btn = btns[1] || btns[0]; if (btn) { btn.click(); return "加入会议"; } return null; } function doCollect() { const el = document.getElementById("collect-meeting"); if (el) { el.click(); return true; } return false; } /* ================= 主流程 ================= */ // 场景一:会议已结束 function handleEnded(d) { toast( COLOR.gray, "⚫", "会议已结束", buildBody( subjectHTML(d), `
${d.dateStr} ${d.timeStr}
`, closeBarHTML(), ), ); startClose(); } // 场景二:进行中 / 即将开始 → 直接加入 function handleJoin(d) { const result = doJoin(); resultToast(result, { okColor: COLOR.green, okIcon: "🟢", okTitle: "正在加入会议", failTitle: "加入失败", body: buildBody( subjectHTML(d), codeHTML(d.code), statusHTML(result, `已点击"${result}"`, "未找到加入按钮"), closeBarHTML(), ), }); startClose(); } // 场景三:未开始 → 添加到会议列表 + 实时倒计时 function handleUpcoming(d, diffSec) { const ok = doCollect(); resultToast(ok, { okColor: COLOR.amber, okIcon: "📋", okTitle: "会议未开始", failTitle: "收藏", body: buildBody( subjectHTML(d), codeHTML(d.code), `
开始:${d.dateStr} ${d.timeStr}
`, ok && `
时长:${d.duration}
`, ok && `
${fmtDuration(diffSec * 1000)}
`, statusHTML(ok, "已添加到会议列表", "未找到添加按钮"), closeBarHTML(), ), }); if (ok) startCountdown(d); startClose(); } // 距会议开始的实时倒计时 function startCountdown(d) { const el = document.getElementById("tm-cd"); const t = everySecond(() => { const left = getDiffSec(d.start); if (left <= 0) { clearInterval(t); el.textContent = "已到时间"; el.className = "tm-countdown tm-green"; return; } el.textContent = fmtDuration(left * 1000); }); } function main() { const d = getData(); const diffSec = getDiffSec(d.start); if (d.state === "已结束") return handleEnded(d); if ( d.state === "进行中" || d.state === "会议中" || diffSec === null || diffSec <= THRESHOLD ) { return handleJoin(d); } handleUpcoming(d, diffSec); } document.readyState === "complete" ? main() : window.addEventListener("load", main); })();