// ==UserScript==
// @name 京东/JD评论图+商品列表图片下载器 评论分组修复版 v3.6
// @namespace jd-comment-and-goods-image-downloader
// @version 3.6
// @description 京东评论图下载 + 商品列表首图下载:专门适配京东评价弹层/Virtuoso虚拟列表;只滚评论弹层,不滚商详页;严格按单条评论分组;支持自定义滑动次数
// @match *://*.jd.com/*
// @match *://*.jd.hk/*
// @match *://jd.com/*
// @match *://jd.hk/*
// @grant GM_download
// @connect 360buyimg.com
// @connect *.360buyimg.com
// @connect jvod.300hu.com
// ==/UserScript==
(function () {
'use strict';
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const commentGroupCache = new Map();
const watchedScrollers = new WeakSet();
let autoCollecting = false;
let scanTimer = null;
let goodsRefreshTimer = null;
let autoWatchTimer = null;
const JD_FORCED_COMMENT_SCROLL_XPATH = '/html/body/div[8]/div';
const JD_RATE_OVERLAY_SELECTOR = [
'.jdc-page-overlay[class*="rateListBox"]',
'.jdc-page-overlay',
'[class*="rateListBox"]'
].join(',');
const JD_RATE_CARD_SELECTOR = [
'.jdc-pc-rate-card',
'[class*="rate-card"]',
'[class*="rateCard"]',
'[class*="listItem"]'
].join(',');
const JD_RATE_IMAGE_AREA_SELECTOR = [
'.jdc-pc-rate-card-images',
'.jd-content-pc-media-list',
'.jd-content-pc-media-list-item',
'.jdc-image'
].join(',');
const JD_COMMENT_ROOT_SELECTOR = [
'#comment-root',
'.comment-root',
'#comment',
'#comments-list',
'.comments-list',
'.J-comments-list',
'.comment-list',
'.comment-container',
'.comments-container',
'.J-comment',
'.review-list',
'.reviews-list',
'.evaluate-list',
'.evaluation-list',
'[id*="comment"]',
'[class*="comment"]',
'[class*="Comment"]',
'[class*="review"]',
'[class*="evaluate"]',
'[class*="evaluation"]'
].join(',');
const JD_COMMENT_ITEM_SELECTOR = [
'.jdc-pc-rate-card',
'[class*="rate-card"]',
'[class*="rateCard"]',
'[class*="listItem"]',
'ul.list > li.item',
'.list > li.item',
'.list > .item',
'li.item',
'.comment-item',
'.J-comment-item',
'.comment-con',
'.comment-column',
'.comment-box',
'.review-item',
'.evaluate-item',
'.evaluation-item',
'[class*="comment-item"]',
'[class*="CommentItem"]',
'[class*="review-item"]',
'[class*="evaluate-item"]',
'[class*="evaluation-item"]'
].join(',');
const JD_COMMENT_IMAGE_AREA_SELECTOR = [
'.jdc-pc-rate-card-images',
'.jd-content-pc-media-list',
'.jd-content-pc-media-list-item',
'.jdc-image',
'.imgs',
'.pic-list',
'.image-list',
'.photo-list',
'.shaidan',
'[class*="shaidan"]',
'[class*="photo-list"]',
'[class*="pic-list"]',
'[class*="image-list"]',
'[class*="comment-image"]',
'[class*="comment-pic"]',
'[class*="comment-photo"]'
].join(',');
const JD_IMAGE_SELECTOR = [
'img[src*="360buyimg"]',
'img[data-src*="360buyimg"]',
'img[data-lazy-img*="360buyimg"]',
'img[data-original*="360buyimg"]',
'img[src*="jvod.300hu.com"]',
'img[data-src*="jvod.300hu.com"]',
'img[data-lazy-img*="jvod.300hu.com"]'
].join(',');
function getElementByXPath(xpath) {
try {
return document.evaluate(
xpath,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
} catch (e) {
return null;
}
}
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);
if (/jvod\.300hu\.com/i.test(url)) {
return 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);
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|jvod\.300hu\.com)/i.test(raw)) return false;
if (/default\.image|imagetools|headimg|avatar|icon|arrow|logo|sprite|blank/i.test(raw)) return false;
return /\.(jpg|jpeg|png|webp|gif)$/i.test(hd);
}
function isImageSizeOk(img) {
const src = getImageSrc(img);
if (/shaidan/i.test(src)) return true;
if (/jvod\.300hu\.com/i.test(src)) return true;
if (/s300x300_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 >= 60 && h >= 60;
}
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 isInsideScriptPanel(el) {
return !!el?.closest?.('#jd-hd-download-panel');
}
function isInsideGoodsArea(el) {
return !!el?.closest?.(`
#J_GoodsList,
.goods-list,
.J-goods-list,
.gl-warp,
.gl-item,
.jSubObject,
.p-img,
.jPic,
.p-name,
.jDesc,
.jGoodsInfo,
.shop-list,
.search-result,
.m-list,
[class*="goods"],
[class*="Goods"],
[class*="product"],
[class*="Product"],
[class*="sku"],
[class*="Sku"]
`);
}
function isBadCommentCandidate(el) {
if (!el) return true;
if (isInsideScriptPanel(el)) return true;
if (isInsideGoodsArea(el)) return true;
return false;
}
function getTextForJudge(el) {
if (!el) return '';
return (el.textContent || '')
.replace(/\s+/g, ' ')
.trim()
.slice(0, 1200);
}
function getRateOverlayRoot() {
const byXpath = getElementByXPath(JD_FORCED_COMMENT_SCROLL_XPATH);
if (
byXpath &&
byXpath instanceof Element &&
(
byXpath.querySelector('.jdc-pc-rate-card') ||
byXpath.querySelector('[data-virtuoso-scroller="true"]') ||
/商品评价|全部评价|图\/视频|好评|中评|差评/.test(byXpath.textContent || '')
)
) {
return byXpath;
}
const overlays = [...document.querySelectorAll(JD_RATE_OVERLAY_SELECTOR)]
.filter(el => {
if (isInsideScriptPanel(el)) return false;
const text = el.textContent || '';
return (
/商品评价|全部评价|图\/视频|好评|中评|差评/.test(text) ||
el.querySelector('.jdc-pc-rate-card') ||
el.querySelector('[data-virtuoso-scroller="true"]')
);
});
return overlays[0] || null;
}
function getForcedCommentScroller() {
const root = getRateOverlayRoot();
if (!root) return null;
const candidates = [
root.querySelector('[data-virtuoso-scroller="true"]'),
root.querySelector('[data-testid="virtuoso-item-list"]')?.parentElement,
root.querySelector('[class*="_list_"]'),
root.querySelector('[class*="_rateListContainer_"]'),
root
].filter(Boolean);
const scrollable = candidates.find(el => {
if (!(el instanceof Element)) return false;
return el.scrollHeight > el.clientHeight + 20;
});
return scrollable || root;
}
function lockDetailPageScroll() {
const root = getRateOverlayRoot();
if (root) {
document.documentElement.classList.add('jd-rate-layer-open');
document.body.classList.add('jd-rate-layer-open');
} else {
document.documentElement.classList.remove('jd-rate-layer-open');
document.body.classList.remove('jd-rate-layer-open');
}
}
function getCommentRoots() {
const set = new Set();
const overlayRoot = getRateOverlayRoot();
const forcedScroller = getForcedCommentScroller();
if (overlayRoot) set.add(overlayRoot);
if (forcedScroller) set.add(forcedScroller);
[...document.querySelectorAll(JD_COMMENT_ROOT_SELECTOR)].forEach(el => {
if (isInsideScriptPanel(el)) return;
if (isInsideGoodsArea(el)) return;
const mark = `${el.id || ''} ${el.className || ''}`.toLowerCase();
const text = el.textContent || '';
const hasCommentMark =
/comment|review|evaluate|evaluation|rate/i.test(mark) ||
/评价|评论|晒单|追评|全部评价|有图评价|晒图|商品评价/i.test(text);
if (hasCommentMark) {
set.add(el);
}
});
return [...set].sort((a, b) => {
const ai = a.querySelectorAll(JD_IMAGE_SELECTOR).length;
const bi = b.querySelectorAll(JD_IMAGE_SELECTOR).length;
return bi - ai;
});
}
function getLooseImages(container) {
if (!container) return [];
if (isBadCommentCandidate(container)) return [];
const urls = [];
[...container.querySelectorAll(JD_IMAGE_SELECTOR)].forEach(img => {
if (isInsideScriptPanel(img)) return;
if (isInsideGoodsArea(img)) return;
if (!isImageSizeOk(img)) return;
const src = getImageSrc(img);
if (!isValidImageUrl(src)) return;
urls.push(getHDImage(src));
});
return [...new Set(urls)];
}
function getImages(commentItem) {
const card = commentItem?.closest?.('.jdc-pc-rate-card') || commentItem;
return getLooseImages(card);
}
function getImagesFromArea(area) {
return getLooseImages(area);
}
function hasEnoughCommentText(el) {
const text = getTextForJudge(el);
if (!text) return false;
if (/评价|评论|晒单|追评|买家|用户|客服回复|颜色|尺码|味道|质量|物流|包装|口感|效果|很好|不错|满意|差评|中评|好评|晒图|图片|视频|商家/.test(text)) {
return true;
}
return text.length >= 8;
}
function countLooseImages(el) {
return getLooseImages(el).length;
}
function countNestedKnownCommentItems(el) {
if (!el) return 0;
return [...el.querySelectorAll(JD_COMMENT_ITEM_SELECTOR)]
.filter(item => item !== el)
.length;
}
function isTooBigListContainer(el, root) {
if (!el) return true;
if (root && el === root) return true;
const imgCount = countLooseImages(el);
const nestedItemCount = countNestedKnownCommentItems(el);
const rect = el.getBoundingClientRect();
if (el.matches?.('[data-virtuoso-scroller="true"], [data-testid="virtuoso-item-list"], .jdc-page-overlay')) {
return true;
}
if (nestedItemCount >= 2) return true;
if (imgCount > 40) return true;
if (rect.height > window.innerHeight * 0.95 && imgCount > 3) return true;
return false;
}
function scoreCommentCandidate(el, root) {
if (!el) return -999;
if (isBadCommentCandidate(el)) return -999;
if (root && !root.contains(el)) return -999;
if (el.matches?.('.jdc-pc-rate-card')) {
const imgs = getLooseImages(el);
return imgs.length ? 100 : -999;
}
const imgs = getLooseImages(el);
if (!imgs.length) return -999;
if (imgs.length > 40) return -999;
const rect = el.getBoundingClientRect();
if (rect.width < 80 || rect.height < 40) return -999;
if (isTooBigListContainer(el, root)) return -999;
const mark = `${el.className || ''} ${el.id || ''}`.toLowerCase();
const text = getTextForJudge(el);
const selectorOk = el.matches?.(JD_COMMENT_ITEM_SELECTOR);
const strongClassOk = /comment|review|evaluate|evaluation|reply|feed|rate/.test(mark);
const weakClassOk = /item|row|cell|card/.test(mark);
const textOk = hasEnoughCommentText(el);
const hasOldImageArea = !!el.querySelector(JD_COMMENT_IMAGE_AREA_SELECTOR);
let score = 0;
if (selectorOk) score += 20;
if (strongClassOk) score += 18;
if (textOk) score += 14;
if (weakClassOk && textOk) score += 8;
if (imgs.length >= 2) score += 5;
if (hasOldImageArea) score += 3;
if (!text || text.length < 4) score -= 10;
if (rect.height < 80 && imgs.length === 1) score -= 6;
return score;
}
function isLikelySingleCommentContainer(el, root) {
return scoreCommentCandidate(el, root) >= 10;
}
function findNearestCommentContainerFromImage(img, root) {
if (!img) return null;
if (isBadCommentCandidate(img)) return null;
const directCard = img.closest('.jdc-pc-rate-card');
if (directCard && (!root || root.contains(directCard))) {
const urls = getLooseImages(directCard);
if (urls.length) {
return directCard;
}
}
const directRateItem = img.closest(JD_RATE_CARD_SELECTOR);
if (directRateItem && (!root || root.contains(directRateItem))) {
const urls = getLooseImages(directRateItem);
if (urls.length && urls.length <= 40) {
return directRateItem;
}
}
let node = img.parentElement;
const candidates = [];
for (let i = 0; node && i < 14; i++, node = node.parentElement) {
if (node === document.body || node === document.documentElement) break;
if (root && !root.contains(node)) break;
if (isBadCommentCandidate(node)) continue;
const score = scoreCommentCandidate(node, root);
if (score >= 10) {
candidates.push({ node, score });
}
}
if (candidates.length) {
candidates.sort((a, b) => b.score - a.score);
return candidates[0].node;
}
const item = img.closest(JD_COMMENT_ITEM_SELECTOR);
if (!item) return null;
if (isBadCommentCandidate(item)) return null;
if (root && !root.contains(item)) return null;
const imgs = getLooseImages(item);
if (!imgs.length || imgs.length > 40) return null;
return item;
}
function looksLikeComment(el) {
return isLikelySingleCommentContainer(el, null);
}
function getCommentElements() {
const result = new Set();
const roots = getCommentRoots();
roots.forEach(root => {
if (isBadCommentCandidate(root)) return;
root.querySelectorAll('.jdc-pc-rate-card').forEach(card => {
const urls = getImages(card);
if (urls.length && urls.length <= 40) {
result.add(card);
}
});
const imgs = [...root.querySelectorAll(JD_IMAGE_SELECTOR)]
.filter(img => {
if (isInsideScriptPanel(img)) return false;
if (isInsideGoodsArea(img)) return false;
if (!isImageSizeOk(img)) return false;
const src = getImageSrc(img);
return isValidImageUrl(src);
});
imgs.forEach(img => {
const comment = findNearestCommentContainerFromImage(img, root);
if (!comment) return;
const urls = getImages(comment);
if (!urls.length) return;
if (urls.length > 40) return;
result.add(comment);
});
[...root.querySelectorAll(JD_COMMENT_ITEM_SELECTOR)].forEach(item => {
if (isLikelySingleCommentContainer(item, root)) {
result.add(item);
}
});
});
return [...result];
}
function getComments() {
return getCommentElements()
.filter(el => {
const imgs = getImages(el);
return imgs.length > 0 && imgs.length <= 40;
});
}
function makeCommentGroupKey(urls) {
return urls.map(getHDImage).sort().join('|');
}
function upsertCommentGroup(comment) {
const urls = getImages(comment);
if (!urls.length || urls.length > 40) return false;
const key = makeCommentGroupKey(urls);
if (!key) return false;
const existed = commentGroupCache.get(key);
if (!existed || urls.length > existed.urls.length) {
commentGroupCache.set(key, {
key,
urls,
count: urls.length,
firstSeen: existed?.firstSeen || Date.now(),
lastSeen: Date.now()
});
return !existed;
}
existed.lastSeen = Date.now();
return false;
}
function scanVisibleCommentsToCache() {
let added = 0;
getComments().forEach(comment => {
if (upsertCommentGroup(comment)) {
added++;
}
});
updateStatus();
return added;
}
function getMinFilterCount() {
const input = document.querySelector('#jd-min-images-input');
return Math.max(1, Number(input?.value || 1) || 1);
}
function getMaxCollectRounds() {
const input = document.querySelector('#jd-max-rounds-input');
const value = Number(input?.value || 80) || 80;
return Math.min(1000, Math.max(1, value));
}
function getCachedCommentGroups(minCount = 1) {
scanVisibleCommentsToCache();
return [...commentGroupCache.values()]
.filter(item => item.count >= minCount)
.sort((a, b) => {
return b.count - a.count || a.firstSeen - b.firstSeen;
});
}
function updateStatus(extraText = '') {
const el = document.querySelector('#jd-cache-status');
if (!el) return;
const groups = [...commentGroupCache.values()];
const imageCount = groups.reduce((sum, group) => sum + group.urls.length, 0);
const minCount = getMinFilterCount();
const filtered = groups.filter(group => group.urls.length >= minCount).length;
const maxRounds = document.querySelector('#jd-max-rounds-input')?.value || 80;
el.textContent = `已缓存 ${groups.length} 组 / ${imageCount} 张;${minCount} 张以上 ${filtered} 组;滑动 ${maxRounds} 次${extraText ? ';' + extraText : ''}`;
}
function scheduleScan(delay = 250) {
clearTimeout(scanTimer);
scanTimer = setTimeout(scanVisibleCommentsToCache, delay);
}
function isScrollable(el) {
if (!el) return false;
if (el === document || el === document.body || el === document.documentElement) return false;
if (!(el instanceof Element)) return false;
const style = getComputedStyle(el);
const overflowY = style.overflowY || '';
return /(auto|scroll|overlay)/i.test(overflowY) &&
el.scrollHeight > el.clientHeight + 60;
}
function findCommentScrollers() {
const set = new Set();
const forcedScroller = getForcedCommentScroller();
if (forcedScroller) {
set.add(forcedScroller);
const root = getRateOverlayRoot();
if (root) {
[...root.querySelectorAll('*')].forEach(el => {
if (isScrollable(el) && !isInsideGoodsArea(el)) {
set.add(el);
}
});
}
return [...set];
}
const roots = getCommentRoots();
roots.forEach(root => {
if (isBadCommentCandidate(root)) return;
let node = root;
while (node && node !== document.body) {
if (isScrollable(node) && !isInsideGoodsArea(node)) {
set.add(node);
}
node = node.parentElement;
}
[...root.querySelectorAll('*')].forEach(el => {
if (isScrollable(el) && !isInsideGoodsArea(el)) {
set.add(el);
}
});
});
return [...set];
}
function bindCommentScrollers() {
const forcedScroller = getForcedCommentScroller();
if (forcedScroller && !watchedScrollers.has(forcedScroller)) {
watchedScrollers.add(forcedScroller);
forcedScroller.addEventListener('scroll', () => {
scheduleScan(80);
}, { passive: true });
forcedScroller.addEventListener('wheel', e => {
e.stopPropagation();
scheduleScan(80);
}, { passive: true });
forcedScroller.addEventListener('touchmove', e => {
e.stopPropagation();
scheduleScan(80);
}, { passive: true });
}
findCommentScrollers().forEach(scroller => {
if (watchedScrollers.has(scroller)) return;
watchedScrollers.add(scroller);
scroller.addEventListener('scroll', () => {
scheduleScan(120);
}, { passive: true });
scroller.addEventListener('wheel', e => {
if (getRateOverlayRoot()) e.stopPropagation();
scheduleScan(120);
}, { passive: true });
scroller.addEventListener('touchmove', e => {
if (getRateOverlayRoot()) e.stopPropagation();
scheduleScan(120);
}, { passive: true });
});
}
function scrollOneCommentStep() {
const forcedScroller = getForcedCommentScroller();
if (forcedScroller) {
const maxTop = forcedScroller.scrollHeight - forcedScroller.clientHeight;
if (maxTop > 0) {
const step = Math.max(420, Math.round(forcedScroller.clientHeight * 0.82));
const oldTop = forcedScroller.scrollTop;
forcedScroller.scrollTop = Math.min(oldTop + step, maxTop);
forcedScroller.dispatchEvent(new Event('scroll', { bubbles: true }));
forcedScroller.dispatchEvent(new WheelEvent('wheel', {
bubbles: true,
cancelable: true,
deltaY: step
}));
scheduleScan(120);
return forcedScroller.scrollTop !== oldTop;
}
scheduleScan(120);
return false;
}
const scrollers = findCommentScrollers()
.filter(scroller => scroller !== window)
.filter(scroller => !isInsideGoodsArea(scroller))
.sort((a, b) => {
const av = (a.scrollHeight - a.clientHeight) - a.scrollTop;
const bv = (b.scrollHeight - b.clientHeight) - b.scrollTop;
return bv - av;
});
const target = scrollers.find(scroller => {
return scroller.scrollTop + scroller.clientHeight < scroller.scrollHeight - 10;
});
if (target) {
const maxTop = target.scrollHeight - target.clientHeight;
const step = Math.max(420, Math.round(target.clientHeight * 0.82));
target.scrollTop = Math.min(target.scrollTop + step, maxTop);
target.dispatchEvent(new Event('scroll', { bubbles: true }));
scheduleScan(120);
return true;
}
return false;
}
function injectStyle() {
if (document.querySelector('#jd-hd-style')) return;
const style = document.createElement('style');
style.id = 'jd-hd-style';
style.innerHTML = `
html.jd-rate-layer-open,
body.jd-rate-layer-open {
overflow: hidden !important;
}
#jd-hd-download-panel {
position: fixed;
right: 18px;
top: 18px;
z-index: 999999999;
width: 820px;
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 !important;
background: linear-gradient(180deg, #fff5f5 0%, #ffffff 120px);
}
.jd-folder-row {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 10px;
}
#jd-folder-name-input,
#jd-min-images-input,
#jd-max-rounds-input {
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-name-input {
flex: 1;
}
#jd-min-images-input {
width: 70px;
}
#jd-max-rounds-input {
width: 86px;
}
.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(7, 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:disabled {
opacity: .55;
cursor: wait;
transform: none;
}
.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-cache-status,
.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-cache-status {
color: #0f5f46;
background: #effaf5;
border: 1px solid #c5f1dd;
}
.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-filter-result-panel,
#jd-goods-result-panel {
margin-top: 12px;
height: calc(100vh - 260px);
max-height: calc(100vh - 260px);
overflow-y: auto !important;
overflow-x: hidden;
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 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 getLiveGoodsSku(item) {
return extractSkuFromGoodsLink(getGoodsLink(item)) || getGoodsSku(item);
}
function getGoodsLink(item) {
return fullUrl(
item.querySelector('.jPic a[href], .p-img a[href], .jDesc a[href], .p-name a[href], a[href*="item.jd.com"], a[href*="item.jd.hk"]')?.getAttribute('href') ||
item.getAttribute('href') ||
''
);
}
function extractSkuFromGoodsLink(href) {
if (!href) return '';
const url = fullUrl(href);
const match = url.match(/(?:item\.jd\.(?:com|hk)\/|\/product\/)(\d+)\.html/i) ||
url.match(/[?&](?:sku|skuId|wareId|itemId)=(\d+)/i) ||
url.match(/\/(\d{5,})(?:\.html|[/?#]|$)/);
return match ? match[1] : '';
}
function getSelectedSpecText() {
const selected = [...document.querySelectorAll('.specification-item-sku--selected .specification-item-sku-text')]
.map(el => cleanName(el.textContent || '', 40))
.filter(Boolean);
return selected.join(' ');
}
function getGoodsSnapshot(item) {
const sku = getLiveGoodsSku(item);
const name = getGoodsName(item);
const images = getGoodsImages(item);
const link = getGoodsLink(item);
const specText = getSelectedSpecText();
return { sku, name, images, link, specText };
}
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 setGoodsHeaderText(count) {
const headerText = document.querySelector('#jd-goods-header-text');
if (headerText) {
headerText.textContent = `商品列表:${count} 个商品,只取首图大图`;
}
}
function renderGoodsRows(resultPanel, goods, keepChecked = true) {
const checkedKeys = new Set();
const checkedIndexes = new Set();
if (keepChecked) {
resultPanel.querySelectorAll('.jd-goods-row').forEach((row, index) => {
const check = row.querySelector('.jd-goods-check');
if (check?.checked) {
checkedKeys.add(row.dataset.jdGoodsKey || row.dataset.jdGoodsSku || row.dataset.jdGoodsName || '');
checkedIndexes.add(index);
}
});
}
resultPanel.querySelectorAll('.jd-goods-row').forEach(row => row.remove());
goods.forEach((item, index) => {
const info = getGoodsSnapshot(item);
const thumb = info.images[0] || '';
const key = info.sku || `${info.name}|${info.link}|${thumb}`;
const row = document.createElement('label');
row.className = 'jd-goods-row';
row.dataset.jdGoodsIndex = String(index);
row.dataset.jdGoodsKey = key;
row.dataset.jdGoodsSku = info.sku;
row.dataset.jdGoodsName = info.name;
row.dataset.jdGoodsImages = JSON.stringify(info.images);
row.dataset.jdGoodsLink = info.link;
row.dataset.jdGoodsSpec = info.specText;
row.innerHTML = `
<input type="checkbox" class="jd-goods-check" ${checkedKeys.has(key) || checkedIndexes.has(index) ? 'checked' : ''}>
<img src="${thumb}" loading="lazy">
<div class="jd-goods-sku" title="${info.sku}">${info.sku}</div>
<div class="jd-goods-name" title="${info.name}">${info.name}</div>
<div class="jd-goods-count" title="${info.specText || '当前规格'}">${info.images.length} 图</div>
`;
resultPanel.appendChild(row);
});
setGoodsHeaderText(goods.length);
}
function refreshGoodsListPanel(keepChecked = true) {
const resultPanel = document.querySelector('#jd-goods-result-panel');
if (!resultPanel) return;
const goods = getGoodsListItems();
renderGoodsRows(resultPanel, goods, keepChecked);
}
function scheduleGoodsRefresh(delay = 650) {
if (!document.querySelector('#jd-goods-result-panel')) return;
clearTimeout(goodsRefreshTimer);
goodsRefreshTimer = setTimeout(() => {
refreshGoodsListPanel(true);
}, delay);
}
function mutationTouchesGoodsOrSpec(mutations) {
return mutations.some(mutation => {
const target = mutation.target;
if (!(target instanceof Element)) return false;
return target.closest('.page-right-spec, .specifications-panel-content, #J_GoodsList, .goods-list, .J-goods-list') ||
[...mutation.addedNodes, ...mutation.removedNodes].some(node =>
node instanceof Element &&
node.matches?.('.page-right-spec, .specifications-panel-content, #J_GoodsList, .goods-list, .J-goods-list, li.gl-item, li.jSubObject, .gl-warp > li')
);
});
}
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">京东图片下载器 评论分组修复版 v3.6</div>
<div class="jd-title-sub">只滚评价弹层 · 不滚商详页 · 从 jdc-pc-rate-card 分组 · 可设置滑动次数</div>
</div>
</div>
</div>
<div class="jd-panel-body">
<div class="jd-folder-row">
<input id="jd-folder-name-input" placeholder="输入主文件夹名,最长255字符">
<input id="jd-min-images-input" type="number" min="1" value="1" title="筛选至少多少张图片的评论分组">
<input id="jd-max-rounds-input" type="number" min="1" max="1000" value="80" title="采集更多时最多自动滑动多少次">
<div class="jd-folder-preview" id="jd-folder-preview">京东图片下载</div>
</div>
<div class="jd-btn-row">
<button class="jd-btn primary" id="jd-collect-more-btn">采集更多</button>
<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-cache-status" id="jd-cache-status">已缓存 0 组 / 0 张;1 张以上 0 组;滑动 80 次</div>
<div class="jd-filter-tip">
当前逻辑:评论弹层打开时,只滚动评价弹层/Virtuoso列表,不再滚动商详页。第一个数字框是“至少几张图才显示”,第二个数字框是“最多自动滑动多少次”。
</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');
const minInput = document.querySelector('#jd-min-images-input');
const roundsInput = document.querySelector('#jd-max-rounds-input');
input.addEventListener('input', () => {
preview.textContent = getRootFolderName();
});
minInput.addEventListener('input', () => {
updateStatus();
});
roundsInput.addEventListener('input', () => {
updateStatus();
});
document.querySelector('#jd-collect-more-btn').onclick = collectMoreComments;
document.querySelector('#jd-scan-group-btn').onclick = markComments;
document.querySelector('#jd-filter-group-btn').onclick = filterCommentGroups;
document.querySelector('#jd-scan-img-btn').onclick = markComments;
document.querySelector('#jd-goods-list-btn').onclick = showGoodsListPanel;
document.querySelector('#jd-download-btn').onclick = downloadSelected;
document.querySelector('#jd-select-all-btn').onclick = toggleAll;
document.addEventListener('click', e => {
if (e.target.closest('.specification-item-sku, .page-right-spec, .specifications-panel-content')) {
scheduleGoodsRefresh(900);
setTimeout(() => {
scheduleGoodsRefresh(1600);
}, 0);
}
}, true);
bindCommentScrollers();
scanVisibleCommentsToCache();
}
async function collectMoreComments() {
if (autoCollecting) return;
autoCollecting = true;
const btn = document.querySelector('#jd-collect-more-btn');
if (btn) {
btn.disabled = true;
btn.textContent = '采集中...';
}
const maxRounds = getMaxCollectRounds();
let idleRounds = 0;
let lastCount = commentGroupCache.size;
let lastImageCount = [...commentGroupCache.values()].reduce((sum, group) => sum + group.urls.length, 0);
for (let round = 1; round <= maxRounds; round++) {
lockDetailPageScroll();
bindCommentScrollers();
scanVisibleCommentsToCache();
updateStatus(`自动采集第 ${round}/${maxRounds} 轮`);
const currentCount = commentGroupCache.size;
const currentImageCount = [...commentGroupCache.values()].reduce((sum, group) => sum + group.urls.length, 0);
if (currentCount === lastCount && currentImageCount === lastImageCount) {
idleRounds++;
} else {
idleRounds = 0;
}
lastCount = currentCount;
lastImageCount = currentImageCount;
if (idleRounds >= 14) {
break;
}
const moved = scrollOneCommentStep();
if (!moved && getRateOverlayRoot()) {
await sleep(500);
scanVisibleCommentsToCache();
idleRounds++;
} else {
await sleep(850);
scanVisibleCommentsToCache();
}
}
scanVisibleCommentsToCache();
updateStatus('采集完成');
if (btn) {
btn.disabled = false;
btn.textContent = '采集更多';
}
autoCollecting = false;
alert(`采集完成:当前缓存 ${commentGroupCache.size} 个评论图片分组。现在点“筛选”。`);
}
function markComments() {
const comments = getComments();
let validCount = 0;
comments.forEach(comment => {
upsertCommentGroup(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);
});
updateStatus();
alert(`当前屏评论分组标记完成:找到 ${validCount} 个评论图片分组;缓存共 ${commentGroupCache.size} 组。`);
}
function filterCommentGroups() {
const body = document.querySelector('#jd-filter-body');
if (!body) return;
body.innerHTML = '';
const minCount = getMinFilterCount();
const groups = getCachedCommentGroups(minCount);
if (!groups.length) {
alert(`缓存里没有找到 ${minCount} 张以上的评论图片分组。请先点“采集更多”,或手动滑动评论区域加载更多。`);
return;
}
const resultPanel = document.createElement('div');
resultPanel.id = 'jd-filter-result-panel';
resultPanel.innerHTML = `
<div class="jd-filter-header">
筛选结果:${groups.length} 组评论图,显示 ${minCount} 张以上
<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 = '';
};
updateStatus();
alert(`筛选完成:从缓存显示 ${groups.length} 个 ${minCount} 张以上评论图片分组。`);
}
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">
<span id="jd-goods-header-text">商品列表:${goods.length} 个商品,只取首图大图</span>
<button id="jd-close-goods-panel" class="jd-goods-close">清空</button>
<button id="jd-select-goods-visible" class="jd-goods-select-visible">全选商品</button>
</div>
`;
renderGoodsRows(resultPanel, goods, false);
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-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;
});
}
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(420);
}
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('.jdc-pc-rate-card') || input.closest(JD_COMMENT_ITEM_SELECTOR))
.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 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评论图按单条 jdc-pc-rate-card 分组。\n商品图只来自“商品列表”。`
);
}
function startAutoWatch() {
if (autoWatchTimer) return;
autoWatchTimer = setInterval(() => {
addPanel();
lockDetailPageScroll();
bindCommentScrollers();
scanVisibleCommentsToCache();
}, 1000);
}
document.addEventListener('wheel', e => {
const root = getRateOverlayRoot();
if (!root) return;
if (root.contains(e.target)) {
e.stopPropagation();
}
}, {
capture: true,
passive: true
});
document.addEventListener('touchmove', e => {
const root = getRateOverlayRoot();
if (!root) return;
if (root.contains(e.target)) {
e.stopPropagation();
}
}, {
capture: true,
passive: true
});
addPanel();
const observer = new MutationObserver(mutations => {
addPanel();
lockDetailPageScroll();
bindCommentScrollers();
scheduleScan(150);
if (mutationTouchesGoodsOrSpec(mutations)) {
scheduleGoodsRefresh(700);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
bindCommentScrollers();
startAutoWatch();
})();
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。