在任意洛谷页面快速发送犇犇
// ==UserScript==
// @name BenBen Better
// @namespace http://tampermonkey.net/
// @version 1.1.0
// @description 在任意洛谷页面快速发送犇犇
// @author Haokee & Claude Sonnet 4.5
// @match https://www.luogu.com.cn/*
// @match https://www.luogu.com/*
// @grant GM_addStyle
// @run-at document-end
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// 添加样式
GM_addStyle(`
/* 浮动按钮 */
#benben-float-btn {
position: fixed;
width: 60px;
height: 60px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
cursor: pointer;
z-index: 9998;
display: flex;
align-items: center;
justify-content: center;
transition: box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
color: white;
font-size: 24px;
font-weight: bold;
font-family: -apple-system, BlinkMacSystemFont, "San Francisco", "Helvetica Neue", "Noto Sans CJK SC", "Noto Sans CJK", "Source Han Sans", "PingFang SC", "Microsoft YaHei", sans-serif;
user-select: none;
touch-action: none;
}
#benben-float-btn::before {
content: '犇';
}
#benben-float-btn:not(.dragging):hover {
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
}
#benben-float-btn:not(.dragging):active {
transform: scale(0.95);
}
#benben-float-btn.dragging {
cursor: grabbing;
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.7);
}
/* 浮动窗口 */
#benben-float-window {
position: fixed;
width: 380px;
background: white;
border-radius: 16px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
z-index: 9999;
opacity: 0;
transform: scale(0.95);
pointer-events: none;
transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1), transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
}
#benben-float-window.show {
opacity: 1;
transform: scale(1);
pointer-events: auto;
}
/* 窗口头部 */
.benben-window-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 16px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.benben-window-title {
font-size: 16px;
font-weight: bold;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "San Francisco", "Helvetica Neue", "Noto Sans CJK SC", "Noto Sans CJK", "Source Han Sans", "PingFang SC", "Microsoft YaHei", sans-serif;
}
.benben-close-btn {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
width: 28px;
height: 28px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
font-size: 18px;
line-height: 1;
font-family: -apple-system, BlinkMacSystemFont, "San Francisco", "Helvetica Neue", "Noto Sans CJK SC", "Noto Sans CJK", "Source Han Sans", "PingFang SC", "Microsoft YaHei", sans-serif;
}
.benben-close-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: rotate(90deg);
}
/* 窗口内容 */
.benben-window-content {
padding: 20px;
}
.benben-textarea {
width: 100%;
min-height: 120px;
padding: 12px;
border: 2px solid #e8e8e8;
border-radius: 8px;
font-size: 14px;
font-family: -apple-system, BlinkMacSystemFont, "San Francisco", "Helvetica Neue", "Noto Sans CJK SC", "Noto Sans CJK", "Source Han Sans", "PingFang SC", "Microsoft YaHei", sans-serif;
resize: vertical;
transition: border-color 0.3s;
box-sizing: border-box;
}
.benben-textarea:focus {
outline: none;
border-color: #667eea;
}
.benben-textarea::placeholder {
color: #999;
}
.benben-submit-btn {
width: 100%;
padding: 12px;
margin-top: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
font-family: -apple-system, BlinkMacSystemFont, "San Francisco", "Helvetica Neue", "Noto Sans CJK SC", "Noto Sans CJK", "Source Han Sans", "PingFang SC", "Microsoft YaHei", sans-serif;
}
.benben-submit-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.benben-submit-btn:active {
transform: translateY(0);
}
.benben-submit-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
/* 字数统计 */
.benben-char-count {
text-align: right;
font-size: 12px;
color: #999;
margin-top: 8px;
font-family: -apple-system, BlinkMacSystemFont, "San Francisco", "Helvetica Neue", "Noto Sans CJK SC", "Noto Sans CJK", "Source Han Sans", "PingFang SC", "Microsoft YaHei", sans-serif;
}
/* 成功提示 */
.benben-toast {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%) translateY(-100px);
background: #52c41a;
color: white;
padding: 12px 24px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 10000;
opacity: 0;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
font-family: -apple-system, BlinkMacSystemFont, "San Francisco", "Helvetica Neue", "Noto Sans CJK SC", "Noto Sans CJK", "Source Han Sans", "PingFang SC", "Microsoft YaHei", sans-serif;
}
.benben-toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.benben-toast.error {
background: #ff4d4f;
}
/* 响应式适配 */
@media (max-width: 768px) {
#benben-float-window {
width: calc(100vw - 20px);
max-width: 380px;
}
}
/* 个人中心动态页面的回复按钮样式 */
.feed-action-buttons {
float: right;
margin-left: 12px;
}
.feed-action-buttons a {
color: #888;
font-size: 14px;
cursor: pointer;
text-decoration: none;
transition: color 0.2s;
}
.feed-action-buttons a:hover {
color: #667eea;
}
`);
// 创建 HTML 元素
function createFloatingUI() {
// 浮动按钮
const floatBtn = document.createElement('div');
floatBtn.id = 'benben-float-btn';
floatBtn.title = '快速发送犇犇';
// 浮动窗口
const floatWindow = document.createElement('div');
floatWindow.id = 'benben-float-window';
floatWindow.innerHTML = `
<div class="benben-window-header">
<h3 class="benben-window-title">发送犇犇</h3>
<button class="benben-close-btn" id="benben-close">×</button>
</div>
<div class="benben-window-content">
<textarea
class="benben-textarea"
id="benben-textarea"
placeholder="有什么新鲜事告诉大家..."
maxlength="1000"
></textarea>
<div class="benben-char-count">
<span id="benben-char-count">0</span> / 1000
</div>
<button class="benben-submit-btn" id="benben-submit">发送</button>
</div>
`;
// 添加到页面
document.body.appendChild(floatBtn);
document.body.appendChild(floatWindow);
return { floatBtn, floatWindow };
}
// 显示提示消息
function showToast(message, isError = false) {
let toast = document.getElementById('benben-toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'benben-toast';
toast.className = 'benben-toast';
document.body.appendChild(toast);
}
toast.textContent = message;
toast.className = 'benben-toast' + (isError ? ' error' : '');
// 显示
setTimeout(() => toast.classList.add('show'), 10);
// 3秒后隐藏
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
// 获取 CSRF Token
function getCSRFToken() {
const metaTag = document.querySelector('meta[name="csrf-token"]');
if (metaTag) {
return metaTag.getAttribute('content');
}
return null;
}
// 发送犇犇
async function sendBenben(content) {
try {
// 优先使用 jQuery(洛谷页面自带)
if (typeof $ !== 'undefined' && $.post) {
return new Promise((resolve) => {
$.post("/api/feed/postBenben", {content: content}, function (resp) {
if (resp.status === 200) {
resolve({ success: true });
} else {
resolve({ success: false, message: resp.data || '发送失败' });
}
}).fail(function() {
resolve({ success: false, message: '网络错误,请稍后重试' });
});
});
}
// 备用方案:使用 fetch + CSRF Token
const csrfToken = getCSRFToken();
const headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest',
};
if (csrfToken) {
headers['X-CSRF-Token'] = csrfToken;
}
const response = await fetch('/api/feed/postBenben', {
method: 'POST',
headers: headers,
credentials: 'include',
body: `content=${encodeURIComponent(content)}`
});
const data = await response.json();
if (data.status === 200) {
return { success: true };
} else {
return { success: false, message: data.data || '发送失败' };
}
} catch (error) {
return { success: false, message: '网络错误:' + error.message };
}
}
// 提取用户名
function extractUsername(feedElement) {
// 方法1: 尝试.luogu-username a
let usernameLink = feedElement.querySelector('.luogu-username a[href^="/user/"]');
if (usernameLink) {
return usernameLink.textContent.trim();
}
// 方法2: 尝试直接查找所有a标签
const allLinks = feedElement.querySelectorAll('a[href^="/user/"]');
console.log('[BenBen Better] 找到的用户链接数量:', allLinks.length);
if (allLinks.length > 0) {
const username = allLinks[0].textContent.trim();
console.log('[BenBen Better] 提取到用户名:', username);
return username;
}
console.log('[BenBen Better] 所有方法都未找到用户名');
return null;
}
// 提取动态内容
function extractFeedContent(feedElement) {
const contentDiv = feedElement.querySelector('.content');
if (contentDiv) {
// 获取纯文本内容,去除多余空白
return contentDiv.textContent.trim();
}
return '';
}
// 为动态元素添加回复按钮
async function addActionButtons(feedElement) {
// 检查是否已经添加过按钮
if (feedElement.querySelector('.feed-action-buttons')) {
return;
}
// 等待Vue渲染完成(最多等待3秒)
for (let i = 0; i < 30; i++) {
const metaDiv = feedElement.querySelector('.meta');
const allLinks = feedElement.querySelectorAll('a[href^="/user/"]');
// 如果找到了meta和用户链接,说明渲染完成
if (metaDiv && allLinks.length > 0) {
console.log('[BenBen Better] Vue渲染完成,用时:', i * 100, 'ms');
break;
}
// 等待100ms后重试
await new Promise(resolve => setTimeout(resolve, 100));
}
const metaDiv = feedElement.querySelector('.meta');
if (!metaDiv) {
console.log('[BenBen Better] 未找到 .meta 元素');
return;
}
const username = extractUsername(feedElement);
const feedContent = extractFeedContent(feedElement);
if (!username) {
return; // 静默失败,不再输出日志
}
console.log('[BenBen Better] 为用户添加按钮:', username);
// 创建按钮容器
const actionButtons = document.createElement('span');
actionButtons.className = 'feed-action-buttons';
// 创建回复按钮
const replyBtn = document.createElement('a');
replyBtn.textContent = '回复';
replyBtn.href = 'javascript:void(0)';
replyBtn.addEventListener('click', (e) => {
e.preventDefault();
handleReply(username, feedContent);
});
actionButtons.appendChild(replyBtn);
// 直接添加到 meta 末尾
metaDiv.appendChild(actionButtons);
console.log('[BenBen Better] 按钮添加成功');
}
// 处理回复
function handleReply(username, feedContent = '') {
const textarea = document.getElementById('benben-textarea');
const floatWindow = document.getElementById('benben-float-window');
if (textarea && floatWindow) {
// 设置回复格式
let replyText = `|| @${username} : `;
// 如果有原内容,直接添加原文
if (feedContent) {
replyText += feedContent + '\n\n';
}
textarea.value = replyText;
// 打开浮动窗口
floatWindow.classList.add('show');
// 聚焦到文本框末尾
setTimeout(() => {
textarea.focus();
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
}, 400);
}
}
// 检查是否在个人中心动态页面
function isUserActivityPage() {
return window.location.pathname.includes('/user/') &&
window.location.pathname.includes('/activity');
}
// 为现有的动态添加按钮
function processExistingFeeds() {
if (!isUserActivityPage()) {
return;
}
const feeds = document.querySelectorAll('.feed:not(.feed-processed)');
if (feeds.length > 0) {
console.log('[BenBen Better] 找到动态数量:', feeds.length);
}
feeds.forEach(feed => {
addActionButtons(feed);
feed.classList.add('feed-processed');
});
}
// 初始化个人中心动态页面的功能
function initUserActivityPage() {
console.log('[BenBen Better] 初始化个人中心功能,当前路径:', window.location.pathname);
// 使用MutationObserver持续监听DOM变化
const observer = new MutationObserver(() => {
processExistingFeeds();
});
// 观察整个文档的变化
observer.observe(document.body, {
childList: true,
subtree: true
});
// 定期检查(适用于Vue渲染的单页应用)
setInterval(() => {
processExistingFeeds();
}, 2000);
console.log('[BenBen Better] 个人中心动态页面监听已启用');
}
// 初始化
function init() {
const { floatBtn, floatWindow } = createFloatingUI();
const closeBtn = document.getElementById('benben-close');
const submitBtn = document.getElementById('benben-submit');
const textarea = document.getElementById('benben-textarea');
const charCount = document.getElementById('benben-char-count');
let isWindowOpen = false;
// 拖动相关变量
const BTN_SIZE = 60;
const WINDOW_WIDTH = 380;
const WINDOW_HEIGHT = 280; // 估计窗口高度
const LONG_PRESS_DURATION = 300; // 长按触发时间(毫秒)
let btnX = window.innerWidth - BTN_SIZE - 30;
let btnY = window.innerHeight - BTN_SIZE - 30;
let isDragging = false;
let longPressTimer = null;
let startX = 0;
let startY = 0;
let hasMoved = false;
// 初始化按钮位置
function updateBtnPosition() {
floatBtn.style.left = btnX + 'px';
floatBtn.style.top = btnY + 'px';
updateWindowPosition();
}
// 更新窗口位置(相对于按钮)
function updateWindowPosition() {
// 计算窗口位置:优先在按钮上方,否则在下方
let windowX = btnX + BTN_SIZE / 2 - WINDOW_WIDTH / 2;
let windowY = btnY - WINDOW_HEIGHT - 10;
// 如果窗口超出顶部,则放到按钮下方
if (windowY < 10) {
windowY = btnY + BTN_SIZE + 10;
}
// 确保窗口不超出左右边界
if (windowX < 10) {
windowX = 10;
} else if (windowX + WINDOW_WIDTH > window.innerWidth - 10) {
windowX = window.innerWidth - WINDOW_WIDTH - 10;
}
// 确保窗口不超出底部
if (windowY + WINDOW_HEIGHT > window.innerHeight - 10) {
windowY = window.innerHeight - WINDOW_HEIGHT - 10;
}
floatWindow.style.left = windowX + 'px';
floatWindow.style.top = windowY + 'px';
}
// 边界约束
function constrainPosition(x, y) {
x = Math.max(0, Math.min(x, window.innerWidth - BTN_SIZE));
y = Math.max(0, Math.min(y, window.innerHeight - BTN_SIZE));
return { x, y };
}
// 开始拖动
function startDrag(clientX, clientY) {
isDragging = true;
hasMoved = false;
floatBtn.classList.add('dragging');
startX = clientX - btnX;
startY = clientY - btnY;
}
// 拖动中
function onDrag(clientX, clientY) {
if (!isDragging) return;
hasMoved = true;
const pos = constrainPosition(clientX - startX, clientY - startY);
btnX = pos.x;
btnY = pos.y;
updateBtnPosition();
}
// 结束拖动
function endDrag() {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
const wasDragging = isDragging && hasMoved;
isDragging = false;
floatBtn.classList.remove('dragging');
return wasDragging;
}
// 鼠标事件
floatBtn.addEventListener('mousedown', (e) => {
e.preventDefault();
hasMoved = false;
longPressTimer = setTimeout(() => {
startDrag(e.clientX, e.clientY);
}, LONG_PRESS_DURATION);
});
document.addEventListener('mousemove', (e) => {
if (isDragging) {
onDrag(e.clientX, e.clientY);
}
});
document.addEventListener('mouseup', (e) => {
const wasDragging = endDrag();
// 如果不是拖动,则处理点击
if (!wasDragging && e.target === floatBtn) {
isWindowOpen = !isWindowOpen;
if (isWindowOpen) {
floatWindow.classList.add('show');
setTimeout(() => textarea.focus(), 400);
} else {
floatWindow.classList.remove('show');
}
}
});
// 触摸事件
floatBtn.addEventListener('touchstart', (e) => {
e.preventDefault();
hasMoved = false;
const touch = e.touches[0];
longPressTimer = setTimeout(() => {
startDrag(touch.clientX, touch.clientY);
}, LONG_PRESS_DURATION);
}, { passive: false });
document.addEventListener('touchmove', (e) => {
if (isDragging) {
const touch = e.touches[0];
onDrag(touch.clientX, touch.clientY);
}
}, { passive: false });
document.addEventListener('touchend', (e) => {
const wasDragging = endDrag();
// 如果不是拖动,则处理点击
if (!wasDragging) {
isWindowOpen = !isWindowOpen;
if (isWindowOpen) {
floatWindow.classList.add('show');
setTimeout(() => textarea.focus(), 400);
} else {
floatWindow.classList.remove('show');
}
}
});
// 窗口大小改变时重新约束位置
window.addEventListener('resize', () => {
const pos = constrainPosition(btnX, btnY);
btnX = pos.x;
btnY = pos.y;
updateBtnPosition();
});
// 初始化位置
updateBtnPosition();
// 点击关闭按钮
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
isWindowOpen = false;
floatWindow.classList.remove('show');
});
// 点击窗口外部关闭
document.addEventListener('click', (e) => {
if (isWindowOpen &&
!floatWindow.contains(e.target) &&
!floatBtn.contains(e.target)) {
isWindowOpen = false;
floatWindow.classList.remove('show');
}
});
// 字数统计
textarea.addEventListener('input', () => {
const length = textarea.value.length;
charCount.textContent = length;
if (length > 900) {
charCount.style.color = '#ff4d4f';
} else if (length > 800) {
charCount.style.color = '#faad14';
} else {
charCount.style.color = '#999';
}
});
// 发送犇犇
submitBtn.addEventListener('click', async () => {
const content = textarea.value.trim();
if (!content) {
showToast('请输入内容', true);
return;
}
// 禁用按钮
submitBtn.disabled = true;
submitBtn.textContent = '发送中...';
// 发送
const result = await sendBenben(content);
if (result.success) {
showToast('发送成功');
textarea.value = '';
charCount.textContent = '0';
// 延迟关闭窗口
setTimeout(() => {
isWindowOpen = false;
floatWindow.classList.remove('show');
}, 1000);
} else {
showToast(result.message || '发送失败,请重试', true);
}
// 恢复按钮
submitBtn.disabled = false;
submitBtn.textContent = '发送';
});
// 支持 Ctrl+Enter 快捷键发送
textarea.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
submitBtn.click();
}
});
console.log('[BenBen Better] 脚本已加载');
// 初始化个人中心动态页面功能
initUserActivityPage();
}
// 等待页面加载完成
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();