聪明豆专属!!提取并展示指标数据(浏览量、评论数、点赞数、收藏数、转发数),并自动更新以应对动态加载的数据。
当前为
// ==UserScript==
// @name Metrics Extractor for Xiaohongshu Note Manager
// @namespace http://tampermonkey.net/
// @version 2.1
// @description 聪明豆专属!!提取并展示指标数据(浏览量、评论数、点赞数、收藏数、转发数),并自动更新以应对动态加载的数据。
// @author 您的名字
// @match https://creator.xiaohongshu.com/new/note-manager*
// @grant none
// ==/UserScript==
(function() {
'use strict';
// 防止脚本多次运行
if (document.getElementById('metrics-extractor-container')) {
console.log('Metrics Extractor 已经运行,退出脚本。');
return;
}
const metrics = ['浏览量', '评论数', '点赞数', '收藏数', '转发数'];
let categorizedData = [];
let totals = {
'浏览量': 0,
'评论数': 0,
'点赞数': 0,
'收藏数': 0,
'转发数': 0
};
/**
* 提取指标数据
* @param {HTMLElement} div - 包含数字的 div.icon 元素
* @returns {number|null} 提取的数字,或 null 失败
*/
function extractNumberFromDiv(div, index) {
// 使用 :scope > span 选择器,确保选择 div.icon 的直接子 span
const span = div.querySelector(':scope > span');
if (span) {
// 使用 innerText 代替 textContent
const numberText = span.innerText.trim();
console.log(`div.icon #${index + 1} - 提取到的span文本: "${numberText}"`);
// 检查是否有数字
const digitMatch = numberText.match(/\d+/g);
if (digitMatch) {
// 将所有找到的数字部分连接起来
const cleanedNumber = digitMatch.join('');
console.log(`div.icon #${index + 1} - 清理后的数字字符串: "${cleanedNumber}"`);
const number = parseInt(cleanedNumber, 10);
if (!isNaN(number)) {
console.log(`div.icon #${index + 1} - 解析后的数字: ${number}`);
return number;
} else {
console.warn(`div.icon #${index + 1} - 无法解析数字: "${cleanedNumber}"`);
return null;
}
} else {
console.warn(`div.icon #${index + 1} - span内未找到数字`);
return null;
}
} else {
console.warn(`div.icon #${index + 1} - 未找到span元素`);
return null;
}
}
/**
* 更新指标总数和详细数据
* @param {number} number - 提取的数字
* @param {number} metricIndex - 指标索引(0-4)
*/
function updateMetrics(number, metricIndex) {
const metric = metrics[metricIndex];
if (metric) {
totals[metric] += number;
// 更新详细数据
let currentItem = categorizedData[categorizedData.length - 1];
if (!currentItem || Object.keys(currentItem).length >= metrics.length) {
currentItem = {};
categorizedData.push(currentItem);
}
currentItem[metric] = number;
}
}
/**
* 创建指标展示面板
*/
function createMetricsPanel() {
console.log('创建指标面板...');
// 创建样式
const style = document.createElement('style');
style.innerHTML = `
#metrics-extractor-container {
position: fixed;
top: 20px;
right: 20px;
width: 350px;
background: rgba(255, 255, 255, 0.95);
border: 1px solid #ccc;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
padding: 15px;
z-index: 10000;
font-family: Arial, sans-serif;
max-height: 90vh;
overflow-y: auto;
}
#metrics-extractor-container h2 {
text-align: center;
margin-top: 0;
font-size: 18px;
}
#metrics-extractor-container table {
width: 100%;
border-collapse: collapse;
margin-bottom: 15px;
}
#metrics-extractor-container th, #metrics-extractor-container td {
border: 1px solid #ddd;
padding: 8px;
text-align: center;
font-size: 14px;
}
#metrics-extractor-container th {
background-color: #f2f2f2;
}
#metrics-extractor-container button {
width: 100%;
padding: 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-bottom: 10px;
}
#metrics-extractor-container button:hover {
background-color: #45a049;
}
#metrics-extractor-details {
display: none;
max-height: 300px;
overflow-y: auto;
}
`;
document.head.appendChild(style);
// 创建容器
const container = document.createElement('div');
container.id = 'metrics-extractor-container';
// 创建标题
const title = document.createElement('h2');
title.textContent = '指标总数';
container.appendChild(title);
// 创建总数表格
const totalsTable = document.createElement('table');
const totalsThead = document.createElement('thead');
const totalsHeaderRow = document.createElement('tr');
const metricHeader = document.createElement('th');
metricHeader.textContent = '指标';
const totalHeader = document.createElement('th');
totalHeader.textContent = '总数';
totalsHeaderRow.appendChild(metricHeader);
totalsHeaderRow.appendChild(totalHeader);
totalsThead.appendChild(totalsHeaderRow);
totalsTable.appendChild(totalsThead);
const totalsTbody = document.createElement('tbody');
for (const [metric, total] of Object.entries(totals)) {
const row = document.createElement('tr');
const metricCell = document.createElement('td');
metricCell.textContent = metric;
const totalCell = document.createElement('td');
totalCell.textContent = total;
row.appendChild(metricCell);
row.appendChild(totalCell);
totalsTbody.appendChild(row);
}
totalsTable.appendChild(totalsTbody);
container.appendChild(totalsTable);
// 创建按钮
const toggleButton = document.createElement('button');
toggleButton.id = 'metrics-extractor-toggle';
toggleButton.textContent = '显示详细数据';
container.appendChild(toggleButton);
// 创建详细数据部分
const detailsSection = document.createElement('div');
detailsSection.id = 'metrics-extractor-details';
const detailsTitle = document.createElement('h2');
detailsTitle.textContent = '详细数据';
detailsSection.appendChild(detailsTitle);
const detailsTable = document.createElement('table');
const detailsThead = document.createElement('thead');
const detailsHeaderRow = document.createElement('tr');
const projectHeader = document.createElement('th');
projectHeader.textContent = '项目';
detailsHeaderRow.appendChild(projectHeader);
metrics.forEach(metric => {
const th = document.createElement('th');
th.textContent = metric;
detailsHeaderRow.appendChild(th);
});
detailsThead.appendChild(detailsHeaderRow);
detailsTable.appendChild(detailsThead);
const detailsTbody = document.createElement('tbody');
detailsSection.appendChild(detailsTable);
detailsSection.appendChild(detailsTbody);
container.appendChild(detailsSection);
document.body.appendChild(container);
// 添加按钮点击事件
toggleButton.addEventListener('click', () => {
if (detailsSection.style.display === 'none' || detailsSection.style.display === '') {
detailsSection.style.display = 'block';
toggleButton.textContent = '隐藏详细数据';
} else {
detailsSection.style.display = 'none';
toggleButton.textContent = '显示详细数据';
}
});
console.log('指标面板已创建。');
}
/**
* 更新指标展示面板的数据
*/
function updateMetricsPanel() {
console.log('更新指标面板数据...');
const totalsTableBody = document.querySelector('#metrics-extractor-container table tbody');
const detailsTableBody = document.querySelector('#metrics-extractor-details table tbody');
// 更新总数表格
totalsTableBody.innerHTML = '';
for (const [metric, total] of Object.entries(totals)) {
const row = document.createElement('tr');
const metricCell = document.createElement('td');
metricCell.textContent = metric;
const totalCell = document.createElement('td');
totalCell.textContent = total;
row.appendChild(metricCell);
row.appendChild(totalCell);
totalsTableBody.appendChild(row);
}
// 更新详细数据表格
detailsTableBody.innerHTML = '';
categorizedData.forEach((item, index) => {
const row = document.createElement('tr');
const projectCell = document.createElement('td');
projectCell.textContent = index + 1;
row.appendChild(projectCell);
metrics.forEach(metric => {
const cell = document.createElement('td');
cell.textContent = item[metric] !== null ? item[metric] : '-';
row.appendChild(cell);
});
detailsTableBody.appendChild(row);
});
console.log('指标面板数据已更新。');
}
/**
* 等待指定的条件满足
* @param {Function} conditionFunction - 返回布尔值的函数,表示条件是否满足
* @param {number} timeout - 最大等待时间(毫秒)
* @param {number} interval - 检查间隔时间(毫秒)
* @returns {Promise<boolean>} 条件是否在超时前满足
*/
function waitFor(conditionFunction, timeout = 30000, interval = 500) { // 延长超时时间至30秒
return new Promise((resolve) => {
const startTime = Date.now();
const checkCondition = () => {
if (conditionFunction()) {
resolve(true);
} else if (Date.now() - startTime >= timeout) {
resolve(false);
} else {
setTimeout(checkCondition, interval);
}
};
checkCondition();
});
}
/**
* 初始化脚本
*/
async function init() {
console.log('Metrics Extractor 脚本初始化...');
// **调整等待条件**:等待 div.icon_list 内至少有 1 个 div.icon 元素,并且其 span 内有内容
const elementsLoaded = await waitFor(() => {
const icons = document.querySelectorAll('div.icon_list div.icon');
if (icons.length >= 1) { // 根据实际情况调整
return Array.from(icons).every(div => {
const span = div.querySelector(':scope > span');
return span && span.innerText.trim() !== '';
});
}
return false;
}, 30000, 500); // 延长超时时间至30秒
if (!elementsLoaded) {
console.warn('等待目标元素超时,未找到足够的div.icon元素或span内无内容。');
return;
}
// 创建指标面板
createMetricsPanel();
// 提取初始数据
const initialIcons = document.querySelectorAll('div.icon_list div.icon');
initialIcons.forEach((div, index) => {
if (!div.dataset.processed) { // 检查是否已处理
const number = extractNumberFromDiv(div, index + 1);
if (number !== null) {
updateMetrics(number, 0); // 假设每5个div.icon对应5个metrics
}
div.dataset.processed = 'true'; // 标记为已处理
}
});
// 更新面板
updateMetricsPanel();
// 设置 MutationObserver 监听 icon_list 的变化
const iconList = document.querySelector('div.icon_list');
if (iconList) {
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('icon')) {
if (!node.dataset.processed) { // 检查是否已处理
const iconIndex = categorizedData.length * metrics.length + metrics.indexOf('浏览量') + 1;
const number = extractNumberFromDiv(node, iconIndex);
if (number !== null) {
updateMetrics(number, 0); // 根据指标顺序调整 index
updateMetricsPanel();
}
node.dataset.processed = 'true'; // 标记为已处理
}
}
});
}
});
});
observer.observe(iconList, { childList: true, subtree: false });
console.log('MutationObserver 已启动,监听 div.icon_list 的变化。');
} else {
console.warn('未找到 div.icon_list 元素,无法启动 MutationObserver。');
}
console.log('Metrics Extractor 脚本运行完毕。');
}
/**
* 提取数字并返回
* @param {HTMLElement} div - div.icon 元素
* @param {number} index - 元素索引
* @returns {number|null} 提取的数字或 null
*/
function extractNumberFromDiv(div, index) {
// 使用 :scope > span 选择器,确保选择 div.icon 的直接子 span
const span = div.querySelector(':scope > span');
if (span) {
// 使用 innerText 代替 textContent
const numberText = span.innerText.trim();
console.log(`div.icon #${index} - 提取到的span文本: "${numberText}"`);
// 检查是否有数字
const digitMatch = numberText.match(/\d+/g);
if (digitMatch) {
// 将所有找到的数字部分连接起来
const cleanedNumber = digitMatch.join('');
console.log(`div.icon #${index} - 清理后的数字字符串: "${cleanedNumber}"`);
const number = parseInt(cleanedNumber, 10);
if (!isNaN(number)) {
console.log(`div.icon #${index} - 解析后的数字: ${number}`);
return number;
} else {
console.warn(`div.icon #${index} - 无法解析数字: "${cleanedNumber}"`);
return null;
}
} else {
console.warn(`div.icon #${index} - span内未找到数字`);
return null;
}
} else {
console.warn(`div.icon #${index} - 未找到span元素`);
return null;
}
}
/**
* 更新指标总数和详细数据
* @param {number} number - 提取的数字
* @param {number} metricIndex - 指标索引(0-4)
*/
function updateMetrics(number, metricIndex) {
const metric = metrics[metricIndex];
if (metric) {
totals[metric] += number;
// 更新详细数据
let currentItem = categorizedData[categorizedData.length - 1];
if (!currentItem || Object.keys(currentItem).length >= metrics.length) {
currentItem = {};
categorizedData.push(currentItem);
}
currentItem[metric] = number;
}
}
/**
* 创建指标展示面板
*/
function createMetricsPanel() {
// 创建面板和相关元素的逻辑
// 已在 init 函数中实现
}
// 运行初始化函数
init();
})();