洛谷提交优化
由 AI 辅助完成。
与[工程] 洛谷修改提交记录方框背景一起使用效果更佳。
V1.0
AC 题目时会有横幅。
:::info[源代码]
// ==UserScript==
// @name 洛谷成就系统
// @namespace http://tampermonkey.net/
// @version V1.0
// @description 由 Gemini 辅助完成。
// @author Otachi
// @match *://*.luogu.com.cn/*
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/confetti.browser.min.js
// @grant none
// ==/UserScript==
(function() {
'use strict';
const AIRSPACES = {
1: { name: "入门", color: "#FE4C61", rarity: "Common" },
2: { name: "普及-", color: "#F39C11", rarity: "Unusual" },
3: { name: "普及/提高−", color: "#FFC116", rarity: "Rare" },
4: { name: "普及+/提高", color: "#52C41A", rarity: "Epic" },
5: { name: "提高+/省选−", color: "#3498DB", rarity: "Legendary" },
6: { name: "省选/NOI−", color: "#9D3DCF", rarity: "Mythic" },
7: { name: "NOI/NOI+/CTSC", color: "#0E1D69", rarity: "Ultra" }
};
let lastRecordId = "";
function launchFireworks(color) {
if (typeof confetti !== 'function') return;
const duration = 3000;
const end = Date.now() + duration;
(function frame() {
confetti({ particleCount: 3, angle: 60, spread: 55, origin: { x: 0, y: 0.8 }, colors: [color, '#ffffff'] });
confetti({ particleCount: 3, angle: 120, spread: 55, origin: { x: 1, y: 0.8 }, colors: [color, '#ffffff'] });
if (Date.now() < end) requestAnimationFrame(frame);
}());
}
// --- 核心修复:平滑离场逻辑 ---
function closeBanner(banner) {
if (!banner || banner.classList.contains('leaving')) return;
// 关键动作:移除所有可能干扰离场的动画类(特别是针对高难度的呼吸灯)
banner.classList.remove('banner-enter', 'glow-pulse');
// 强制触发重绘,确保浏览器立即停止之前的动画
void banner.offsetWidth;
// 标记正在离场
banner.classList.add('leaving');
// 400ms 后从 DOM 彻底移除
setTimeout(() => {
if (banner.parentNode) banner.remove();
}, 400);
}
function showAchievementBanner(target, pid) {
const oldBanner = document.getElementById('florr-achievement-banner');
if (oldBanner) oldBanner.remove();
const banner = document.createElement('div');
banner.id = 'florr-achievement-banner';
banner.style = `
position: fixed; top: 0; left: 0; width: 100%; height: 140px;
z-index: 1000000; display: flex; align-items: center; justify-content: center;
background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.98) 20%, rgba(255,255,255,0.98) 80%, transparent 100%);
border-bottom: 8px solid ${target.color};
box-shadow: 0 15px 50px rgba(0,0,0,0.15);
cursor: pointer;
user-select: none;
`;
const isHighRank = ["Legendary", "Mythic", "Ultra"].includes(target.rarity);
banner.className = `banner-enter ${isHighRank ? 'glow-pulse' : ''}`;
banner.innerHTML = `
<div style="display: flex; align-items: center; gap: 50px; width: 95%; max-width: 1300px; justify-content: center;">
<div style="
flex-shrink: 0; width: 240px; height: 100px; border-radius: 20px; border: 6px solid ${target.color};
background: #fff; display: flex; align-items: center; justify-content: center;
font-size: 26px; font-weight: 900; color: ${target.color}; text-align: center;
line-height: 1.1; padding: 0 15px; box-sizing: border-box;
box-shadow: inset 0 0 20px ${target.color}22;
letter-spacing: 0.5px;
">
${target.name}
</div>
<div style="text-align: left; font-family: 'Arial Black', sans-serif; min-width: 450px;">
<div style="font-size: 16px; font-weight: 900; color: #777; letter-spacing: 4px; margin-bottom: 4px;">MISSION COMPLETE</div>
<div style="font-size: 45px; font-weight: 900; color: ${target.color}; line-height: 1.1;">
Destroy Target <span style="font-family: 'Courier New', Courier, monospace; letter-spacing: -1px;">${pid}</span>
</div>
<div style="font-size: 18px; font-weight: 400; color: #aaa; margin-top: 4px; font-family: sans-serif; letter-spacing: 2px;">
RANK: ${target.rarity.toUpperCase()}
</div>
</div>
</div>`;
document.body.appendChild(banner);
banner.onclick = () => closeBanner(banner);
setTimeout(() => closeBanner(banner), 7000);
}
function lockAndFire() {
try {
const feData = window._feInjection || window._feConfig;
if (!feData || !feData.currentData) return;
const record = feData.currentData.record;
if (!record) return;
if ((record.status === 12 || record.status === "Accepted") && record.id !== lastRecordId) {
const currentUid = feData.currentUser?.uid || feData.currentUser?.id;
const targetUid = record.user.uid || record.user.id;
if (currentUid && targetUid && currentUid == targetUid) {
lastRecordId = record.id;
const target = AIRSPACES[record.problem.difficulty];
if (target) {
showAchievementBanner(target, record.problem.pid);
if (record.problem.difficulty >= 5) launchFireworks(target.color);
}
}
}
} catch (e) { }
}
const style = document.createElement('style');
style.innerHTML = `
/* 进场动画 */
.banner-enter { animation: slideIn 0.6s cubic-bezier(0.18, 0.89, 0.32, 1.28) forwards; }
/* 离场动画:增加优先级,确保覆盖无限循环的呼吸效果 */
.leaving {
animation: slideOut 0.4s cubic-bezier(0.6, -0.28, 0.735, 0.045) forwards !important;
pointer-events: none;
}
@keyframes slideIn { from { transform: translateY(-100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
@keyframes slideOut {
0% { transform: translateY(0); opacity: 1; }
30% { transform: translateY(10px); opacity: 1; }
100% { transform: translateY(-120%); opacity: 0; }
}
/* 呼吸灯动画 */
.glow-pulse { animation: slideIn 0.6s cubic-bezier(0.18, 0.89, 0.32, 1.28) forwards, pulse 2s infinite ease-in-out; }
@keyframes pulse {
0% { box-shadow: 0 15px 50px rgba(0,0,0,0.15); }
50% { box-shadow: 0 15px 60px rgba(52, 152, 219, 0.3); border-bottom-color: #fff; }
100% { box-shadow: 0 15px 50px rgba(0,0,0,0.15); }
}
`;
document.head.appendChild(style);
setInterval(lockAndFire, 1000);
})();
:::
效果:
通过蓝/紫/黑题时会有烟花:
V2.0
在 V1.0 的基础上,增加了 WA、TLE 等评测状态的横幅。
感谢 @ZAR_Ferarri_F1_Team 提供的灵感。
:::info[源代码]
// ==UserScript==
// @name 洛谷提交状态优化
// @namespace http://tampermonkey.net/
// @version V2.0
// @description 感谢 ZAR_Ferarri_F1_Team & Gemini 的贡献。
// @author Otachi
// @match *://*.luogu.com.cn/*
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/confetti.browser.min.js
// @grant none
// ==/UserScript==
(function() {
'use strict';
// 难度定义 (仅用于 AC 时的装饰)
const AIRSPACES = {
1: { name: "入门", color: "#FE4C61", rarity: "COMMON" },
2: { name: "普及-", color: "#F39C11", rarity: "UNUSUAL" },
3: { name: "普及/提高−", color: "#FFC116", rarity: "RARE" },
4: { name: "普及+/提高", color: "#52C41A", rarity: "EPIC" },
5: { name: "提高+/省选−", color: "#3498DB", rarity: "LEGENDARY" },
6: { name: "省选/NOI−", color: "#9D3DCF", rarity: "MYTHIC" },
7: { name: "NOI/NOI+/CTSC", color: "#0E1D69", rarity: "ULTRA" }
};
// 核心:洛谷原色定义
const JUDGE_STATUS = {
"AC": { text: "ACCEPTED", color: "#52C41A", bgColor: "#E8F5E9" },
"WA": { text: "WRONG ANSWER", color: "#E74C3C", bgColor: "#FDEDEC" },
"TLE": { text: "TIME LIMIT EXCEEDED", color: "#052242", bgColor: "#EBEDEF" },
"MLE": { text: "MEMORY LIMIT EXCEEDED", color: "#052242", bgColor: "#EBEDEF" },
"RE": { text: "RUNTIME ERROR", color: "#9D3DCF", bgColor: "#F5EEF8" },
"CE": { text: "COMPILE ERROR", color: "#F39C11", bgColor: "#FEF5E7" },
"OLE": { text: "OUTPUT LIMIT EXCEEDED", color: "#052242", bgColor: "#EBEDEF" },
"UKE": { text: "UNKNOWN ERROR", color: "#0E1D69", bgColor: "#EAECEE" }
};
const TESTCASE_STATUS_MAP = { 3: "OLE", 4: "MLE", 5: "TLE", 6: "WA", 7: "RE", 12: "AC" };
let lastRecordId = "";
function launchFireworks(color) {
if (typeof confetti !== 'function') return;
const duration = 2000, end = Date.now() + duration;
(function frame() {
confetti({ particleCount: 3, angle: 60, spread: 55, origin: { x: 0, y: 0.8 }, colors: [color, '#ffffff'] });
confetti({ particleCount: 3, angle: 120, spread: 55, origin: { x: 1, y: 0.8 }, colors: [color, '#ffffff'] });
if (Date.now() < end) requestAnimationFrame(frame);
}());
}
function closeBanner(banner) {
if (!banner || banner.classList.contains('leaving')) return;
banner.classList.remove('glow-pulse');
banner.classList.add('leaving');
banner.style.transform = 'translateY(-140px)';
banner.style.opacity = '0';
setTimeout(() => { if (banner.parentNode) banner.remove(); }, 600);
}
function getJudgeStatus(record) {
const sc = Number(record.status);
if (sc === 0) return "WAIT";
if (sc === 1) return "JUDGE";
if (sc === 2) return "CE";
const detail = record.detail;
if (!detail || !detail.judgeResult) return sc === 12 ? "AC" : "UKE";
const subtasks = detail.judgeResult.subtasks || [];
let firstErr = null;
Object.values(subtasks).forEach(sub => {
Object.values(sub.testCases || {}).forEach(tc => {
if (tc.status !== 12 && !firstErr) firstErr = tc.status;
});
});
return firstErr ? (TESTCASE_STATUS_MAP[firstErr] || "WA") : "AC";
}
function showAchievementBanner(target, pid, statusKey) {
const old = document.getElementById('florr-achievement-banner');
if (old) old.remove();
const banner = document.createElement('div');
banner.id = 'florr-achievement-banner';
const info = JUDGE_STATUS[statusKey] || JUDGE_STATUS["UKE"];
const isAC = statusKey === "AC";
const color = isAC ? target.color : info.color; // AC用难度色,其余用原色
banner.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; height: 140px;
z-index: 1000000; display: flex; align-items: center; justify-content: center;
background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.98) 20%, rgba(255,255,255,0.98) 80%, transparent 100%);
border-bottom: 8px solid ${color};
box-shadow: 0 15px 50px rgba(0,0,0,0.15);
cursor: pointer; user-select: none;
transform: translateY(-100%); opacity: 0;
transition: transform 0.6s cubic-bezier(0.18, 0.89, 0.32, 1.28), opacity 0.4s ease;
`;
const isHigh = ["LEGENDARY", "MYTHIC", "ULTRA"].includes(target.rarity);
banner.className = (isAC && isHigh) ? 'glow-pulse' : '';
const actionText = isAC ? 'Destroyed Target' : 'Destroyed by Target';
const missionSub = isAC ? 'MISSION COMPLETE' : 'MISSION FAILED';
banner.innerHTML = `
<div style="display: flex; align-items: center; gap: 50px; width: 95%; max-width: 1300px; justify-content: center;">
<div style="flex-shrink: 0; width: 240px; height: 100px; border-radius: 20px; border: 6px solid ${color};
background: ${info.bgColor}; display: flex; align-items: center; justify-content: center;
font-size: 26px; font-weight: 900; color: ${color}; text-align: center;
box-shadow: inset 0 0 20px ${color}22;">
${isAC ? target.name : statusKey}
</div>
<div style="text-align: left; font-family: 'Arial Black', sans-serif; min-width: 450px;">
<div style="font-size: 16px; font-weight: 900; color: #777; letter-spacing: 4px; margin-bottom: 4px;">
${missionSub}
</div>
<div style="font-size: 45px; font-weight: 900; color: ${color}; line-height: 1.1;">
${actionText} <span style="font-family: 'Courier New', Courier, monospace; letter-spacing: -1px;">${pid}</span>
</div>
<div style="font-size: 18px; color: #aaa; margin-top: 4px; letter-spacing: 2px;">
RANK: ${target.rarity}
</div>
</div>
</div>`;
document.body.appendChild(banner);
banner.onclick = () => closeBanner(banner);
setTimeout(() => { banner.style.transform = 'translateY(0)'; banner.style.opacity = '1'; }, 100);
// 动态消失时间:CE短,AC长
let duration = isAC ? 7000 : 5000;
if (statusKey === "CE") duration = 3000;
setTimeout(() => closeBanner(banner), duration);
}
function lockAndFire() {
try {
const feData = window._feInjection || window._feConfig;
if (!feData || !feData.currentData) return;
const record = feData.currentData.record;
if (!record || record.id === lastRecordId) return;
if ((feData.currentUser?.uid || feData.currentUser?.id) == (record.user.uid || record.user.id)) {
const status = getJudgeStatus(record);
if (status === "WAIT" || status === "JUDGE") return;
lastRecordId = record.id;
const target = AIRSPACES[record.problem.difficulty] || AIRSPACES[1];
showAchievementBanner(target, record.problem.pid, status);
if (status === "AC" && record.problem.difficulty >= 5) launchFireworks(target.color);
}
} catch (e) {}
}
const style = document.createElement('style');
style.innerHTML = `
.glow-pulse { animation: pulse 2s infinite ease-in-out; }
@keyframes pulse {
0% { box-shadow: 0 15px 50px rgba(0,0,0,0.15); }
50% { box-shadow: 0 15px 70px rgba(52, 152, 219, 0.4); border-bottom-color: #fff; }
100% { box-shadow: 0 15px 50px rgba(0,0,0,0.15); }
}
.leaving { pointer-events: none; transition: transform 0.5s cubic-bezier(0.55, 0, 1, 0.45), opacity 0.4s ease !important; }
`;
document.head.appendChild(style);
setInterval(lockAndFire, 1000);
})();
:::