Greasy Fork

Greasy Fork is available in English.

dlsite购物车增强

为 DLsite 购物车添加评分、销量、发售日、标签等信息

当前为 2025-11-23 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         dlsite购物车增强
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  为 DLsite 购物车添加评分、销量、发售日、标签等信息
// @author       0moi
// @match        https://www.dlsite.com/maniax/cart
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        GM_xmlhttpRequest
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const ajaxApi = 'https://www.dlsite.com/maniax/product/info/ajax?cdn_cache_min=1&product_id=';
    const jsonApi = 'https://www.dlsite.com/maniax/api/=/product.json?workno=';

    const workMap = new Map(); // id → DOM
    const tagMap = new Map();  // id → [{ old, new }]

    const formatter = new Intl.DateTimeFormat("zh-CN", {
        year: "numeric",
        month: "long",
        day: "numeric",
        hour: "numeric",
        hour12: false
    });

    async function main() {
        importShoelace();
        collectCartWorks();
        await loadJsonTagData();

        const ajaxJson = await loadAjaxData();
        injectInfo(ajaxJson);
    }

    /* -----------------------
       Step 1. 解析购物车作品
    -------------------------*/
    function collectCartWorks() {
        const works = document.querySelectorAll('#cart_wrapper > ul > li');

        works.forEach(work => {
            const id = work.getAttribute('data-workno');
            if (id) workMap.set(id, work);
        });
    }

    /* -----------------------
       Step 2. 加载 JSON 标签数据
    -------------------------*/
    async function loadJsonTagData() {
        const tasks = [...workMap.keys()].map(id => fetchJsonData(id));
        await Promise.all(tasks);
    }

    function fetchJsonData(id) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: jsonApi + id,
                responseType: 'json',
                onload: res => {
                    const data = res.response?.[0];
                    if (!data) return resolve();

                    const original = data.genres || [];
                    const replaced = data.genres_replaced || [];

                    // 通过 genre id 匹配,避免顺序问题
                    const tags = original.map(orig => {
                        const rep = replaced.find(x => x.id === orig.id);
                        return {
                            old: orig.name,
                            new: rep?.name ?? orig.name
                        };
                    });

                    tagMap.set(id, tags);
                    resolve(tags);
                },
                onerror: err => reject(err)
            });
        });
    }

    /* -----------------------
       Step 3. 加载 AJAX 评分+销量数据
    -------------------------*/
    function loadAjaxData() {
        const idStr = [...workMap.keys()].join(',');
        const url = ajaxApi + idStr;

        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url,
                responseType: 'json',
                onload: res => resolve(res.response || {}),
                onerror: err => reject(err)
            });
        });
    }

    /* -----------------------
       Step 4. 注入到 DOM
    -------------------------*/
    function injectInfo(json) {
        workMap.forEach((dom, id) => {
            const info = json[id];
            if (!info) return;

            const content = dom.querySelector('.work_content');
            if (!content) return;

            const frag = document.createDocumentFragment();

            /* ---- 发售日 与 销量/评分 ---- */
            const registDiv = createInfoBlock(info);
            frag.appendChild(registDiv);

            /* ---- 标签 ---- */
            const tags = tagMap.get(id);
            if (tags?.length) {
                const tagDiv = document.createElement('dd');

                tags.forEach(tag => {
                    tagDiv.appendChild(buildTag(tag.old, tag.new));
                });

                frag.appendChild(tagDiv);
            }

            content.appendChild(frag);
        });
    }

    function createInfoBlock(info) {
        const dl_count = info.dl_count ?? 0;
        const avg = info.rate_average_2dp;
        const rate_count = info.rate_count;

        const date = new Date(info.regist_date);
        const dateStr = formatter.format(date).replace(":", " 时");
        const days = Math.floor((Date.now() - date.getTime()) / 86400000);

        const dd = document.createElement('dd');

        const rateHtml = rate_count
            ? `<span>&nbsp;&nbsp;评分:</span>
               <span class="rate">${avg}</span>
               <span>(${rate_count})</span>
               <sl-rating readonly value="${avg}"></sl-rating>`
            : '';

        dd.innerHTML = `
            <div class="registDate">
                <span>发售日:</span><span>${dateStr}</span><span>&nbsp;&nbsp;发售于 ${days} 天前</span>
            </div>
            <div class="countData" style="display:flex;align-items:center;">
                <span>销量:</span><span>${dl_count}</span>
                ${rateHtml}
            </div>
        `;
        return dd;
    }

    /* -----------------------
       生成 Shoelace Tag + Tooltip
    -------------------------*/
    function buildTag(oldName, newName) {
        const tooltip = document.createElement('sl-tooltip');
        tooltip.setAttribute('content', oldName);

        const tag = document.createElement('sl-tag');
        tag.textContent = newName;
        tag.setAttribute('size', 'small');
        tag.setAttribute('pill', '');

        tag.setAttribute('variant', oldName === newName ? 'primary' : 'warning');

        tooltip.appendChild(tag);
        return tooltip;
    }

    /* -----------------------
       Shoelace 注入(带重复检测)
    -------------------------*/
    function importShoelace() {
        if (document.querySelector('link[href*="shoelace"]')) return;

        const head = document.head;
        

        const css = document.createElement('link');
        css.rel = 'stylesheet';
        css.href = 'https://cdn.jsdelivr.net/npm/@shoelace-style/[email protected]/cdn/themes/light.css';

        const script = document.createElement('script');
        script.type = 'module';
        script.src = 'https://cdn.jsdelivr.net/npm/@shoelace-style/[email protected]/cdn/shoelace-autoloader.js';

        head.appendChild(css);
        head.appendChild(script);
    }

    /* -----------------------
       启动
    -------------------------*/
    main().catch(console.error);

})();