Greasy Fork

Greasy Fork is available in English.

Shopee 悬浮按钮

悬浮快捷入口

当前为 2025-07-28 提交的版本,查看 最新版本

// ==UserScript==
// @name         Shopee 悬浮按钮
// @namespace    http://tampermonkey.net/
// @version      2.1.1
// @license      Rayu
// @description  悬浮快捷入口
// @author       Rayu
// @match        https://seller.shopee.tw/*
// @exclude      https://seller.shopee.tw/webchat/conversations
// @match        *://shopee.tw/*
// @match        *://shopee.ph/*
// @match        *://shopee.sg/*
// @match        *://shopee.com.my/*
// @icon         https://www.wikimedia.org/static/favicon/wikipedia.ico
// @grant        GM_addStyle
// @run-at       document-end
// ==/UserScript==

(function () {
    'use strict';

    // 配置中心
    const CONFIG = {
        BUTTONS: [
            {
                id: 'shipment-btn',
                content: '待<br>出货',
                path: '/portal/sale/shipment?type=toship',
                title: '待处理订单'
            },
            {
                id: 'products-btn',
                content: '我的<br>商品',
                path: '/portal/product/list/all?page=1&size=48',
                title: '商品管理'
            },
            {
                id: 'analytics-btn',
                content: '商品<br>分析',
                path: '/datacenter/product/overview',
                title: '数据分析'
            },
            {
                id: 'ads-btn',
                content: '我的<br>广告',
                path: '/portal/marketing/pas/assembly',
                title: '广告管理'
            },
            {
                id: 'campaign-btn',
                content: '行销<br>活动',
                path: '/portal/marketing',
                title: '营销活动'
            },
            {
                id: 'returns-btn',
                content: '退貨<br>退款',
                path: '/portal/sale/return',
                title: '退货退款'
            },
            {
                id: 'roi-edit-btn',
                content: '修改ROI',
                path: 'javascript:void(0);',
                title: '一键把本页ROI值全部设为7'
            }
        ],
        TRACKING_PATTERN: /-i\.(\d+)\.(\d+)/,
        STYLES: `
            :root {
                --primary-color: #ee4d2d;
                --button-size: 52px;
                --hover-scale: 1.08;
                --gap: 8px;
            }
            #floating-buttons-container {
                position: fixed;
                top: 50vh;
                right: 50px;
                transform: translateY(-50%);
                display: flex;
                flex-direction: column;
                gap: var(--gap);
                z-index: 9999;
                filter: drop-shadow(0 2px 6px rgba(0,0,0,0.16));
            }
            .floating-button {
                display: flex;
                align-items: center;
                justify-content: center;
                width: var(--button-size);
                height: var(--button-size);
                background: var(--primary-color);
                color: white !important;
                border-radius: 8px;
                font-size: 15px;
                font-weight: 500;
                line-height: 1.3;
                text-align: center;
                text-decoration: none !important;
                cursor: pointer;
                transition:
                    transform 0.2s cubic-bezier(0.18, 0.89, 0.32, 1.28),
                    opacity 0.2s,
                    background 0.2s;
                user-select: none;
            }
            .floating-button:hover {
                transform: scale(var(--hover-scale));
                background: #d14327;
                opacity: 0.95;
            }
            .floating-button:active {
                transition-duration: 0.1s;
                transform: scale(0.96);
            }
            #roi-left-bottom-btn-container {
                position: fixed !important;
                left: 30px !important;
                bottom: 30px !important;
                z-index: 9999 !important;
            }
            #roi-edit-btn {
                width: 110px !important;
                height: 38px !important;
                border-radius: 8px !important;
                font-size: 16px !important;
                line-height: 38px !important;
                text-align: center !important;
                padding: 0 !important;
            }
            @media (max-width: 768px) {
                #floating-buttons-container {
                    right: 4px;
                    transform: translateY(-50%) scale(0.82);
                    gap: 6px;
                }
                .floating-button {
                    width: 46px;
                    height: 46px;
                    font-size: 14px;
                }
            }
        `
    };

    // 核心功能模块
    class ShopeeEnhancer {
        constructor() {
            this.observer = null;
            this.reg = CONFIG.TRACKING_PATTERN;
            this.init();
        }

        // 判断是否为 ROI 按钮需要出现的页面
        isRoiPage() {
            // 只要当前路径以 /portal/marketing/pas/product/ 开头
            return /^\/portal\/marketing\/pas\/product\//.test(location.pathname);
        }

        init() {
            this.injectStyles();
            this.createFloatingButtons();
            this.hijackHistoryMethods();
            this.sanitizeLinks();
        }

        injectStyles() {
            GM_addStyle(CONFIG.STYLES);
        }

        createFloatingButtons() {
            // 清理老容器
            let container = document.getElementById('floating-buttons-container');
            if (container) container.remove();

            // 新建右侧容器
            container = document.createElement('div');
            container.id = 'floating-buttons-container';
            document.body.appendChild(container);

            // 右侧按钮(不含roi-edit-btn)
            CONFIG.BUTTONS.filter(btn => btn.id !== 'roi-edit-btn').forEach(btn => {
                let a = document.createElement('a');
                a.className = 'floating-button';
                a.id = btn.id;
                a.href = btn.path.startsWith('javascript:') ? '#' : new URL(btn.path, window.location.origin);
                a.target = '_blank';
                a.rel = 'noopener noreferrer';
                a.title = btn.title;
                a.innerHTML = btn.content;
                container.appendChild(a);
            });

            // 清理老的左下角ROI容器
            let roiContainer = document.getElementById('roi-left-bottom-btn-container');
            if (roiContainer) roiContainer.remove();

            // 新建左下ROI容器
            roiContainer = document.createElement('div');
            roiContainer.id = 'roi-left-bottom-btn-container';
            document.body.appendChild(roiContainer);

            // 只在指定页面插入ROI按钮
            if (this.isRoiPage()) {
                let roiBtnConf = CONFIG.BUTTONS.find(btn => btn.id === 'roi-edit-btn');
                if (roiBtnConf) {
                    let roiBtn = document.createElement('a');
                    roiBtn.className = 'floating-button';
                    roiBtn.id = 'roi-edit-btn';
                    roiBtn.href = '#';
                    roiBtn.title = roiBtnConf.title;
                    roiBtn.innerHTML = roiBtnConf.content;
                    roiContainer.appendChild(roiBtn);

                    roiBtn.addEventListener('click', function (e) {
                        e.preventDefault();

                        const els = document.querySelectorAll('.roi-edit-popover-container .roi-value');
                        if (els.length === 0) {
                            alert('未发现ROI内容元素!');
                            return;
                        }
                        els.forEach(el => el.innerText = '7');
                        alert('全部ROI值已修改为7!');

                        // 勾选radio
                        let yesLimitRadio = document.querySelector('.yes-limit input[type="radio"]');
                        if (yesLimitRadio) {
                            yesLimitRadio.checked = true;
                            yesLimitRadio.dispatchEvent(new Event('change', { bubbles: true }));
                        }

                        // 填写预算输入框,加延时和原生setter双保险
                        setTimeout(function () {
                            let input = document.querySelector('.budget-input .eds-input__input');
                            if (input) {
                                const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
                                nativeInputValueSetter.call(input, '40');
                                input.dispatchEvent(new Event('input', { bubbles: true }));
                                input.dispatchEvent(new Event('change', { bubbles: true }));
                            } else {
                                console.warn('未找到预算输入框!');
                            }
                        }, 300);
                    });
                }
            }
        }

        cleanURL(url) {
            const match = url.match(this.reg);
            if (!match) return url;
            return `${window.location.origin}/product/${match[1]}/${match[2]}`;
        }

        hijackHistoryMethods() {
            const self = this;
            const originalPushState = history.pushState;
            const originalReplaceState = history.replaceState;

            history.pushState = function (state, title, url) {
                if (url) url = self.cleanURL(url);
                return originalPushState.call(this, state, title, url);
            };

            history.replaceState = function (state, title, url) {
                if (url) url = self.cleanURL(url);
                return originalReplaceState.call(this, state, title, url);
            };
        }

        sanitizeLinks() {
            // 当前页面处理
            if (this.reg.test(window.location.href)) {
                window.location.replace(this.cleanURL(window.location.href));
                return;
            }

            // 初始化清理
            this.processLinks(document);

            // 动态内容监控
            this.observer = new MutationObserver(mutations => {
                mutations.forEach(({ addedNodes }) => {
                    addedNodes.forEach(node => {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            this.processLinks(node);
                        }
                    });
                });
            });

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

        processLinks(root) {
            const links = root.querySelectorAll('a[href*="-i."]');
            links.forEach(link => {
                link.href = this.cleanURL(link.href);
            });
        }
    }

    // 启动
    const enhancer = new ShopeeEnhancer();

    // 单页应用路由变更时保持悬浮按钮
    let lastUrl = location.href;
    setInterval(() => {
        if (location.href !== lastUrl) {
            lastUrl = location.href;
            enhancer.createFloatingButtons();
        }
    }, 1000);

})();