// ==UserScript==
// @name ScoreBoard - 扣分计分器
// @namespace http://tampermonkey.net/
// @version 2.8
// @description 悬浮小窗扣分器:自定义扣分项、记录模式、Excel 导入导出、拖拽缩放、最小化悬浮球、全局黑名单 / 白名单(跨站共享)、UI 缩放与字体优化等。
// @author Tukumi
// @match *://*/*
// @run-at document-end
// @require https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @license MIT
// @downloadURL none
// ==/UserScript==
(function () {
'use strict';
const STORAGE_KEY = 'lex_scoreboard_pro_v2_2';
const defaultItems = [
{ reason: '未考虑八进制和十六进制数', score: -1 },
{ reason: '未区分运算符和标点符号', score: -2 },
{ reason: '未能正确识别字符串', score: -2 },
{ reason: '未考虑字符类型', score: -2 },
{ reason: '未对注释识别功能进行充分测试', score: -1 },
{ reason: '未能统计语句行数,字符总数,并输出统计结果', score: -4 },
{ reason: '未能统计各类单词的个数', score: -4 },
{ reason: '未进行词法错误案例测试', score: -4 },
{ reason: '未报告错误所在的位置', score: -2 },
{ reason: '测试案例单一', score: -2 },
{ reason: '缺乏处理函数和代码', score: -5 }
];
// 读取配置:优先使用 GM_* 全局存储,兼容旧版 localStorage(自动迁移一次)
function loadConfig() {
try {
let cfg = null;
// 1) 全局(推荐):GM_getValue
if (typeof GM_getValue === 'function') {
const rawGM = GM_getValue(STORAGE_KEY, null);
if (rawGM) {
if (typeof rawGM === 'string') {
cfg = JSON.parse(rawGM);
} else if (typeof rawGM === 'object') {
cfg = rawGM;
}
}
}
// 2) 若 GM 中没有,则尝试从当前站点 localStorage 迁移(兼容旧版,只迁移一次)
if (!cfg && typeof localStorage !== 'undefined') {
const rawLS = localStorage.getItem(STORAGE_KEY);
if (rawLS) {
try {
cfg = JSON.parse(rawLS);
if (typeof GM_setValue === 'function') {
GM_setValue(STORAGE_KEY, rawLS);
}
} catch (e) {
// ignore parse error
}
}
}
if (!cfg) throw 0;
return {
items: Array.isArray(cfg.items) && cfg.items.length ? cfg.items : defaultItems.slice(),
rememberSelection: !!cfg.rememberSelection,
initialSelectionMode: cfg.initialSelectionMode === 'all' ? 'all' : 'none',
lastCheckedIndices: Array.isArray(cfg.lastCheckedIndices) ? cfg.lastCheckedIndices : [],
initialX: typeof cfg.initialX === 'number' ? cfg.initialX : null,
initialY: typeof cfg.initialY === 'number' ? cfg.initialY : null,
width: typeof cfg.width === 'number' ? cfg.width : 420,
height: typeof cfg.height === 'number' ? cfg.height : 360,
uiScale: typeof cfg.uiScale === 'number' ? cfg.uiScale : 1.0,
disabledSites: cfg.disabledSites || {},
whitelistSites: cfg.whitelistSites || {},
globallyDisabled: !!cfg.globallyDisabled,
isMinimizeOnStart: cfg.isMinimizeOnStart,
};
} catch {
return {
items: defaultItems.slice(),
rememberSelection: false,
initialSelectionMode: 'none',
lastCheckedIndices: [],
initialX: null,
initialY: null,
width: 420,
height: 360,
uiScale: 1.0,
disabledSites: {},
whitelistSites: {},
globallyDisabled: false,
isMinimizeOnStart: false,
};
}
}
let config = loadConfig();
const host = location.host;
function isWhitelistMode() {
return !!(config.whitelistSites && Object.keys(config.whitelistSites).length);
}
// 启用逻辑:
// 1. 若全局禁用,则直接退出。
// 2. 若白名单非空,则仅在白名单域名上启用(黑名单忽略)。
// 3. 若白名单为空,则按黑名单与全局禁用逻辑处理。
if (config.globallyDisabled) return;
if (isWhitelistMode()) {
if (!config.whitelistSites[host]) return;
} else {
if (config.disabledSites && config.disabledSites[host]) return;
}
let saveTimer = null;
function saveConfig(throttle = true) {
const data = {
items: config.items,
rememberSelection: config.rememberSelection,
initialSelectionMode: config.initialSelectionMode,
lastCheckedIndices: config.rememberSelection ? config.lastCheckedIndices.slice(0, 400) : [],
initialX: config.initialX,
initialY: config.initialY,
width: config.width,
height: config.height,
uiScale: config.uiScale,
disabledSites: config.disabledSites || {},
whitelistSites: config.whitelistSites || {},
globallyDisabled: !!config.globallyDisabled,
isMinimizeOnStart: config.isMinimizeOnStart,
};
const serialized = JSON.stringify(data);
const doSave = () => {
try {
if (typeof GM_setValue === 'function') {
GM_setValue(STORAGE_KEY, serialized); // 跨站共享
}
if (typeof localStorage !== 'undefined') {
localStorage.setItem(STORAGE_KEY, serialized); // 同步一份到当前域,方便调试
}
} catch (e) {
console.warn('[LexScoreBoard] save failed', e);
}
};
if (!throttle) {
doSave();
} else {
clearTimeout(saveTimer);
saveTimer = setTimeout(doSave, 200);
}
}
// ===== CSS =====
const css = `
:root{
--lex-bg: rgba(10,16,30,0.98);
--lex-bg-soft: rgba(17,24,39,0.98);
--lex-accent: #3b82f6;
--lex-accent-soft: rgba(59,130,246,0.18);
--lex-border-subtle: rgba(148,163,253,0.28);
--lex-text-main: #e5e7eb;
--lex-text-sub: #9ca3af;
--lex-radius-xl: 16px;
--lex-shadow-soft: 0 16px 44px rgba(15,23,42,0.78);
--lex-font: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
--lex-trans-fast: 0.16s ease-out;
}
body{
line-height: 22px;
}
.lex-sb-card{
position: fixed;
top: 20px; right: 20px;
width: ${config.width}px; height: ${config.height}px;
max-width: 86vw; max-height: 86vh;
min-width: 300px; min-height: 220px;
background: radial-gradient(circle at top left, rgba(56,189,248,0.06), transparent) var(--lex-bg-soft);
border-radius: var(--lex-radius-xl);
box-shadow: var(--lex-shadow-soft);
border: 1px solid rgba(148,163,253,0.40);
backdrop-filter: blur(16px) saturate(160%);
display:flex; flex-direction:column; overflow:hidden;
z-index: 999999;
font-family: var(--lex-font);
color: var(--lex-text-main);
transform-origin: top left;
transform: scale(var(--lex-ui-scale,1));
}
.lex-sb-header{
padding: 8px 10px 6px;
display:flex; align-items:center; justify-content:space-between; gap:8px;
cursor: move; user-select:none;
background: linear-gradient(to right, rgba(59,130,246,0.30), transparent);
border-bottom: 1px solid rgba(148,163,253,0.26);
}
.lex-sb-title-wrap{display:flex;flex-direction:column;gap:2px;}
.lex-sb-title{ font-size:16px; font-weight:700; color:#f3f4f6; }
.lex-sb-sub{ font-size:12px; color:var(--lex-text-sub); }
.lex-sb-actions{ display:flex; align-items:center; gap:6px; }
.lex-sb-btn-icon{
width:18px; height:18px; border-radius: 50%; border: 1px solid rgba(148,163,253,0.5);
display:flex; align-items:center; justify-content:center; cursor:pointer;
color:#e5e7eb; background:transparent; padding:0 0 0 0.5px; font-size:12px;
box-sizing:border-box; line-height:12;
transition:all var(--lex-trans-fast);
}
.lex-sb-btn-icon:hover{
border-color:rgba(148,163,253,0.7);
background:rgba(15,23,42,0.98);
box-shadow:0 0 8px rgba(148,163,253,0.6);
}
.lex-sb-body{ padding:10px; display:flex; flex-direction:column; gap:8px; flex:1; overflow:hidden; font-size:14px; }
.lex-sb-mode-bar{ display:flex; align-items:center; justify-content:space-between; gap:8px; padding:8px 10px; border-radius:10px; background:rgba(9,9,11,0.98); border:1px solid rgba(75,85,99,0.9); color:var(--lex-text-sub); font-size:13px; }
.lex-sb-mode-label span.lex-on{color:#22c55e;font-weight:700;}
.lex-sb-mode-label span.lex-off{color:#9ca3af;}
.lex-sb-row-head{ display:grid; grid-template-columns:2.6fr 0.8fr; gap:6px; padding:4px 8px; text-transform:uppercase; letter-spacing:0.12em; font-size:11px; color:var(--lex-text-sub); }
.lex-sb-list{
flex:1; overflow-y:auto; padding-right:4px; margin-right:-2px;
}
.lex-sb-row{
display:grid; grid-template-columns:2.6fr 0.8fr; gap:6px;
padding:6px 8px; align-items:center;
border-radius:10px;
background:radial-gradient(circle at top,rgba(148,163,253,0.07),transparent);
border:1px solid transparent;
transition:all var(--lex-trans-fast);
}
.lex-sb-row:hover{
background:radial-gradient(circle at top,rgba(59,130,246,0.14),rgba(6,8,15,0.98));
border-color:rgba(59,130,246,0.5);
box-shadow:0 6px 18px rgba(15,23,42,0.92);
transform:translateY(-1px);
}
.lex-sb-reason{ font-size:14px; line-height:1.6; cursor:pointer; position:relative; padding-right:18px; }
.lex-sb-reason::after{
content:"复制";
position:absolute; right:0; top:50%;
transform:translateY(-50%);
font-size:10px; padding:1px 6px;
border-radius:999px;
background:rgba(10,16,30,1);
border:1px solid rgba(148,163,253,0.5);
color:var(--lex-accent);
opacity:0; pointer-events:none;
transition:opacity var(--lex-trans-fast);
}
.lex-sb-row:hover .lex-sb-reason::after{opacity:1;}
.lex-sb-score-col{ display:flex; justify-content:flex-end; align-items:center; }
.lex-sb-check-label{ display:inline-flex; align-items:center; gap:6px; padding:4px 10px 4px 6px; border-radius:999px; background:rgba(5,5,8,0.98); border:1px solid rgba(148,163,253,0.6); color:#f97316; font-size:14px; cursor:pointer; transition:all var(--lex-trans-fast); }
.lex-sb-check-label:hover{
background:var(--lex-accent-soft);
box-shadow:0 3px 10px rgba(15,23,42,0.96);
transform:translateY(-1px);
}
.lex-sb-checkbox{
width:16px; height:16px; margin:0;
accent-color:var(--lex-accent);
cursor:pointer;
}
.lex-sb-summary{
margin-top:2px; padding:6px 8px;
border-radius:12px;
border:1px solid var(--lex-border-subtle);
background:radial-gradient(circle at top,rgba(59,130,246,0.12),rgba(5,5,8,0.98));
display:grid; grid-template-columns:2.6fr 1fr;
gap:8px; align-items:flex-start;
}
.lex-sb-summary-label{ font-size:11px; letter-spacing:0.12em; text-transform:uppercase; color:var(--lex-text-sub); }
.lex-sb-summary-text{ min-height:30px; max-height:70px; overflow-y:auto; font-size:14px; line-height:1.5; padding:8px 10px; border-radius:8px; background:rgba(5,5,8,0.98); border:1px solid rgba(75,85,99,0.96); color:#bfdbfe; cursor:pointer; position:relative; word-break:break-all; }
.lex-sb-summary-text::after{
content:"点击复制汇总";
position:absolute; right:6px; bottom:4px;
font-size:10px; color:var(--lex-text-sub);
opacity:0; transition:opacity var(--lex-trans-fast);
}
.lex-sb-summary-text:hover{
border-color:var(--lex-accent);
box-shadow:0 3px 12px rgba(15,23,42,0.96);
}
.lex-sb-summary-text:hover::after{opacity:1;}
.lex-sb-total{ font-size:22px; font-weight:700; padding:8px 10px; border-radius:10px; text-align:right; background:rgba(5,5,8,0.98); border:1px solid rgba(75,85,99,1); color:#f97316; box-shadow:inset 0 0 10px rgba(15,23,42,0.96); }
.lex-sb-footer{
display:flex; justify-content:space-between;
align-items:center; gap:8px;
}
.lex-sb-btn{ padding:7px 12px; border-radius:999px; border:1px solid rgba(148,163,253,0.6); background:transparent; color:#e5e7eb; font-size:13px; cursor:pointer; display:inline-flex; align-items:center; gap:6px; transition:all var(--lex-trans-fast); }
.lex-sb-btn:hover{
color:#fee2e2; border-color:#f97316;
background:radial-gradient(circle at top,rgba(248,250,252,0.04),transparent);
box-shadow:0 3px 12px rgba(15,23,42,0.96);
transform:translateY(-1px);
}
.lex-sb-resize{
position:absolute; right:4px; bottom:4px;
width:14px; height:14px; cursor:se-resize; opacity:0.8;
background:linear-gradient(135deg,transparent 0,transparent 50%, rgba(148,163,253,0.9) 51%, rgba(148,163,253,1) 100%);
border-bottom-right-radius:var(--lex-radius-xl);
}
.lex-sb-settings-mask{
position:absolute; inset:0;
background:rgba(15,23,42,0.84);
backdrop-filter:blur(6px);
display:flex; align-items:center; justify-content:center;
z-index:10;
}
.lex-sb-settings{
width:94%; max-height:94%;
background:rgba(9,9,11,0.98);
border-radius:12px;
border:1px solid rgba(148,163,253,0.6);
padding:10px;
display:flex; flex-direction:column; gap:8px;
font-size:13px; color:var(--lex-text-main);
overflow:auto;
}
.lex-sb-settings-title{
font-size:15px; font-weight:700;
display:flex; justify-content:space-between; align-items:center;
}
.lex-sb-settings-close{
cursor:pointer; padding:4px 8px;
border-radius:6px;
border:1px solid rgba(148,163,253,0.6);
font-size:12px; color:var(--lex-text-main);
}
.lex-sb-settings-row{
display:flex; gap:10px;
align-items:center; flex-wrap:wrap;
}
.lex-sb-settings input[type="number"],
.lex-sb-settings textarea,
.lex-sb-settings select{
background:rgba(3,7,18,0.98);
border:1px solid rgba(75,85,99,1);
color:var(--lex-text-main);
border-radius:6px;
font-size:13px;
padding:6px 8px;
box-sizing:border-box;
}
.lex-sb-settings textarea{
width:100%; min-height:96px;
resize:vertical; line-height:1.6;
}
.lex-sb-settings small{
color:var(--lex-text-sub); font-size:12px;
}
.lex-sb-settings-btn{
padding:6px 10px;
border-radius:8px;
border:1px solid rgba(148,163,253,0.6);
background:transparent;
color:var(--lex-text-main); font-size:12px;
cursor:pointer;
transition:all var(--lex-trans-fast);
}
.lex-sb-settings-btn:hover{
color:#bfdbfe; background:rgba(17,24,39,0.98);
}
.lex-file-wrap{
position:relative;
display:inline-flex;
align-items:center;
cursor:pointer;
}
.lex-file-choose{
padding:6px 10px;
border-radius:8px;
border:1px dashed rgba(148,163,253,0.6);
background:rgba(17,24,39,0.98);
color:#bfdbfe;
cursor:pointer;
transition:all var(--lex-trans-fast);
}
.lex-hidden-input{
position:absolute;
inset:0;
opacity:0;
cursor:pointer;
}
.lex-sb-close-mask{
position:fixed; inset:0;
background:rgba(15,23,42,0.6);
backdrop-filter:blur(4px);
display:flex; align-items:center; justify-content:center;
z-index:1000000;
}
.lex-sb-close-dialog{
width:300px; background:#fff;
border-radius:14px;
padding:16px 16px 12px;
box-shadow:0 16px 40px rgba(15,23,42,0.32);
color:#111827;
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
font-size:14px;
}
.lex-sb-close-title{
font-size:16px; font-weight:700;
margin-bottom:8px;
display:flex; justify-content:space-between; align-items:center;
}
.lex-sb-close-x{ cursor:pointer; font-size:18px; color:#9ca3af; }
.lex-sb-close-option{
display:flex; align-items:center; gap:8px;
margin-bottom:8px; font-size:14px;
}
.lex-sb-close-option input{ accent-color:#ec4899; }
.lex-sb-close-actions{
display:flex; justify-content:flex-end;
gap:10px; margin-top:8px;
}
.lex-sb-close-btn{
padding:6px 14px;
border-radius:999px;
border:1px solid #f9a8d4;
font-size:14px;
cursor:pointer;
background:#fff; color:#ec4899;
}
.lex-sb-close-btn.cancel{
border-color:#e5e7eb; color:#6b7280;
}
.lex-sb-close-btn.confirm{
background:#ec4899; color:#fff;
border-color:#ec4899;
}
.lex-minimized .lex-sb-body,
.lex-minimized .lex-sb-resize{
display:none;
}
.lex-minimized .lex-sb-sub{ font-size:12px; color:var(--lex-text-sub); }
.lex-minimized{
height:56px !important;
min-width:0 !important;
min-height:0 !important;
max-height:56px !important;
overflow:hidden !important;
padding:0 !important;
box-shadow:0 20px 40px rgba(15,23,42,0.55);
background:linear-gradient(to right,#0f172a,#111827,#020817) !important;
}
.lex-minimized .lex-sb-header{
padding:8px 10px 6px; border-bottom:none; cursor:move; align-items:center;
}
.lex-bubble{
width:48px !important;
height:48px !important;
min-width:0 !important;
min-height:0 !important;
max-width:48px !important;
max-height:48px !important;
border-radius:9999px !important;
padding:0 !important;
box-shadow:0 10px 26px rgba(15,23,42,0.65);
overflow:hidden !important;
display:flex;
align-items:center;
justify-content:center;
position:fixed;
background:radial-gradient(circle at top,rgba(59,130,246,0.22),rgba(9,9,11,1));
}
.lex-bubble .lex-sb-title-wrap{ display:none; }
.lex-bubble .lex-sb-body,
.lex-bubble .lex-sb-resize{ display:none; }
.lex-bubble .lex-sb-header{
padding:0;
border:none;
background:transparent;
cursor:pointer;
justify-content:center;
align-items:center;
display:flex;
width:100%;
height:100%;
}
.lex-bubble .lex-sb-actions{
gap:0;
display:none;
align-items:center;
justify-content:center;
width:100%;
}
.lex-bubble .lex-sb-btn-icon{
display:none;
}
.lex-icon-bubble{
width:22px;
height:22px;
border-radius:6px;
background:linear-gradient(to bottom, #111827, #020817);
box-shadow:0 0 4px rgba(148,163,253,0.7) inset, 0 0 6px rgba(148,163,253,0.45);
position:relative;
display:flex;
align-items:center;
justify-content:center;
}
.lex-icon-bubble::before{
content:"";
position:absolute;
inset:4px 0px;
border-radius:4px;
background:
repeating-linear-gradient(
to right,
rgba(248,250,252,0.0),
rgba(248,250,252,0.0) 3px,
rgba(248,250,252,0.9) 3px,
rgba(248,250,252,0.9) 4px
);
opacity:0.9;
}
.lex-icon-bubble::after{
content:"";
position:absolute;
inset:4px 0px;
left:3px;
right:3px;
bottom:5px;
border-radius:50%;
background:linear-gradient(to right,#f97316,#22c55e,#3b82f6);
opacity:0.96;
}
.lex-bubble:hover{
transform:translateY(-2px);
box-shadow:0 18px 40px rgba(15,23,42,0.9);
}
.lex-bubble:hover .lex-icon-bubble{
transform:scale(1.06);
box-shadow:0 0 9px rgba(148,163,253,1);
}
.lex-sb-list::-webkit-scrollbar,
.lex-sb-summary-text::-webkit-scrollbar{
width:6px; height:6px;
}
.lex-sb-list::-webkit-scrollbar-thumb,
.lex-sb-summary-text::-webkit-scrollbar-thumb{
background:rgba(148,163,253,0.55);
border-radius:999px;
}
.lex-sb-toast{
position:fixed;
left:50%; bottom:18px;
transform:translateX(-50%) translateY(40px);
padding:4px 10px;
border-radius:999px;
font-size:10px;
background:rgba(5,5,8,0.98);
color:var(--lex-text-main);
border:1px solid rgba(148,163,253,0.55);
box-shadow:0 8px 26px rgba(15,23,42,0.98);
opacity:0; pointer-events:none;
transition:all 0.22s cubic-bezier(.33,.02,.11,.99);
z-index:999999;
backdrop-filter:blur(14px) saturate(160%);
}
.lex-sb-toast-show{
opacity:1;
transform:translateX(-50%) translateY(0);
}
.lex-minimized .lex-sb-actions{
margin-left:auto;
gap:6px;
}
`;
if (typeof GM_addStyle !== 'undefined') GM_addStyle(css);
else { const st = document.createElement('style'); st.textContent = css; document.head.appendChild(st); }
// ===== UI 构建 =====
const card = document.createElement('div');
card.className = 'lex-sb-card';
card.style.setProperty('--lex-ui-scale', config.uiScale);
if (config.initialX != null && config.initialY != null) {
card.style.left = config.initialX + 'px';
card.style.top = config.initialY + 'px';
card.style.right = 'auto';
}
const header = document.createElement('div'); header.className = 'lex-sb-header';
const titleWrap = document.createElement('div'); titleWrap.className = 'lex-sb-title-wrap';
const title = document.createElement('div'); title.className = 'lex-sb-title'; title.textContent = '扣分计分器';
const sub = document.createElement('div'); sub.className = 'lex-sb-sub'; sub.textContent = '悬浮小窗 · 自定义 · 记录模式';
titleWrap.appendChild(title); titleWrap.appendChild(sub);
const actions = document.createElement('div'); actions.className = 'lex-sb-actions';
const settingsBtn = btnIcon('⚙','设置');
const recordToggleBtn = btnIcon('📑','切换记录模式');
const minimizeBtn = btnIcon('—','最小化');
const closeBtn = btnIcon('✕','关闭');
const bubbleIcon = document.createElement('span');
bubbleIcon.className = 'lex-icon-bubble';
bubbleIcon.textContent = '';
actions.appendChild(settingsBtn);
actions.appendChild(recordToggleBtn);
actions.appendChild(minimizeBtn);
actions.appendChild(closeBtn);
header.appendChild(titleWrap);
header.appendChild(actions);
const body = document.createElement('div'); body.className = 'lex-sb-body';
const modeBar = document.createElement('div'); modeBar.className = 'lex-sb-mode-bar';
const modeLabel = document.createElement('div'); modeLabel.className = 'lex-sb-mode-label';
const modeButtons = document.createElement('div'); modeButtons.style.display='flex'; modeButtons.style.gap='6px';
const startRecordBtn = mkBtn('开始记录');
const confirmRecordBtn = mkBtn('确认本次扣分'); confirmRecordBtn.style.display='none';
const endRecordBtn = mkBtn('结束并导出'); endRecordBtn.style.display='none';
modeButtons.appendChild(startRecordBtn);
modeButtons.appendChild(confirmRecordBtn);
modeButtons.appendChild(endRecordBtn);
modeBar.appendChild(modeLabel);
modeBar.appendChild(modeButtons);
body.appendChild(modeBar);
const headRow = document.createElement('div'); headRow.className = 'lex-sb-row-head';
const h1 = document.createElement('div'); h1.textContent = '错误类型(点击复制)';
const h2 = document.createElement('div'); h2.style.textAlign = 'right'; h2.textContent = '扣分';
headRow.appendChild(h1); headRow.appendChild(h2);
body.appendChild(headRow);
const list = document.createElement('div'); list.className = 'lex-sb-list';
body.appendChild(list);
const summaryWrap = document.createElement('div'); summaryWrap.className = 'lex-sb-summary';
const summaryItem = block('原因汇总');
const summaryText = document.createElement('div'); summaryText.className = 'lex-sb-summary-text';
summaryItem.appendChild(summaryText);
const totalItem = block('总扣分');
const totalText = document.createElement('div'); totalText.className = 'lex-sb-total';
totalText.textContent = '0';
totalItem.appendChild(totalText);
summaryWrap.appendChild(summaryItem);
summaryWrap.appendChild(totalItem);
body.appendChild(summaryWrap);
const footer = document.createElement('div'); footer.className = 'lex-sb-footer';
const selectAllBtn = mkBtn('全选');
const clearSelectionBtn = mkBtn('清空勾选');
const clearItemsBtn = mkBtn('清空扣分项');
footer.appendChild(selectAllBtn);
footer.appendChild(clearSelectionBtn);
footer.appendChild(clearItemsBtn);
body.appendChild(footer);
const resizeHandle = document.createElement('div'); resizeHandle.className = 'lex-sb-resize';
card.appendChild(header);
card.appendChild(body);
card.appendChild(resizeHandle);
const toast = document.createElement('div'); toast.className = 'lex-sb-toast';
document.body.appendChild(card);
document.body.appendChild(toast);
function btnIcon(txt, title){
const b=document.createElement('button');
b.className='lex-sb-btn-icon';
b.title=title;
b.textContent=txt;
return b;
}
function mkBtn(txt){
const b=document.createElement('button');
b.className='lex-sb-btn';
b.textContent=txt;
return b;
}
function block(label){
const wrap=document.createElement('div');
wrap.className='lex-sb-summary-item';
const lab=document.createElement('div');
lab.className='lex-sb-summary-label';
lab.textContent=label;
wrap.appendChild(lab);
return wrap;
}
// ===== 状态 =====
let currentItems = (config.items && config.items.length ? config.items : defaultItems.slice())
.map(i => ({ reason:String(i.reason||'').trim(), score:Number(i.score)||0 }));
let inRecordMode=false, recordStarted=false, recordData=[], lastConfirmClickTime=0;
let minimized=false, bubble=false;
let __lex_justDragged=false;
// ===== 工具 =====
function showToast(msg){
toast.textContent=msg;
toast.classList.add('lex-sb-toast-show');
clearTimeout(showToast._t);
showToast._t=setTimeout(()=>toast.classList.remove('lex-sb-toast-show'),1300);
}
function copyText(text){
if (!text) return;
if (navigator.clipboard?.writeText)
navigator.clipboard.writeText(text)
.then(()=>showToast('已复制:'+(text.length>40?text.slice(0,40)+'...':text)))
.catch(()=>fallbackCopy(text));
else fallbackCopy(text);
}
function fallbackCopy(text){
const ta=document.createElement('textarea');
ta.value=text;
ta.style.position='fixed';
ta.style.opacity='0';
document.body.appendChild(ta);
ta.select();
try{ document.execCommand('copy'); showToast('已复制'); }catch{}
document.body.removeChild(ta);
}
// ===== 渲染 =====
function renderModeLabel(){
if (!inRecordMode) modeLabel.innerHTML='模式:普通模式';
else if (!recordStarted) modeLabel.innerHTML='模式:记录模式(未开始)';
else modeLabel.innerHTML='模式:记录模式(记录中 · '+recordData.length+'条)';
}
function renderRecordButtons(){
if (!inRecordMode){
startRecordBtn.style.display='';
confirmRecordBtn.style.display='none';
endRecordBtn.style.display='none';
return;
}
if (!recordStarted){
startRecordBtn.style.display='';
confirmRecordBtn.style.display='none';
endRecordBtn.style.display='none';
} else {
startRecordBtn.style.display='none';
confirmRecordBtn.style.display='';
endRecordBtn.style.display='';
}
}
function renderList(){
list.innerHTML='';
currentItems.forEach((item, index)=>{
if (!item || !item.reason) return;
const row=document.createElement('div'); row.className='lex-sb-row';
const reason=document.createElement('div');
reason.className='lex-sb-reason';
reason.textContent=item.reason;
reason.dataset.index=index;
const scoreCol=document.createElement('div'); scoreCol.className='lex-sb-score-col';
const label=document.createElement('label'); label.className='lex-sb-check-label';
const cb=document.createElement('input');
cb.type='checkbox';
cb.className='lex-sb-checkbox';
cb.dataset.index=index;
cb.dataset.score=item.score;
const span=document.createElement('span'); span.textContent=item.score;
label.appendChild(cb); label.appendChild(span);
scoreCol.appendChild(label);
row.appendChild(reason); row.appendChild(scoreCol);
list.appendChild(row);
});
const cbs=list.querySelectorAll('.lex-sb-checkbox');
if (config.rememberSelection && config.lastCheckedIndices?.length) {
cbs.forEach(cb=>{
const idx=Number(cb.dataset.index);
cb.checked=config.lastCheckedIndices.includes(idx);
});
} else {
const all=config.initialSelectionMode==='all';
cbs.forEach(cb=>cb.checked=all);
}
updateSummaryAndRemember();
}
function updateSummaryAndRemember(){
const cbs=list.querySelectorAll('.lex-sb-checkbox');
const reasons=[]; let total=0; const checkedIdx=[];
cbs.forEach(cb=>{
if(cb.checked){
const idx=Number(cb.dataset.index);
const it=currentItems[idx];
if(!it) return;
reasons.push(it.reason);
total+=Number(it.score)||0;
checkedIdx.push(idx);
}
});
summaryText.textContent=reasons.join(';');
totalText.textContent=String(total);
if (config.rememberSelection){
config.lastCheckedIndices=checkedIdx;
saveConfig(true);
}
}
function clearSelection(){
list.querySelectorAll('.lex-sb-checkbox').forEach(cb=>cb.checked=false);
summaryText.textContent='';
totalText.textContent='0';
config.lastCheckedIndices=[];
saveConfig(true);
}
function selectAll(){
list.querySelectorAll('.lex-sb-checkbox').forEach(cb=>cb.checked=true);
updateSummaryAndRemember();
}
// ===== 事件:列表复制 & 勾选 =====
list.addEventListener('click', e=>{
const n=e.target.closest('.lex-sb-reason');
if(n){
const idx=Number(n.dataset.index);
const it=currentItems[idx];
if(it) copyText(it.reason);
}
});
list.addEventListener('change', e=>{
if(e.target.classList.contains('lex-sb-checkbox')) updateSummaryAndRemember();
});
summaryText.addEventListener('click', ()=>{
const t=summaryText.textContent.trim();
if(!t){ showToast('当前没有已选原因'); return; }
copyText(t);
});
selectAllBtn.addEventListener('click', selectAll);
clearSelectionBtn.addEventListener('click', clearSelection);
clearItemsBtn.addEventListener('click', ()=>{
if(!currentItems.length) return;
if(!confirm('确定要清空所有扣分项吗?(可在设置中恢复默认)')) return;
currentItems=[];
config.items=[];
renderList();
saveConfig(false);
showToast('已清空所有扣分项');
});
// ===== 拖拽 =====
(function enableDrag(){
let down=false, dx=0, dy=0, sx=0, sy=0, moved=false;
function start(x,y){
down=true; moved=false; sx=x; sy=y;
const r=card.getBoundingClientRect();
dx=x-r.left; dy=y-r.top;
header.classList.add('dragging');
}
function move(x,y){
if(!down) return;
if (Math.abs(x - sx) + Math.abs(y - sy) > 3) moved = true;
const scale=config.uiScale||1;
let nx=x-dx, ny=y-dy;
const maxX=window.innerWidth - card.offsetWidth*scale;
const maxY=window.innerHeight - card.offsetHeight*scale;
nx=Math.max(4, Math.min(maxX-4, nx));
ny=Math.max(4, Math.min(maxY-4, ny));
card.style.left=nx+'px';
card.style.top=ny+'px';
card.style.right='auto';
}
function end(){
if(!down) return;
down=false;
header.classList.remove('dragging');
const r=card.getBoundingClientRect();
config.initialX=r.left;
config.initialY=r.top;
saveConfig(true);
if (minimized) maybeSnapToEdge();
__lex_justDragged = moved;
if (__lex_justDragged) setTimeout(()=>{ __lex_justDragged=false; }, 220);
}
header.addEventListener('mousedown', e=>{
if(e.button!==0) return;
start(e.clientX,e.clientY);
e.preventDefault();
});
document.addEventListener('mousemove', e=>move(e.clientX,e.clientY));
document.addEventListener('mouseup', end);
header.addEventListener('touchstart', e=>{
const t=e.touches[0];
start(t.clientX,t.clientY);
}, {passive:true});
document.addEventListener('touchmove', e=>{
if(!down) return;
const t=e.touches[0];
move(t.clientX,t.clientY);
}, {passive:true});
document.addEventListener('touchend', end);
})();
// ===== 缩放 =====
(function enableResize(){
let resizing=false, sx=0, sy=0, sw=0, sh=0;
resizeHandle.addEventListener('mousedown', e=>{
e.preventDefault();
resizing=true;
sx=e.clientX; sy=e.clientY;
const r=card.getBoundingClientRect();
sw=r.width; sh=r.height;
});
document.addEventListener('mousemove', e=>{
if(!resizing) return;
let nw=sw+(e.clientX-sx);
let nh=sh+(e.clientY-sy);
nw=Math.max(300, Math.min(window.innerWidth*0.86, nw));
nh=Math.max(220, Math.min(window.innerHeight*0.86, nh));
card.style.width=nw+'px';
card.style.height=nh+'px';
});
document.addEventListener('mouseup', ()=>{
if(!resizing) return;
resizing=false;
const r=card.getBoundingClientRect();
config.width=r.width;
config.height=r.height;
saveConfig(true);
});
})();
// ===== 设置面板 =====
function openSettings(){
if (card.querySelector('.lex-sb-settings-mask')) return;
const mask=document.createElement('div'); mask.className='lex-sb-settings-mask';
const panel=document.createElement('div'); panel.className='lex-sb-settings';
const titleRow=document.createElement('div'); titleRow.className='lex-sb-settings-title';
titleRow.innerHTML='设置';
const closeS=document.createElement('div'); closeS.className='lex-sb-settings-close'; closeS.textContent='关闭';
titleRow.appendChild(closeS);
panel.appendChild(titleRow);
// 记忆 + 初始勾选 + UI缩放
const row1=document.createElement('div'); row1.className='lex-sb-settings-row';
const rememberCb=checkboxWithLabel('记住上次勾选状态', config.rememberSelection, v=>{
config.rememberSelection=v;
if(!v) config.lastCheckedIndices=[];
saveConfig(true);
});
row1.appendChild(rememberCb.wrap);
const selLabel=document.createElement('label'); selLabel.textContent=' 初始勾选:';
const selSelect=document.createElement('select');
['none','all'].forEach(v=>{
const o=document.createElement('option');
o.value=v;
o.textContent=(v==='none'?'默认全不选':'默认全选(排除法)');
if(config.initialSelectionMode===v) o.selected=true;
if(v === 'all') selectAll();
else if(v === 'none') clearSelection();
selSelect.appendChild(o);
});
selSelect.onchange=()=>{
config.initialSelectionMode=selSelect.value;
saveConfig(true);
};
selLabel.appendChild(selSelect);
row1.appendChild(selLabel);
const scaleLabel=document.createElement('label'); scaleLabel.textContent=' UI缩放:';
const scaleRange=document.createElement('input');
scaleRange.type='range';
scaleRange.min='0.85'; scaleRange.max='1.6'; scaleRange.step='0.05';
scaleRange.value=String(config.uiScale||1);
const scaleVal=document.createElement('span'); scaleVal.textContent=String(config.uiScale||1);
scaleRange.oninput=()=>{
config.uiScale=parseFloat(scaleRange.value)||1;
card.style.setProperty('--lex-ui-scale', config.uiScale);
scaleVal.textContent=String(config.uiScale);
saveConfig(true);
};
scaleLabel.appendChild(scaleRange);
scaleLabel.appendChild(scaleVal);
row1.appendChild(scaleLabel);
const minimizeOnStart = checkboxWithLabel('启动即最小化', config.isMinimizeOnStart, v=>{
config.isMinimizeOnStart = v;
if(v) {
minimized = true;
toggleMinimize();
}
saveConfig(true);
})
row1.appendChild(minimizeOnStart.wrap)
panel.appendChild(row1);
// 初始位置
const row2=document.createElement('div'); row2.className='lex-sb-settings-row';
row2.innerHTML='初始位置 (px):';
const inputX=mkNum(config.initialX ?? '');
const inputY=mkNum(config.initialY ?? '');
const useCur=btnS('使用当前');
useCur.onclick=()=>{
const r=card.getBoundingClientRect();
inputX.value=Math.round(r.left);
inputY.value=Math.round(r.top);
};
row2.appendChild(inputX);
row2.appendChild(inputY);
row2.appendChild(useCur);
panel.appendChild(row2);
panel.appendChild(small('留空则使用默认位置;“使用当前”会记录窗口当前左上角。'));
// 窗口尺寸
const row2b=document.createElement('div'); row2b.className='lex-sb-settings-row';
row2b.innerHTML='窗口尺寸 (px):';
const inputW=mkNum(Math.round(config.width));
const inputH=mkNum(Math.round(config.height));
const applyWH=btnS('应用尺寸');
applyWH.onclick=()=>{
const w=parseInt(inputW.value,10);
const h=parseInt(inputH.value,10);
if(w>0 && h>0){
card.style.width=w+'px';
card.style.height=h+'px';
config.width=w; config.height=h;
saveConfig(true);
showToast('尺寸已应用并保存');
}
};
row2b.appendChild(inputW);
row2b.appendChild(inputH);
row2b.appendChild(applyWH);
panel.appendChild(row2b);
// 扣分项配置
panel.appendChild(document.createElement('hr'));
const labelScore=document.createElement('div');
labelScore.style.fontWeight='700';
labelScore.textContent='扣分项配置';
panel.appendChild(labelScore);
const textarea=document.createElement('textarea');
textarea.value=currentItems.map(i=>`${i.reason} | ${i.score}`).join('\n');
panel.appendChild(textarea);
panel.appendChild(small('每行:错误原因 | 分值(负数)。保存后覆盖当前扣分项。'));
const row3=document.createElement('div'); row3.className='lex-sb-settings-row';
const applyItems=btnS('保存扣分项');
const restoreDefault=btnS('恢复默认扣分项');
const resetAll=btnS('恢复全部默认设置');
row3.appendChild(applyItems);
row3.appendChild(restoreDefault);
row3.appendChild(resetAll);
panel.appendChild(row3);
applyItems.onclick=()=>{
const lines=textarea.value.split('\n').map(s=>s.trim()).filter(Boolean);
const arr=[];
for(const line of lines){
const p=line.split('|');
if(!p[0]) continue;
const reason=p[0].trim();
const score=Number((p[1]||'').trim())||0;
arr.push({reason,score});
}
if(!arr.length){ alert('没有有效条目'); return; }
currentItems=arr;
config.items=currentItems;
config.lastCheckedIndices=[];
saveConfig(false);
renderList();
showToast('扣分项已更新');
};
restoreDefault.onclick=()=>{
if(!confirm('恢复默认扣分项?')) return;
currentItems=defaultItems.map(i=>({...i}));
config.items=currentItems;
config.lastCheckedIndices=[];
saveConfig(false);
renderList();
textarea.value=currentItems.map(i=>`${i.reason} | ${i.score}`).join('\n');
showToast('已恢复默认');
};
resetAll.onclick=()=>{
if(!confirm('恢复全部默认设置(位置/尺寸/勾选/扣分项/缩放等)?')) return;
if (typeof GM_setValue === 'function') GM_setValue(STORAGE_KEY, '');
if (typeof localStorage !== 'undefined') localStorage.removeItem(STORAGE_KEY);
showToast('已恢复默认,请刷新页面');
};
// Excel 导入
panel.appendChild(document.createElement('hr'));
const labelExcel=document.createElement('div');
labelExcel.style.fontWeight='700';
labelExcel.textContent='从 Excel 导入扣分项';
panel.appendChild(labelExcel);
const fileRow=document.createElement('div'); fileRow.className='lex-sb-settings-row';
const fileWrap=document.createElement('div'); fileWrap.className='lex-file-wrap';
const chooseBtn=document.createElement('div'); chooseBtn.className='lex-file-choose'; chooseBtn.textContent='选择文件';
const fileInput=document.createElement('input');
fileInput.type='file';
fileInput.accept='.xlsx,.xls';
fileInput.className='lex-hidden-input';
fileWrap.appendChild(chooseBtn);
fileWrap.appendChild(fileInput);
fileRow.appendChild(fileWrap);
const fileNameSpan=document.createElement('span');
fileRow.appendChild(fileNameSpan);
panel.appendChild(fileRow);
panel.appendChild(small('规范:第一张表,A列“错误类型”,B列“扣分”(负整数)。首行可为表头。'));
chooseBtn.onclick=()=>fileInput.click();
fileInput.onchange=(e)=>{
const f=e.target.files[0];
fileNameSpan.textContent=f ? ('已选择:'+f.name) : '';
if(!f) return;
const reader=new FileReader();
reader.onload=evt=>{
try{
const wb=XLSX.read(evt.target.result,{type:'binary'});
const name=wb.SheetNames[0];
const sheet=wb.Sheets[name];
const arr=XLSX.utils.sheet_to_json(sheet,{header:1});
const imported=[];
for(let i=0;i`${i.reason} | ${i.score}`).join('\n');
showToast('已从 Excel 导入');
}catch(err){
console.error(err);
alert('Excel 解析失败');
}
};
reader.readAsBinaryString(f);
};
// 白名单
panel.appendChild(document.createElement('hr'));
const wlLabel=document.createElement('div');
wlLabel.style.fontWeight='700';
wlLabel.textContent='网站白名单(优先于黑名单,非空时仅在以下站点启用,全局共享)';
panel.appendChild(wlLabel);
const wlTextarea=document.createElement('textarea');
wlTextarea.placeholder='每行一个域名,例如:\nexample.com';
wlTextarea.value=Object.keys(config.whitelistSites||{}).join('\n');
panel.appendChild(wlTextarea);
const wlRow=document.createElement('div'); wlRow.className='lex-sb-settings-row';
const wlApply=btnS('应用白名单');
const wlAddCur=btnS('添加当前站点');
const wlClear=btnS('清空白名单');
wlRow.appendChild(wlApply);
wlRow.appendChild(wlAddCur);
wlRow.appendChild(wlClear);
panel.appendChild(wlRow);
wlApply.onclick=()=>{
const lines=wlTextarea.value.split('\n').map(s=>s.trim()).filter(Boolean);
const map={}; lines.forEach(h=>map[h]=true);
config.whitelistSites=map;
saveConfig(false);
showToast('白名单已更新(全局生效)');
};
wlAddCur.onclick=()=>{
if(!host) return;
config.whitelistSites=config.whitelistSites||{};
config.whitelistSites[host]=true;
wlTextarea.value=Object.keys(config.whitelistSites).join('\n');
saveConfig(false);
showToast('已将当前站点加入白名单(全局)');
};
wlClear.onclick=()=>{
if(!confirm('确定清空白名单?(将恢复为使用黑名单控制)')) return;
config.whitelistSites={};
wlTextarea.value='';
saveConfig(false);
showToast('白名单已清空,恢复黑名单模式');
};
// 黑名单
panel.appendChild(document.createElement('hr'));
const blLabel=document.createElement('div');
blLabel.style.fontWeight='700';
blLabel.textContent='网站黑名单(仅在白名单为空时生效,全局共享)';
panel.appendChild(blLabel);
const blTextarea=document.createElement('textarea');
blTextarea.placeholder='每行一个域名,例如:\nexample.com';
blTextarea.value=Object.keys(config.disabledSites||{}).join('\n');
panel.appendChild(blTextarea);
const blRow=document.createElement('div'); blRow.className='lex-sb-settings-row';
const blApply=btnS('应用黑名单');
const blClearSite=btnS('移除当前站点');
const blClearAll=btnS('清空黑名单');
blRow.appendChild(blApply);
blRow.appendChild(blClearSite);
blRow.appendChild(blClearAll);
panel.appendChild(blRow);
blApply.onclick=()=>{
const lines=blTextarea.value.split('\n').map(s=>s.trim()).filter(Boolean);
const map={}; lines.forEach(h=>map[h]=true);
config.disabledSites=map;
saveConfig(false);
showToast('黑名单已更新(全局生效)');
};
blClearSite.onclick=()=>{
if(config.disabledSites && config.disabledSites[host]){
delete config.disabledSites[host];
saveConfig(false);
blTextarea.value=Object.keys(config.disabledSites).join('\n');
showToast('已移除当前站点');
}
};
blClearAll.onclick=()=>{
if(!confirm('清空所有黑名单?')) return;
config.disabledSites={};
saveConfig(false);
blTextarea.value='';
showToast('已清空黑名单');
};
// 保存位置按钮
const rowSave=document.createElement('div'); rowSave.className='lex-sb-settings-row';
const applyPos=btnS('应用位置');
applyPos.onclick=()=>{
const x=parseInt(inputX.value,10);
const y=parseInt(inputY.value,10);
if(!Number.isFinite(x)||!Number.isFinite(y)){
alert('请输入数字');
return;
}
card.style.left=x+'px';
card.style.top=y+'px';
card.style.right='auto';
config.initialX=x;
config.initialY=y;
saveConfig(true);
showToast('位置已应用并保存');
};
rowSave.appendChild(applyPos);
panel.appendChild(rowSave);
mask.appendChild(panel);
card.appendChild(mask);
function close(){ mask.remove(); }
closeS.onclick=close;
mask.addEventListener('click', e=>{
if(e.target===mask) close();
});
function checkboxWithLabel(text, checked, onChange){
const wrap=document.createElement('label');
wrap.style.display='inline-flex';
wrap.style.alignItems='center';
wrap.style.gap='8px';
const cb=document.createElement('input');
cb.type='checkbox'; cb.checked=checked;
cb.onchange=()=>onChange(cb.checked);
wrap.appendChild(cb);
wrap.appendChild(document.createTextNode(text));
return {wrap, cb};
}
function mkNum(val){
const i=document.createElement('input');
i.type='number'; i.value=val;
return i;
}
function btnS(txt){
const b=document.createElement('button');
b.className='lex-sb-settings-btn';
b.textContent=txt;
return b;
}
function small(t){
const s=document.createElement('small');
s.textContent=t;
return s;
}
}
settingsBtn.addEventListener('click', e=>{
e.stopPropagation();
if(minimized)
toggleMinimize(false);
openSettings();
});
// ===== 关闭对话框 =====
function openCloseDialog(){
const mask=document.createElement('div'); mask.className='lex-sb-close-mask';
const dialog=document.createElement('div'); dialog.className='lex-sb-close-dialog';
const titleRow=document.createElement('div'); titleRow.className='lex-sb-close-title';
const t=document.createElement('span'); t.textContent='关闭悬浮球';
const x=document.createElement('span'); x.className='lex-sb-close-x'; x.textContent='×';
titleRow.appendChild(t); titleRow.appendChild(x);
const o1=radio('本次关闭直到下次访问', true);
const o2=radio('当前网站禁用');
const o3=radio('永久禁用(本地存储中清除可恢复)');
const actions=document.createElement('div'); actions.className='lex-sb-close-actions';
const cancel=btnC('取消','cancel');
const ok=btnC('确定','confirm');
actions.appendChild(cancel); actions.appendChild(ok);
dialog.appendChild(titleRow);
dialog.appendChild(o1.wrap);
dialog.appendChild(o2.wrap);
dialog.appendChild(o3.wrap);
dialog.appendChild(actions);
mask.appendChild(dialog);
document.body.appendChild(mask);
function close(){ mask.remove(); }
x.onclick=close;
cancel.onclick=close;
mask.onclick=(e)=>{ if(e.target===mask) close(); };
ok.onclick=()=>{
if(o2.input.checked){
// 当前网站禁用:若在白名单中则移除,并加入黑名单(用于白名单清空后的回退)
if (isWhitelistMode()) {
if (config.whitelistSites && config.whitelistSites[host]) {
delete config.whitelistSites[host];
}
}
config.disabledSites = config.disabledSites || {};
config.disabledSites[host] = true;
saveConfig(false);
} else if(o3.input.checked){
config.globallyDisabled=true;
saveConfig(false);
}
card.remove();
toast.remove();
close();
};
function radio(text, checked){
const wrap=document.createElement('label');
wrap.className='lex-sb-close-option';
const input=document.createElement('input');
input.type='radio'; input.name='lex-close'; input.checked=!!checked;
wrap.appendChild(input);
wrap.appendChild(document.createTextNode(text));
return {wrap,input};
}
function btnC(txt, cls){
const b=document.createElement('button');
b.className='lex-sb-close-btn '+(cls||'');
if(cls==='confirm') b.classList.add('confirm');
if(cls==='cancel') b.classList.add('cancel');
b.textContent=txt;
return b;
}
}
closeBtn.addEventListener('click', e=>{
e.stopPropagation();
openCloseDialog();
});
// ===== 最小化 / 悬浮球逻辑 =====
function toggleMinimize(force){
if (typeof force === 'boolean') {
minimized = force;
} else {
minimized = !minimized;
}
card.classList.toggle('lex-minimized', minimized);
if (!minimized){
// 恢复正常
bubble = false;
card.classList.remove('lex-bubble');
if (header.contains(bubbleIcon)) header.removeChild(bubbleIcon);
if (config.width) card.style.width = config.width + 'px';
if (config.height) card.style.height = config.height + 'px';
return;
}
// 进入最小化时尝试吸附为悬浮球
maybeSnapToEdge();
}
function maybeSnapToEdge(){
if (!minimized){
bubble = false;
card.classList.remove('lex-bubble');
if (header.contains(bubbleIcon)) header.removeChild(bubbleIcon);
return;
}
const r = card.getBoundingClientRect();
const margin = 12;
const distL = r.left;
const distR = window.innerWidth - r.right;
const nearLeft = distL < 30;
const nearRight = distR < 30;
if (nearLeft || nearRight){
bubble = true;
card.classList.add('lex-bubble');
if (nearLeft){
card.style.left = margin + 'px';
card.style.right = 'auto';
} else {
card.style.left = 'auto';
card.style.right = margin + 'px';
}
if (!header.contains(bubbleIcon)) header.appendChild(bubbleIcon);
} else {
bubble = false;
card.classList.remove('lex-bubble');
if (header.contains(bubbleIcon)) header.removeChild(bubbleIcon);
}
}
minimizeBtn.addEventListener('click', e=>{
e.stopPropagation();
toggleMinimize();
});
// 标题栏双击最小化/还原
header.addEventListener('dblclick', ()=>{
toggleMinimize();
});
// 悬浮球点击恢复
header.addEventListener('click', ()=>{
if (__lex_justDragged) return; // ignore click triggered by drag end
if (card.classList.contains('lex-bubble')) {
toggleMinimize(false);
}
});
window.addEventListener('resize', ()=>{ if(minimized) maybeSnapToEdge(); });
// ===== 记录模式 =====
function toggleRecordMode(){
inRecordMode=!inRecordMode;
if(!inRecordMode){
recordStarted=false;
recordData=[];
showToast('已切换普通模式');
}else{
showToast('已切换记录模式,点击“开始记录”');
}
renderModeLabel();
renderRecordButtons();
}
recordToggleBtn.addEventListener('click', e=>{
e.stopPropagation();
toggleRecordMode();
});
startRecordBtn.addEventListener('click', ()=>{
if(!inRecordMode) return;
recordStarted=true;
recordData=[];
renderModeLabel();
renderRecordButtons();
showToast('记录开始');
});
confirmRecordBtn.addEventListener('click', ()=>{
if(!recordStarted) return;
const now=Date.now();
if(now - lastConfirmClickTime < 600){
const rec=buildCurrentRecord();
if(!rec){ showToast('当前没有勾选'); return; }
recordData.push(rec);
renderModeLabel();
showToast('已记录一条');
}else{
lastConfirmClickTime=now;
showToast('再次点击确认以防误触');
}
});
endRecordBtn.addEventListener('click', ()=>{
if(!recordStarted){
showToast('尚未开始记录');
return;
}
if(!recordData.length){
if(!confirm('没有记录,是否结束?')) return;
inRecordMode=false;
recordStarted=false;
recordData=[];
renderModeLabel();
renderRecordButtons();
return;
}
try{
const wb=XLSX.utils.book_new();
const sum=[['序号','时间','总扣分','条目数']];
recordData.forEach((r,i)=>sum.push([i+1,r.time,r.total,r.details.length]));
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(sum), '汇总');
const det=[['记录序号','时间','错误类型','扣分']];
recordData.forEach((r,i)=>r.details.forEach(d=>det.push([i+1,r.time,d.reason,d.score])));
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(det), '详情');
const out=XLSX.write(wb,{bookType:'xlsx',type:'array'});
const blob=new Blob([out],{type:'application/octet-stream'});
const url=URL.createObjectURL(blob);
const a=document.createElement('a');
a.href=url;
a.download='扣分记录_'+new Date().toISOString().replace(/[:T]/g,'-').split('.')[0]+'.xlsx';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showToast('已导出 Excel');
}catch(e){
console.error(e);
showToast('导出失败');
}
inRecordMode=false;
recordStarted=false;
recordData=[];
renderModeLabel();
renderRecordButtons();
});
function buildCurrentRecord(){
const cbs=list.querySelectorAll('.lex-sb-checkbox');
const rows=[]; let total=0;
cbs.forEach(cb=>{
if(cb.checked){
const idx=Number(cb.dataset.index);
const it=currentItems[idx];
if(!it) return;
rows.push({reason:it.reason, score:it.score});
total+=Number(it.score)||0;
}
});
if(!rows.length) return null;
return {
time: new Date().toLocaleString(),
total,
details: rows
};
}
function initiateFunc(){
renderModeLabel();
renderRecordButtons();
renderList();
if(config.isMinimizeOnStart){
minimized = true;
toggleMinimize(true);
}
}
// 初始化
initiateFunc();
})();