feat(script): 新增支持仅提取勾选的作品链接

This commit is contained in:
Quan 2025-08-10 16:13:58 +08:00
parent 120e5d65c3
commit 5b54e78582
2 changed files with 287 additions and 10 deletions

View File

@ -18,11 +18,12 @@
**用户脚本更新内容:**
**版本号2.1.2**
**版本号2.1.4**
1. 修复单张图片的作品下载后文件损坏问题
2. 添加未登录状态下无法提取作品链接的提示
3. 脚本设置新增 `菜单始终显示` 配置项
4. 修复用户脚本 CSS 样式冲突问题
5. 优化部分弹窗提示的交互界面样式
6. 优化提取作品 ID 的正则表达式
6. 新增支持仅提取勾选的作品链接
7. 优化提取作品 ID 的正则表达式

View File

@ -1,7 +1,7 @@
// ==UserScript==
// @name XHS-Downloader
// @namespace https://github.com/JoeanAmier/XHS-Downloader
// @version 2.1.3
// @version 2.1.4
// @description 提取小红书作品/用户链接,下载小红书无水印图文/视频作品文件
// @author JoeanAmier
// @match http*://xhslink.com/*
@ -485,7 +485,7 @@
const extractNotesInfo = order => {
const notesRawValue = unsafeWindow.__INITIAL_STATE__.user.notes._rawValue[order];
return notesRawValue.map(item => [item.id, item.xsecToken,]);
return notesRawValue.map(item => [item.id, item.xsecToken, item.noteCard.cover.urlDefault, item.noteCard.user.nickName, item.noteCard.displayTitle,]);
};
const extractBoardInfo = () => {
@ -501,7 +501,7 @@
const id = match[1]; // match[0] 是整个匹配的字符串match[1] 是第一个括号内的匹配
const notesRawValue = unsafeWindow.__INITIAL_STATE__.board.boardFeedsMap._rawValue[id].notes;
return notesRawValue.map(item => [item.noteId, item.xsecToken,]);
return notesRawValue.map(item => [item.noteId, item.xsecToken, item.noteCard.cover.urlDefault, item.noteCard.user.nickName, item.noteCard.displayTitle,]);
} else {
console.error("从链接提取专辑 ID 失败", currentUrl,);
return [];
@ -510,12 +510,12 @@
const extractFeedInfo = () => {
const notesRawValue = unsafeWindow.__INITIAL_STATE__.feed.feeds._rawValue;
return notesRawValue.map(item => [item.id, item.xsecToken,]);
return notesRawValue.map(item => [item.id, item.xsecToken, item.noteCard.cover.urlDefault, item.noteCard.user.nickName, item.noteCard.displayTitle,]);
};
const extractSearchNotes = () => {
const notesRawValue = unsafeWindow.__INITIAL_STATE__.search.feeds._rawValue;
return notesRawValue.map(item => [item.id, item.xsecToken,]);
return notesRawValue.map(item => [item.id, item.xsecToken, item.noteCard.cover.urlDefault, item.noteCard.user.nickName, item.noteCard.displayTitle,]);
}
const extractSearchUsers = () => {
@ -547,10 +547,26 @@
} else if (order === 5) {
data = extractBoardInfo()
} else {
data = [];
data = []
}
if (data.length === 0) {
callback("");
return;
}
let urlsString;
if (order === 4) {
urlsString = generateUserUrls(data);
callback(urlsString);
} else {
showListSelectionModal(data.map(([id, token, cover, author, title,]) => ({
id: id, token: token, image: cover, author: author, title: title,
})),).then((selected) => {
if (selected.length > 0) {
urlsString = generateNoteUrls(selected.map(item => [item.id, item.token]));
callback(urlsString);
}
});
}
let urlsString = order !== 4 ? generateNoteUrls(data) : generateUserUrls(data);
callback(urlsString);
}, [0, 1, 2, 5].includes(order))
};
@ -1230,6 +1246,266 @@
overlay.addEventListener('click', (e) => e.target === overlay && closeImagesModal());
};
(() => {
if (!document.getElementById('listSelectionStyle')) {
const style = document.createElement('style');
style.id = 'listSelectionStyle';
style.textContent = `
/* 列表弹窗容器,仅定义差异尺寸,其他沿用通用 modal 样式 */
.list-selection-modal {
background: #fff;
border-radius: 16px;
width: 80%;
max-width: 800px;
max-height: 80vh;
box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);
overflow: hidden;
animation: scaleUp 0.3s;
display: flex;
flex-direction: column;
}
/* 列表容器 */
.list-container {
display: flex;
flex-direction: column;
gap: 10px;
}
/* 列表项 */
.list-item {
display: grid;
grid-template-columns: 24px 64px 1fr; /* 复选框、缩略图、文本区 */
align-items: center;
gap: 12px;
padding: 10px;
border: 1px solid #eee;
border-radius: 10px;
transition: background 0.15s, border-color 0.15s, box-shadow 0.15s;
cursor: pointer;
}
.list-item:hover {
background: #fafafa;
border-color: #e6e6e6;
}
.list-item.selected {
border-color: #2196F3;
box-shadow: 0 0 0 4px rgba(33,150,243,0.12) inset;
}
/* 复选框样式(使用原生复选框以保证可访问性与简单性) */
.list-checkbox {
width: 18px;
height: 18px;
margin: 0 0 0 2px;
cursor: pointer;
}
/* 缩略图 */
.list-thumb {
width: 64px;
height: 64px;
object-fit: cover;
border-radius: 8px;
user-select: none;
pointer-events: none; /* 点击行切换选择,避免图片拦截点击 */
background: #f2f2f2;
}
/* 文本区 */
.list-texts {
display: flex;
flex-direction: column;
min-width: 0; /* 允许文本正确换行截断 */
}
.list-author {
font-size: 0.95rem;
color: #212121;
font-weight: 500;
line-height: 1.4;
word-break: break-word;
}
.list-title {
margin-top: 4px;
font-size: 0.85rem;
color: #757575;
line-height: 1.4;
word-break: break-word;
}
`;
document.head.appendChild(style);
}
})();
/**
* 显示列表选择弹窗
* @param {Array<{id:any, image:string, author:string, title:string}>} list
* @param {Object} [options]
* @param {string} [options.title='请选择'] 弹窗标题
* @param {string} [options.confirmText='确认'] 确认按钮文本
* @param {string} [options.cancelText='取消'] 取消按钮文本
* @param {boolean} [options.prechecked=true] 是否默认勾选全部
* @returns {Promise<Array|null>} 点击确认返回选中项数组取消/关闭返回 null
*/
function showListSelectionModal(list, options = {}) {
const {
title = '请选择需要提取的项目', confirmText = '确认', cancelText = '取消', prechecked = true,
} = options;
if (document.getElementById('listSelectionOverlay')) return Promise.resolve(null);
return new Promise((resolve) => {
// 覆盖层
const overlay = document.createElement('div');
overlay.id = 'listSelectionOverlay';
overlay.style.cssText = `
position: fixed; inset: 0; background: rgba(0,0,0,0.32);
backdrop-filter: blur(4px); display: flex; justify-content: center; align-items: center;
z-index: 10000; animation: fadeIn 0.3s;
`;
// 弹窗
const modal = document.createElement('div');
modal.className = 'list-selection-modal';
// 头部
const header = document.createElement('div');
header.className = 'modal-header';
header.innerHTML = `<span>${title}</span>`;
// 内容
const body = document.createElement('div');
body.className = 'modal-body';
const container = document.createElement('div');
container.className = 'list-container';
// id -> item 映射
const map = new Map();
list.forEach(item => {
const row = document.createElement('div');
row.className = 'list-item';
row.dataset.key = item.id;
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'list-checkbox';
checkbox.checked = prechecked;
const img = document.createElement('img');
img.className = 'list-thumb';
img.src = item.image || '';
img.alt = 'thumb';
const texts = document.createElement('div');
texts.className = 'list-texts';
const author = document.createElement('div');
author.className = 'list-author';
author.textContent = item.author ?? '';
const title = document.createElement('div');
title.className = 'list-title';
title.textContent = item.title ?? '';
texts.appendChild(author);
texts.appendChild(title);
row.appendChild(checkbox);
row.appendChild(img);
row.appendChild(texts);
if (checkbox.checked) row.classList.add('selected');
row.addEventListener('click', (e) => {
if ((e.target instanceof HTMLElement) && e.target.classList.contains('list-checkbox')) return;
checkbox.checked = !checkbox.checked;
row.classList.toggle('selected', checkbox.checked);
});
checkbox.addEventListener('change', () => {
row.classList.toggle('selected', checkbox.checked);
});
map.set(row.dataset.key, item);
container.appendChild(row);
});
body.appendChild(container);
// 底部按钮
const footer = document.createElement('div');
footer.className = 'modal-footer';
// 新增:全选 / 全不选
const selectAllBtn = document.createElement('button');
selectAllBtn.className = 'secondary-btn';
selectAllBtn.textContent = '全选';
const selectNoneBtn = document.createElement('button');
selectNoneBtn.className = 'secondary-btn';
selectNoneBtn.textContent = '全不选';
// 右侧操作:确认 / 取消
const confirmBtn = document.createElement('button');
confirmBtn.className = 'primary-btn';
confirmBtn.textContent = confirmText;
const cancelBtn = document.createElement('button');
cancelBtn.className = 'secondary-btn';
cancelBtn.textContent = cancelText;
// 将按钮加入 footer顺序全选、全不选、确认、取消
footer.appendChild(selectAllBtn);
footer.appendChild(selectNoneBtn);
footer.appendChild(confirmBtn);
footer.appendChild(cancelBtn);
// 组装
modal.appendChild(header);
modal.appendChild(body);
modal.appendChild(footer);
overlay.appendChild(modal);
document.body.appendChild(overlay);
// 辅助:批量设置选择状态
const setAllChecked = (checked) => {
container.querySelectorAll('.list-item').forEach((row) => {
const box = row.querySelector('.list-checkbox');
if (box) {
box.checked = checked;
row.classList.toggle('selected', checked);
}
});
};
// 关闭
const close = (result) => {
overlay.style.animation = 'fadeOut 0.2s';
setTimeout(() => {
overlay.remove();
resolve(result);
}, 200);
};
// 事件绑定
selectAllBtn.addEventListener('click', () => setAllChecked(true));
selectNoneBtn.addEventListener('click', () => setAllChecked(false));
cancelBtn.addEventListener('click', () => close(null));
overlay.addEventListener('click', (e) => {
if (e.target === overlay) close(null);
});
confirmBtn.addEventListener('click', () => {
const selected = [];
container.querySelectorAll('.list-item').forEach((row) => {
const checkbox = row.querySelector('.list-checkbox');
if (checkbox && checkbox.checked) {
const key = row.dataset.key;
if (map.has(key)) selected.push(map.get(key));
}
});
close(selected);
});
});
}
// 创建主图标
const createIcon = () => {
const icon = document.createElement('div');