Greasy Fork

Greasy Fork is available in English.

网页右上角备忘录

在所有网页右上角添加一个可添加/删除备忘的备忘录工具,支持紧急标记和数量徽章

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         网页右上角备忘录
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  在所有网页右上角添加一个可添加/删除备忘的备忘录工具,支持紧急标记和数量徽章
// @author       Nuclear_Fish_cyq
// @license MIT
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // 检查是否为顶层窗口,如果不是则退出
    if (window.self !== window.top) {
        // 当前窗口是iframe或嵌套页面,不创建按钮
        return;
    }

    // 从存储中获取备忘录数据,如果没有则使用空数组
    let memos = GM_getValue('memos', []);

    // 创建悬浮按钮
    const createMemoButton = () => {
        const buttonContainer = document.createElement('div');
        buttonContainer.id = 'memo-button-container';

        // 按钮容器样式
        Object.assign(buttonContainer.style, {
            position: 'fixed',
            top: '40px',
            right: '20px',
            width: '60px',
            height: '60px',
            zIndex: '9999',
            cursor: 'pointer'
        });

        // 主按钮
        const button = document.createElement('div');
        button.id = 'memo-floating-btn';
        button.innerHTML = '📝';
        button.title = '备忘录 (点击打开)';

        // 根据是否有紧急备忘录设置按钮颜色
        updateButtonColor(button);

        // 按钮样式
        Object.assign(button.style, {
            position: 'absolute',
            top: '0',
            left: '0',
            width: '100%',
            height: '100%',
            borderRadius: '50%',
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
            fontSize: '28px',
            boxShadow: '0 4px 12px rgba(0, 0, 0, 0.2)',
            transition: 'all 0.3s ease',
            userSelect: 'none',
            fontWeight: 'bold',
            textShadow: '1px 1px 2px rgba(0,0,0,0.3)'
        });

        // 创建数量徽章
        const badge = document.createElement('div');
        badge.id = 'memo-count-badge';
        badge.style.position = 'absolute';
        badge.style.top = '-5px';
        badge.style.right = '-5px';
        badge.style.minWidth = '24px';
        badge.style.height = '24px';
        badge.style.borderRadius = '12px';
        badge.style.backgroundColor = '#ff4757';
        badge.style.color = 'white';
        badge.style.display = 'flex';
        badge.style.justifyContent = 'center';
        badge.style.alignItems = 'center';
        badge.style.fontSize = '12px';
        badge.style.fontWeight = 'bold';
        badge.style.boxShadow = '0 2px 6px rgba(0, 0, 0, 0.3)';
        badge.style.zIndex = '10000';
        badge.style.padding = '0 6px';

        // 更新徽章显示
        updateBadge(badge);

        buttonContainer.appendChild(button);
        buttonContainer.appendChild(badge);

        // 悬停效果
        buttonContainer.addEventListener('mouseenter', () => {
            button.style.transform = 'scale(1.1)';
            button.style.boxShadow = '0 6px 16px rgba(0, 0, 0, 0.3)';
        });

        buttonContainer.addEventListener('mouseleave', () => {
            button.style.transform = 'scale(1)';
            button.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.2)';
        });

        // 点击按钮显示/隐藏备忘录界面
        buttonContainer.addEventListener('click', toggleMemoPanel);

        document.body.appendChild(buttonContainer);
        return {buttonContainer, button, badge};
    };

    // 更新按钮颜色(根据是否有紧急备忘录)
    const updateButtonColor = (button) => {
        const hasUrgentMemo = memos.some(memo => memo.isUrgent === true);

        if (hasUrgentMemo) {
            // 有紧急备忘录时显示渐变红色
            button.style.background = 'linear-gradient(135deg, #ff5e5e, #ff2d2d)';
            button.title = '备忘录 (有紧急备忘)';
        } else {
            // 无紧急备忘录时显示渐变绿色
            button.style.background = 'linear-gradient(135deg, #4CAF50, #2E7D32)';
            button.title = '备忘录 (点击打开)';
        }
    };

    // 更新徽章显示
    const updateBadge = (badge) => {
        const memoCount = memos.length;

        if (memoCount > 0) {
            badge.textContent = memoCount > 99 ? '99+' : memoCount.toString();
            badge.style.display = 'flex';
        } else {
            badge.style.display = 'none';
        }
    };

    // 创建备忘录面板
    const createMemoPanel = () => {
        const panel = document.createElement('div');
        panel.id = 'memo-panel';

        // 面板样式
        Object.assign(panel.style, {
            position: 'fixed',
            top: '110px',
            right: '20px',
            width: '350px',
            maxHeight: '500px',
            backgroundColor: '#fff',
            borderRadius: '12px',
            boxShadow: '0 8px 32px rgba(0, 0, 0, 0.2)',
            zIndex: '9998',
            display: 'none',
            flexDirection: 'column',
            fontFamily: 'Arial, sans-serif',
            overflow: 'hidden',
            border: '1px solid #e0e0e0'
        });

        // 面板头部
        const header = document.createElement('div');
        header.innerHTML = `
            <div style="display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; background: linear-gradient(135deg, #6a11cb, #2575fc); color: white; border-bottom: 1px solid rgba(255,255,255,0.1);">
                <div style="display: flex; align-items: center;">
                    <h3 style="margin: 0; font-size: 18px;">📝 我的备忘录</h3>
                    <span id="urgent-count-badge" style="margin-left: 10px; background-color: #ff4757; color: white; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: bold; display: none;">0</span>
                </div>
                <span id="memo-close-btn" style="cursor: pointer; font-size: 20px; line-height: 1;">&times;</span>
            </div>
        `;
        panel.appendChild(header);

        // 关闭按钮事件
        header.querySelector('#memo-close-btn').addEventListener('click', () => {
            panel.style.display = 'none';
        });

        // 内容区域
        const content = document.createElement('div');
        content.style.padding = '20px';
        content.style.flex = '1';
        content.style.overflowY = 'auto';
        content.style.maxHeight = '400px';

        // 添加新备忘的表单
        const form = document.createElement('div');
        form.innerHTML = `
            <div style="margin-bottom: 20px;">
                <textarea id="memo-input" placeholder="输入新的备忘内容..." style="width: 100%; height: 80px; padding: 12px; border: 1px solid #ddd; border-radius: 8px; resize: none; font-size: 14px; font-family: inherit;"></textarea>

                <div style="display: flex; align-items: center; margin-top: 10px; padding: 10px; background-color: #f8f9fa; border-radius: 8px;">
                    <input type="checkbox" id="urgent-checkbox" style="margin-right: 8px; width: 18px; height: 18px; cursor: pointer;">
                    <label for="urgent-checkbox" style="display: flex; align-items: center; cursor: pointer; font-size: 14px;">
                        <span style="display: inline-block; width: 20px; height: 20px; background-color: #ff4757; color: white; border-radius: 4px; text-align: center; line-height: 20px; margin-right: 6px; font-size: 12px;">!</span>
                        标记为紧急
                    </label>
                    <span style="margin-left: auto; font-size: 12px; color: #666; font-style: italic;">紧急备忘会使图标变红</span>
                </div>

                <button id="add-memo-btn" style="width: 100%; padding: 10px; background: linear-gradient(135deg, #6a11cb, #2575fc); color: white; border: none; border-radius: 8px; font-size: 16px; cursor: pointer; margin-top: 10px; font-weight: bold;">添加备忘</button>
            </div>
        `;
        content.appendChild(form);

        // 备忘录列表容器
        const memoList = document.createElement('div');
        memoList.id = 'memo-list';
        memoList.style.marginTop = '10px';
        content.appendChild(memoList);

        panel.appendChild(content);

        // 底部信息
        const footer = document.createElement('div');
        footer.innerHTML = `
            <div style="padding: 12px 20px; background-color: #f8f9fa; border-top: 1px solid #e9ecef; font-size: 12px; color: #6c757d; display: flex; justify-content: space-between;">
                <div>
                    总计: <span id="memo-count">0</span> 条备忘
                </div>
                <div>
                    紧急: <span id="urgent-count">0</span> 条
                </div>
            </div>
        `;
        panel.appendChild(footer);

        document.body.appendChild(panel);

        // 添加备忘按钮事件
        document.getElementById('add-memo-btn').addEventListener('click', addMemo);

        // 按Enter键添加备忘(Ctrl+Enter换行)
        document.getElementById('memo-input').addEventListener('keydown', (e) => {
            if (e.key === 'Enter' && !e.ctrlKey) {
                e.preventDefault();
                addMemo();
            }
        });

        return panel;
    };

    // 显示/隐藏备忘录面板
    const toggleMemoPanel = () => {
        const panel = document.getElementById('memo-panel');
        if (panel.style.display === 'flex' || panel.style.display === '') {
            panel.style.display = 'none';
        } else {
            panel.style.display = 'flex';
            // 刷新备忘录列表
            renderMemos();
        }
    };

    // 添加新备忘
    const addMemo = () => {
        const input = document.getElementById('memo-input');
        const urgentCheckbox = document.getElementById('urgent-checkbox');
        const content = input.value.trim();

        if (content === '') {
            showNotification('请输入备忘内容', 'warning');
            return;
        }

        // 创建新备忘对象
        const newMemo = {
            id: Date.now(),
            content: content,
            date: new Date().toLocaleString('zh-CN'),
            isUrgent: urgentCheckbox.checked
        };

        // 添加到数组开头
        memos.unshift(newMemo);

        // 保存到存储
        GM_setValue('memos', memos);

        // 清空输入框并重置紧急复选框
        input.value = '';
        urgentCheckbox.checked = false;

        // 刷新列表
        renderMemos();

        // 更新按钮颜色和徽章
        updateAllUI();

        showNotification('备忘已添加' + (newMemo.isUrgent ? ' (紧急)' : ''), 'success');
    };

    // 删除备忘
    const deleteMemo = (id) => {
        if (confirm('确定要删除这条备忘吗?')) {
            memos = memos.filter(memo => memo.id !== id);

            // 保存到存储
            GM_setValue('memos', memos);

            // 刷新列表
            renderMemos();

            // 更新按钮颜色和徽章
            updateAllUI();

            showNotification('备忘已删除', 'info');
        }
    };

    // 切换备忘录紧急状态
    const toggleUrgentStatus = (id) => {
        memos = memos.map(memo => {
            if (memo.id === id) {
                return { ...memo, isUrgent: !memo.isUrgent };
            }
            return memo;
        });

        // 保存到存储
        GM_setValue('memos', memos);

        // 刷新列表
        renderMemos();

        // 更新按钮颜色和徽章
        updateAllUI();

        // 显示通知
        const updatedMemo = memos.find(memo => memo.id === id);
        showNotification(`已${updatedMemo.isUrgent ? '标记为紧急' : '取消紧急标记'}`, 'info');
    };

    // 更新所有UI元素
    const updateAllUI = () => {
        const button = document.getElementById('memo-floating-btn');
        const badge = document.getElementById('memo-count-badge');

        if (button) updateButtonColor(button);
        if (badge) updateBadge(badge);
    };

    // 渲染备忘录列表
    const renderMemos = () => {
        const memoList = document.getElementById('memo-list');
        const memoCount = document.getElementById('memo-count');
        const urgentCount = document.getElementById('urgent-count');
        const urgentCountBadge = document.getElementById('urgent-count-badge');

        // 计算紧急备忘录数量
        const urgentMemosCount = memos.filter(memo => memo.isUrgent).length;

        // 更新计数
        memoCount.textContent = memos.length;
        urgentCount.textContent = urgentMemosCount;

        // 更新紧急备忘录徽章
        if (urgentMemosCount > 0) {
            urgentCountBadge.textContent = urgentMemosCount;
            urgentCountBadge.style.display = 'inline-block';
        } else {
            urgentCountBadge.style.display = 'none';
        }

        if (memos.length === 0) {
            memoList.innerHTML = `
                <div style="text-align: center; padding: 30px 0; color: #6c757d;">
                    <div style="font-size: 48px; margin-bottom: 10px;">📋</div>
                    <p>暂无备忘,添加第一条吧!</p>
                </div>
            `;
            return;
        }

        memoList.innerHTML = '';

        memos.forEach(memo => {
            const memoItem = document.createElement('div');
            memoItem.className = 'memo-item';
            memoItem.style.marginBottom = '12px';
            memoItem.style.padding = '12px';
            memoItem.style.borderRadius = '8px';
            memoItem.style.backgroundColor = memo.isUrgent ? '#fff5f5' : '#f8f9fa';
            memoItem.style.borderLeft = `4px solid ${memo.isUrgent ? '#ff4757' : '#6a11cb'}`;
            memoItem.style.boxShadow = memo.isUrgent ? '0 2px 6px rgba(255, 71, 87, 0.2)' : 'none';

            // 紧急标记样式
            const urgentBadge = memo.isUrgent ? `
                <span style="background-color: #ff4757; color: white; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: bold; margin-right: 6px;">紧急</span>
            ` : '';

            memoItem.innerHTML = `
                <div style="display: flex; justify-content: space-between; align-items: flex-start;">
                    <div style="flex: 1; display: flex; align-items: flex-start;">
                        ${urgentBadge}
                        <div style="flex: 1; word-break: break-word; font-size: 14px; line-height: 1.4;">${escapeHtml(memo.content)}</div>
                    </div>
                    <div style="display: flex; align-items: center; margin-left: 8px;">
                        <button class="urgent-toggle-btn" data-id="${memo.id}" title="${memo.isUrgent ? '取消紧急标记' : '标记为紧急'}" style="background: none; border: none; cursor: pointer; font-size: 16px; padding: 0 4px; color: ${memo.isUrgent ? '#ff4757' : '#aaa'};">
                            ${memo.isUrgent ? '⚠️' : '⚪'}
                        </button>
                        <button class="delete-btn" data-id="${memo.id}" style="background: none; border: none; color: #ff5e5e; cursor: pointer; font-size: 18px; padding: 0 4px;">&times;</button>
                    </div>
                </div>
                <div style="font-size: 12px; color: #6c757d; margin-top: 8px; display: flex; justify-content: space-between;">
                    <span>${memo.date}</span>
                    <span>ID: ${memo.id.toString().slice(-4)}</span>
                </div>
            `;

            memoList.appendChild(memoItem);
        });

        // 为所有删除按钮添加事件
        document.querySelectorAll('.delete-btn').forEach(btn => {
            btn.addEventListener('click', (e) => {
                const id = parseInt(e.target.getAttribute('data-id'));
                deleteMemo(id);
            });
        });

        // 为所有紧急标记切换按钮添加事件
        document.querySelectorAll('.urgent-toggle-btn').forEach(btn => {
            btn.addEventListener('click', (e) => {
                const id = parseInt(e.target.getAttribute('data-id'));
                toggleUrgentStatus(id);
            });
        });
    };

    // 显示通知
    const showNotification = (message, type) => {
        // 移除可能存在的旧通知
        const oldNotification = document.getElementById('memo-notification');
        if (oldNotification) oldNotification.remove();

        const notification = document.createElement('div');
        notification.id = 'memo-notification';

        // 根据类型设置颜色
        let bgColor, borderColor;
        switch(type) {
            case 'success':
                bgColor = '#d4edda';
                borderColor = '#c3e6cb';
                break;
            case 'warning':
                bgColor = '#fff3cd';
                borderColor = '#ffeaa7';
                break;
            case 'info':
                bgColor = '#d1ecf1';
                borderColor = '#bee5eb';
                break;
            default:
                bgColor = '#f8f9fa';
                borderColor = '#e9ecef';
        }

        Object.assign(notification.style, {
            position: 'fixed',
            top: '120px',
            right: '20px',
            padding: '12px 20px',
            backgroundColor: bgColor,
            border: `1px solid ${borderColor}`,
            borderRadius: '8px',
            zIndex: '10000',
            boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
            fontSize: '14px',
            fontWeight: 'bold',
            transition: 'all 0.3s ease',
            transform: 'translateX(120%)',
            maxWidth: '300px'
        });

        notification.textContent = message;
        document.body.appendChild(notification);

        // 动画显示
        setTimeout(() => {
            notification.style.transform = 'translateX(0)';
        }, 10);

        // 3秒后自动消失
        setTimeout(() => {
            notification.style.transform = 'translateX(120%)';
            setTimeout(() => {
                if (notification.parentNode) {
                    notification.parentNode.removeChild(notification);
                }
            }, 300);
        }, 3000);
    };

    // HTML转义函数,防止XSS攻击
    const escapeHtml = (text) => {
        const div = document.createElement('div');
        div.textContent = text;
        return div.innerHTML;
    };

    // 初始化
    const init = () => {
        // 检查是否已经存在按钮,避免重复创建
        if (document.getElementById('memo-button-container')) {
            return;
        }

        // 创建悬浮按钮
        createMemoButton();

        // 创建备忘录面板
        createMemoPanel();

        // 初始渲染备忘录列表
        renderMemos();

        // 点击面板外部关闭面板
        document.addEventListener('click', (e) => {
            const panel = document.getElementById('memo-panel');
            const buttonContainer = document.getElementById('memo-button-container');

            if (panel && panel.style.display === 'flex' &&
                !panel.contains(e.target) &&
                !buttonContainer.contains(e.target)) {
                panel.style.display = 'none';
            }
        });
    };

    // 等待DOM加载完成后初始化
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();