淘宝素材步骤

发表于 视频

1:淘宝素材整理

// ==UserScript==
// @name         淘宝/天猫评论图集高清原图分组下载器【筛选增强版】
// @namespace    tb-comment-hd-gallery-downloader
// @version      3.6
// @description  淘宝/天猫评论图下载:商品文件夹、分组文件夹、筛选3张以上评论组、按图片数量排序、高清原图、自动去重编号
// @match        *://*.taobao.com/*
// @match        *://*.tmall.com/*
// @match        *://*.tmall.hk/*
// @match        *://*.world.taobao.com/*
// @grant        GM_download
// @connect      gw.alicdn.com
// @connect      img.alicdn.com
// @connect      alicdn.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('#tb-folder-name-input');
        const value = input?.value?.trim() || '';
        return cleanName(value || '淘宝评论原图', 255);
    }

    function makeGroupFolderName(rootFolder, indexOrId) {
        const suffix = String(indexOrId).padStart(2, '0');
        return cleanName(`${rootFolder} ${suffix}`, 255);
    }

    function getHDImage(url) {
        if (!url) return '';

        url = fullUrl(url).split('?')[0];
        url = url.replace(/\/i\d+\//i, '/i0/');

        const match = url.match(/.*?-(0|2)-(rate|tbbala)(?:_livephoto)?\.(jpg|jpeg|png)/i);
        if (match) return match[0];

        const match2 = url.match(/.*?!!\d+-(rate|tbbala)(?:_livephoto)?\.(jpg|jpeg|png)/i);
        if (match2) return match2[0];

        const imgMatch = url.match(/.*?\.(jpg|jpeg|png|webp)/i);
        if (imgMatch) return imgMatch[0];

        return url;
    }

    function getImageSrc(img) {
        return img.getAttribute('data-src') ||
            img.getAttribute('src') ||
            img.currentSrc ||
            img.src ||
            '';
    }

    function isValidImageUrl(url) {
        if (!url) return false;
        if (url.includes('tps-145-145')) return false;
        if (url.includes('tps-56-56')) return false;
        if (url.includes('playerIcon')) return false;
        if (url.includes('6000000002189-2-tps')) return false;
        if (url.includes('6000000006294-2-tps')) return false;
        return /\.(jpg|jpeg|png|webp)$/i.test(url);
    }

    function isImageSizeOk(img) {
        const w = img.naturalWidth || img.width || 0;
        const h = img.naturalHeight || img.height || 0;
        return w >= 100 && h >= 100;
    }

    function getFileExt(url) {
        const match = url.match(/\.(jpg|jpeg|png|webp)$/i);
        return match ? match[1].toLowerCase() : 'jpg';
    }

    function getComments() {
        return [...document.querySelectorAll(
            '[class*="Comment--"], [class*="comment--"], [class*="comment"]'
        )];
    }

    function getImages(comment) {
        const imgs = [...comment.querySelectorAll('img')];

        const urls = imgs
            .filter(img => isImageSizeOk(img))
            .map(img => getHDImage(getImageSrc(img)))
            .filter(isValidImageUrl);

        return [...new Set(urls)];
    }

    function getImageItems() {
        return [...document.querySelectorAll(
            '[class*="commentsImgItem"], [class*="photo--"], [class*="cover--"]'
        )];
    }

    function injectStyle() {
        if (document.querySelector('#tb-hd-style')) return;

        const style = document.createElement('style');
        style.id = 'tb-hd-style';

        style.innerHTML = `
            #tb-hd-download-panel {
                position: fixed;
                right: 24px;
                top: 110px;
                z-index: 999999999;
                width: 560px;
                padding: 14px 16px;
                border-radius: 18px;
                background: rgba(255,255,255,.95);
                backdrop-filter: blur(16px);
                box-shadow: 0 14px 40px rgba(0,0,0,.12), 0 6px 18px rgba(255,80,0,.18);
                border: 1px solid rgba(255,80,0,.18);
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, "Microsoft YaHei", sans-serif;
                color: #222;
            }

            .tb-panel-top {
                display: flex;
                align-items: center;
                justify-content: space-between;
                gap: 12px;
                margin-bottom: 10px;
            }

            .tb-panel-title {
                display: flex;
                align-items: center;
                gap: 10px;
            }

            .tb-logo {
                width: 34px;
                height: 34px;
                border-radius: 11px;
                background: linear-gradient(135deg, #ff8a1c, #ff3d00);
                color: #fff;
                display: flex;
                align-items: center;
                justify-content: center;
                font-weight: 900;
                font-size: 18px;
                box-shadow: 0 6px 14px rgba(255,80,0,.25);
            }

            .tb-title-main {
                font-size: 16px;
                font-weight: 800;
                line-height: 1.2;
            }

            .tb-title-sub {
                font-size: 12px;
                color: #999;
                margin-top: 2px;
            }

            .tb-folder-row {
                display: flex;
                gap: 8px;
                align-items: center;
                margin-bottom: 10px;
            }

            #tb-folder-name-input {
                flex: 1;
                height: 36px;
                border-radius: 12px;
                border: 1px solid rgba(255,80,0,.22);
                background: #fff8f3;
                padding: 0 12px;
                font-size: 13px;
                outline: none;
                color: #333;
            }

            #tb-folder-name-input:focus {
                border-color: #ff5000;
                box-shadow: 0 0 0 3px rgba(255,80,0,.10);
                background: #fff;
            }

            .tb-folder-preview {
                width: 125px;
                height: 36px;
                line-height: 36px;
                border-radius: 12px;
                background: #fff3eb;
                color: #ff5000;
                font-size: 12px;
                font-weight: 700;
                text-align: center;
                overflow: hidden;
                white-space: nowrap;
                text-overflow: ellipsis;
                border: 1px solid rgba(255,80,0,.16);
            }

            .tb-btn-row {
                display: grid;
                grid-template-columns: repeat(5, 1fr);
                gap: 8px;
            }

            .tb-btn {
                height: 38px;
                border: none;
                outline: none;
                border-radius: 12px;
                font-size: 13px;
                font-weight: 700;
                cursor: pointer;
                transition: all .18s ease;
            }

            .tb-btn:hover {
                transform: translateY(-1px);
                filter: brightness(1.04);
            }

            .tb-btn:active {
                transform: scale(.98);
            }

            .tb-btn.primary {
                color: #fff;
                background: linear-gradient(135deg, #ff8a1c, #ff5000);
                box-shadow: 0 6px 14px rgba(255,80,0,.24);
            }

            .tb-btn.success {
                color: #fff;
                background: linear-gradient(135deg, #24c6a0, #12b886);
                box-shadow: 0 6px 14px rgba(18,184,134,.20);
            }

            .tb-btn.blue {
                color: #fff;
                background: linear-gradient(135deg, #4dabf7, #228be6);
                box-shadow: 0 6px 14px rgba(34,139,230,.20);
            }

            .tb-btn.light {
                color: #ff5000;
                background: #fff4ed;
                border: 1px solid rgba(255,80,0,.22);
            }

            .tb-download-label {
                display: inline-flex;
                align-items: center;
                gap: 8px;
                padding: 7px 12px;
                margin-bottom: 10px;
                border-radius: 999px;
                background: linear-gradient(135deg, #fff7f2, #fff);
                color: #ff5000;
                font-size: 13px;
                font-weight: 700;
                border: 1px solid rgba(255,80,0,.25);
                box-shadow: 0 4px 12px rgba(255,80,0,.08);
                cursor: pointer;
                user-select: none;
            }

            .tb-download-label input {
                accent-color: #ff5000;
                width: 15px;
                height: 15px;
            }

            .tb-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);
                backdrop-filter: blur(8px);
                display: flex;
                align-items: center;
                justify-content: center;
                cursor: pointer;
                box-shadow: 0 4px 12px rgba(0,0,0,.18);
                border: 1px solid rgba(255,255,255,.45);
            }

            .tb-img-select-wrap input {
                width: 17px;
                height: 17px;
                margin: 0;
                accent-color: #ff5000;
                cursor: pointer;
            }

            .tb-img-selected {
                outline: 3px solid #ff5000 !important;
                outline-offset: -3px !important;
                border-radius: 8px !important;
            }

            .tb-img-selected::after {
                content: "已选";
                position: absolute;
                left: 8px;
                top: 8px;
                z-index: 99999998;
                color: #fff;
                background: linear-gradient(135deg, #ff8a1c, #ff5000);
                font-size: 12px;
                font-weight: 700;
                padding: 4px 8px;
                border-radius: 999px;
                box-shadow: 0 4px 10px rgba(255,80,0,.25);
            }

            .tb-comment-hidden-by-filter {
                display: none !important;
            }

            .tb-filter-rank-label {
                display: inline-flex;
                align-items: center;
                gap: 6px;
                padding: 5px 10px;
                margin: 0 0 10px 8px;
                border-radius: 999px;
                background: linear-gradient(135deg, #ff5000, #ff8a1c);
                color: #fff;
                font-size: 12px;
                font-weight: 800;
                box-shadow: 0 4px 12px rgba(255,80,0,.22);
            }
        `;

        document.head.appendChild(style);
    }

    function addPanel() {
        if (document.querySelector('#tb-hd-download-panel')) return;

        injectStyle();

        const panel = document.createElement('div');
        panel.id = 'tb-hd-download-panel';

        panel.innerHTML = `
            <div class="tb-panel-top">
                <div class="tb-panel-title">
                    <span class="tb-logo">淘</span>
                    <div>
                        <div class="tb-title-main">评论原图下载</div>
                        <div class="tb-title-sub">商品文件夹 · 分组排序筛选 · 自动去重编号</div>
                    </div>
                </div>
            </div>

            <div class="tb-folder-row">
                <input id="tb-folder-name-input" placeholder="输入商品文件夹名,最长255字符">
                <div class="tb-folder-preview" id="tb-folder-preview">淘宝评论原图</div>
            </div>

            <div class="tb-btn-row">
                <button class="tb-btn primary" id="tb-scan-group-btn">评论分组</button>
                <button class="tb-btn blue" id="tb-filter-group-btn">筛选</button>
                <button class="tb-btn blue" id="tb-scan-img-btn">图集选择</button>
                <button class="tb-btn success" id="tb-download-btn">下载选中</button>
                <button class="tb-btn light" id="tb-select-all-btn">全选/反选</button>
            </div>
        `;

        document.body.appendChild(panel);

        const input = document.querySelector('#tb-folder-name-input');
        const preview = document.querySelector('#tb-folder-preview');

        input.addEventListener('input', () => {
            preview.textContent = getRootFolderName();
        });

        document.querySelector('#tb-scan-group-btn').onclick = markComments;
        document.querySelector('#tb-filter-group-btn').onclick = filterCommentGroups;
        document.querySelector('#tb-scan-img-btn').onclick = markImageItems;
        document.querySelector('#tb-download-btn').onclick = downloadSelected;
        document.querySelector('#tb-select-all-btn').onclick = toggleAll;
    }

    function markComments() {
        const comments = getComments();
        let validCount = 0;

        comments.forEach((comment) => {
            if (comment.querySelector('.tb-download-check')) return;

            const imgs = getImages(comment);
            if (!imgs.length) return;

            validCount++;

            const box = document.createElement('label');
            box.className = 'tb-download-label';

            box.innerHTML = `
                <input type="checkbox" class="tb-download-check tb-group-check">
                <span>本组 ${imgs.length} 张原图</span>
            `;

            comment.prepend(box);
        });

        alert(`评论分组扫描完成:找到 ${validCount} 个图片评论分组`);
    }

    function filterCommentGroups() {
        markComments();

        const comments = getComments();

        comments.forEach(comment => {
            comment.classList.remove('tb-comment-hidden-by-filter');

            const oldRank = comment.querySelector('.tb-filter-rank-label');
            if (oldRank) oldRank.remove();
        });

        const groups = comments.map(comment => {
            const imgs = getImages(comment);
            return {
                comment,
                count: imgs.length
            };
        }).filter(item => item.count >= 3);

        comments.forEach(comment => {
            comment.classList.add('tb-comment-hidden-by-filter');
        });

        groups.sort((a, b) => b.count - a.count);

        const parent = groups[0]?.comment?.parentElement;

        if (!parent) {
            alert('没有找到图片数量大于等于 3 张的评论分组');
            return;
        }

        groups.forEach((item, index) => {
            item.comment.classList.remove('tb-comment-hidden-by-filter');
            item.comment.dataset.tbFilterIndex = String(index + 1);
            item.comment.dataset.tbImageCount = String(item.count);

            parent.appendChild(item.comment);

            const label = item.comment.querySelector('.tb-download-label span');
            if (label) {
                label.textContent = `第 ${index + 1} 组 · ${item.count} 张原图`;
            }

            const rank = document.createElement('span');
            rank.className = 'tb-filter-rank-label';
            rank.textContent = `筛选排序 #${index + 1} · ${item.count}张`;
            const downloadLabel = item.comment.querySelector('.tb-download-label');

            if (downloadLabel) {
                downloadLabel.insertAdjacentElement('afterend', rank);
            } else {
                item.comment.prepend(rank);
            }
        });

        alert(
            `筛选完成:保留 ${groups.length} 个分组。\n` +
            `规则:图片少于 3 张的不显示。\n` +
            `排序:单组图片最多的排在最前面。`
        );
    }

    function markImageItems() {
        const items = getImageItems();
        let validCount = 0;
        const groupMap = new Map();

        items.forEach((item) => {
            if (item.querySelector('.tb-single-check')) return;

            const img = item.querySelector('img');
            if (!img) return;
            if (!isImageSizeOk(img)) return;

            const url = getHDImage(getImageSrc(img));
            if (!isValidImageUrl(url)) return;

            const hdUrl = getHDImage(url);

            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.tbImageUrl = url;
            item.dataset.tbGalleryGroupId = groupId;

            const wrap = document.createElement('label');
            wrap.className = 'tb-img-select-wrap';
            wrap.title = `选中下载高清原图,分组:${groupId}`;

            wrap.innerHTML = `<input type="checkbox" class="tb-single-check">`;

            const checkbox = wrap.querySelector('input');

            checkbox.addEventListener('change', () => {
                item.classList.toggle('tb-img-selected', checkbox.checked);
            });

            wrap.addEventListener('click', e => {
                e.stopPropagation();
            });

            item.appendChild(wrap);
            validCount++;
        });

        alert(`图集扫描完成:找到 ${validCount} 张可选择图片`);
    }

    function toggleAll() {
        const checks = [
            ...document.querySelectorAll('.tb-download-check'),
            ...document.querySelectorAll('.tb-single-check')
        ];

        if (!checks.length) {
            alert('请先点击“评论分组”或“图集选择”');
            return;
        }

        const hasUnchecked = checks.some(c => !c.checked);

        checks.forEach(c => {
            c.checked = hasUnchecked;

            const item = c.closest('[class*="commentsImgItem"], [class*="photo--"], [class*="cover--"]');
            if (item) {
                item.classList.toggle('tb-img-selected', c.checked);
            }
        });
    }

    function addTask(tasks, seen, groupCounters, rawUrl, folder) {
        if (!rawUrl || !folder) return;

        const hdUrl = getHDImage(rawUrl);

        if (!isValidImageUrl(hdUrl)) return;
        if (seen.has(hdUrl)) return;

        seen.add(hdUrl);

        if (!groupCounters[folder]) {
            groupCounters[folder] = 0;
        }

        groupCounters[folder]++;

        const ext = getFileExt(hdUrl);
        const filename = `${folder}/${String(groupCounters[folder]).padStart(2, '0')}.${ext}`;

        tasks.push({
            url: hdUrl,
            filename
        });
    }

    async function downloadSelected() {
        const rootFolder = getRootFolderName();

        const tasks = [];
        const seen = new Set();
        const groupCounters = {};

        const selectedGroups = [...document.querySelectorAll('.tb-group-check:checked')]
            .map(input => input.closest('[class*="Comment--"], [class*="comment--"], [class*="comment"]'))
            .filter(Boolean);

        selectedGroups.forEach((comment, groupIndex) => {
            const groupName = makeGroupFolderName(rootFolder, groupIndex + 1);
            const folder = `${rootFolder}/评论分组/${groupName}`;
            const imgs = getImages(comment);

            imgs.forEach(url => {
                addTask(tasks, seen, groupCounters, url, folder);
            });
        });

        const selectedSingles = [...document.querySelectorAll('.tb-single-check:checked')]
            .map(input => input.closest('[class*="commentsImgItem"], [class*="photo--"], [class*="cover--"]'))
            .filter(Boolean);

        selectedSingles.forEach(item => {
            const rawUrl = item.dataset.tbImageUrl;
            if (!rawUrl) return;

            const hdUrl = getHDImage(rawUrl);
            const groupId = item.dataset.tbGalleryGroupId || '01';

            const groupName = makeGroupFolderName(rootFolder, groupId);
            const folder = `${rootFolder}/图集分组/${groupName}`;

            addTask(tasks, seen, groupCounters, hdUrl, folder);
        });

        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(
            `已开始下载 ${tasks.length} 张高清原图。\n\n` +
            `主文件夹:${rootFolder}\n` +
            `主文件夹最长255字符。\n` +
            `已过滤低于100×100的图片。\n` +
            `分组文件夹已按:商品文件夹名 01、商品文件夹名 02 命名。\n` +
            `每个分组内图片仍按 01、02、03 自动编号。`
        );
    }

    addPanel();

    const observer = new MutationObserver(() => {
        addPanel();
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

})();



2:京东商详页:

// ==UserScript==
// @name         京东商详页面下载
// @namespace    http://tampermonkey.net/
// @version      4.1
// @description  主图/媒体图单选切换,3:4预览,排除功能,粘贴自动识别SKU/标题/描述
// @author       You
// @include      http*://*jd*/*
// @grant        GM_registerMenuCommand
// @require      https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js
// ==/UserScript==

(function() {
    'use strict';

    const CONFIG = { waitParseTime: 1500, autoRun: true, defaultMode: 'main' };

    let globalProductInfo = {};
    let globalMainImageList = [];
    let globalMediaImageList = [];
    let currentMode = CONFIG.defaultMode;
    let panelDom = null;
    let globalExcludedMediaUrls = new Set();
    let globalRootDirHandle = null;
    let globalFromClipboard = false;
    let globalOriginalInfo = null;
    let globalEmbeddedInfo = null;
    let clipboardTimer = null;
    let lastClipboard = '';
    let clipboardWatching = false;

    function generateRandomSuffix() {
        const len = Math.floor(Math.random() * 4) + 3;
        const c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
        let r = '';
        for (let i = 0; i < len; i++) r += c[Math.floor(Math.random() * c.length)];
        return r;
    }

    function setStatus(msg, persistent) {
        const el = document.getElementById('jd-status');
        if (!el) return;
        el.textContent = msg;
        el.style.opacity = '1';
        if (!persistent) {
            clearTimeout(el._timer);
            el._timer = setTimeout(() => { el.style.opacity = '0'; }, 3000);
        }
    }

    function setStatusRed(msg) {
        const el = document.getElementById('jd-status');
        if (!el) return;
        el.textContent = msg;
        el.style.opacity = '1';
        el.style.color = '#e4393c';
    }

    function updateParseInfo(parsed) {
        globalEmbeddedInfo = { sku: parsed.sku, title: parsed.title, description: parsed.description };
        const info = document.getElementById('jd-parse-info');
        if (!info) return;
        const orig = globalOriginalInfo || parsed;
        info.textContent = `原来:SKU ${orig.sku} ${orig.title.slice(0,20)} → 嵌入:SKU ${parsed.sku} ${parsed.title.slice(0,20)} ✅`;
    }

    function clearEmbedded() {
        globalEmbeddedInfo = null;
        globalFromClipboard = false;
        const info = document.getElementById('jd-parse-info');
        if (info) info.textContent = '';
    }

    // ───── 剪贴板轮询监控 ─────
    function stopClipWatch() {
        if (clipboardTimer) { clearInterval(clipboardTimer); clipboardTimer = null; }
        clipboardWatching = false;
    }

    async function startClipWatch() {
        stopClipWatch();
        try {
            // 用户点击按钮后触发 -> 浏览器弹出剪贴板权限提示
            const t = await navigator.clipboard.readText();
            lastClipboard = t || '';
        } catch (_) {
            setStatusRed('⚠️ 剪贴板授权失败,请重试');
            return;
        }
        clipboardWatching = true;
        setStatusRed('📋 剪贴板监控中');
        clipboardTimer = setInterval(async () => {
            try {
                const t = await navigator.clipboard.readText();
                if (t && t !== lastClipboard) {
                    console.log('[剪贴板] 内容变化:', t.slice(0, 60));
                    lastClipboard = t;
                    const parsed = parseProductText(t);
                    if (parsed) {
                        globalFromClipboard = true;
                        updateParseInfo(parsed);
                        setStatusRed(`✅ 嵌入 SKU ${parsed.sku}`);
                    }
                }
            } catch (_) { /* polling继续 */ }
        }, 1500);
    }

    function dbOpen() {
        return new Promise((res, rej) => {
            const r = indexedDB.open('jd-store', 1);
            r.onupgradeneeded = () => r.result.createObjectStore('kv');
            r.onsuccess = () => res(r.result);
            r.onerror = () => rej(r.error);
        });
    }

    async function saveRootHandle(h) {
        try {
            const db = await dbOpen();
            await new Promise((res, rej) => {
                const tx = db.transaction('kv', 'readwrite');
                tx.objectStore('kv').put(h, 'rootDir');
                tx.oncomplete = res;
                tx.onerror = () => rej(tx.error);
            });
        } catch (e) { console.warn('保存目录失败:', e); }
    }

    async function loadRootHandle() {
        try {
            const db = await dbOpen();
            return await new Promise((res, rej) => {
                const tx = db.transaction('kv', 'readonly');
                const g = tx.objectStore('kv').get('rootDir');
                g.onsuccess = () => res(g.result);
                g.onerror = () => rej(g.error);
            });
        } catch (e) { return null; }
    }

    async function getDirHandle() {
        if (globalRootDirHandle) {
            const p = await globalRootDirHandle.queryPermission({ mode: 'readwrite' });
            if (p === 'granted') return globalRootDirHandle;
        }
        const stored = await loadRootHandle();
        if (stored) {
            let p = await stored.queryPermission({ mode: 'readwrite' });
            if (p !== 'granted') p = await stored.requestPermission({ mode: 'readwrite' });
            if (p === 'granted') { globalRootDirHandle = stored; return stored; }
        }
        const h = await window.showDirectoryPicker({ mode: 'readwrite', startIn: 'downloads' });
        globalRootDirHandle = h;
        saveRootHandle(h);
        return h;
    }

    function parseProductText(text) {
        console.log('[解析] 原始:', JSON.stringify(text.slice(0, 100)));
        const clean = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
            .replace(/[\u200B-\u200D\uFEFF\u00AD]/g, '').trim();
        // 用 ====== (6个以上等号) 分隔,不要求换行
        const parts = clean.split(/={6,}/).map(s => s.trim());
        console.log('[解析] 分段数:', parts.length);
        if (parts.length < 3) { console.log('[解析] 段数不足3'); return null; }
        const digits = parts[0].replace(/\D/g, '');
        if (digits.length < 6) { console.log('[解析] SKU数字不足'); return null; }
        const title = parts[1].replace(/[\\/:*?"<>|\r\n]/g, '').replace(/\s+/g, ' ').trim();
        if (!title) { console.log('[解析] 标题为空'); return null; }
        const description = parts.slice(2).join('\n').trim();
        console.log('[解析] 成功 SKU:', digits, '标题:', title);
        return { sku: digits, title, description };
    }

    function handlePastedText(text) {
        console.log('[粘贴] 收到文本:', text.slice(0, 80));
        const parsed = parseProductText(text);
        if (!parsed) {
            setStatusRed('⚠️ 格式不符:需用 ====== 分隔 SKU / 标题 / 描述');
            return;
        }
        globalFromClipboard = true;
        updateParseInfo(parsed);
        setStatusRed(`✅ 嵌入 SKU ${parsed.sku}`);
    }

    // ───── 1. 商品信息提取 ─────
    function extractProductInfo() {
        let sku = null, productTitle = '未知商品';
        const fullHtml = document.documentElement.innerHTML;
        const cr = /<a[^>]+href="([^"]*chat\.jd\.com[^"]*)"[^>]*>/g;
        let m;
        while ((m = cr.exec(fullHtml)) !== null) {
            const p = m[1].match(/[?&]pid=(\d+)/);
            if (p && p[1]) { sku = p[1]; console.log('SKU(pid):', sku); break; }
        }
        if (!sku) {
            const u = window.location.pathname.match(/\/(\d+)\.html/);
            sku = u ? u[1] : 'unknown_sku';
        }
        const t1 = document.querySelector('.sku-title-name');
        const t2 = document.querySelector('.sku-name');
        if (t1) productTitle = t1.innerText.trim();
        else if (t2) productTitle = t2.innerText.trim();
        else productTitle = document.title.replace(/【.*?】|京东|,.*/g, '').trim();
        const safe = productTitle.replace(/[\\/:*?"<>|\r\n]/g, '').replace(/\s+/g, ' ');
        globalFromClipboard = false;
        globalEmbeddedInfo = null;
        globalOriginalInfo = { sku, title: safe };
        globalProductInfo = { sku, title: safe, folderName: `${sku}----${safe}`, description: '' };
        return globalProductInfo;
    }

    // ───── 2. 主图提取 ─────
    function extractMainImages() {
        const set = new Set();
        const cc = document.querySelector('.image-carousel-content');
        if (cc) {
            cc.querySelectorAll('img').forEach(img => {
                let s = img.src || img.getAttribute('data-lazy-img') || img.getAttribute('src');
                if (s) {
                    if (s.startsWith('//')) s = 'https:' + s;
                    set.add(s.replace(/\/s\d+x\d+_/, '/').replace('.avif', ''));
                }
            });
        } else {
            const re = /(https?:)?\/\/img\d+\.360buyimg\.com\/[^"'\s]+\.(avif|jpg|png)/g;
            let m;
            while ((m = re.exec(fullHtml)) !== null) {
                let u = m[0].startsWith('//') ? 'https:' + m[0] : m[0];
                set.add(u.replace(/\/s\d+x\d+_/, '/').replace('.avif', ''));
            }
        }
        globalMainImageList = Array.from(set);
        return globalMainImageList;
    }

    // ───── 3. 媒体图提取 ─────
    function extractMediaImages() {
        const set = new Set();
        const mc = document.querySelector('.jdc-pc-media-preview-list');
        if (!mc) { setStatusRed('⚠️ 未找到媒体层容器'); return []; }
        mc.querySelectorAll('img').forEach(img => {
            let s = img.src || img.getAttribute('data-lazy-img') || img.getAttribute('data-src')
                || img.getAttribute('origin-src') || img.getAttribute('src');
            if (s) {
                if (s.startsWith('//')) s = 'https:' + s;
                set.add(s.replace(/\/s\d+x\d+_/, '/').replace('.avif', ''));
            }
        });
        globalExcludedMediaUrls = new Set();
        globalMediaImageList = Array.from(set);
        renderInfoPanel();
        if (globalMediaImageList.length > 0) setStatusRed(`✅ 已提取 ${globalMediaImageList.length} 张`);
        return globalMediaImageList;
    }

    function switchMode(t) {
        if (t === currentMode) return;
        currentMode = t;
        renderInfoPanel();
    }

    // ───── 4. 面板渲染 ─────
    function renderInfoPanel() {
        if (panelDom) document.body.removeChild(panelDom);
        panelDom = document.createElement('div');
        panelDom.id = 'jd-extract-panel';
        const ss = document.createElement('style');
        ss.textContent = `
            #jd-extract-panel { --jd-red:#e4393c; --jd-orange:#ff6a00; --jd-blue:#1a73e8; --jd-green:#34a853; }
            #jd-extract-panel .jb { transition:all .2s; cursor:pointer; user-select:none; }
            #jd-extract-panel .jb:hover { filter:brightness(1.08); transform:translateY(-1px); }
            #jd-extract-panel .jb:active { transform:translateY(0); filter:brightness(.95); }
            #jd-extract-panel .jb:disabled { opacity:.4; cursor:not-allowed; filter:none; transform:none; }
            #jd-extract-panel .jc { transition:all .25s; }
            #jd-extract-panel .jc:hover { transform:translateY(-3px); box-shadow:0 6px 16px rgba(0,0,0,.1); }
            #jd-extract-panel .jc-ex { opacity:.5; }
            #jd-extract-panel ::-webkit-scrollbar { height:5px; }
            #jd-extract-panel ::-webkit-scrollbar-track { background:transparent; }
            #jd-extract-panel ::-webkit-scrollbar-thumb { background:#ddd; border-radius:3px; }
            #jd-extract-panel #jd-paste-area:focus { outline:none; border-color:var(--jd-red); }
        `;
        panelDom.appendChild(ss);
        const w = document.createElement('div');
        w.style.cssText = `position:fixed;bottom:0;left:0;right:0;z-index:99999999;background:#fff;box-shadow:0 -6px 30px rgba(0,0,0,.06);border-radius:16px 16px 0 0;max-height:52vh;display:flex;flex-direction:column;font-family:-apple-system,BlinkMacSystemFont,"Microsoft YaHei",sans-serif;font-size:13px;color:#1a1a1a;border-top:3px solid var(--jd-red);`;

        const { sku, title } = globalProductInfo;
        const main = currentMode === 'main';
        const list = main ? globalMainImageList : globalMediaImageList;
        const cnt = list.length;

        w.innerHTML = `
            <div style="display:flex;align-items:center;gap:6px;padding:7px 14px;border-bottom:1px solid #f0f0f0;flex-shrink:0;background:linear-gradient(180deg,#fefefe,#f8f8f8);">
                <span style="background:var(--jd-red);color:#fff;font-size:10px;font-weight:700;padding:2px 8px 2px 6px;border-radius:4px;white-space:nowrap;">⬦ 京东</span>
                <span style="background:#fff0f0;color:var(--jd-red);font-size:10px;font-weight:600;padding:1px 6px;border-radius:3px;white-space:nowrap;">${sku}</span>
                <span style="color:#888;font-size:11px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;">${title}</span>
                <span style="font-size:11px;font-weight:600;color:${main?'var(--jd-red)':'var(--jd-orange)'};background:${main?'#fff0f0':'#fff3e0'};padding:1px 8px;border-radius:20px;white-space:nowrap;">${cnt}张</span>
                <div style="display:flex;gap:1px;background:#ebebeb;border-radius:5px;padding:2px;">
                    <button id="mmb" class="jb" style="border:none;border-radius:4px;padding:3px 10px;font-size:10px;font-weight:600;white-space:nowrap;${main?'background:var(--jd-red);color:#fff':'background:transparent;color:#888'};">主图</button>
                    <button id="mmd" class="jb" style="border:none;border-radius:4px;padding:3px 10px;font-size:10px;font-weight:600;white-space:nowrap;${!main?'background:var(--jd-orange);color:#fff':'background:transparent;color:#888'};">媒体</button>
                </div>
                ${main ? `
                    <button id="dlb" class="jb" style="border:none;background:var(--jd-red);color:#fff;border-radius:4px;padding:3px 10px;font-size:10px;font-weight:600;white-space:nowrap;">⬇ 下载</button>
                    <button id="refb" class="jb" style="border:none;background:var(--jd-blue);color:#fff;border-radius:4px;padding:3px 10px;font-size:10px;font-weight:600;white-space:nowrap;">↻ 刷新</button>
                ` : `
                    <button id="extb" class="jb" style="border:none;background:var(--jd-orange);color:#fff;border-radius:4px;padding:3px 10px;font-size:10px;font-weight:600;white-space:nowrap;">⟐ 提取</button>
                    <button id="dlb" class="jb" style="border:none;background:#e67e22;color:#fff;border-radius:4px;padding:3px 10px;font-size:10px;font-weight:600;white-space:nowrap;" ${cnt===0?'disabled':''}>⬇ 下载</button>
                `}
                <button id="cpyb" class="jb" style="border:none;background:var(--jd-green);color:#fff;border-radius:4px;padding:3px 10px;font-size:10px;font-weight:600;white-space:nowrap;">📋 复制</button>
                <button id="clipb" class="jb" style="border:none;background:#9e9e9e;color:#fff;border-radius:4px;padding:3px 10px;font-size:10px;font-weight:600;white-space:nowrap;">📋 监控</button>
                <span id="jd-status" style="font-size:10px;font-weight:600;white-space:nowrap;transition:opacity .5s;opacity:0;flex-shrink:0;color:#e4393c;"></span>
                <button id="closeb" class="jb" style="border:none;background:transparent;padding:2px 4px;font-size:14px;color:#bbb;line-height:1;">✕</button>
            </div>
            <div style="display:flex;gap:6px;padding:4px 14px;flex-shrink:0;align-items:center;border-bottom:1px solid #f5f5f5;">
                <span style="font-size:10px;color:#999;white-space:nowrap;">📝 粘贴识别</span>
                <input id="jd-paste-area" type="text" placeholder="粘贴商品描述(首行SKU/次行标题/余下描述)" style="flex:1;font-size:10px;padding:3px 8px;border:1px solid #e0e0e0;border-radius:4px;color:#333;background:#fafafa;min-width:0;">
                <span id="jd-parse-info" style="font-size:10px;color:var(--jd-red);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:280px;"></span>
            </div>
            <div style="flex:1;overflow-x:auto;overflow-y:hidden;padding:6px 14px 8px;display:flex;gap:6px;align-items:stretch;">
                ${cnt===0
                    ? `<div style="display:flex;align-items:center;justify-content:center;width:100%;color:#ccc;font-size:12px;">${main?'暂无主图':'暂无媒体图,请点击【提取】'}</div>`
                    : list.map((url,i) => {
                        const ex = !main && globalExcludedMediaUrls.has(url);
                        return `<div class="jc ${ex?'jc-ex':''}" style="flex:0 0 auto;width:85px;position:relative;border:1px solid ${ex?'var(--jd-red)':'#e8e8e8'};border-radius:8px;overflow:hidden;background:${ex?'#fafafa':'#fff'};display:flex;flex-direction:column;">
                            ${!main ? `<button class="exb" data-url="${url}" style="position:absolute;top:2px;right:2px;z-index:10;width:18px;height:18px;border:none;border-radius:50%;background:${ex?'var(--jd-red)':'rgba(0,0,0,.25)'};color:#fff;font-size:10px;line-height:18px;text-align:center;cursor:pointer;padding:0;font-weight:700;">${ex?'↺':'×'}</button>` : ''}
                            <div style="position:relative;">
                                <div style="position:absolute;top:2px;left:2px;z-index:5;background:rgba(0,0,0,.45);color:#fff;font-size:8px;font-weight:600;padding:0 5px;border-radius:3px;line-height:1.6;">${i}</div>
                                <a href="${url}" target="_blank" style="display:block;text-decoration:none;">
                                    <img src="${url}" style="width:85px;height:113px;object-fit:cover;display:block;${ex?'opacity:.35':''}" alt="" onerror="this.parentElement.innerHTML='<div style=\\'width:85px;height:113px;display:flex;align-items:center;justify-content:center;background:#f5f5f5;color:#ccc;font-size:9px;\\'>×</div>'">
                                </a>
                            </div>
                            <div style="padding:1px 4px;text-align:center;font-size:9px;color:${ex?'var(--jd-red)':'#aaa'};line-height:1.5;background:${ex?'#fff5f5':'#fafafa'};">${main?'P':'M'}${i}${ex?' ✕':''}</div>
                        </div>`;
                    }).join('')
                }
            </div>
        `;
        panelDom.appendChild(w);
        document.body.appendChild(panelDom);
        bindEvents();
    }

    // ───── 5. 事件绑定 ─────
    function bindEvents() {
        const $ = id => document.getElementById(id);

        $('closeb').addEventListener('click', () => { stopClipWatch(); if (panelDom) document.body.removeChild(panelDom); panelDom = null; });
        $('mmb').addEventListener('click', () => switchMode('main'));
        $('mmd').addEventListener('click', () => switchMode('media'));
        $('cpyb').addEventListener('click', async () => {
            const { sku, title } = globalProductInfo;
            try {
                await navigator.clipboard.writeText(`${sku}----${title}`);
                const b = $('cpyb');
                const ot = b.innerText;
                b.innerText = '✅ 已复制'; b.style.background = '#218838';
                setTimeout(() => { b.innerText = ot; b.style.background = '#34a853'; }, 1500);
            } catch (e) { setStatusRed('❌ 复制失败'); }
        });

        // 剪贴板监控开关
        const clipBtn = $('clipb');
        if (clipBtn) {
            clipBtn.addEventListener('click', async () => {
                if (clipboardWatching) {
                    stopClipWatch();
                    clipBtn.innerText = '📋 监控';
                    clipBtn.style.background = '#9e9e9e';
                    setStatusRed('⏹ 监控已停止');
                } else {
                    clipBtn.innerText = '⏳ 授权中...';
                    clipBtn.disabled = true;
                    await startClipWatch();
                    clipBtn.disabled = false;
                    if (clipboardWatching) {
                        clipBtn.innerText = '📋 监控中';
                        clipBtn.style.background = '#e4393c';
                    } else {
                        clipBtn.innerText = '📋 监控';
                        clipBtn.style.background = '#9e9e9e';
                    }
                }
            });
        }

        if (currentMode === 'main') {
            $('refb').addEventListener('click', () => { extractMainImages(); renderInfoPanel(); setStatusRed(`✅ ${globalMainImageList.length}张`); });
            $('dlb').addEventListener('click', downloadMain);
        } else {
            $('extb').addEventListener('click', extractMediaImages);
            $('dlb').addEventListener('click', downloadMedia);
            document.querySelectorAll('.exb').forEach(b => {
                b.addEventListener('click', function(e) {
                    e.preventDefault(); e.stopPropagation();
                    const u = this.dataset.url;
                    globalExcludedMediaUrls.has(u) ? globalExcludedMediaUrls.delete(u) : globalExcludedMediaUrls.add(u);
                    renderInfoPanel();
                });
            });
        }

        // 全局粘贴监听(页面任意位置 Ctrl+V 自动识别)
        document.addEventListener('paste', e => {
            // 如果粘贴目标是我们自己的输入框,跳过(由输入框处理器处理)
            if (e.target && e.target.id === 'jd-paste-area') return;
            const text = (e.clipboardData || window.clipboardData).getData('text');
            if (text && text.trim()) {
                const test = parseProductText(text.trim());
                if (test) {
                    e.preventDefault();
                    handlePastedText(text.trim());
                }
            }
        });

        // 输入框粘贴/输入识别
        const pa = $('jd-paste-area');
        if (pa) {
            pa.addEventListener('paste', () => {
                setTimeout(() => {
                    const v = pa.value.trim();
                    if (v) { handlePastedText(v); pa.value = ''; }
                }, 50);
            });
            pa.addEventListener('blur', () => {
                const v = pa.value.trim();
                if (v) { handlePastedText(v); pa.value = ''; }
            });
        }
    }

    // ───── 6. 下载(主图 → index/) ─────
    async function downloadMain() {
        const embed = globalEmbeddedInfo;
        const useSku = embed ? embed.sku : globalProductInfo.sku;
        const useTitle = embed ? embed.title : globalProductInfo.title;
        const useDesc = embed ? (embed.description || useTitle) : globalProductInfo.title;
        const urls = globalMainImageList;
        const sfx = globalFromClipboard ? '' : generateRandomSuffix();
        const folder = `${useSku}----${useTitle}${sfx}`;
        if (!urls.length) { setStatusRed('⚠️ 无主图'); return; }
        if (!window.showDirectoryPicker) { setStatusRed('⚠️ 需 Chrome 86+'); return; }

        const btn = document.getElementById('dlb');
        if (!btn) return;
        const ot = btn.innerText;
        btn.innerText = '⏳...'; btn.disabled = true;

        try {
            const root = await getDirHandle();
            const idx = await root.getDirectoryHandle('index', { create: true });
            const pd = await idx.getDirectoryHandle(folder, { create: true });
            const tf = await pd.getFileHandle('skutitle.txt', { create: true });
            const tw = await tf.createWritable();
            await tw.write(useDesc);
            await tw.close();

            let ok = 0;
            for (let i = 0; i < urls.length; i++) {
                try {
                    btn.innerText = `${i+1}/${urls.length}`;
                    const r = await axios.get(urls[i], { responseType: 'blob', timeout: 15000 });
                    const ext = urls[i].includes('.png') ? 'png' : urls[i].includes('.webp') ? 'webp' : 'jpg';
                    const f = await pd.getFileHandle(`${i}.${ext}`, { create: true });
                    const w = await f.createWritable();
                    await w.write(r.data);
                    await w.close();
                    ok++;
                } catch (e) { console.error('主图失败', i, e); }
            }
            btn.innerText = ot; btn.disabled = false;
            clearEmbedded();
            setStatusRed(`✅ ${ok}/${urls.length}`);
        } catch (e) {
            console.error('[下载]', e);
            btn.innerText = ot; btn.disabled = false;
            if (e.name !== 'AbortError') setStatusRed('❌ 下载出错');
        }
    }

    // ───── 7. 下载(媒体图 → contents/) ─────
    async function downloadMedia() {
        const embed = globalEmbeddedInfo;
        const useSku = embed ? embed.sku : globalProductInfo.sku;
        const useTitle = embed ? embed.title : globalProductInfo.title;
        const useDesc = embed ? (embed.description || useTitle) : globalProductInfo.title;
        const sfx = globalFromClipboard ? '' : generateRandomSuffix();
        const folder = `${useSku}----${useTitle}${sfx}`;
        const urls = globalMediaImageList.filter(u => !globalExcludedMediaUrls.has(u));
        if (!urls.length) { setStatusRed(globalExcludedMediaUrls.size ? '⚠️ 已全部排除' : '⚠️ 无媒体图'); return; }
        if (!window.showDirectoryPicker) { setStatusRed('⚠️ 需 Chrome 86+'); return; }

        const btn = document.getElementById('dlb');
        if (!btn) return;
        const ot = btn.innerText;
        btn.innerText = '⏳...'; btn.disabled = true;

        try {
            const root = await getDirHandle();
            const ct = await root.getDirectoryHandle('contents', { create: true });
            const pd = await ct.getDirectoryHandle(folder, { create: true });
            try { await pd.getFileHandle('skutitle.txt', { create: false }); }
            catch {
                const f = await pd.getFileHandle('skutitle.txt', { create: true });
                const w = await f.createWritable();
                await w.write(useDesc);
                await w.close();
            }

            let ok = 0;
            for (let i = 0; i < urls.length; i++) {
                try {
                    btn.innerText = `${i+1}/${urls.length}`;
                    const r = await axios.get(urls[i], { responseType: 'blob', timeout: 15000 });
                    const ext = urls[i].includes('.png') ? 'png' : urls[i].includes('.webp') ? 'webp' : 'jpg';
                    const f = await pd.getFileHandle(`media_${i}.${ext}`, { create: true });
                    const w = await f.createWritable();
                    await w.write(r.data);
                    await w.close();
                    ok++;
                } catch (e) { console.error('媒体失败', i, e); }
            }
            btn.innerText = ot; btn.disabled = false;
            clearEmbedded();
            setStatusRed(`✅ ${ok}/${urls.length}`);
        } catch (e) {
            console.error('[下载]', e);
            btn.innerText = ot; btn.disabled = false;
            if (e.name !== 'AbortError') setStatusRed('❌ 下载出错');
        }
    }

    // ───── 8. 启动 ─────
    function mainExtract() {
        extractProductInfo();
        extractMainImages();
        renderInfoPanel();
    }

    window.addEventListener('load', () => { if (CONFIG.autoRun) setTimeout(mainExtract, CONFIG.waitParseTime); });
    GM_registerMenuCommand('🔍 手动提取', mainExtract);
    GM_registerMenuCommand('📸 提取媒体图', extractMediaImages);

})();


3:京东预览

// ==UserScript==
// @name         JD DR 本地内容预览工具(单实例可移动版)
// @namespace    http://tampermonkey.net/
// @version      4.3.1
// @match        https://dr.jd.com/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const RUNNING_KEY = "__JD_DR_PREVIEW_TOOL_RUNNING__";
    const PANEL_ID = "jd_preview_panel";

    // 防止多次初始化
    if (window[RUNNING_KEY] || document.getElementById(PANEL_ID)) {
        console.log("[JD DR PREVIEW] 已有实例或面板存在,阻止重复创建");
        return;
    }
    window[RUNNING_KEY] = true;

    let fileMap = new Map();

    function waitForBody() {
        return new Promise(resolve => {
            const timer = setInterval(() => {
                if (document.body) {
                    clearInterval(timer);
                    resolve();
                }
            }, 200);
        });
    }

    function removeDuplicatePanels() {
        const panels = Array.from(document.querySelectorAll(
            "#jd_preview_panel, #jd_preview_bar, #jd_auto_panel"
        ));
        panels.forEach((el, index) => {
            if (el.id === PANEL_ID && index === panels.findIndex(p => p.id === PANEL_ID)) return;
            el.remove();
        });
    }

    function log(msg) {
        const box = document.getElementById("previewLogBox");
        if (!box) return;
        const time = new Date().toLocaleTimeString();
        box.innerText = `[${time}] ${msg}`;
    }

    function createUI() {
        removeDuplicatePanels();
        if (document.getElementById(PANEL_ID)) return;

        const panel = document.createElement("div");
        panel.id = PANEL_ID;

        panel.style.cssText = `
            position: fixed;
            right: 20px;
            bottom: 20px;
            width: 520px;
            max-height: 520px;
            z-index: 2147483647;
            background: rgba(255,255,255,0.98);
            color: #222;
            border-radius: 16px;
            box-shadow: 0 12px 36px rgba(0,0,0,.28);
            overflow: hidden;
            font-family: Arial, "Microsoft YaHei", sans-serif;
            border: 1px solid rgba(0,0,0,.08);
        `;

        panel.innerHTML = `
            <div id="previewHeader" style="
                height: 46px;
                background: linear-gradient(90deg, #ff5000, #ff7a18);
                color:#fff;
                display:flex;
                align-items:center;
                justify-content:space-between;
                padding:0 14px;
                cursor:move;
                user-select:none;
            ">
                <div style="font-weight:700;font-size:15px;">
                    DR 内容预览
                    <span style="font-size:12px;font-weight:400;opacity:.9;margin-left:6px;">
                        只看不发
                    </span>
                </div>
                <div>
                    <button id="pickFolder" style="
                        background:#fff;
                        color:#ff5000;
                        border:none;
                        padding:6px 12px;
                        border-radius:999px;
                        cursor:pointer;
                        font-weight:700;
                    ">加载文件夹</button>
                    <button id="clearPreview" style="
                        background:rgba(255,255,255,.2);
                        color:#fff;
                        border:1px solid rgba(255,255,255,.45);
                        padding:6px 12px;
                        border-radius:999px;
                        cursor:pointer;
                        margin-left:6px;
                    ">清空</button>
                </div>
            </div>

            <div id="previewList" style="
                max-height: 420px;
                overflow:auto;
                padding:12px;
                background:#f7f8fa;
            ">
                <div style="
                    color:#999;
                    text-align:center;
                    padding:32px 0;
                    font-size:13px;
                ">请先加载文件夹</div>
            </div>

            <div id="previewLogBox" style="
                height: 30px;
                line-height:30px;
                padding:0 12px;
                font-size:12px;
                color:#666;
                background:#fff;
                border-top:1px solid #eee;
                white-space:nowrap;
                overflow:hidden;
                text-overflow:ellipsis;
            ">等待加载文件夹</div>

            <input id="folderInput" type="file" webkitdirectory multiple style="display:none;">
        `;

        document.body.appendChild(panel);
        bindDrag(panel, panel.querySelector("#previewHeader"));
        bindEvents();
        log("预览面板初始化完成");
    }

    function bindEvents() {
        const pickFolder = document.getElementById("pickFolder");
        const clearPreview = document.getElementById("clearPreview");
        const folderInput = document.getElementById("folderInput");

        pickFolder.onclick = () => folderInput.click();

        clearPreview.onclick = () => {
            fileMap.clear();
            document.getElementById("previewList").innerHTML = `
                <div style="
                    color:#999;
                    text-align:center;
                    padding:32px 0;
                    font-size:13px;
                ">暂无内容</div>
            `;
            log("已清空预览");
        };

        folderInput.onchange = handleFolder;
    }

    function bindDrag(panel, handle) {
        let isDragging = false, startX = 0, startY = 0, startLeft = 0, startTop = 0;

        handle.addEventListener("mousedown", function (e) {
            if (e.target.tagName === "BUTTON") return;
            isDragging = true;
            const rect = panel.getBoundingClientRect();
            startX = e.clientX; startY = e.clientY;
            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", function (e) {
            if (!isDragging) return;
            let nextLeft = startLeft + e.clientX - startX;
            let nextTop = startTop + e.clientY - startY;
            nextLeft = Math.max(0, Math.min(nextLeft, window.innerWidth - panel.offsetWidth));
            nextTop = Math.max(0, Math.min(nextTop, window.innerHeight - panel.offsetHeight));
            panel.style.left = nextLeft + "px";
            panel.style.top = nextTop + "px";
        });

        document.addEventListener("mouseup", function () {
            isDragging = false;
            document.body.style.userSelect = "";
        });
    }

    async function handleFolder(e) {
        fileMap.clear();
        for (const file of e.target.files) {
            const root = file.webkitRelativePath.split("/")[0];
            if (!fileMap.has(root)) fileMap.set(root, []);
            fileMap.get(root).push(file);
        }
        await renderPreview();
        log(`已加载 ${fileMap.size} 个商品`);
    }

    async function renderPreview() {
        const list = document.getElementById("previewList");
        list.innerHTML = "";
        if (!fileMap.size) {
            list.innerHTML = `<div style="color:#999;text-align:center;padding:32px 0;font-size:13px;">暂无内容</div>`;
            return;
        }

        for (const [folder, files] of fileMap.entries()) {
            const sku = folder.split("----")[0] || "";
            const title = folder.split("----")[1] || "";
            const contentFile = files.find(f => f.name === "content.txt");
            const content = contentFile ? await contentFile.text() : "无 content.txt";

            const card = document.createElement("div");
            card.style.cssText = `
                background:#fff;
                border-radius:14px;
                padding:12px 14px;
                margin-bottom:10px;
                box-shadow:0 4px 14px rgba(0,0,0,.06);
                border:1px solid rgba(0,0,0,.04);
            `;
            card.innerHTML = `
                <div style="display:flex;justify-content:space-between;gap:10px;align-items:flex-start;margin-bottom:8px;">
                    <div style="font-size:14px;font-weight:700;color:#111827;line-height:1.45;">${escapeHTML(title || "无标题")}</div>
                    <div style="flex-shrink:0;color:#ff5000;background:#fff3ec;border:1px solid #ffd7c2;border-radius:999px;padding:3px 8px;font-size:12px;font-weight:700;">SKU ${escapeHTML(sku || "-")}</div>
                </div>
                <div style="color:#4b5563;font-size:13px;line-height:1.65;max-height:110px;overflow:auto;white-space:pre-wrap;background:#f9fafb;border-radius:10px;padding:10px;">
                    ${escapeHTML(content)}
                </div>
            `;
            list.appendChild(card);
        }
    }

    function escapeHTML(str) {
        return String(str)
            .replaceAll("&", "&amp;")
            .replaceAll("<", "&lt;")
            .replaceAll(">", "&gt;")
            .replaceAll('"', "&quot;")
            .replaceAll("'", "&#039;");
    }

    function observeDuplicatePanels() {
        const observer = new MutationObserver(() => {
            const panels = document.querySelectorAll("#jd_preview_panel");
            if (panels.length > 1) {
                Array.from(panels).slice(1).forEach(el => el.remove());
                log("已移除重复窗口");
            }
            document.querySelectorAll("#jd_preview_bar, #jd_auto_panel").forEach(el => el.remove());
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    async function init() {
        await waitForBody();
        removeDuplicatePanels();
        observeDuplicatePanels();
        createUI();
    }

    // 全局初始化锁,防止 SPA 页面重复加载
    if (!window.__JD_DR_PREVIEW_TOOL_INITIALIZED__) {
        window.__JD_DR_PREVIEW_TOOL_INITIALIZED__ = true;
        init();
    }

})();


发表评论:

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