Greasy Fork

Greasy Fork is available in English.

ScoreBoard - 扣分计分器

悬浮小窗扣分器:自定义扣分项、记录模式、Excel 导入导出、拖拽缩放、最小化悬浮球、全局黑名单 / 白名单(跨站共享)、UI 缩放与字体优化等。

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==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/[email protected]/dist/xlsx.full.min.js
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// ==/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='模式:<span class="lex-off">普通模式</span>';
    else if (!recordStarted) modeLabel.innerHTML='模式:<span class="lex-on">记录模式</span>(未开始)';
    else modeLabel.innerHTML='模式:<span class="lex-on">记录模式</span>(记录中 · '+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='<span>设置</span>';
    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='<span>初始位置 (px):</span>';
    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='<span>窗口尺寸 (px):</span>';
    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<arr.length;i++){
            const row=arr[i];
            if(!row || !row.length) continue;
            let reason=(row[0]||'').toString().trim();
            let score=row[1];
            if(!reason) continue;
            if(i===0 && /错|分/.test(reason)) continue; // 表头
            score=Number(score);
            if(Number.isNaN(score)) continue;
            imported.push({reason,score});
          }
          if(!imported.length){
            alert('未解析到有效数据');
            return;
          }
          currentItems=imported;
          config.items=currentItems;
          config.lastCheckedIndices=[];
          saveConfig(false);
          renderList();
          textarea.value=currentItems.map(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();


})();