// ==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 = `
${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);
})();