Bangumi-topic-ShareCard

使用 GreasyFork 认可的 JSDelivr 源,支持 AI 标签、链接展示

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Bangumi-topic-ShareCard
// @namespace    http://tampermonkey.net/
// @version      1.6
// @description  使用 GreasyFork 认可的 JSDelivr 源,支持 AI 标签、链接展示
// @author       Bangumi_0809_Mewtw0
// @match        *://bgm.tv/group/topic/*
// @match        *://bangumi.tv/group/topic/*
// @match        *://chii.in/group/topic/*
// @grant        GM_xmlhttpRequest
// @connect      *
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/html2canvas.min.js
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ================= 配置区 =================
    const AI_CONFIG = {
        apiUrl: "在此处填入你的_API_URL",
        apiKey: "在此处填入你的_API_KEY",
        model: "gpt-3.5-turbo",
    };
    // =========================================

    // 用于存储当前活动的overlay元素
    let currentOverlay = null;

    const style = document.createElement('style');
    style.innerHTML = `
        #bgm-share-overlay {
            position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(0,0,0,0.85); display: none; justify-content: center;
            align-items: center; z-index: 100000;
            cursor: pointer; /* 添加指针样式提示可点击 */
        }

        /* 添加关闭按钮 */
        .close-overlay-btn {
            position: absolute;
            top: 20px;
            right: 20px;
            background: rgba(255, 255, 255, 0.2);
            border: none;
            width: 40px;
            height: 40px;
            border-radius: 50%;
            color: white;
            font-size: 24px;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 100001;
            backdrop-filter: blur(5px);
            -webkit-backdrop-filter: blur(5px);
        }

        .close-overlay-btn:hover {
            background: rgba(255, 255, 255, 0.3);
            transform: scale(1.1);
        }

        .share-card {
            width: 420px; background: rgba(40, 40, 40, 0.85); border-radius: 20px; overflow: hidden;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
            box-shadow: 0 25px 60px rgba(0,0,0,0.5);
            /* 毛玻璃核心样式 */
            background: rgba(40, 40, 40, 0.85);
            backdrop-filter: blur(10px);
            -webkit-backdrop-filter: blur(10px); /* Safari 支持 */
            cursor: default; /* 卡片本身恢复默认光标 */
        }

        .card-top-bar { height: 0px; background: #F09199; }

        .card-header {
            padding: 25px 25px 20px;
            display: flex;
            align-items: center;
            gap: 15px;
            text-align: left;
            /* 添加以下确保居中更准确 */
            position: relative;
            /* 毛玻璃核心样式 */
            background: rgba(40, 40, 40, 0.85);
            backdrop-filter: blur(10px);
            -webkit-backup-filter: blur(10px); /* Safari 支持 */
            border-bottom: 1px solid #fff; /* 添加白色实线分界线 */
        }

        .avatar-img {
            width: 54px;
            height: 54px;
            border-radius: 12px;
            background: #eee;
            background-size: cover;
            background-position: center;
            border: 1px solid #f0f0f0;
            flex-shrink: 0;
            /* 确保自身垂直居中 */
            position: relative;
            top: 0;
            transform: translateY(0);
        }

        .user-meta {
            text-align: left;
            /* 使用flex让内部元素垂直居中 */
            display: flex;
            flex-direction: column;
            justify-content: center;
            height: 54px; /* 与头像同高 */
            padding: 0; /* 移除内边距 */
        }

        .user-meta .name {
            display: block;
            font-weight: bold;
            color: #F09199;
            font-size: 17px;
            line-height: 1.2;
            margin: 0; /* 清除默认margin */
            padding: 0;
        }

        .user-meta .time {
            font-size: 12px;
            color: #aaa;
            margin-top: 4px;
            display: block;
            padding: 0;
        }


        .card-body { padding: 15px 25px 25px; text-align: left;

        /* 毛玻璃核心样式 */
        background: rgba(40, 40, 40, 0.85);
        backdrop-filter: blur(10px);
        -webkit-backdrop-filter: blur(10px); /* Safari 支持 */
        }
        .main-title { font-size: 20px; color: #fff; margin: 0 0 15px 0; line-height: 1.5; font-weight: 800; }

        .content-box {
            background: #262626;
            padding: 20px;
            border-radius: 12px;
            position: relative; /* 为伪元素定位做准备 */
        }

        .content-box.hover-visible,
        .content-box:hover {
            /* 移除之前的边框效果 */
        }

        .content-box.hover-visible::after,
        .content-box:hover::after {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            border: 1px solid #F09199;
            border-radius: 12px;
            pointer-events: none; /* 确保伪元素不干扰鼠标事件 */
            z-index: 1;
        }

        .content-text { font-size: 14px; color: #fff; line-height: 1.8; margin: 0; white-space: pre-wrap; word-break: break-all; }
        .tags-container { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 15px; }
        .tag-item { background: #FEEFF0; color: #F09199; font-size: 3px; padding: 6px 12px; border-radius: 20px; font-weight: Bold; border: 1px solid #F0919944; }
        .card-footer { background: rgba(40, 40, 40, 0.85); padding: 20px 25px; display: flex; justify-content: space-between; align-items: center; border-top: 1px solid #fff; }
        .qr-img { width: 55px; height: 55px; background: rgba(40, 40, 40, 0.85); }
        #loading-info { position: fixed; top: 55%; left: 50%; transform: translateX(-50%); color: #fff; font-size: 14px; z-index: 100001; }
        .copy-success {
            position: fixed; top: 20px; right: 20px; background: #4CAF50;
            color: white; padding: 12px 20px; border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.3); z-index: 100002;
            font-size: 14px; font-weight: bold;
        }
    `;
    document.head.appendChild(style);

    // 全局点击事件监听器
    function setupGlobalClickHandler() {
        document.addEventListener('click', function(event) {
            // 如果当前没有活动的overlay,直接返回
            if (!currentOverlay || currentOverlay.style.display !== 'flex') {
                return;
            }

            // 检查点击的是否是overlay本身(而不是卡片内容)
            if (event.target === currentOverlay ||
                event.target.classList.contains('close-overlay-btn')) {
                removeOverlay();
            }
        });

        // ESC键也可以关闭overlay
        document.addEventListener('keydown', function(event) {
            if (event.key === 'Escape' && currentOverlay && currentOverlay.style.display === 'flex') {
                removeOverlay();
            }
        });
    }

    // 移除overlay的函数
    function removeOverlay() {
        if (currentOverlay) {
            currentOverlay.remove();
            currentOverlay = null;
        }
    }

    function getElementByXpath(path) {
        return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
    }

    function fetchAsBase64(url) {
        return new Promise((resolve) => {
            if (!url) { resolve(""); return; }
            const finalUrl = url.startsWith('//') ? 'https:' + url : url;
            GM_xmlhttpRequest({
                method: "GET", url: finalUrl, responseType: "blob",
                onload: (res) => {
                    const reader = new FileReader();
                    reader.onloadend = () => resolve(reader.result);
                    reader.readAsDataURL(res.response);
                },
                onerror: () => resolve("")
            });
        });
    }

    function showCopySuccess() {
        const successDiv = document.createElement('div');
        successDiv.className = 'copy-success';
        successDiv.textContent = '✓ 图片已复制到剪贴板!';
        document.body.appendChild(successDiv);
        setTimeout(() => successDiv.remove(), 1000);
    }

    function fallbackDownload(canvas, overlay) {
        const dataUrl = canvas.toDataURL('image/png');
        const link = document.createElement('a');
        link.download = `Bangumi分享卡片_${new Date().getTime()}.png`;
        link.href = dataUrl;
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);

        // 显示下载提示
        const downloadDiv = document.createElement('div');
        downloadDiv.className = 'copy-success';
        downloadDiv.textContent = '✓ 图片已保存到本地!';
        downloadDiv.style.background = '#2196F3';
        document.body.appendChild(downloadDiv);

        // 10秒后移除提示
        setTimeout(() => {
            downloadDiv.remove();
        }, 1000);
    }

    async function getAITags(title, content) {
        if (!AI_CONFIG.apiKey || AI_CONFIG.apiKey.includes("填入")) return ["话题", "讨论", "Bangumi"];
        return new Promise((resolve) => {
            const prompt = `根据标题和内容生成3个短标签,只要标签名,空格隔开。内容:${title} ${content.substring(0, 150)}`;
            GM_xmlhttpRequest({
                method: "POST", url: AI_CONFIG.apiUrl,
                headers: { "Content-Type": "application/json", "Authorization": `Bearer ${AI_CONFIG.apiKey}` },
                data: JSON.stringify({ model: AI_CONFIG.model, messages: [{ role: "user", content: prompt }], temperature: 0.5 }),
                onload: (res) => {
                    try {
                        const tags = JSON.parse(res.responseText).choices[0].message.content.trim().split(/\s+/).slice(0, 3);
                        resolve(tags);
                    } catch (e) { resolve(["话题", "讨论", "Bangumi"]); }
                },
                onerror: () => resolve(["话题", "讨论", "Bangumi"])
            });
        });
    }

    async function createShareImage() {
        if (typeof html2canvas === 'undefined') {
            alert("截图库加载失败,请刷新页面或检查网络。");
            return;
        }

        const loading = document.createElement('div');
        loading.innerHTML = '<div id="bgm-share-overlay" style="display:flex"><div id="loading-info">AI 正在提炼标签...</div></div>';
        document.body.appendChild(loading);

        const idNode = getElementByXpath("/html/body/div[1]/div[2]/div[1]/div[1]/div[2]/div[2]/strong/a");
        const username = idNode ? idNode.innerText.trim() : "未知用户";
        const timeNode = getElementByXpath("/html/body/div[1]/div[2]/div[1]/div[1]/div[2]/div[1]/div[1]/small");
        let postTime = timeNode ? (timeNode.innerText.match(/\d{4}-\d{1,2}-\d{1,2}\s\d{1,2}:\d{1,2}/)?.[0] || "未知时间") : "未知时间";

        const h1Node = document.querySelector('#pageHeader h1') || document.querySelector('h1');
        let pureTitle = "";
        if (h1Node) h1Node.childNodes.forEach(n => { if (n.nodeType === 3) pureTitle += n.textContent; });
        pureTitle = pureTitle.replace(/[»\n]/g, '').trim() || "分享话题";

        const masterPost = document.querySelector('.postTopic') || document.querySelector('[id^="post_"]');
        let fullContent = (masterPost?.querySelector('.topic_content') || masterPost?.querySelector('.inner'))?.innerText?.trim() || "";
        let displayContent = fullContent.length > 300 ? fullContent.substring(0, 300) + "..." : fullContent;

        const avatarBox = masterPost?.querySelector('.avatarSize48');
        let avatarUrl = avatarBox ? window.getComputedStyle(avatarBox).backgroundImage.replace(/url\(["']?([^"']+)["']?\)/, '$1') : "";

        const currentFullUrl = window.location.origin + window.location.pathname;
        const displayUrl = currentFullUrl.replace(/^https?:\/\//, '');

        const [tags, base64Avatar, base64QR] = await Promise.all([
            getAITags(pureTitle, fullContent),
            fetchAsBase64(avatarUrl),
            fetchAsBase64(`https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${encodeURIComponent(currentFullUrl)}&color=F09199&bgcolor=262626`)
        ]);

        const tagsHtml = tags.map(tag => `<span class="tag-item">#${tag}</span>`).join('');
        loading.remove();

        const overlay = document.createElement('div');
        overlay.id = 'bgm-share-overlay';
        overlay.style.display = 'flex';
        overlay.innerHTML = `
            <button class="close-overlay-btn" title="关闭">×</button>
            <div id="capture-area" style="padding: 2px; background: transparent;">
                <div class="share-card">
                    <div class="card-top-bar"></div>
                    <div class="card-header">
                        <img class="avatar-img" src="${base64Avatar}">
                        <div class="user-meta">
                            <span class="name">${username}</span>
                            <span class="time">${postTime}</span>
                        </div>
                    </div>
                    <div class="card-body">
                        <h1 class="main-title">${pureTitle}</h1>
                        <div class="content-box"><p class="content-text">${displayContent}</p></div>
                        <div class="tags-container">${tagsHtml}</div>
                    </div>
                    <div class="card-footer">
                        <div style="text-align:left">
                            <div style="font-size:14px; font-weight:bold; color:#f09199">Bangumi 番组计划</div>
                            <div style="font-size:10px; color:#fff; margin-top:2px;">${displayUrl}</div>
                        </div>
                        <img class="qr-img" src="${base64QR}">
                    </div>
                </div>
            </div>
        `;
        document.body.appendChild(overlay);

        // 设置当前活动的overlay
        currentOverlay = overlay;

        setTimeout(async () => {
            const captureArea = document.querySelector('#capture-area');
            if (!captureArea) return;

            // 添加 hover 类用于截图
            const contentBox = captureArea.querySelector('.content-box');
            if (contentBox) {
                contentBox.classList.add('hover-visible');
            }

            // 等待样式应用
            await new Promise(resolve => setTimeout(resolve, 50));

            const canvas = await html2canvas(captureArea, {
                scale: 2,
                backgroundColor: null,  // 改为透明背景
                useCORS: true,
                logging: false
            });

            // 截图完成后移除 hover 类
            if (contentBox) {
                contentBox.classList.remove('hover-visible');
            }

            canvas.toBlob(async (blob) => {
                try {
                    await navigator.clipboard.write([
                        new ClipboardItem({
                            'image/png': blob
                        })
                    ]);

                    showCopySuccess();

                } catch (err) {
                    console.log('使用Clipboard API复制失败,尝试备选方案:', err);
                    fallbackDownload(canvas, overlay);
                }
            }, 'image/png');
        }, 800);
    }

    function insertButton() {
        const containerXpath = "/html/body/div[1]/div[2]/div[1]/div[1]/div[2]/div[2]/div[2]";
        const container = getElementByXpath(containerXpath);
        if (container && !document.getElementById('gen-card-btn')) {
            const btn = document.createElement('a');
            btn.id = 'gen-card-btn';
            btn.href = "javascript:void(0);";
            btn.className = 'chiiBtn';
            btn.style.backgroundColor = "transparent";
            btn.style.color = "#F09199";
            btn.style.marginLeft = "10px";
            btn.style.marginBottom = "-10px";
            btn.style.padding = "1px 10px";
            btn.style.borderRadius = "16px";
            btn.style.display = "inline-block";
            btn.style.verticalAlign = "middle";
            btn.innerHTML = '<span>生成分享卡片</span>';
            container.appendChild(btn);
            btn.addEventListener('click', createShareImage);
        }
    }

    // 初始化全局事件监听器
    setupGlobalClickHandler();

    // 等待页面加载完成后插入按钮
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', insertButton);
    } else {
        setTimeout(insertButton, 500);
    }
})();