// ==UserScript== // @name 真免费-智慧中小学全自动学习助手 // @namespace https://github.com/berain/smart-edu-assistant // @version 1.0 // @description 智慧中小学平台全自动学习助手,免费体验1次,加群即可在获取一次密钥。8.8元/3次超值刷课支持效率/狂暴双模式,自动完成课程视频学习。全新ui,傻瓜式操作. // @author berain // @match https://basic.smartedu.cn/* // @match https://teacher.ykt.eduyun.cn/* // @icon https://img.icons8.com/color/96/000000/books.png // @grant GM_addStyle // @antifeature payment // @antifeature membership // @antifeature tracking // @tag 智慧中小学 // @tag 暑期研修 // @tag 6.8元刷3次 // @tag 寒假研修 // @tag 免费学习 // @tag 教师研修 // @license LGPL-3.0 // @run-at document-end // ==/UserScript== (function() { 'use strict'; // ============================================================ // 🎨 样式(完整) // ============================================================ GM_addStyle(` #sl3-panel{position:fixed;right:12px;top:60px;width:370px;height:660px;background:#fffef8;color:#333;z-index:99999;font:13px/1.5 "Microsoft YaHei",sans-serif;border-radius:10px;box-shadow:0 6px 30px rgba(0,0,0,.25);overflow:hidden;display:flex;flex-direction:column;transition:width .3s,height .3s,border-radius .3s} #sl3-panel.sl3-minimized{width:50px;height:50px;border-radius:50%;cursor:pointer} #sl3-panel.sl3-minimized>*{display:none} #sl3-book-icon{display:none;position:absolute;top:0;left:0;width:50px;height:50px;border-radius:50%;background:linear-gradient(135deg,#ff9a44,#fc6076);align-items:center;justify-content:center;font-size:22px;cursor:pointer;box-shadow:0 4px 16px rgba(0,0,0,.3);z-index:10} #sl3-panel.sl3-minimized #sl3-book-icon{display:flex} @media (max-width:768px){#sl3-panel{right:5%;top:5%;width:90%;height:90vh;max-height:600px;min-width:0;min-height:0}} #sl3-panel .hd{background:linear-gradient(135deg,#ff9a44,#fc6076);padding:10px 14px;display:flex;justify-content:space-between;align-items:center;border-radius:10px 10px 0 0;cursor:move;user-select:none} #sl3-panel .hd .hd-left{display:flex;align-items:center;gap:8px} #sl3-panel .hd .hd-icon{width:24px;height:24px;background:rgba(255,255,255,.25);border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:14px;color:#fff} #sl3-panel .hd .hd-title{font-size:15px;font-weight:bold;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.15)} #sl3-panel .hd .hd-ver{font-size:11px;color:rgba(255,255,255,.8);margin-left:4px;font-weight:normal} #sl3-panel .hd .hd-min{width:27px;height:27px;border:0;background:rgba(255,255,255,.26);border-radius:50%;color:#fff;font-size:17px;cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:1} #sl3-panel .hd .hd-min:hover{background:rgba(255,255,255,.45)} #sl3-panel .status-bar{padding:4px 10px;background:#fafafa;border-bottom:1px solid #eee;flex-shrink:0} #sl3-panel .sb-row{display:flex;align-items:center;gap:6px;flex-wrap:nowrap} #sl3-panel .sb-label{font-size:11px;color:#555;font-weight:bold;display:flex;align-items:center;gap:3px;white-space:nowrap;flex-shrink:0} #sl3-panel .sb-dot{width:7px;height:7px;border-radius:50%;background:#4caf50;display:inline-block;flex-shrink:0} #sl3-panel .sb-dot.running{background:#ff9800;animation:pulse 1s infinite} #sl3-panel .sb-dot.stopped{background:#999} @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}} #sl3-panel .sb-stat{font-size:11px;color:#555;white-space:nowrap;flex-shrink:0} #sl3-panel .sb-pct{font-size:14px;font-weight:bold;color:#ff6f00;margin-left:auto;white-space:nowrap;flex-shrink:0} #sl3-panel .sb-sub{font-size:11px;color:#888;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0} #sl3-panel .ctrl-bar{padding:5px 10px;background:#fafafa;border-bottom:1px solid #eee;display:flex;flex-direction:column;gap:5px;flex-shrink:0} #sl3-panel .cb-row{display:flex;align-items:center;gap:5px;flex-wrap:nowrap} #sl3-panel .speed-row{width:100%;display:flex;align-items:center;gap:5px} #sl3-panel .speed-box{display:flex;align-items:center;gap:4px;font-size:11px;color:#555;font-weight:bold;flex:1;width:100%} #sl3-panel .speed-slider{flex:1;width:100%;min-width:0;accent-color:#ff6f00;cursor:pointer} #sl3-panel .mode-sel{padding:3px 7px;border:2px solid #ff9800;border-radius:6px;background:#fff;color:#ff6f00;font-size:13px;font-weight:bold;cursor:pointer;outline:none;flex-shrink:0} #sl3-panel .mode-sel option{color:#333} #sl3-panel .ctrl-input{width:38px;padding:3px 5px;border:1px solid #ddd;border-radius:5px;font-size:11px;text-align:center;outline:none} #sl3-panel .btn-go{padding:4px 10px;border:0;border-radius:6px;cursor:pointer;font-size:12px;font-weight:bold;background:linear-gradient(135deg,#ff6f00,#ff9a44);color:#fff;box-shadow:0 2px 8px rgba(255,111,0,.35);flex:1 1 auto;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} #sl3-panel .btn-go:hover{box-shadow:0 4px 12px rgba(252,96,118,.45);transform:translateY(-1px)} #sl3-panel .btn-go:active{transform:translateY(0)} #sl3-panel .btn-stop{padding:4px 12px;border:1px solid #bbb;border-radius:6px;cursor:pointer;font-size:12px;font-weight:bold;background:#f5f5f5;color:#666;flex:1 1 auto;white-space:nowrap} #sl3-panel .btn-stop:not(:disabled){border-color:#ff9800;color:#ff6f00;background:#fff} #sl3-panel .btn-stop:hover{background:#e0e0e0} #sl3-panel .btn-go:disabled,.btn-stop:disabled{opacity:.5;cursor:not-allowed;transform:none;box-shadow:none} #sl3-panel .tabs{display:flex;padding:0 14px;background:#fafafa;border-bottom:2px solid #eee;flex-shrink:0;gap:3px;flex-wrap:nowrap;overflow:hidden} #sl3-panel .tab{padding:6px 10px;cursor:pointer;font-size:11px;color:#666;border-bottom:2px solid transparent;margin-bottom:-2px;transition:all .2s;white-space:nowrap;flex-shrink:1} #sl3-panel .tab:hover{color:#ff6f00} #sl3-panel .tab.active{color:#ff6f00;border-bottom-color:#ff6f00;font-weight:bold} @media (max-width:370px){#sl3-panel .tab{padding:4px 6px;font-size:10px}} #sl3-panel .action-row{padding:5px 10px;display:flex;align-items:center;gap:4px;flex-wrap:nowrap;overflow:hidden;border-bottom:1px solid #f0f0f0;flex-shrink:0} #sl3-panel .btn-xs{padding:3px 8px;border:1px solid #ddd;border-radius:5px;cursor:pointer;font-size:11px;background:#fff;color:#555;white-space:nowrap} #sl3-panel .btn-xs:hover{border-color:#ff9800;color:#ff6f00} #sl3-panel .btn-xs.primary{background:#fff3e0;border-color:#ffb74d;color:#e65100} #sl3-panel .btn-xs.goal{background:#ff7a45;border-color:#ff7a45;color:#fff;font-weight:bold} #sl3-panel .btn-xs.goal:hover{background:#ff5722} #sl3-panel .list-area{flex:1;overflow-y:auto;padding:4px 0;display:flex;flex-direction:column;gap:4px} #sl3-panel .group-container{border:1px solid #f0e8dc;border-radius:8px;overflow:hidden;background:#fffcf5;display:flex;flex-direction:column} #sl3-panel .group-container.req-group{flex-shrink:0} #sl3-panel .group-container.elec-group{flex:1;min-height:0;border-color:#e8f0dc;background:#f5fff8} #sl3-panel .group-body{overflow-y:auto;min-height:0} #sl3-panel .course-row{display:flex;align-items:center;padding:6px 12px;gap:6px;border-bottom:1px solid #f5f5f5;transition:background .15s} #sl3-panel .course-row:hover{background:#fff8e1} #sl3-panel .course-row.cr-current{background:#fff3e0} #sl3-panel .cchk{width:16px;height:16px;flex-shrink:0;cursor:pointer;accent-color:#ff6f00} #sl3-panel .cexp{width:20px;height:20px;border:0;background:transparent;color:#ff9800;cursor:pointer;font-size:11px;flex-shrink:0;line-height:20px;text-align:center;border-radius:4px} #sl3-panel .cexp:hover{background:#fff3e0} #sl3-panel .cinfo{flex:1;min-width:0;display:flex;align-items:baseline;gap:6px} #sl3-panel .ctitle{flex:1;min-width:0;font-size:13px;color:#333;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} #sl3-panel .cmeta{font-size:11px;color:#999;white-space:nowrap;flex-shrink:0} #sl3-panel .cbadge{font-size:10px;padding:2px 8px;border-radius:8px;flex-shrink:0;font-weight:bold;white-space:nowrap} #sl3-panel .cbadge.done{background:#e8f5e9;color:#2e7d32} #sl3-panel .cbadge.fail{background:#ffebee;color:#c62828} #sl3-panel .cbadge.cur{background:#fff3e0;color:#e65100} #sl3-panel .cbadge.skip{background:#f5f5f5;color:#999} #sl3-panel .cbadge.pend{background:#f5f5f5;color:#888} #sl3-panel .group-head{padding:5px 12px;font-size:12px;font-weight:bold;color:#e65100;background:#fff3e0;border-bottom:1px solid #ffe0b2;flex-shrink:0;position:sticky;top:0;z-index:2;display:flex;align-items:center} #sl3-panel .vlist{background:#fafafa;padding:2px 0 4px 32px} #sl3-panel .vrow{display:flex;align-items:center;gap:5px;padding:3px 10px;font-size:11px;border-bottom:1px dashed #f0f0f0} #sl3-panel .vrow:hover{background:#fff} #sl3-panel .vchk{width:13px;height:13px;flex-shrink:0;cursor:pointer;accent-color:#ff9800} #sl3-panel .vname{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#444} #sl3-panel .vmeta{color:#aaa;font-size:10px;flex-shrink:0} #sl3-panel .vrow.learned{opacity:.5} #sl3-panel .vrow.learned .vname{color:#bbb;text-decoration:line-through} #sl3-panel .vrow.watching .vname{color:#e65100} #sl3-panel .vbar{display:inline-block;width:36px;height:5px;background:#eee;border-radius:3px;overflow:hidden;flex-shrink:0;vertical-align:middle;margin-left:3px} #sl3-panel .vbar i{display:block;height:100%;background:linear-gradient(90deg,#4caf50,#81c784);transition:width .3s} #sl3-panel .vrow.learned .vbar i{background:#4caf50} #sl3-panel .vpct{font-size:9px;color:#999;flex-shrink:0;margin-left:2px} #sl3-panel .log-area{flex:1;min-height:0;overflow-y:auto;padding:6px 14px;font-size:11px;border-top:1px solid #eee;background:#fafafa} #sl3-panel .log-entry{padding:2px 0;border-bottom:1px dotted #eee;line-height:1.5} #sl3-panel .log-entry.ok{color:#2e7d32} #sl3-panel .log-entry.err{color:#c62828} #sl3-panel .log-entry.info{color:#1565c0} #sl3-panel .log-entry.warn{color:#e65100} #sl3-panel .log-ts{color:#bbb;margin-right:5px} /* ★★★ 底部两行 ★★★ */ #sl3-panel .ft { padding: 6px 10px; background: #fafafa; border-top: 1px solid #eee; display: flex; flex-direction: column; gap: 4px; flex-shrink: 0; font-size: 11px; color: #888; } #sl3-panel .ft .ft-row { display: flex; align-items: center; justify-content: space-between; flex-wrap: nowrap; gap: 4px; min-height: 22px; } #sl3-panel .ft .ft-row .ft-left { display: flex; align-items: center; gap: 6px; flex-shrink: 1; min-width: 0; } #sl3-panel .ft .ft-row .ft-right { display: flex; align-items: center; gap: 4px; flex-shrink: 0; } #sl3-panel .ft .ft-label { font-size: 10px; color: #555; white-space: nowrap; flex-shrink: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; } #sl3-panel .ft .btn-ft { padding: 1px 6px; border: 1px solid #ddd; border-radius: 3px; cursor: pointer; font-size: 10px; background: #fff; color: #555; white-space: nowrap; } #sl3-panel .ft .btn-ft:hover { border-color: #bbb; background: #f0f0f0; } #sl3-panel .ft .btn-qq { padding: 1px 6px; border: 1px solid #ff9800; border-radius: 3px; cursor: pointer; font-size: 10px; background: #fff3e0; color: #e65100; text-decoration: none; display: inline-flex; align-items: center; gap: 2px; white-space: nowrap; } #sl3-panel .ft .btn-qq:hover { background: #ffe0b2; } #sl3-panel .ft .btn-qq-copy { padding: 1px 6px; border: 1px solid #1976d2; border-radius: 3px; cursor: pointer; font-size: 10px; background: #e3f2fd; color: #0d47a1; display: inline-flex; align-items: center; gap: 2px; white-space: nowrap; } #sl3-panel .ft .btn-qq-copy:hover { background: #bbdefb; } #sl3-panel .ft #sl3-userinfo { font-size: 10px; color: #888; white-space: nowrap; } /* ★★★ 进度卡片 ★★★ */ #sl3-panel .prog-card{background:linear-gradient(135deg,#fff8e1,#fffde7);border-radius:8px;padding:6px 12px;margin:4px 14px;display:flex;align-items:center;gap:10px;border:1px solid #ffe082;flex-shrink:0} #sl3-panel .prog-ring{width:36px;height:36px;flex-shrink:0;position:relative} #sl3-panel .prog-ring svg{transform:rotate(-90deg)} #sl3-panel .ring-bg{fill:none;stroke:#ffe082;stroke-width:3.5} #sl3-panel .ring-fg{fill:none;stroke:#ff6f00;stroke-width:3.5;stroke-linecap:round;transition:stroke-dashoffset .6s ease} #sl3-panel .pct-text{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:10px;font-weight:bold;color:#e65100} #sl3-panel .prog-info{flex:1;min-width:0} #sl3-panel .pi-row{display:flex;justify-content:space-between;font-size:10px;line-height:1.7;color:#666} #sl3-panel .pi-val{font-weight:bold;color:#e65100} #sl3-panel .pi-label{color:#999} #sl3-panel .prog-auth { margin-left: auto; text-align: right; font-size: 12px; color: #666; flex-shrink: 0; padding-left: 10px; border-left: 1px solid #e0d5c8; white-space: normal; line-height: 1.4; } #sl3-panel .prog-auth .auth-remaining { font-weight: bold; color: #e65100; } #sl3-panel .prog-auth .auth-sub { font-size: 11px; color: #999; } #sl3-panel .prog-auth .auth-locked { color: #c62828; } #sl3-panel .pane{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0} #sl3-panel .vd-top{padding:8px 14px;background:#fff3e0;font-size:12px;color:#e65100;border-bottom:1px solid #ffe0b2;flex-shrink:0} #sl3-panel .vd-count{color:#999;margin-left:6px;font-weight:normal} #sl3-panel .vd-list-area{flex:1;overflow-y:auto;padding:4px 0;min-height:0} #sl3-panel .vd-row{display:flex;align-items:center;gap:6px;padding:5px 12px;border-bottom:1px solid #f5f5f5;font-size:11px} #sl3-panel .vd-idx{width:22px;color:#bbb;flex-shrink:0;text-align:right} #sl3-panel .vd-name{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#444} #sl3-panel .vd-dur{color:#aaa;flex-shrink:0} #sl3-panel .vd-bar{display:inline-block;width:60px;height:6px;background:#eee;border-radius:3px;overflow:hidden;flex-shrink:0} #sl3-panel .vd-bar i{display:block;height:100%;transition:width .3s} #sl3-panel .vd-pct{width:34px;text-align:right;color:#999;flex-shrink:0} #sl3-panel .vd-empty{padding:40px;text-align:center;color:#ccc;font-size:12px} #sl3-panel .vd-course-label{padding:6px 12px;background:#fff8e1;font-size:11px;font-weight:bold;color:#e65100;border-bottom:1px solid #ffe0b2;position:sticky;top:0;z-index:2} #sl3-panel .vd-course-meta{font-weight:normal;color:#999;margin-left:4px} #sl3-panel .vd-course-item{border-bottom:1px solid #f0f0f0} #sl3-panel .vd-course-header{display:flex;align-items:center;gap:6px;padding:7px 12px;cursor:pointer;font-size:11px;background:#fafafa;border-bottom:1px solid #f0f0f0;transition:background .15s} #sl3-panel .vd-course-header:hover{background:#fff8e1} #sl3-panel .vd-course-arrow{color:#ff9800;width:14px;text-align:center;flex-shrink:0;font-size:10px} #sl3-panel .vd-course-name{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:bold;color:#444} #sl3-panel .vd-course-videos{background:#fff} #sl3-panel .order-group{display:inline-flex;gap:4px;margin-left:6px} #sl3-panel .btn-order{padding:2px 8px;border:1px solid #ddd;border-radius:5px;background:#fff;color:#888;font-size:11px;cursor:pointer;outline:none} #sl3-panel .btn-order:hover{border-color:#ffb74d} #sl3-panel .btn-order.active{background:#ff9800;border-color:#ff9800;color:#fff;font-weight:bold} #sl3-panel .speed-box b{color:#ff6f00;min-width:32px} /* ★★★ 激活授权面板 ★★★ */ #sl3-license-pane { padding: 20px; display: flex; flex-direction: column; gap: 12px; } #sl3-license-pane .license-status-display { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: #f5f5f5; border-radius: 6px; border-left: 4px solid #ff6f00; font-size: 13px; } #sl3-license-pane .license-status-display .license-status-label { color: #666; } #sl3-license-pane .license-status-display .license-status-value { font-weight: bold; color: #c62828; } #sl3-license-pane .license-status-display .license-status-value.ok { color: #2e7d32; } #sl3-license-pane .license-input-row { display: flex; gap: 8px; flex-wrap: wrap; } #sl3-license-pane .license-input-row input { flex: 1; padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; min-width: 150px; } #sl3-license-pane .license-input-row input:focus { border-color: #ff6f00; outline: none; } #sl3-license-pane .license-input-row button { padding: 8px 20px; background: #ff6f00; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 13px; transition: 0.2s; } #sl3-license-pane .license-input-row button:hover { background: #e65c00; } #sl3-license-pane .btn-buy-key { display: inline-block; padding: 8px 20px; background: #4caf50; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; font-weight: bold; text-decoration: none; text-align: center; transition: 0.2s; width: 100%; } #sl3-license-pane .btn-buy-key:hover { background: #388e3c; } #sl3-license-pane .license-helper { text-align: center; font-size: 12px; color: #999; margin-top: 2px; line-height: 1.5; } #sl3-license-pane .license-status { margin-top: 4px; font-size: 13px; color: #333; text-align: center; } #sl3-license-pane .license-status .success { color: #2e7d32; } #sl3-license-pane .license-status .error { color: #c62828; } /* ====== 次数用完弹窗 ====== */ #sl3-overlay{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:999999;justify-content:center;align-items:center} #sl3-overlay.active{display:flex} #sl3-credits-modal{background:#fff;border-radius:12px;padding:30px 30px 24px;max-width:400px;width:90%;box-shadow:0 10px 40px rgba(0,0,0,0.3);text-align:center;position:relative} #sl3-credits-modal .modal-icon{font-size:48px;margin-bottom:8px} #sl3-credits-modal .modal-title{font-size:20px;font-weight:bold;color:#c62828;margin:8px 0} #sl3-credits-modal .modal-desc{font-size:14px;color:#555;margin:8px 0 16px;line-height:1.6} #sl3-credits-modal .modal-qq-group{font-size:16px;font-weight:bold;color:#1976d2;margin:8px 0} #sl3-credits-modal .modal-actions{display:flex;flex-direction:column;gap:8px;margin:12px 0} #sl3-credits-modal .modal-actions .btn-qq-large{display:block;padding:10px;border-radius:6px;font-size:14px;font-weight:bold;text-decoration:none;transition:0.2s;cursor:pointer;border:none} #sl3-credits-modal .modal-actions .btn-qq-large.primary{background:#ff6f00;color:#fff} #sl3-credits-modal .modal-actions .btn-qq-large.primary:hover{background:#e65c00} #sl3-credits-modal .modal-actions .btn-qq-large.secondary{background:#1976d2;color:#fff} #sl3-credits-modal .modal-actions .btn-qq-large.secondary:hover{background:#1565c0} #sl3-credits-modal .modal-close{display:inline-block;margin-top:12px;padding:8px 24px;border:1px solid #ddd;border-radius:6px;background:#fff;color:#666;font-size:13px;cursor:pointer;transition:0.2s} #sl3-credits-modal .modal-close:hover{background:#f5f5f5;border-color:#bbb} #sl3-credits-modal .modal-note{font-size:12px;color:#999;margin-top:10px} /* ====== 其他 ====== */ #sl3-lock-banner{background:#ffebee;border-bottom:1px solid #ffcdd2;padding:6px 12px;font-size:12px;color:#c62828;display:none;align-items:center;gap:8px;flex-shrink:0} #sl3-lock-banner .lock-icon{font-size:16px} #sl3-lock-banner .lock-msg{flex:1} #sl3-lock-banner .lock-close{cursor:pointer;opacity:0.6} #sl3-lock-banner .lock-close:hover{opacity:1} #sl3-announcement{background:#fff8e1;border-bottom:1px solid #ffe082;padding:6px 12px;font-size:12px;color:#e65100;display:flex;align-items:center;gap:8px;flex-shrink:0} #sl3-announcement .ann-icon{font-size:16px} #sl3-announcement .ann-content{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} #sl3-announcement .ann-close{cursor:pointer;opacity:0.6;font-size:14px} #sl3-announcement .ann-close:hover{opacity:1} `); // ============================================================ // ⚙️ 配置 // ============================================================ const CLOUD_FUNCTION_URL = 'https://1300815573-5c1s2m3wok.ap-shanghai.tencentscf.com'; const TRAIN_ID = 'dc6d78f2-bad8-4d09-b8da-0d758803dbe4'; const LIBRARY_ID = 'bb042e69-9a11-49a1-af22-0c3fab2e92b9'; const APP_ID = 'e5649925-441d-4a53-b525-51a2f1c4e0a8'; const ELECTIVE_CREDIT_CAP_H = 3; const REQUIRED_CREDIT_TOTAL = 10; const ANNOUNCEMENT_INTERVAL = 60000; const REMINDER_DURATION = 3600000; const QQ_GROUP_LINK = 'https://qm.qq.com/q/EBLDisQRBm'; const QQ_GROUP_NUMBER = '1041375393'; const QQ_AUTHOR = '3352639207'; const LOG_STORAGE_KEY = 'sl3_persistent_logs'; const MAX_LOG_ENTRIES = 500; const SCRIPT_VERSION = 'v1.0 berain'; // ============================================================ // 📦 全局状态 // ============================================================ let auth = null; let isRunning = false; let currentIdx = -1; let runMode = 'efficiency'; let videoOrderMode = 'random'; let speedMultiplier = 20; let coreLoaded = false; let authInfo = { remaining: 0, expireDate: '', isFreeTrialAvailable: false, locked: false }; let deviceFingerprint = ''; let announcementShown = false; let panelMinimized = false; let creditsModalShown = false; let creditsModalShownThisSession = false; // ============================================================ // 🔧 工具函数 // ============================================================ function loadAuth() { if (auth) return auth; var key = Object.keys(localStorage).find(function(k) { return k.indexOf('ND_UC_AUTH') === 0 && k.indexOf('&token') > -1; }); if (!key) return null; try { var raw = JSON.parse(localStorage.getItem(key)); var td = JSON.parse(raw.value); var user = JSON.parse(localStorage.getItem('X-EDU-WEB-USER') || '{}'); auth = { accessToken: td.access_token, macKey: td.mac_key, diff: td.diff || 0, userId: user.user_id, userName: user.real_name }; return auth; } catch(e) { return null; } } function getBrowserInfo() { try { const nav = navigator; return { userAgent: nav.userAgent, language: nav.language, platform: nav.platform, hardwareConcurrency: nav.hardwareConcurrency || 0, deviceMemory: nav.deviceMemory || 0, screenWidth: screen.width, screenHeight: screen.height, colorDepth: screen.colorDepth, timezoneOffset: new Date().getTimezoneOffset(), vendor: nav.vendor || '', productSub: nav.productSub || '' }; } catch(e) { return { error: 'Failed to collect browser info' }; } } function getDeviceId() { let id = localStorage.getItem('sl3_device_id'); if (!id) { id = 'did_' + Date.now().toString(36) + '_' + Math.random().toString(36).substring(2, 8); localStorage.setItem('sl3_device_id', id); } return id; } // ★★★ 设备指纹固定(不含时间戳后缀)★★★ function getDeviceFingerprint() { let stored = localStorage.getItem('sl3_device_fingerprint'); if (stored) { return stored; } try { const nav = navigator; const components = [ nav.userAgent, nav.language, nav.platform, nav.hardwareConcurrency || 0, nav.deviceMemory || 0, screen.width, screen.height, screen.colorDepth, new Date().getTimezoneOffset(), (function() { const canvas = document.createElement('canvas'); canvas.width = 200; canvas.height = 50; const ctx = canvas.getContext('2d'); ctx.textBaseline = 'top'; ctx.font = '14px Arial'; ctx.fillStyle = '#f60'; ctx.fillRect(125, 1, 62, 20); ctx.fillStyle = '#069'; ctx.fillText('智慧中小学', 2, 15); ctx.fillStyle = 'rgba(102, 204, 0, 0.7)'; ctx.fillText('静默学习', 4, 17); return canvas.toDataURL().substring(0, 100); })() ]; const raw = components.join('||'); let hash = 0, i, chr; for (i = 0; i < raw.length; i++) { chr = raw.charCodeAt(i); hash = ((hash << 5) - hash) + chr; hash |= 0; } const fp = 'fp_' + Math.abs(hash).toString(36); localStorage.setItem('sl3_device_fingerprint', fp); return fp; } catch(e) { let fallback = localStorage.getItem('sl3_device_fingerprint_fallback'); if (!fallback) { fallback = 'fp_fallback_' + Date.now().toString(36); localStorage.setItem('sl3_device_fingerprint_fallback', fallback); } localStorage.setItem('sl3_device_fingerprint', fallback); return fallback; } } function callCloudFunction(action, payload, retries = 2, timeout = 30000) { return new Promise((resolve, reject) => { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); fetch(CLOUD_FUNCTION_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action, payload: { ...payload, deviceFingerprint: deviceFingerprint } }), signal: controller.signal }) .then(resp => { clearTimeout(timer); if (!resp.ok) throw new Error('HTTP ' + resp.status); return resp.json(); }) .then(data => { if (data.code === 0) resolve(data.data); else reject(new Error(data.message || '云函数返回错误')); }) .catch(err => { clearTimeout(timer); if (retries > 0) { console.warn('[callCloud] 重试 (剩余' + retries + '次): ' + action, err.message); return callCloudFunction(action, payload, retries - 1, timeout); } reject(err); }); }); } // ★★★ 日志函数(过滤请求体 + 永久存储)★★★ function log(msg, cl) { if (typeof msg === 'string' && msg.indexOf('📤 请求体') === 0) { return; } var area = document.getElementById('sl3-log-area'); var entry = { time: new Date().toLocaleTimeString(), msg: String(msg), cl: cl || 'info' }; var logs = getStoredLogs(); logs.push(entry); if (logs.length > MAX_LOG_ENTRIES) { logs = logs.slice(-MAX_LOG_ENTRIES); } try { localStorage.setItem(LOG_STORAGE_KEY, JSON.stringify(logs)); } catch(e) {} if (area) { var d = document.createElement('div'); d.className = 'log-entry ' + entry.cl; d.innerHTML = '' + entry.time + '' + entry.msg.replace(/[&<>"]/g, c => ({ '&':'&','<':'<','>':'>','"':'"' }[c])); area.appendChild(d); if (document.getElementById('sl3-auto-scroll')?.checked !== false) { area.scrollTop = area.scrollHeight; } } updateSubStat(msg); } function getStoredLogs() { try { var data = localStorage.getItem(LOG_STORAGE_KEY); return data ? JSON.parse(data) : []; } catch(e) { return []; } } function loadLogsFromStorage() { var area = document.getElementById('sl3-log-area'); if (!area) return; var logs = getStoredLogs(); if (logs.length === 0) return; area.innerHTML = ''; logs.forEach(function(entry) { var d = document.createElement('div'); d.className = 'log-entry ' + entry.cl; d.innerHTML = '' + entry.time + '' + entry.msg.replace(/[&<>"]/g, c => ({ '&':'&','<':'<','>':'>','"':'"' }[c])); area.appendChild(d); }); area.scrollTop = area.scrollHeight; } function clearLogs() { localStorage.removeItem(LOG_STORAGE_KEY); var area = document.getElementById('sl3-log-area'); if (area) area.innerHTML = ''; } function updateSubStat(msg) { var el = document.getElementById('sl3-substat'); if (el) { const short = msg.length > 40 ? msg.substring(0, 40) + '...' : msg; el.textContent = short; } } function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function(c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); } function shuffle(arr) { var a = arr.slice(); for (var i = a.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var tmp = a[i]; a[i] = a[j]; a[j] = tmp; } return a; } function delay(ms) { return new Promise(function(r) { setTimeout(r, ms); }); } // ============================================================ // 📥 从云函数加载核心代码 // ============================================================ async function loadCoreCode() { try { const data = await callCloudFunction('getCoreCode', {}); if (data && data.coreCode) { eval(data.coreCode); coreLoaded = true; log('✅ 核心代码加载成功', 'ok'); if (typeof window.loadCourseList === 'function') { await window.loadCourseList(); } else { log('⚠️ 未暴露 loadCourseList', 'warn'); } return true; } log('❌ 核心代码为空', 'err'); return false; } catch(e) { log('❌ 加载核心代码失败: ' + e.message, 'err'); return false; } } // ============================================================ // 🔑 授权相关 // ============================================================ function saveLicenseKey(key) { if (key) { window.__sl3.licenseKey = key; localStorage.setItem('sl3_license_key', key); } else { localStorage.removeItem('sl3_license_key'); if (window.__sl3) delete window.__sl3.licenseKey; } } function getSavedLicenseKey() { return localStorage.getItem('sl3_license_key') || (window.__sl3 && window.__sl3.licenseKey); } function saveFreeTrialState(available) { localStorage.setItem('sl3_free_trial_available', available ? '1' : '0'); } function getFreeTrialState() { const avail = localStorage.getItem('sl3_free_trial_available'); return { available: avail === '1', used: false }; } async function updateAuthInfo() { try { const licenseKey = getSavedLicenseKey(); let payload = { deviceFingerprint: deviceFingerprint }; if (licenseKey) { payload.licenseKey = licenseKey; } const data = await callCloudFunction('authCheck', payload); if (data) { authInfo.remaining = data.remaining || 0; authInfo.expireDate = data.expireDate || ''; authInfo.isFreeTrialAvailable = data.isFreeTrialAvailable || false; authInfo.locked = data.locked || false; window.authInfo = { remaining: authInfo.remaining, isFreeTrialAvailable: authInfo.isFreeTrialAvailable, locked: authInfo.locked }; saveFreeTrialState(authInfo.isFreeTrialAvailable); updateProgAuth(); updateLicenseStatusUI(); updateFreeTrialUI(); updateLockBanner(); // ★★★ 检测次数用完弹窗(仅本次会话弹一次)★★★ var isLoggedIn = !!(window.__sl3 && window.__sl3.auth && window.__sl3.auth.userId); var hasLicense = !!getSavedLicenseKey(); var isOutOfCredits = isLoggedIn && !hasLicense && authInfo.remaining <= 0 && !authInfo.isFreeTrialAvailable; var isLocked = authInfo.locked === true; if (isOutOfCredits && !isLocked && !creditsModalShownThisSession) { setTimeout(function() { showCreditsModal(); }, 2000); } log('授权状态已更新: 剩余 ' + authInfo.remaining + ' 次' + (authInfo.isFreeTrialAvailable ? ' (可免费体验)' : '') + (authInfo.locked ? ' (设备已被锁定)' : ''), 'info'); } } catch(e) { log('查询授权状态失败: ' + e.message, 'err'); } } async function autoActivateFreeTrial() { if (authInfo.isFreeTrialAvailable) { log('🎁 检测到免费体验可用,自动激活...', 'info'); try { const data = await callCloudFunction('useFreeTrial', { deviceFingerprint: deviceFingerprint }); if (data && data.remaining !== undefined) { authInfo.remaining = data.remaining; authInfo.isFreeTrialAvailable = false; window.authInfo = { remaining: authInfo.remaining, isFreeTrialAvailable: false, locked: authInfo.locked }; saveFreeTrialState(false); updateProgAuth(); updateLicenseStatusUI(); updateFreeTrialUI(); log('✅ 免费体验自动激活成功,剩余 ' + data.remaining + ' 次', 'ok'); return true; } else { log('⚠️ 免费体验激活失败,请手动点击按钮', 'warn'); return false; } } catch (e) { log('❌ 免费体验自动激活异常: ' + e.message, 'err'); return false; } } return false; } async function verifyLicense(key) { try { const data = await callCloudFunction('verifyLicense', { licenseKey: key }); if (data && data.message === '激活成功') { authInfo.remaining = data.remaining; authInfo.expireDate = data.expireDate || ''; authInfo.locked = false; window.authInfo = { remaining: authInfo.remaining, isFreeTrialAvailable: false, locked: false }; saveLicenseKey(key); saveFreeTrialState(false); updateProgAuth(); updateLicenseStatusUI(); updateFreeTrialUI(); updateLockBanner(); log('✅ 激活成功,剩余 ' + data.remaining + ' 次', 'ok'); if (window.__sl3) window.__sl3.licenseKey = key; return true; } else { log('❌ 激活失败: ' + (data ? data.message : '未知错误'), 'err'); return false; } } catch(e) { log('❌ 激活异常: ' + e.message, 'err'); return false; } } async function useFreeTrial() { try { const data = await callCloudFunction('useFreeTrial', { deviceFingerprint: deviceFingerprint }); if (data && data.remaining !== undefined) { authInfo.remaining = data.remaining; authInfo.isFreeTrialAvailable = false; window.authInfo = { remaining: authInfo.remaining, isFreeTrialAvailable: false, locked: authInfo.locked }; saveFreeTrialState(false); updateProgAuth(); updateLicenseStatusUI(); updateFreeTrialUI(); log('✅ 免费体验成功,剩余 ' + data.remaining + ' 次', 'ok'); if (typeof window.startLearning === 'function') { window.startLearning(); } return true; } else { log('❌ 免费体验失败: ' + (data ? data.message : '未知错误'), 'err'); return false; } } catch(e) { log('❌ 免费体验异常: ' + e.message, 'err'); return false; } } // ============================================================ // 📢 公告系统 // ============================================================ async function fetchAnnouncement() { const lastClose = localStorage.getItem('sl3_announcement_close_time'); if (lastClose) { const diff = Date.now() - parseInt(lastClose); if (diff < REMINDER_DURATION) { return; } } try { const data = await callCloudFunction('getAnnouncement', {}); if (data && data.content) { showAnnouncement(data.content, data.type); } } catch(e) { console.warn('公告获取失败:', e.message); } } function showAnnouncement(content, type) { if (announcementShown) return; const panel = document.getElementById('sl3-panel'); if (!panel) return; const old = document.getElementById('sl3-announcement'); if (old) old.remove(); const div = document.createElement('div'); div.id = 'sl3-announcement'; const iconMap = { info: 'ℹ️', warning: '⚠️', success: '✅' }; div.innerHTML = ` ${iconMap[type] || 'ℹ️'} ${esc(content)} 暂缓1小时 `; const statusBar = document.getElementById('sl3-statusbar'); if (statusBar) { statusBar.parentNode.insertBefore(div, statusBar.nextSibling); } else { panel.insertBefore(div, panel.firstChild); } document.getElementById('ann-close').addEventListener('click', function() { div.remove(); announcementShown = true; }); document.getElementById('ann-snooze').addEventListener('click', function() { localStorage.setItem('sl3_announcement_close_time', Date.now().toString()); div.remove(); announcementShown = true; log('公告已暂缓1小时', 'info'); }); announcementShown = true; } // ============================================================ // 🖥️ 渲染函数 // ============================================================ function effectiveLearnedHours(c) { var plat = (c.learnedHours || 0); var localSec = 0; (c.videos || []).forEach(function(v) { if (v.vstatus === 2 || v.learned) localSec += (v.dur || 0); }); return Math.max(plat, localSec / 3600); } function courseReachedCap(c) { var cap = c.capped ? parseFloat(c.maxPeriodRaw) : ELECTIVE_CREDIT_CAP_H; return effectiveLearnedHours(c) >= cap; } function electiveGroupMet() { var courses = window.__sl3.courses || []; var total = 0, any = false; courses.forEach(function(c) { if (c.kind !== 'elective') return; total += effectiveLearnedHours(c); var cap = c.capped ? parseFloat(c.maxPeriodRaw) : ELECTIVE_CREDIT_CAP_H; if (effectiveLearnedHours(c) >= cap) any = true; }); return any || total >= ELECTIVE_CREDIT_CAP_H; } function render() { var l = document.getElementById('sl3-list'); if (!l) return; var courses = window.__sl3.courses || []; if (courses.length === 0) { l.innerHTML = '
暂无课程数据
'; return; } var html = []; var groups = { required: [], elective: [] }; courses.forEach(function(c) { if (c.kind === 'required') groups.required.push({ c: c, i: courses.indexOf(c) }); else groups.elective.push({ c: c, i: courses.indexOf(c) }); }); ['required', 'elective'].forEach(function(kind) { var items = groups[kind]; if (!items.length) return; var elecAnyMet = false; var elecTotalLearned = 0; if (kind === 'elective') { items.forEach(function(item) { elecTotalLearned += effectiveLearnedHours(item.c); var cap = item.c.capped ? parseFloat(item.c.maxPeriodRaw) : ELECTIVE_CREDIT_CAP_H; if (effectiveLearnedHours(item.c) >= cap) elecAnyMet = true; }); if (elecTotalLearned >= ELECTIVE_CREDIT_CAP_H) elecAnyMet = true; } var gtitle = kind === 'required' ? '必修 全部完成即可达标' : '选修(任一门达标即可)已学' + elecTotalLearned.toFixed(2) + 'h · 认定3学时'; var gcls = kind === 'elective' ? ' elec-group' : ' req-group'; var rows = items.map(function(item) { var c = item.c, i = item.i; var vids = c.videos || []; var doneV = vids.filter(function(v) { return v.vstatus === 2; }).length; var cap = c.capped ? parseFloat(c.maxPeriodRaw) : ELECTIVE_CREDIT_CAP_H; var isMet = effectiveLearnedHours(c) >= cap; if (kind === 'elective' && elecAnyMet) isMet = true; var badgeText = isMet ? '已达标' : '未达标'; var badgeCls = isMet ? 'done' : 'skip'; var row = '
' + '' + '' + '
' + '
' + esc(c.title) + '
' + '
' + vids.length + '/' + doneV + ' · ' + (c.capped ? ('封顶' + c.maxPeriodRaw) : '') + (effectiveLearnedHours(c) > 0.001 ? (' · 已学' + effectiveLearnedHours(c).toFixed(2)) : '') + '
' + '
' + '' + badgeText + '' + '
'; if (c.expanded && vids.length) { row += '
'; var sortedVi = vids.map(function(v, vi) { return vi; }).sort(function(a, b) { var aDone = (vids[a].vstatus === 2) || (vids[a].progress >= 100); var bDone = (vids[b].vstatus === 2) || (vids[b].progress >= 100); if (aDone && !bDone) return 1; if (!aDone && bDone) return -1; return 0; }); sortedVi.forEach(function(vi) { var v = vids[vi]; var isDone = (v.vstatus === 2) || (v.progress >= 100); var isWatch = !isDone && (v.progress > 0 || v.vstatus === 1); var vl = isDone ? ' learned' : (isWatch ? ' watching' : ''); var vsym = isDone ? ' ✓' : (isWatch ? ' ◐' : ' ○'); var pct = (v.progress || 0); row += '
' + '' + '' + esc(v.actName || '资源') + '' + '视频 · ' + (v.dur / 60).toFixed(1) + '分' + vsym + '' + '' + '' + pct + '%' + '
'; }); row += '
'; } return row; }).join(''); html.push('
' + gtitle + '
' + rows + '
'); }); l.innerHTML = html.join(''); updateSelCount(); } function renderVideoDetail() { var nameEl = document.getElementById('sl3-vd-course'); var countEl = document.getElementById('sl3-vd-count'); var listEl = document.getElementById('sl3-vd-list'); if (!listEl) return; var courses = window.__sl3.courses || []; if (courses.length === 0 || !courses.some(function(c) { return (c.videos || []).length; })) { if (nameEl) nameEl.textContent = ''; if (countEl) countEl.textContent = ''; listEl.innerHTML = '
暂无视频数据,请先加载课程
'; return; } var totalVids = 0, doneVids = 0; courses.forEach(function(c) { totalVids += (c.videos || []).length; doneVids += (c.videos || []).filter(function(v){return v.vstatus===2||v.progress>=100;}).length; }); var html = []; courses.forEach(function(c, ci) { var vids = c.videos || []; if (!vids.length) return; var isExp = c.vdExpanded; html.push('
' + '
' + '' + (isExp ? '▼' : '▶') + '' + '' + esc(c.title) + '' + '' + vids.length + '/' + vids.filter(function(v){return v.vstatus===2||v.progress>=100;}).length + (c.capped ? (' · 封顶' + c.maxPeriodRaw) : ' · 认定3') + (effectiveLearnedHours(c) > 0.001 ? (' · 已学' + effectiveLearnedHours(c).toFixed(2)) : '') + '' + '
'); if (isExp) { html.push('
'); vids.forEach(function(v, vi) { var isDone = (v.vstatus === 2) || (v.progress >= 100); var isWatch = !isDone && (v.progress > 0 || v.vstatus === 1); var color = isDone ? '#4caf50' : (isWatch ? '#ff9800' : '#ccc'); var pct = v.progress || 0; html.push('
' + '' + esc(v.actName || '资源') + '' + '' + (v.dur / 60).toFixed(1) + '分' + '' + '' + pct + '%' + '
'); }); html.push('
'); } html.push('
'); }); if (nameEl) nameEl.textContent = '全部课程'; if (countEl) countEl.textContent = totalVids + ' 个视频(✓' + doneVids + ')'; listEl.innerHTML = html.join(''); listEl.querySelectorAll('[data-vd-toggle]').forEach(function(el) { el.onclick = function() { var ci = parseInt(this.getAttribute('data-vd-toggle')); courses[ci].vdExpanded = !courses[ci].vdExpanded; renderVideoDetail(); }; }); } function renderProgCard() { var totalProg = window.__sl3.totalProg || { learned: 0, credited: 0, required: 10 }; var pct = totalProg.required > 0 ? Math.min(100, (totalProg.credited / totalProg.required) * 100) : 0; var C = 94.2; var ringFg = document.getElementById('sl3-ringfg'); var ringPct = document.getElementById('sl3-ringpct'); var piLearned = document.getElementById('sl3-pi-learned'); var piCredited = document.getElementById('sl3-pi-credited'); var piTotal = document.getElementById('sl3-pi-total'); if (ringFg) ringFg.setAttribute('stroke-dashoffset', (C * (1 - pct / 100)).toFixed(1)); if (ringPct) ringPct.textContent = Math.round(pct) + '%'; if (piLearned) piLearned.textContent = totalProg.learned.toFixed(2) + ' 学时'; if (piCredited) piCredited.textContent = totalProg.credited.toFixed(2) + ' / ' + totalProg.required + ' 学时'; if (piTotal) piTotal.textContent = Math.round(pct) + '%'; updateProgAuth(); updateLicenseStatusUI(); } function updateProgAuth() { var el = document.getElementById('sl3-auth-remaining'); var expire = document.getElementById('sl3-auth-expire'); if (!el) return; var isLoggedIn = !!(window.__sl3 && window.__sl3.auth && window.__sl3.auth.userId); var freeAvailable = authInfo.isFreeTrialAvailable || false; var remaining = authInfo.remaining || 0; var hasLicense = !!getSavedLicenseKey(); var isOutOfCredits = isLoggedIn && !hasLicense && remaining <= 0 && !freeAvailable; if (!isLoggedIn) { el.innerHTML = '🔓 登录免费体验
剩余 1 次'; if (expire) expire.textContent = ''; return; } if (isOutOfCredits) { el.innerHTML = '🔒 次数已用完
(联系作者购买)'; if (expire) expire.textContent = ''; return; } if (freeAvailable) { el.innerHTML = '🎁 免费体验
剩余 ' + (remaining || 1) + ' 次'; if (expire) expire.textContent = ''; return; } el.innerHTML = '剩余 ' + remaining + ' 次'; if (expire) expire.textContent = authInfo.expireDate ? '有效期至 ' + authInfo.expireDate : ''; } function updateLicenseStatusUI() { var statusValue = document.getElementById('license-status-value'); if (!statusValue) return; var isLoggedIn = !!(window.__sl3 && window.__sl3.auth && window.__sl3.auth.userId); var freeAvailable = authInfo.isFreeTrialAvailable || false; var remaining = authInfo.remaining || 0; var hasLicense = !!getSavedLicenseKey(); var isOutOfCredits = isLoggedIn && !hasLicense && remaining <= 0 && !freeAvailable; if (!isLoggedIn) { statusValue.textContent = '请先登录'; statusValue.className = 'license-status-value'; return; } if (isOutOfCredits) { statusValue.textContent = '次数已用完(联系作者购买)'; statusValue.className = 'license-status-value'; return; } if (freeAvailable) { statusValue.textContent = '🎁 免费体验可用'; statusValue.className = 'license-status-value ok'; return; } statusValue.textContent = '已激活,剩余 ' + remaining + ' 次'; statusValue.className = 'license-status-value ok'; } // ★★★ 次数用完弹窗(本次会话只弹一次)★★★ function showCreditsModal() { creditsModalShownThisSession = true; if (creditsModalShown) return; creditsModalShown = true; var overlay = document.createElement('div'); overlay.id = 'sl3-overlay'; overlay.innerHTML = `
`; document.body.appendChild(overlay); overlay.classList.add('active'); document.getElementById('sl3-modal-close').addEventListener('click', function() { overlay.classList.remove('active'); setTimeout(function() { overlay.remove(); }, 300); creditsModalShown = false; var tab = document.querySelector('#sl3-tabs .tab[data-tab="license"]'); if (tab) tab.click(); }); document.getElementById('sl3-modal-add-qq').addEventListener('click', function() { var qq = QQ_AUTHOR; if (navigator.clipboard) { navigator.clipboard.writeText(qq).then(function() { alert('✅ QQ号 ' + qq + ' 已复制,请添加好友'); }).catch(function() { fallbackCopy(qq); }); } else { fallbackCopy(qq); } }); function fallbackCopy(text) { var ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); alert('✅ QQ号 ' + text + ' 已复制,请添加好友'); } overlay.addEventListener('click', function(e) { if (e.target === overlay) { overlay.classList.remove('active'); setTimeout(function() { overlay.remove(); }, 300); creditsModalShown = false; var tab = document.querySelector('#sl3-tabs .tab[data-tab="license"]'); if (tab) tab.click(); } }); } function updateLockBanner() { const banner = document.getElementById('sl3-lock-banner'); if (!banner) return; if (authInfo.locked) { banner.style.display = 'flex'; banner.innerHTML = ` 🔒 设备已被锁定,无法使用。请联系管理员解锁。 `; document.getElementById('lock-close')?.addEventListener('click', function() { banner.style.display = 'none'; }); } else { banner.style.display = 'none'; } } function updateFreeTrialUI() { var btn = document.getElementById('sl3-free-trial-btn'); var statusEl = document.getElementById('sl3-license-status'); if (!btn || !statusEl) return; if (authInfo.isFreeTrialAvailable && !getSavedLicenseKey()) { btn.style.display = 'inline-block'; statusEl.innerHTML = '🎁 可免费体验一次,点击下方按钮开始学习'; } else { btn.style.display = 'none'; if (!getSavedLicenseKey() && !authInfo.isFreeTrialAvailable) { statusEl.innerHTML = '未激活,且无免费体验机会'; } } } function updateSelCount() { var courses = window.__sl3.courses || []; var n = courses.filter(function(c) { return c.selected; }).length; var v = 0; courses.forEach(function(c) { (c.videos || []).forEach(function(x) { if (x.selected) v++; }); }); var b = document.getElementById('sl3-start'); if (b && !isRunning) b.textContent = '▶ 开始' + (n > 0 || v > 0 ? (' (' + n + '课/' + v + '视频)') : ''); } function setRunStatus(running) { var dot = document.getElementById('sl3-sbdot'); var stat = document.getElementById('sl3-runstat'); var goBtn = document.getElementById('sl3-start'); var stopBtn = document.getElementById('sl3-stop'); isRunning = running; if (dot) dot.className = 'sb-dot ' + (running ? 'running' : 'stopped'); if (stat) stat.textContent = running ? '运行中' : '已停止'; if (goBtn) goBtn.disabled = running; if (stopBtn) stopBtn.disabled = !running; if (!running) { setTimeout(updateAuthInfo, 500); } } function updateProgressPct(pct) { var el = document.getElementById('sl3-sbpct'); if (el) el.textContent = Math.round(pct) + '%'; } function loadTotalProgress() { if (!auth || !auth.userId) return Promise.resolve(); return callCloudFunction('loadCourses', { auth: auth }) .then(function(data) { if (data && data.totalProgress) { window.__sl3.totalProg = data.totalProgress; renderProgCard(); log('总进度已更新: 已学习 ' + data.totalProgress.learned.toFixed(2) + 'h / 已认定 ' + data.totalProgress.credited.toFixed(2) + 'h', 'ok'); } return data; }) .catch(function(e) { log('总进度加载失败: ' + e.message, 'err'); }); } // ============================================================ // 🖥️ 创建 UI // ============================================================ function createUI() { var p = document.createElement('div'); p.id = 'sl3-panel'; const savedPos = localStorage.getItem('sl3_panel_pos'); if (savedPos) { try { const pos = JSON.parse(savedPos); p.style.right = 'auto'; p.style.top = pos.top + 'px'; p.style.left = pos.left + 'px'; } catch(e) {} } const minimized = localStorage.getItem('sl3_panel_minimized'); if (minimized === 'true') { p.classList.add('sl3-minimized'); panelMinimized = true; } p.innerHTML = `
📚
智慧中小学学习助手${SCRIPT_VERSION}
运行状态:已停止 等待开始... --%
0%
已学习0.00 学时
已认定0.00 / 10 学时
总进度0%
0
速度 20x
📋 课程目录
🎬 视频详情
🔑 激活授权
📝 日志
正在加载课程列表…
未登录
💬 购买密钥/反馈 (QQ群: ${QQ_GROUP_NUMBER})
加入QQ群
📚
`; document.body.appendChild(p); bindEvents(p); enableDrag(p); loadLogsFromStorage(); updateProgAuth(); updateLicenseStatusUI(); updateLockBanner(); updateAuthInfo(); autoActivateFreeTrial(); fetchAnnouncement(); setInterval(fetchAnnouncement, ANNOUNCEMENT_INTERVAL); } // ============================================================ // 🖱️ 拖拽 // ============================================================ function enableDrag(p) { var header = p.querySelector('.hd'); var isDragging = false, startX, startY, startLeft, startTop; header.onmousedown = function(e) { if (e.target.tagName === 'BUTTON' || e.target.tagName === 'SELECT' || e.target.tagName === 'INPUT') return; isDragging = true; startX = e.clientX; startY = e.clientY; var rect = p.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; p.style.right = 'auto'; p.style.top = startTop + 'px'; p.style.left = startLeft + 'px'; document.body.style.userSelect = 'none'; }; document.addEventListener('mousemove', function(e) { if (!isDragging) return; p.style.left = (startLeft + e.clientX - startX) + 'px'; p.style.top = (startTop + e.clientY - startY) + 'px'; }); document.addEventListener('mouseup', function() { if (isDragging) { const rect = p.getBoundingClientRect(); localStorage.setItem('sl3_panel_pos', JSON.stringify({ left: rect.left, top: rect.top })); } isDragging = false; document.body.style.userSelect = ''; }); } // ============================================================ // 🔗 事件绑定 // ============================================================ function bindEvents(p) { var minBtn = document.getElementById('sl3-min'); var bookIcon = document.getElementById('sl3-book-icon'); minBtn.onclick = function() { p.classList.toggle('sl3-minimized'); const isMin = p.classList.contains('sl3-minimized'); localStorage.setItem('sl3_panel_minimized', isMin ? 'true' : 'false'); panelMinimized = isMin; }; bookIcon.onclick = function() { p.classList.remove('sl3-minimized'); localStorage.setItem('sl3_panel_minimized', 'false'); panelMinimized = false; }; document.getElementById('sl3-tabs').addEventListener('click', function(e) { var t = e.target; if (!t.classList.contains('tab')) return; document.querySelectorAll('#sl3-tabs .tab').forEach(function(x) { x.classList.remove('active'); }); t.classList.add('active'); var tab = t.dataset.tab; document.getElementById('sl3-pane-courses').style.display = tab === 'courses' ? '' : 'none'; document.getElementById('sl3-pane-videos').style.display = tab === 'videos' ? '' : 'none'; document.getElementById('sl3-pane-license').style.display = tab === 'license' ? '' : 'none'; document.getElementById('sl3-pane-log').style.display = tab === 'log' ? '' : 'none'; if (tab === 'videos') renderVideoDetail(); }); document.getElementById('sl3-license-activate-btn').addEventListener('click', async function() { var input = document.getElementById('sl3-license-key-input'); var status = document.getElementById('sl3-license-status'); var key = input.value.trim(); if (!key) { status.innerHTML = '请输入激活码'; return; } status.innerHTML = '⏳ 验证中...'; var ok = await verifyLicense(key); if (ok) { status.innerHTML = '✅ 激活成功!剩余 ' + authInfo.remaining + ' 次'; input.value = ''; updateProgAuth(); updateLicenseStatusUI(); updateFreeTrialUI(); if (window.__sl3) window.__sl3.licenseKey = key; } else { status.innerHTML = '❌ 激活失败,请检查激活码'; } }); document.getElementById('sl3-buy-key-btn').addEventListener('click', function() { window.open(QQ_GROUP_LINK, '_blank'); }); document.getElementById('sl3-free-trial-btn')?.addEventListener('click', function() { if (authInfo.isFreeTrialAvailable) { useFreeTrial(); } else { log('免费体验已使用或不可用', 'warn'); } }); document.getElementById('sl3-mode').onchange = function() { runMode = this.value; if (window.__sl3) window.__sl3.runMode = runMode; log('模式切换为: ' + (runMode === 'berserk' ? '🔥 狂暴' : '⚡ 效率'), 'info'); }; var rndBtn = document.getElementById('sl3-video-random'); var ordBtn = document.getElementById('sl3-video-order'); function setOrderMode(m) { videoOrderMode = m; if (window.__sl3) window.__sl3.videoOrderMode = m; rndBtn.classList.toggle('active', m === 'random'); ordBtn.classList.toggle('active', m === 'sequential'); log('视频选择: ' + (m === 'random' ? '🎲 随机' : '🔢 按顺序'), 'info'); } rndBtn.onclick = function() { setOrderMode('random'); }; ordBtn.onclick = function() { setOrderMode('sequential'); }; var speedEl = document.getElementById('sl3-speed'); var speedValEl = document.getElementById('sl3-speed-val'); speedEl.oninput = function() { speedMultiplier = parseInt(speedEl.value) || 20; speedValEl.textContent = speedMultiplier + 'x'; if (window.__sl3) window.__sl3.speedMultiplier = speedMultiplier; }; document.getElementById('sl3-selall').onclick = function() { var courses = window.__sl3.courses || []; courses.forEach(function(c) { c.selected = true; (c.videos || []).forEach(function(v) { v.selected = false; }); }); render(); updateSelCount(); }; document.getElementById('sl3-selnone').onclick = function() { var courses = window.__sl3.courses || []; courses.forEach(function(c) { c.selected = false; (c.videos || []).forEach(function(v) { v.selected = v.vstatus !== 2; }); }); render(); updateSelCount(); }; document.getElementById('sl3-clearsel').onclick = function() { var courses = window.__sl3.courses || []; courses.forEach(function(c) { c.selected = false; (c.videos || []).forEach(function(v) { v.selected = false; }); }); render(); updateSelCount(); }; document.getElementById('sl3-smart').onclick = function() { if (typeof window.autoSelectGoal === 'function') { window.autoSelectGoal(); if (typeof window.render === 'function') { window.render(); } else if (window.__sl3 && typeof window.__sl3.render === 'function') { window.__sl3.render(); } if (typeof window.updateSelCount === 'function') { window.updateSelCount(); } else if (window.__sl3 && typeof window.__sl3.updateSelCount === 'function') { window.__sl3.updateSelCount(); } log('🎲 随机选课完成(4必修 + 随机1选修)', 'ok'); } else { log('autoSelectGoal 未定义,请确保核心代码已加载', 'err'); } }; document.getElementById('sl3-start').onclick = function() { if (isRunning) return; var isLoggedIn = !!(window.__sl3 && window.__sl3.auth && window.__sl3.auth.userId); if (!isLoggedIn) { log('请先登录平台', 'err'); return; } var hasLicense = !!getSavedLicenseKey(); var freeAvailable = authInfo.isFreeTrialAvailable || false; var remaining = authInfo.remaining || 0; if (!hasLicense && !freeAvailable && remaining <= 0) { showCreditsModal(); return; } if (hasLicense && remaining <= 0) { showCreditsModal(); return; } setRunStatus(true); if (typeof window.startLearning === 'function') { window.startLearning(); } else { log('startLearning 未定义', 'err'); setRunStatus(false); } }; document.getElementById('sl3-stop').onclick = function() { if (!isRunning) return; if (typeof window.stopLearning === 'function') { window.stopLearning(); } else { log('stopLearning 未定义,使用本地停止', 'warn'); window.__sl3.isRunning = false; setRunStatus(false); } }; document.getElementById('sl3-copy-log').onclick = function() { var area = document.getElementById('sl3-log-area'); if (!area) return; var entries = area.querySelectorAll('.log-entry'); if (!entries.length) { log('日志为空', 'info'); return; } var text = Array.from(entries).map(function(el) { return el.textContent; }).join('\n'); if (navigator.clipboard) { navigator.clipboard.writeText(text).then(function() { log('已复制日志', 'ok'); }).catch(function() { log('复制失败', 'err'); }); } }; document.getElementById('sl3-clearlog').onclick = function() { if (confirm('确定要清空所有日志吗?(不可恢复)')) { clearLogs(); log('🗑 日志已清空', 'info'); } }; document.getElementById('sl3-copy-qq').addEventListener('click', function() { var qq = QQ_GROUP_NUMBER; if (navigator.clipboard) { navigator.clipboard.writeText(qq).then(function() { alert('✅ QQ群号 ' + qq + ' 已复制,请添加'); }).catch(function() { fallbackCopy(qq); }); } else { fallbackCopy(qq); } function fallbackCopy(text) { var ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); alert('✅ QQ群号 ' + text + ' 已复制,请添加'); } }); var l = document.getElementById('sl3-list'); l.addEventListener('change', function(e) { var t = e.target; var courses = window.__sl3.courses || []; if (t.classList.contains('cchk')) { var i = +t.dataset.ci; courses[i].selected = t.checked; if (t.checked) { if (typeof window.randomPickCourseVideos === 'function') { window.randomPickCourseVideos(courses[i]); log('🎲 已为「' + courses[i].title + '」随机选 ' + courses[i].videos.filter(function(v) { return v.selected; }).length + ' 个视频(够封顶学时)', 'info'); } else { log('randomPickCourseVideos 未定义', 'err'); } } else { (courses[i].videos || []).forEach(function(v) { v.selected = false; }); } render(); updateSelCount(); } else if (t.classList.contains('vchk')) { var ci = +t.dataset.ci, vi = +t.dataset.vi; courses[ci].videos[vi].selected = t.checked; if (t.checked) courses[ci].selected = false; render(); updateSelCount(); } }); l.addEventListener('click', function(e) { var t = e.target; var courses = window.__sl3.courses || []; if (t.classList.contains('cexp')) { var i = +t.dataset.ci; courses[i].expanded = !courses[i].expanded; render(); if (document.getElementById('sl3-pane-videos').style.display !== 'none') renderVideoDetail(); } }); } // ============================================================ // 🚀 包装 startLearning // ============================================================ function wrapStartLearning() { if (window._startLearningWrapped) return; window._startLearningWrapped = true; window.originalStartLearning = window.startLearning; window.__sl3 = window.__sl3 || {}; window.__sl3._originalStartLearning = window.originalStartLearning; window.startLearning = async function() { if (authInfo.locked) { log('❌ 设备已被锁定,无法学习', 'err'); setRunStatus(false); return; } const licenseKey = window.__sl3?.licenseKey || null; const fingerprint = window.__sl3?.fingerprint || ''; const callCloud = window.callCloudFunction || window.__sl3?.callCloudFunction; const userId = window.__sl3?.auth?.userId || null; let remaining = window.authInfo?.remaining || 0; if (remaining <= 0 && licenseKey) { try { const fresh = await callCloud('authCheck', { licenseKey, deviceFingerprint: fingerprint }); if (fresh && fresh.remaining > 0) { remaining = fresh.remaining; window.authInfo = { remaining: fresh.remaining, isFreeTrialAvailable: fresh.isFreeTrialAvailable || false, locked: fresh.locked || false }; authInfo = window.authInfo; if (authInfo.locked) { log('❌ 设备已被锁定', 'err'); setRunStatus(false); return; } } else { log('⚠️ 授权不足,请充值', 'warn'); setRunStatus(false); return; } } catch (e) { log('❌ 授权查询失败: ' + e.message, 'err'); setRunStatus(false); return; } } if (!licenseKey && !window.authInfo?.isFreeTrialAvailable) { log('❌ 未激活且无免费体验机会,请激活', 'err'); setRunStatus(false); return; } log('✅ 授权检查通过,开始学习...', 'ok'); setRunStatus(true); try { await window.originalStartLearning(); } catch (e) { log('❌ 学习过程异常: ' + e.message, 'err'); window.__sl3.isRunning = false; setRunStatus(false); return; } finally { window.__sl3.isRunning = false; setRunStatus(false); } if (licenseKey) { try { const useResult = await callCloud('authUse', { licenseKey, amount: 1, deviceFingerprint: fingerprint, userId: userId }); if (useResult && useResult.remaining !== undefined) { log('✅ 学习完成,已扣除 1 次,剩余 ' + useResult.remaining + ' 次', 'ok'); window.authInfo.remaining = useResult.remaining; authInfo.remaining = useResult.remaining; updateProgAuth(); updateLicenseStatusUI(); } else { log('⚠️ 扣减授权失败,请手动检查剩余次数', 'warn'); } } catch (e) { log('❌ 扣减授权失败: ' + e.message, 'err'); } } else { log('✅ 免费体验学习完成', 'ok'); } updateAuthInfo(); }; console.log('✅ startLearning 已修正(含锁检查 & userId传递)'); } // ============================================================ // 🚀 初始化 // ============================================================ function init() { // 重置会话弹窗标记 creditsModalShownThisSession = false; creditsModalShown = false; var url = location.href; if (url.indexOf('teacher.ykt.eduyun.cn') === -1 && url.indexOf('basic.smartedu.cn') === -1) return; if (document.getElementById('sl3-panel')) return; deviceFingerprint = getDeviceFingerprint(); const savedKey = localStorage.getItem('sl3_license_key'); if (savedKey) { if (!window.__sl3) window.__sl3 = {}; window.__sl3.licenseKey = savedKey; } const freeState = getFreeTrialState(); window.authInfo = { remaining: 0, isFreeTrialAvailable: freeState.available, locked: false }; authInfo = { remaining: 0, isFreeTrialAvailable: freeState.available, locked: false }; window.getBrowserInfo = getBrowserInfo; setTimeout(function() { if (document.getElementById('sl3-panel')) return; createUI(); var authData = loadAuth(); if (authData) { document.getElementById('sl3-userinfo').textContent = '👤 ' + authData.userName; log('📚 ' + SCRIPT_VERSION + ' 就绪 - ' + authData.userName, 'ok'); window.__sl3 = window.__sl3 || {}; window.__sl3.auth = authData; window.__sl3.courses = []; window.__sl3.totalProg = { learned: 0, credited: 0, required: 10 }; window.__sl3.runMode = runMode; window.__sl3.videoOrderMode = videoOrderMode; window.__sl3.speedMultiplier = speedMultiplier; window.__sl3.isRunning = false; window.__sl3.log = log; window.__sl3.render = render; window.__sl3.updateSelCount = updateSelCount; window.__sl3.setRunStatus = setRunStatus; window.__sl3.updateProgressPct = updateProgressPct; window.__sl3.callCloudFunction = callCloudFunction; window.__sl3.TRAIN_ID = TRAIN_ID; window.__sl3.LIBRARY_ID = LIBRARY_ID; window.__sl3.APP_ID = APP_ID; window.__sl3.ELECTIVE_CREDIT_CAP_H = ELECTIVE_CREDIT_CAP_H; window.__sl3.REQUIRED_CREDIT_TOTAL = REQUIRED_CREDIT_TOTAL; if (savedKey) window.__sl3.licenseKey = savedKey; window.__sl3.fingerprint = deviceFingerprint; window.__sl3.authInfo = window.authInfo; window.render = render; window.renderVideoDetail = renderVideoDetail; window.renderProgCard = renderProgCard; window.updateSelCount = updateSelCount; window.setRunStatus = setRunStatus; window.updateProgressPct = updateProgressPct; window.effectiveLearnedHours = effectiveLearnedHours; window.courseReachedCap = courseReachedCap; window.electiveGroupMet = electiveGroupMet; window.loadTotalProgress = loadTotalProgress; window.shuffle = shuffle; window.delay = delay; window.log = log; window.esc = esc; window.updateAuthInfo = updateAuthInfo; window.verifyLicense = verifyLicense; window.useFreeTrial = useFreeTrial; window.saveLicenseKey = saveLicenseKey; window.getSavedLicenseKey = getSavedLicenseKey; window.getDeviceFingerprint = getDeviceFingerprint; window.getBrowserInfo = getBrowserInfo; window.callCloudFunction = callCloudFunction; window.__sl3.cloudFunctionUrl = CLOUD_FUNCTION_URL; loadCoreCode().then(() => { wrapStartLearning(); updateAuthInfo(); }); } else { document.getElementById('sl3-userinfo').textContent = '❌ 未登录'; log('请先登录平台', 'err'); var l = document.getElementById('sl3-list'); if (l) l.innerHTML = '
🔒 请先登录
'; updateProgAuth(); updateLicenseStatusUI(); } }, 500); } window.callCloudFunction = callCloudFunction; window.getSavedLicenseKey = getSavedLicenseKey; window.getDeviceFingerprint = getDeviceFingerprint; window.getBrowserInfo = getBrowserInfo; init(); })();