// ==UserScript==
// @name 52pojie全量收藏-右下角菜单+批量可正常启停+更新收藏时间
// @namespace http://tampermonkey.net/
// @version 4.3
// @description 完美修复:真实删除 + 正确采集favid + 总数/上限显示 + 点击更新重置收藏时间
// @AuThor 专属定制
// @match *://www.52pojie.cn/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @connect 52pojie.cn
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// ========== 可自定义配置项 ==========
const DEFAULT_MAX_PAGE = 50;
// =================================
function isFavoritePage() {
return location.href.includes('mod=space') && location.href.includes('do=favorite');
}
let favCacheList = null;
let isLoading = false;
let stopCheck = false;
let formhashCache = '';
function getMaxPage() {
const val = GM_getValue("favMaxPage", "");
const num = parseInt(val);
return isNaN(num) || num < 1 ? DEFAULT_MAX_PAGE : num;
}
function getFormhash() {
if (formhashCache) return formhashCache;
const hashInput = document.querySelector('input[name="formhash"]');
if (hashInput) formhashCache = hashInput.value;
return formhashCache || '';
}
try {
const cacheStr = GM_getValue("favCacheData", "");
if (cacheStr) favCacheList = JSON.parse(cacheStr);
} catch (e) {
favCacheList = null;
}
GM_addStyle(`
#cfMenuWrap {position:fixed;bottom:20px;right:20px;z-index:10000;font-size:13px;}
#cfMenuBtn {width:50px;height:50px;background:#007bff;color:#fff;border:none;border-radius:8px;cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,0.25);}
#cfMenuBtn:hover {background:#0056b3;}
#cfMenuList {display:none;position:absolute;bottom:58px;right:0;background:#fff;border-radius:6px;box-shadow:0 2px 12px rgba(0,0,0,0.2);overflow:hidden;min-width:160px;}
#cfMenuList.show {display:block;}
.cf-menu-item {padding:10px 12px;cursor:pointer;white-space:nowrap;border-bottom:1px solid #f0f0f0;}
.cf-menu-item:last-child {border-bottom:none;}
.cf-menu-item:hover {background:#f5f7fa;}
.update-indicator {color:#ff0000;font-weight:bold;margin-left:5px;}
.progress-container {position:fixed;bottom:90px;right:20px;width:220px;background:#fff;border-radius:5px;box-shadow:0 2px 10px rgba(0,0,0,0.2);padding:10px;z-index:9999;display:none;}
.progress-bar {height:10px;background:#e9ecef;border-radius:5px;overflow:hidden;}
.progress-fill {height:100%;background:#28a745;width:0%;transition:width 0.3s ease;}
.progress-text {font-size:12px;margin-top:5px;text-align:center;}
#favModal {position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:90%;max-width:700px;max-height:75vh;background:#fff;border-radius:10px;box-shadow:0 4px 30px rgba(0,0,0,0.35);z-index:100001;display:none;overflow:hidden;}
.modal-head {padding:12px 15px;background:#f8f9fa;border-bottom:1px solid #eee;display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap;}
.modal-title {font-size:14px;font-weight:bold;color:#333;}
.modal-close {color:#f56c6c;cursor:pointer;font-size:18px;font-weight:bold;}
.head-btn {padding:4px 8px;border:none;border-radius:3px;cursor:pointer;font-size:12px;}
#refreshFavBtn {background:#007bff;color:#fff;}
#checkAllFavBtn {background:#dc3545;color:#fff;}
#checkAllFavBtn.stop-bg {background:#6c757d;}
#favSearchInput {width:calc(100% - 20px);margin:10px;padding:8px 12px;border:1px solid #007bff;border-radius:4px;outline:none;font-size:14px;background:#fff;}
.modal-body {padding:10px;overflow-y:auto;max-height:calc(75vh - 120px);}
.fav-item {padding:8px 10px;border-bottom:1px solid #f1f1f1;color:#0066cc;font-size:13px;display:flex;justify-content:space-between;align-items:center;gap:8px;}
.fav-title {flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer;}
.fav-update {color:red;font-weight:bold;}
.fav-page-tag {color:#999;font-size:12px;white-space:nowrap;}
.fav-btn {padding:2px 6px;color:#fff;border:none;border-radius:3px;font-size:12px;cursor:pointer;white-space:nowrap;margin-left:4px;}
.fav-update-btn {background:#009688;}
.fav-update-btn:hover {background:#00796b;}
.fav-del-btn {background:#ff4444;}
.fav-del-btn:hover {background:#cc0000;}
.mask-layer {position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:100000;display:none;}
.load-all-tip {text-align:center;padding:15px;color:#666;}
.empty-tip {text-align:center;padding:30px;color:#999;}
`);
const menuWrap = document.createElement("div");
menuWrap.id = "cfMenuWrap";
menuWrap.innerHTML = `
<button id="cfMenuBtn">收藏</button>
<div id="cfMenuList">
<div class="cf-menu-item" data-action="openFav">全部收藏列表</div>
<div class="cf-menu-item" data-action="checkSingle">本页检测更新</div>
<div class="cf-menu-item" data-action="setPageLimit">设置采集页数上限</div>
</div>
`;
document.body.appendChild(menuWrap);
const menuBtn = document.getElementById("cfMenuBtn");
const menuList = document.getElementById("cfMenuList");
const menuItems = menuList.querySelectorAll(".cf-menu-item");
menuBtn.onclick = () => menuList.classList.toggle("show");
function closeMenu() { menuList.classList.remove("show"); }
function setPageLimit() {
closeMenu();
const now = getMaxPage();
const input = prompt(`请输入收藏采集最大页数上限(当前:${now})`, now);
if (input === null) return;
const num = parseInt(input.trim());
if (isNaN(num) || num < 1) {
alert("请输入合法数字!");
return;
}
GM_setValue("favMaxPage", num);
alert(`设置成功!当前采集上限:${num} 页`);
}
const progressContainer = document.createElement('div');
progressContainer.className = 'progress-container';
progressContainer.innerHTML = `<div class="progress-bar"><div class="progress-fill"></div></div><div class="progress-text">准备中...</div>`;
document.body.appendChild(progressContainer);
function updateProgress(current, total, text) {
const fill = progressContainer.querySelector('.progress-fill');
const txt = progressContainer.querySelector('.progress-text');
const pct = total ? Math.round((current / total) * 100) : 0;
fill.style.width = pct + "%";
txt.textContent = text || `${current}/${total} ${pct}%`;
}
function showProgress() { progressContainer.style.display = "block"; }
function hideProgress() { progressContainer.style.display = "none"; }
function parseChineseDate(str) {
const m = str.match(/(\d{4})-(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{1,2})/);
return m ? new Date(m[1], m[2] - 1, m[3], m[4], m[5]) : null;
}
function getLastEditDate(url) {
return new Promise(resolve => {
GM_xmlhttpRequest({
method: "GET", url: url, timeout: 8000,
onload: res => {
const doc = new DOMParser().parseFromString(res.responseText, "text/html");
const s = doc.querySelector("i.pstatus");
if (!s) return resolve(null);
const m = s.textContent.match(/于\s+(\d{4}-\d{1,2}-\d{1,2}\s+\d{1,2}:\d{1,2})\s+编辑/);
resolve(m ? parseChineseDate(m[1]) : null);
},
onerror: () => resolve(null), ontimeout: () => resolve(null)
});
});
}
// ====================== 核心修复:正确采集 favid ======================
function parseFavItem(item, pageNum) {
const link = item.querySelector('a[href*="thread-"]');
const dateDom = item.querySelector(".xg1");
const favDate = dateDom ? parseChineseDate(dateDom.textContent.trim()) : null;
// 从页面标签 id="fav_18081556" 提取真实收藏ID
const favid = item.id?.replace("fav_", "") || "";
if (!link || !favid) return null;
const tid = link.href.match(/thread-(\d+)-/)?.[1] || "";
return {
title: link.title || link.textContent.trim(),
url: link.href.startsWith("http") ? link.href : "https://www.52pojie.cn/" + link.href,
favDate: favDate,
update: false,
page: pageNum,
favid: favid,
tid: tid
};
}
// ====================== 真实删除接口 ======================
async function deleteFavorite(favid, itemEl) {
const formhash = getFormhash();
if (!formhash || !favid) {
alert("删除失败:formhash 或 favid 缺失");
return false;
}
return new Promise(resolve => {
GM_xmlhttpRequest({
method: "POST",
url: "https://www.52pojie.cn/home.php?mod=spacecp&ac=favorite&op=delete&favid=" + favid,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Referer": "https://www.52pojie.cn/home.php?mod=space&do=favorite&view=me"
},
data: `deletesubmit=true&formhash=${formhash}&handlekey=a_delete_${favid}`,
onload: function (res) {
if (res.status === 200) {
if (itemEl) itemEl.style.display = "none";
favCacheList = favCacheList.filter(x => x.favid !== favid);
GM_setValue("favCacheData", JSON.stringify(favCacheList));
resolve(true);
} else {
alert("删除失败:服务器返回异常");
resolve(false);
}
},
onerror: function () {
alert("删除失败:网络错误");
resolve(false);
}
});
});
}
// ====================== 【核心】使用你抓包的收藏接口 ======================
async function addFavorite(tid) {
const formhash = getFormhash();
if (!formhash || !tid) {
alert("收藏失败:formhash 或 帖子ID 缺失");
return false;
}
return new Promise(resolve => {
GM_xmlhttpRequest({
method: "GET",
url: `https://www.52pojie.cn/home.php?mod=spacecp&ac=favorite&type=thread&id=${tid}&formhash=${formhash}&infloat=yes&handlekey=k_favorite&inajax=1&ajaxtarget=fwin_content_k_favorite`,
headers: {
"Referer": "https://www.52pojie.cn/home.php?mod=space&do=favorite&view=me"
},
onload: function (res) {
if (res.status === 200 && res.responseText.includes("收藏成功")) {
resolve(true);
} else {
alert("收藏失败:服务器异常");
resolve(false);
}
},
onerror: function () {
alert("收藏失败:网络错误");
resolve(false);
}
});
});
}
// 单独采集第一页,获取最新收藏数据
async function fetchFirstPage() {
return new Promise(resolve => {
GM_xmlhttpRequest({
method: "GET",
url: `https://www.52pojie.cn/home.php?mod=space&do=favorite&type=thread&page=1`,
onload: res => {
const doc = new DOMParser().parseFromString(res.responseText, "text/html");
const items = doc.querySelectorAll('li[id^="fav_"]');
const list = [];
items.forEach(it => {
const obj = parseFavItem(it, 1);
if (obj) list.push(obj);
});
resolve(list);
},
onerror: () => resolve([])
});
});
}
// ====================== 【更新收藏时间】删除+重新收藏+置顶+改页数=1 ======================
async function updateFavorite(item, itemEl) {
if (!item.tid || !item.favid) {
alert("帖子信息不完整,无法更新");
return;
}
if (!confirm("确定要【更新收藏时间】吗?\n操作:删除旧收藏 → 重新收藏")) {
return;
}
// 1. 删除旧数据
const delOk = await deleteFavorite(item.favid, null);
if (!delOk) return;
await delay(800);
// 2. 重新收藏
const addOk = await addFavorite(item.tid);
if (!addOk) return;
alert("✅ 收藏时间更新成功!正在置顶本条...");
// 3. 采集最新第一页,找到本条
const newList = await fetchFirstPage();
const newItem = newList.find(x => x.tid === item.tid);
if (newItem) {
// 4. 强制设置为第1页
newItem.page = 1;
newItem.update = false;
// 5. 从缓存删除旧的 → 加到最前面
favCacheList = favCacheList.filter(x => x.tid !== item.tid);
favCacheList.unshift(newItem);
// 保存缓存
GM_setValue("favCacheData", JSON.stringify(favCacheList));
}
// 6. 刷新列表,本条自动置顶
renderFavList();
}
function delay(ms) { return new Promise(r => setTimeout(r, ms)); }
function resetCheckState() {
stopCheck = false;
isLoading = false;
hideProgress();
menuItems.forEach(item => {
if (item.dataset.action === "checkSingle") item.textContent = "检测更新";
});
}
function resetBatchBtn() {
stopCheck = false;
isLoading = false;
checkAllBtn.innerText = "批量检测更新";
checkAllBtn.classList.remove("stop-bg");
}
async function checkSingleUpdate() {
closeMenu();
if (!isFavoritePage()) {
alert("请进入【我的-收藏】页面再使用");
return;
}
const singleItem = [...menuItems].find(i => i.dataset.action === "checkSingle");
if (singleItem.textContent === "停止检测") {
stopCheck = true;
updateProgress(0, 0, "已手动停止");
setTimeout(resetCheckState, 600);
return;
}
stopCheck = false;
isLoading = true;
singleItem.textContent = "停止检测";
showProgress();
try {
const items = document.querySelectorAll('li[id^="fav_"]');
const total = items.length;
let cur = 0, upd = 0;
for (const item of items) {
if (stopCheck) break;
cur++;
const dDom = item.querySelector(".xg1");
if (!dDom) continue;
const favDate = parseChineseDate(dDom.textContent.trim());
const link = item.querySelector('a[href*="thread-"]');
if (!link || !favDate) continue;
const url = link.href.startsWith("http") ? link.href : "https://www.52pojie.cn/" + link.href;
updateProgress(cur, total, `检测:${cur}/${total}`);
const editDate = await getLastEditDate(url);
if (editDate && editDate > favDate) {
if (!dDom.nextElementSibling?.classList.contains("update-indicator")) {
const span = document.createElement("span");
span.className = "update-indicator";
span.innerText = "有更新";
dDom.after(span);
}
upd++;
}
await delay(700);
}
updateProgress(total, total, `完成:${upd}个有更新`);
await delay(1500);
} catch (e) { console.error(e); } finally {
resetCheckState();
}
}
const mask = document.createElement('div'); mask.className = "mask-layer"; document.body.appendChild(mask);
const favModal = document.createElement('div');
favModal.id = "favModal";
favModal.innerHTML = `
<div class="modal-head">
<span class="modal-title">全部收藏列表</span>
<div style="display:flex;gap:6px;">
<button class="head-btn" id="refreshFavBtn">重新采集</button>
<button class="head-btn" id="checkAllFavBtn">批量检测更新</button>
</div>
<span class="modal-close">×</span>
</div>
<input id="favSearchInput" placeholder="🔍 搜索全部收藏标题...">
<div class="modal-body"></div>
`;
document.body.appendChild(favModal);
const searchInput = favModal.querySelector("#favSearchInput");
const refreshBtn = favModal.querySelector("#refreshFavBtn");
const checkAllBtn = favModal.querySelector("#checkAllFavBtn");
const closeBtn = favModal.querySelector(".modal-close");
const modalBody = favModal.querySelector(".modal-body");
const modalTitle = favModal.querySelector(".modal-title");
function closeModal() { favModal.style.display = "none"; mask.style.display = "none"; }
closeBtn.onclick = closeModal;
mask.onclick = closeModal;
async function goPage(page) {
return new Promise(resolve => {
GM_xmlhttpRequest({
method: "GET",
url: `https://www.52pojie.cn/home.php?mod=space&do=favorite&type=thread&page=${page}`,
onload: res => {
const doc = new DOMParser().parseFromString(res.responseText, "text/html");
resolve(doc);
},
onerror: () => resolve(null)
});
});
}
async function collectAllFav(forceRefresh = false) {
if (isLoading) return favCacheList;
if (favCacheList && !forceRefresh) return favCacheList;
isLoading = true;
let allList = [], page = 1;
const maxPage = getMaxPage();
modalBody.innerHTML = `<div class="load-all-tip">正在采集收藏...</div>`;
while (true) {
if (page > maxPage) break;
const pageDoc = await goPage(page);
if (!pageDoc) break;
const items = pageDoc.querySelectorAll('li[id^="fav_"]');
if (items.length === 0) break;
modalBody.innerHTML = `<div class="load-all-tip">采集第 ${page} 页 / 上限${maxPage}页</div>`;
items.forEach(item => {
const res = parseFavItem(item, page);
res && allList.push(res);
});
page++;
await delay(400);
}
favCacheList = allList;
GM_setValue("favCacheData", JSON.stringify(allList));
isLoading = false;
return favCacheList;
}
async function checkBatchUpdate() {
closeMenu();
if (checkAllBtn.innerText === "停止") {
stopCheck = true;
return;
}
if (isLoading || !favCacheList) {
alert("请先加载收藏列表");
return;
}
stopCheck = false;
isLoading = true;
checkAllBtn.innerText = "停止";
checkAllBtn.classList.add("stop-bg");
let upd = 0;
for (let i = 0; i < favCacheList.length; i++) {
if (stopCheck) break;
const item = favCacheList[i];
modalBody.innerHTML = `<div class="load-all-tip">批量检测:${i+1}/${favCacheList.length}</div>`;
const editDate = await getLastEditDate(item.url);
if (editDate && item.favDate && editDate > item.favDate) {
item.update = true;
upd++;
}
await delay(450);
}
if (!stopCheck) {
renderFavList();
alert(`批量检测完成:共 ${upd} 个有更新`);
} else {
modalBody.innerHTML = `<div class="load-all-tip">批量检测已手动停止</div>`;
}
resetBatchBtn();
}
checkAllBtn.onclick = checkBatchUpdate;
// 渲染列表
function renderFavList() {
if (!favCacheList) return;
const key = searchInput.value.trim().toLowerCase();
const filterList = favCacheList.filter(item => item.title.toLowerCase().includes(key));
const maxPage = getMaxPage();
modalTitle.textContent = `全部收藏:${favCacheList.length} 条 / 上限:${maxPage} 页`;
modalBody.innerHTML = "";
if (filterList.length === 0) {
modalBody.innerHTML = '<div class="empty-tip">无匹配收藏</div>';
return;
}
filterList.forEach(item => {
const div = document.createElement("div");
div.className = "fav-item";
div.innerHTML = `
<div class="fav-title">
${item.title}${item.update ? '<span class="fav-update">●有更新</span>' : ""}
</div>
<span class="fav-page-tag">第${item.page}页</span>
<button class="fav-btn fav-update-btn">更新收藏</button>
<button class="fav-btn fav-del-btn">删除</button>
`;
div.querySelector('.fav-title').onclick = () => window.open(item.url, "_blank");
const updateBtn = div.querySelector('.fav-update-btn');
updateBtn.onclick = (e) => {
e.stopPropagation();
updateFavorite(item, div);
};
const delBtn = div.querySelector('.fav-del-btn');
delBtn.onclick = (e) => {
e.stopPropagation();
if (confirm('确定要删除该收藏吗?')) {
deleteFavorite(item.favid, div);
}
};
modalBody.appendChild(div);
});
}
async function openFavModal() {
closeMenu();
getFormhash();
favModal.style.display = "block";
mask.style.display = "block";
searchInput.value = "";
if (!favCacheList) await collectAllFav(false);
renderFavList();
}
async function refreshCache() {
closeMenu();
if (isLoading) return;
favCacheList = null;
GM_setValue("favCacheData", "");
await collectAllFav(true);
renderFavList();
alert("缓存已强制刷新");
}
menuItems.forEach(item => {
item.onclick = function () {
const act = this.dataset.action;
switch (act) {
case "checkSingle": checkSingleUpdate(); break;
case "openFav": openFavModal(); break;
case "setPageLimit": setPageLimit(); break;
}
};
});
refreshBtn.onclick = async function () {
if (isLoading) return;
await collectAllFav(true);
renderFavList();
};
searchInput.addEventListener("input", renderFavList);
})();