Greasy Fork is available in English.
在所有网页右上角添加一个可添加/删除备忘的备忘录工具,支持紧急标记和数量徽章
// ==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;">×</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;">×</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();
}
})();