京东/JD评论图+商品列表图片下载器
发表于 ・ 视频
// ==UserScript==
// @name 京东/JD评论图+商品列表图片下载器
// @namespace jd-comment-and-goods-image-downloader
// @version 1.6
// @description 京东评论图下载 + 商品列表首图下载:商品列表显示缩略图/SKU/商品名,只取首图大图,按 SKU----商品名 建文件夹
// @match *://*.jd.com/*
// @match *://*.jd.hk/*
// @match *://jd.com/*
// @match *://jd.hk/*
// @grant GM_download
// @connect 360buyimg.com
// @connect *.360buyimg.com
// ==/UserScript==
(function () {
'use strict';
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
function fullUrl(url) {
if (!url) return '';
if (url.startsWith('//')) return 'https:' + url;
return url;
}
function cleanName(name, maxLen = 255) {
return (name || '未命名')
.replace(/[\\/:*?"<>|]/g, '')
.replace(/[\r\n\t]/g, '')
.replace(/\s+/g, ' ')
.trim()
.slice(0, maxLen);
}
function getRootFolderName() {
const input = document.querySelector('#jd-folder-name-input');
return cleanName(input?.value?.trim() || '京东图片下载');
}
function makeGroupFolderName(rootFolder, indexOrId) {
return cleanName(`${rootFolder} ${String(indexOrId).padStart(2, '0')}`);
}
function normalizeUrl(url) {
return fullUrl(url || '').split('?')[0].split('#')[0];
}
// 晒单/评论图原逻辑,不影响商品列表专用逻辑
function getHDImage(url) {
if (!url) return '';
url = normalizeUrl(url);
url = url.replace(/\/shaidan\/s\d+x\d+_jfs\//i, '/shaidan/jfs/');
url = url.replace(/\/shaidan\/n\d+\/s\d+x\d+_jfs\//i, '/shaidan/jfs/');
url = url.replace(/\/shaidan\/s\d+x\d+_\//i, '/shaidan/');
url = url.replace(/\/n\d+\/jfs\//i, '/jfs/');
url = url.replace(/\/s\d+x\d+_jfs\//i, '/jfs/');
url = url.replace(/(\.jpg|\.jpeg|\.png|\.webp|\.gif)\.(dpg|avif|webp)$/i, '$1');
url = url.replace(/!(cc_)?[^/]+$/i, '');
const match = url.match(/.*?\.(jpg|jpeg|png|webp|gif)/i);
return match ? match[0] : url;
}
// 商品列表专用:只给商品列表首图使用
function getGoodsHDImage(url) {
if (!url) return '';
url = normalizeUrl(url);
// 商品列表:/jfs/ 前面的目录不管是 n7、n5、n1、n12 等,全部改成 pcpubliccms
url = url.replace(/\/[^/]+\/jfs\//i, '/pcpubliccms/jfs/');
url = url.replace(/\/s\d+x\d+_jfs\//i, '/pcpubliccms/jfs/');
url = url.replace(/(\.jpg|\.jpeg|\.png|\.webp|\.gif)\.(dpg|avif|webp)$/i, '$1');
url = url.replace(/!(cc_)?[^/]+$/i, '');
const match = url.match(/.*?\.(jpg|jpeg|png|webp|gif)/i);
return match ? match[0] : url;
}
function getImageSrc(img) {
return img?.getAttribute('data-lazy-img') ||
img?.getAttribute('data-original') ||
img?.getAttribute('data-src') ||
img?.getAttribute('src') ||
img?.currentSrc ||
img?.src ||
'';
}
function isValidImageUrl(url) {
if (!url) return false;
const raw = fullUrl(url);
const hd = getHDImage(raw);
if (!/360buyimg\.com/i.test(raw)) return false;
if (/default\.image|imagetools|headimg|avatar|icon|arrow|logo/i.test(raw)) return false;
return /\.(jpg|jpeg|png|webp|gif)$/i.test(hd);
}
function isImageSizeOk(img) {
const src = getImageSrc(img);
if (/s300x300_jfs|\/n\d+\/jfs\//i.test(src)) return true;
const w = img.naturalWidth || img.width || img.clientWidth || 0;
const h = img.naturalHeight || img.height || img.clientHeight || 0;
if (!w && !h) return true;
return w >= 300 && h >= 300;
}
function getFileExt(url) {
const match = url.match(/\.(jpg|jpeg|png|webp|gif)$/i);
return match ? match[1].toLowerCase() : 'jpg';
}
function addTask(tasks, seen, groupCounters, rawUrl, folder, prefix = '', useGoodsHD = false) {
if (!rawUrl || !folder) return;
const hdUrl = useGoodsHD ? getGoodsHDImage(rawUrl) : getHDImage(rawUrl);
if (!isValidImageUrl(hdUrl)) return;
if (seen.has(folder + '|' + hdUrl)) return;
seen.add(folder + '|' + hdUrl);
if (!groupCounters[folder]) groupCounters[folder] = 0;
groupCounters[folder]++;
const ext = getFileExt(hdUrl);
const index = String(groupCounters[folder]).padStart(2, '0');
const filename = `${folder}/${prefix ? prefix + '-' : ''}${index}.${ext}`;
tasks.push({
url: hdUrl,
filename
});
}
function getImages(container) {
const imgs = [...container.querySelectorAll('.imgs .term img.img, .imgs img, img.img')];
const urls = imgs
.filter(img => isImageSizeOk(img))
.map(img => getImageSrc(img))
.filter(isValidImageUrl)
.map(getHDImage);
return [...new Set(urls)];
}
function getComments() {
const root = document.querySelector('#comment-root, .comment-root') || document;
return [...root.querySelectorAll('ul.list > li.item, .list > li.item, .list > .item')]
.filter(el => {
const imgs = getImages(el);
return imgs.length > 0 && imgs.length <= 80;
});
}
function getImageItems() {
const root = document.querySelector('#comment-root, .comment-root') || document;
return [...root.querySelectorAll('ul.list > li.item .imgs .term, .list > .item .imgs .term, .imgs .term')];
}
function getAliveUrlsFromFilterCard(card) {
let urls = [];
try {
urls = JSON.parse(card.dataset.urls || '[]');
} catch (e) {
urls = [];
}
const removed = new Set();
try {
JSON.parse(card.dataset.removedUrls || '[]').forEach(url => removed.add(url));
} catch (e) {}
return urls.filter(url => !removed.has(url));
}
function setFilterCardRemovedUrl(card, url, removed) {
let removedUrls = [];
try {
removedUrls = JSON.parse(card.dataset.removedUrls || '[]');
} catch (e) {
removedUrls = [];
}
const set = new Set(removedUrls);
if (removed) set.add(url);
else set.delete(url);
card.dataset.removedUrls = JSON.stringify([...set]);
}
function updateFilterCardCount(card) {
const alive = getAliveUrlsFromFilterCard(card);
card.querySelectorAll('[data-jd-alive-count]').forEach(el => {
el.textContent = String(alive.length);
});
}
function getGoodsListItems() {
const root = document.querySelector('#J_GoodsList, .goods-list, .J-goods-list') || document;
return [...root.querySelectorAll('li.gl-item, li.jSubObject, .gl-warp > li')]
.filter(item => {
const sku = getGoodsSku(item);
const name = getGoodsName(item);
const imgs = getGoodsImages(item);
return sku && name && imgs.length;
});
}
function getGoodsSku(item) {
return item.querySelector('.jPic a[data-sku]')?.getAttribute('data-sku') ||
item.querySelector('a[data-sku]')?.getAttribute('data-sku') ||
item.querySelector('.jScroll li[sid]')?.getAttribute('sid') ||
item.getAttribute('data-sku') ||
'';
}
function getGoodsName(item) {
const desc = item.querySelector('.jDesc a, .p-name a, .p-name em, .jGoodsInfo .jDesc a');
return cleanName(desc?.getAttribute('title') || desc?.textContent || '未命名商品', 120);
}
// 商品列表:只取首图大图
function getGoodsImages(item) {
const cover = getImageSrc(item.querySelector('.jPic img, .p-img img'));
if (!isValidImageUrl(cover)) return [];
return [getGoodsHDImage(cover)];
}
function injectStyle() {
if (document.querySelector('#jd-hd-style')) return;
const style = document.createElement('style');
style.id = 'jd-hd-style';
style.innerHTML = `
#jd-hd-download-panel {
position: fixed;
right: 18px;
top: 18px;
z-index: 999999999;
width: 720px;
max-height: calc(100vh - 36px);
overflow: hidden;
border-radius: 20px;
background: rgba(255,255,255,.96);
backdrop-filter: blur(18px);
box-shadow: 0 18px 46px rgba(0,0,0,.14), 0 8px 24px rgba(227,24,55,.22);
border: 1px solid rgba(227,24,55,.22);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, "Microsoft YaHei", sans-serif;
color: #222;
}
.jd-panel-top {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
cursor: move;
user-select: none;
background: linear-gradient(135deg, #e1251b, #ff5b5b);
color: #fff;
}
.jd-panel-title {
display: flex;
align-items: center;
gap: 10px;
}
.jd-logo {
width: 38px;
height: 38px;
border-radius: 13px;
background: rgba(255,255,255,.22);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-weight: 900;
font-size: 19px;
}
.jd-title-main {
font-size: 17px;
font-weight: 900;
}
.jd-title-sub {
font-size: 12px;
opacity: .9;
margin-top: 3px;
}
.jd-panel-body {
padding: 14px 16px 16px;
max-height: calc(100vh - 105px);
overflow-y: auto;
background: linear-gradient(180deg, #fff5f5 0%, #ffffff 120px);
}
.jd-folder-row {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 12px;
}
#jd-folder-name-input {
flex: 1;
height: 38px;
border-radius: 13px;
border: 1px solid rgba(227,24,55,.24);
background: #fff;
padding: 0 13px;
font-size: 13px;
outline: none;
}
.jd-folder-preview {
width: 150px;
height: 38px;
line-height: 38px;
border-radius: 13px;
background: #fff1f1;
color: #e1251b;
font-size: 12px;
font-weight: 800;
text-align: center;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
border: 1px solid rgba(227,24,55,.18);
}
.jd-btn-row {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 8px;
}
.jd-btn {
height: 40px;
border: none;
outline: none;
border-radius: 13px;
font-size: 13px;
font-weight: 800;
cursor: pointer;
transition: all .18s ease;
}
.jd-btn:hover {
transform: translateY(-1px);
filter: brightness(1.04);
}
.jd-btn.primary {
color: #fff;
background: linear-gradient(135deg, #ff5b5b, #e1251b);
}
.jd-btn.success {
color: #fff;
background: linear-gradient(135deg, #20c997, #12b886);
}
.jd-btn.blue {
color: #fff;
background: linear-gradient(135deg, #4dabf7, #228be6);
}
.jd-btn.purple {
color: #fff;
background: linear-gradient(135deg, #9775fa, #7048e8);
}
.jd-btn.light {
color: #e1251b;
background: #fff;
border: 1px solid rgba(227,24,55,.26);
}
.jd-filter-tip {
margin: 10px 0 0;
padding: 8px 10px;
border-radius: 12px;
color: #9a1616;
background: #fff0f0;
font-size: 12px;
line-height: 1.5;
}
.jd-download-label {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 7px 12px;
margin-bottom: 10px;
border-radius: 999px;
background: #fff7f7;
color: #e1251b;
font-size: 13px;
font-weight: 800;
border: 1px solid rgba(227,24,55,.25);
cursor: pointer;
user-select: none;
}
.jd-download-label input,
.jd-filter-check,
.jd-single-check,
.jd-goods-check {
accent-color: #e1251b;
}
.jd-img-select-wrap {
position: absolute !important;
right: 8px;
top: 8px;
z-index: 99999999;
width: 28px;
height: 28px;
border-radius: 50%;
background: rgba(0,0,0,.42);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border: 1px solid rgba(255,255,255,.45);
}
.jd-img-select-wrap input {
width: 17px;
height: 17px;
margin: 0;
cursor: pointer;
}
.jd-img-selected {
outline: 3px solid #e1251b !important;
outline-offset: -3px !important;
border-radius: 8px !important;
}
#jd-filter-result-panel,
#jd-goods-result-panel {
margin-top: 12px;
max-height: 560px;
overflow-y: auto;
background: #fffafa;
border: 1px solid #ffd0d0;
border-radius: 18px;
padding: 12px;
}
.jd-filter-header,
.jd-goods-header {
position: sticky;
top: 0;
z-index: 2;
background: linear-gradient(135deg,#e1251b,#ff5b5b);
color: #fff;
padding: 11px 13px;
border-radius: 13px;
font-weight: 900;
margin-bottom: 12px;
}
.jd-filter-close,
.jd-goods-close,
.jd-goods-select-visible {
float: right;
border: none;
background: #fff;
color: #e1251b;
border-radius: 999px;
padding: 4px 11px;
cursor: pointer;
font-weight: 800;
margin-left: 6px;
}
.jd-filter-card {
margin-bottom: 13px;
padding: 11px;
border: 1px solid #ffd0d0;
border-radius: 15px;
background: #fff;
}
.jd-filter-title {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
font-weight: 900;
color: #e1251b;
margin-bottom: 10px;
}
.jd-filter-title label {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
}
.jd-filter-pics {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 9px;
}
.jd-filter-pic-wrap {
position: relative;
border-radius: 12px;
overflow: hidden;
background: #fff;
border: 1px solid #ffe0e0;
}
.jd-filter-pic-wrap img {
display: block;
width: 100%;
aspect-ratio: 3 / 4;
object-fit: cover;
background: #fff;
}
.jd-filter-remove {
position: absolute;
right: 6px;
top: 6px;
z-index: 2;
width: 24px;
height: 24px;
border: none;
border-radius: 50%;
background: rgba(0,0,0,.62);
color: #fff;
cursor: pointer;
font-size: 18px;
line-height: 24px;
font-weight: 900;
text-align: center;
padding: 0;
}
.jd-filter-pic-wrap.jd-filter-pic-removed {
opacity: .28;
filter: grayscale(1);
}
.jd-filter-pic-wrap.jd-filter-pic-removed::after {
content: "不下载";
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 3;
padding: 5px 9px;
border-radius: 999px;
background: rgba(0,0,0,.72);
color: #fff;
font-size: 12px;
font-weight: 900;
white-space: nowrap;
}
.jd-goods-row {
display: grid;
grid-template-columns: 28px 58px 120px 1fr 70px;
align-items: center;
gap: 10px;
min-height: 62px;
margin-bottom: 9px;
padding: 8px 10px;
border-radius: 14px;
border: 1px solid #ffd8d8;
background: #fff;
cursor: pointer;
}
.jd-goods-row:hover {
background: #fff7f7;
}
.jd-goods-row img {
width: 54px;
height: 54px;
object-fit: cover;
border-radius: 10px;
border: 1px solid #ffe1e1;
background: #fff;
}
.jd-goods-sku {
font-size: 12px;
color: #e1251b;
font-weight: 900;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.jd-goods-name {
font-size: 13px;
color: #222;
line-height: 1.35;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.jd-goods-count {
font-size: 12px;
color: #888;
text-align: right;
white-space: nowrap;
}
`;
document.head.appendChild(style);
}
function makePanelDraggable(panel, handle) {
let dragging = false;
let startX = 0;
let startY = 0;
let startLeft = 0;
let startTop = 0;
handle.addEventListener('mousedown', e => {
if (e.target.closest('button, input, label')) return;
dragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = panel.getBoundingClientRect();
startLeft = rect.left;
startTop = rect.top;
panel.style.left = `${startLeft}px`;
panel.style.top = `${startTop}px`;
panel.style.right = 'auto';
panel.style.bottom = 'auto';
document.body.style.userSelect = 'none';
});
document.addEventListener('mousemove', e => {
if (!dragging) return;
panel.style.left = `${Math.max(0, startLeft + e.clientX - startX)}px`;
panel.style.top = `${Math.max(0, startTop + e.clientY - startY)}px`;
});
document.addEventListener('mouseup', () => {
dragging = false;
document.body.style.userSelect = '';
});
}
function addPanel() {
if (document.querySelector('#jd-hd-download-panel')) return;
injectStyle();
const panel = document.createElement('div');
panel.id = 'jd-hd-download-panel';
panel.innerHTML = `
<div class="jd-panel-top">
<div class="jd-panel-title">
<span class="jd-logo">京</span>
<div>
<div class="jd-title-main">京东图片下载器</div>
<div class="jd-title-sub">评论图 · 商品列表首图 · SKU文件夹 · 可拖动</div>
</div>
</div>
</div>
<div class="jd-panel-body">
<div class="jd-folder-row">
<input id="jd-folder-name-input" placeholder="输入主文件夹名,最长255字符">
<div class="jd-folder-preview" id="jd-folder-preview">京东图片下载</div>
</div>
<div class="jd-btn-row">
<button class="jd-btn primary" id="jd-scan-group-btn">评论分组</button>
<button class="jd-btn blue" id="jd-filter-group-btn">筛选</button>
<button class="jd-btn blue" id="jd-scan-img-btn">图集选择</button>
<button class="jd-btn purple" id="jd-goods-list-btn">商品列表</button>
<button class="jd-btn success" id="jd-download-btn">下载选中</button>
<button class="jd-btn light" id="jd-select-all-btn">全选/反选</button>
</div>
<div class="jd-filter-tip">
商品列表:点击后显示“缩略图 + SKU + 商品名”单行列表,可勾选、全选/反选,再点“下载选中”。商品列表只下载首图大图,并将 /jfs/ 前目录统一改为 pcpubliccms;晒单/评论图功能不变。
</div>
<div id="jd-filter-body"></div>
</div>
`;
document.body.appendChild(panel);
makePanelDraggable(panel, panel.querySelector('.jd-panel-top'));
const input = document.querySelector('#jd-folder-name-input');
const preview = document.querySelector('#jd-folder-preview');
input.addEventListener('input', () => {
preview.textContent = getRootFolderName();
});
document.querySelector('#jd-scan-group-btn').onclick = markComments;
document.querySelector('#jd-filter-group-btn').onclick = filterCommentGroups;
document.querySelector('#jd-scan-img-btn').onclick = markImageItems;
document.querySelector('#jd-goods-list-btn').onclick = showGoodsListPanel;
document.querySelector('#jd-download-btn').onclick = downloadSelected;
document.querySelector('#jd-select-all-btn').onclick = toggleAll;
}
function markComments() {
const comments = getComments();
let validCount = 0;
comments.forEach(comment => {
if (comment.querySelector('.jd-download-check')) return;
const imgs = getImages(comment);
if (!imgs.length) return;
validCount++;
const box = document.createElement('label');
box.className = 'jd-download-label';
box.innerHTML = `
<input type="checkbox" class="jd-download-check jd-group-check">
<span>本组 ${imgs.length} 张原图</span>
`;
comment.prepend(box);
});
alert(`评论分组扫描完成:找到 ${validCount} 个图片评论分组`);
}
function filterCommentGroups() {
const body = document.querySelector('#jd-filter-body');
if (!body) return;
body.innerHTML = '';
const groups = getComments()
.map(comment => {
const urls = getImages(comment);
return { urls, count: urls.length };
})
.filter(item => item.count >= 3)
.sort((a, b) => b.count - a.count);
if (!groups.length) {
alert('当前已加载评论里没有找到 3 张以上的分组。请先滚动评论加载更多,再点筛选。');
return;
}
const resultPanel = document.createElement('div');
resultPanel.id = 'jd-filter-result-panel';
resultPanel.innerHTML = `
<div class="jd-filter-header">
筛选结果:${groups.length} 组,只显示 3 张以上
<button id="jd-close-filter-panel" class="jd-filter-close">清空</button>
</div>
`;
groups.forEach((group, index) => {
const card = document.createElement('div');
card.className = 'jd-filter-card';
card.dataset.urls = JSON.stringify(group.urls);
card.dataset.removedUrls = JSON.stringify([]);
const pics = group.urls.map(url => `
<div class="jd-filter-pic-wrap" data-jd-filter-url="${url}">
<button type="button" class="jd-filter-remove" title="排除这张,不下载">×</button>
<img src="${url}" loading="lazy">
</div>
`).join('');
card.innerHTML = `
<div class="jd-filter-title">
<label>
<input type="checkbox" class="jd-filter-check">
第 ${index + 1} 组 · 剩余 <span data-jd-alive-count>${group.count}</span> 张
</label>
<span>原始 ${group.count} 张</span>
</div>
<div class="jd-filter-pics">${pics}</div>
`;
resultPanel.appendChild(card);
card.querySelectorAll('.jd-filter-remove').forEach(btn => {
btn.addEventListener('click', e => {
e.preventDefault();
e.stopPropagation();
const wrap = btn.closest('.jd-filter-pic-wrap');
if (!wrap) return;
const url = wrap.dataset.jdFilterUrl;
const removed = !wrap.classList.contains('jd-filter-pic-removed');
wrap.classList.toggle('jd-filter-pic-removed', removed);
btn.textContent = removed ? '↺' : '×';
btn.title = removed ? '恢复这张,参与下载' : '排除这张,不下载';
setFilterCardRemovedUrl(card, url, removed);
updateFilterCardCount(card);
});
});
});
body.appendChild(resultPanel);
document.querySelector('#jd-close-filter-panel').onclick = () => {
body.innerHTML = '';
};
alert(`筛选完成:显示 ${groups.length} 个 3 张以上分组。`);
}
function markImageItems() {
const items = getImageItems();
let validCount = 0;
const groupMap = new Map();
items.forEach(item => {
if (item.querySelector('.jd-single-check')) return;
const img = item.querySelector('img');
if (!img) return;
if (!isImageSizeOk(img)) return;
const hdUrl = getHDImage(getImageSrc(img));
if (!isValidImageUrl(hdUrl)) return;
if (!groupMap.has(hdUrl)) {
groupMap.set(hdUrl, String(groupMap.size + 1).padStart(2, '0'));
}
const groupId = groupMap.get(hdUrl);
item.style.position = 'relative';
item.dataset.jdImageUrl = hdUrl;
item.dataset.jdGalleryGroupId = groupId;
const wrap = document.createElement('label');
wrap.className = 'jd-img-select-wrap';
wrap.title = `选中下载高清原图,分组:${groupId}`;
wrap.innerHTML = `<input type="checkbox" class="jd-single-check">`;
const checkbox = wrap.querySelector('input');
checkbox.addEventListener('change', () => {
item.classList.toggle('jd-img-selected', checkbox.checked);
});
wrap.addEventListener('click', e => {
e.stopPropagation();
});
item.appendChild(wrap);
validCount++;
});
alert(`图集扫描完成:找到 ${validCount} 张可选择图片`);
}
function showGoodsListPanel() {
const body = document.querySelector('#jd-filter-body');
if (!body) return;
body.innerHTML = '';
const goods = getGoodsListItems();
if (!goods.length) {
alert('没有找到商品列表。请确认页面存在 #J_GoodsList,并且商品已加载。');
return;
}
const resultPanel = document.createElement('div');
resultPanel.id = 'jd-goods-result-panel';
resultPanel.innerHTML = `
<div class="jd-goods-header">
商品列表:${goods.length} 个商品,只取首图大图
<button id="jd-close-goods-panel" class="jd-goods-close">清空</button>
<button id="jd-select-goods-visible" class="jd-goods-select-visible">全选商品</button>
</div>
`;
goods.forEach(item => {
const sku = getGoodsSku(item);
const name = getGoodsName(item);
const images = getGoodsImages(item);
const thumb = images[0] || '';
const row = document.createElement('label');
row.className = 'jd-goods-row';
row.dataset.jdGoodsSku = sku;
row.dataset.jdGoodsName = name;
row.dataset.jdGoodsImages = JSON.stringify(images);
row.innerHTML = `
<input type="checkbox" class="jd-goods-check">
<img src="${thumb}" loading="lazy">
<div class="jd-goods-sku" title="${sku}">${sku}</div>
<div class="jd-goods-name" title="${name}">${name}</div>
<div class="jd-goods-count">${images.length} 图</div>
`;
resultPanel.appendChild(row);
});
body.appendChild(resultPanel);
document.querySelector('#jd-close-goods-panel').onclick = () => {
body.innerHTML = '';
};
document.querySelector('#jd-select-goods-visible').onclick = e => {
e.preventDefault();
e.stopPropagation();
const checks = [...document.querySelectorAll('.jd-goods-check')];
const hasUnchecked = checks.some(c => !c.checked);
checks.forEach(c => c.checked = hasUnchecked);
e.target.textContent = hasUnchecked ? '反选商品' : '全选商品';
};
alert(`商品列表扫描完成:找到 ${goods.length} 个商品。请勾选后点击“下载选中”。`);
}
function toggleAll() {
const checks = [
...document.querySelectorAll('.jd-download-check'),
...document.querySelectorAll('.jd-single-check'),
...document.querySelectorAll('.jd-filter-check'),
...document.querySelectorAll('.jd-goods-check')
];
if (!checks.length) {
alert('请先点击“评论分组”“筛选”“图集选择”或“商品列表”');
return;
}
const hasUnchecked = checks.some(c => !c.checked);
checks.forEach(c => {
c.checked = hasUnchecked;
const item = c.closest('[data-jd-image-url]');
if (item) {
item.classList.toggle('jd-img-selected', c.checked);
}
});
}
async function runDownloadTasks(tasks, doneMessage) {
if (!tasks.length) {
alert('没有可下载图片');
return;
}
for (let i = 0; i < tasks.length; i++) {
const task = tasks[i];
console.log('下载:', task.url, task.filename);
GM_download({
url: task.url,
name: task.filename,
saveAs: false,
onload: () => console.log('下载完成:', task.filename),
onerror: err => console.warn('下载失败:', task.url, err)
});
await sleep(500);
}
alert(doneMessage || `已开始下载 ${tasks.length} 张图片`);
}
async function downloadSelected() {
const rootFolder = getRootFolderName();
const tasks = [];
const seen = new Set();
const groupCounters = {};
const selectedGroups = [...document.querySelectorAll('.jd-group-check:checked')]
.map(input => input.closest('li.item, .list > .item'))
.filter(Boolean);
selectedGroups.forEach((comment, groupIndex) => {
const groupName = makeGroupFolderName(rootFolder, groupIndex + 1);
const folder = `${rootFolder}/评论分组/${groupName}`;
getImages(comment).forEach(url => {
addTask(tasks, seen, groupCounters, url, folder);
});
});
const selectedFilterGroups = [...document.querySelectorAll('.jd-filter-check:checked')]
.map(input => input.closest('.jd-filter-card'))
.filter(Boolean);
selectedFilterGroups.forEach((card, groupIndex) => {
const urls = getAliveUrlsFromFilterCard(card);
if (!urls.length) return;
const groupName = makeGroupFolderName(rootFolder, groupIndex + 1);
const folder = `${rootFolder}/筛选分组/${groupName}`;
urls.forEach(url => {
addTask(tasks, seen, groupCounters, url, folder);
});
});
const selectedSingles = [...document.querySelectorAll('.jd-single-check:checked')]
.map(input => input.closest('[data-jd-image-url]'))
.filter(Boolean);
selectedSingles.forEach(item => {
const rawUrl = item.dataset.jdImageUrl;
const groupId = item.dataset.jdGalleryGroupId || '01';
const groupName = makeGroupFolderName(rootFolder, groupId);
const folder = `${rootFolder}/图集分组/${groupName}`;
addTask(tasks, seen, groupCounters, rawUrl, folder);
});
const selectedGoods = [...document.querySelectorAll('.jd-goods-check:checked')]
.map(input => input.closest('.jd-goods-row'))
.filter(Boolean);
selectedGoods.forEach(row => {
const sku = row.dataset.jdGoodsSku || '';
const name = row.dataset.jdGoodsName || '未命名商品';
let images = [];
try {
images = JSON.parse(row.dataset.jdGoodsImages || '[]');
} catch (e) {
images = [];
}
const folderName = cleanName(`${sku}----${name}`, 180);
const folder = `${rootFolder}/商品列表/${folderName}`;
images.forEach(url => {
addTask(tasks, seen, groupCounters, url, folder, '首图大图', true);
});
});
await runDownloadTasks(
tasks,
`已开始下载 ${tasks.length} 张图片。\n商品列表只下载首图大图,并按“SKU----商品名”建立文件夹。\n筛选分组中点过 × 的图片不会下载。`
);
}
addPanel();
const observer = new MutationObserver(() => {
addPanel();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
})();
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。