// ==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
    });

})();

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。