Repository navigation
Expand file tree
/
Copy pathzy-eclass-enhancer.user.js
More file actions
828 lines (751 loc) · 29.3 KB
/
Copy pathzy-eclass-enhancer.user.js
File metadata and controls
828 lines (751 loc) · 29.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
// ==UserScript==
// @name 新乐学优化:待办显示课程名 · 隐藏课程 · 修正返回链接 · 移动端按钮
// @namespace https://zy-eclass.bit.edu.cn/
// @version 1.3.4
// @description 首页「待办事项」显示所属课程名称;「我的课程」默认隐藏指定关键字的课程(默认:大学物理实验),并在列表顶部提示隐藏数量,可点「显示」临时取消隐藏(仅当前标签页有效,关闭浏览器后自动恢复隐藏);课程页左上角「返回」改回课程列表(原地址为站外域名);修复手机竖屏下弹窗底部按钮(交作业「确认」)跑到屏幕外点不到的问题;本地上传后自动刷新个人资源库;课程封面占位图不再显示;窄屏下全屏课件页的章节侧边栏收进抽屉,课件独占整屏宽度。
// @match https://zy-eclass.bit.edu.cn/*
// @run-at document-start
// @grant none
// ==/UserScript==
(function () {
'use strict';
/* ============================== 配置区 ============================== */
const CONFIG = {
// 首页 - 待办事项:在标题前显示所属课程
todo: {
enabled: true,
},
// 我的课程:默认隐藏某些课程
courses: {
enabled: true,
// 课程名称包含以下任一关键字即隐藏(区分大小写,按需修改)
hideKeywords: ['大学物理实验'],
// 列表顶部的提示文案,{n} = 课程数量,{keywords} = 关键字
notice: '已隐藏 {n} 门「{keywords}」课程',
noticeRevealed: '已显示 {n} 门「{keywords}」课程',
},
// 课程页(章节/作业等子页面)左上角的「返回」:
// 站点模板把它的地址硬编码成了站外的 AI 工作台,这里改回课程列表
backLink: {
enabled: true,
// 优先用页面自带的 #courseListUrl 值,取不到时用这个
fallbackUrl: '/user/courses',
},
// 移动端弹窗(如交作业时「选择附件」):站点按 layout viewport 计算 100% 高度,
// 手机竖屏下底部按钮会落到屏幕外且无法滚动到
mobilePopup: {
enabled: true,
// 可视宽度不超过该值时启用(手机/窄屏)
maxWidth: 820,
},
// 附件选择弹窗:本地上传完成后,服务端资源列表可能还没收录新文件,
// 导致「个人资源库」里看不到它,需要来回切 tab 才会出现。
// 这里在上传完成后按需重试刷新资源列表。
resourceLibrary: {
enabled: true,
},
// 课程封面:站点用的都是同一张占位图,没有有效信息,直接去掉
courseCover: {
enabled: true,
// 首页右侧「最近访问」卡片里的封面也一起去掉
hideOnDashboard: true,
},
// 全屏课件页(从「章节」里点开的课件):站点的章节侧边栏会把课件挤没,
// 窄屏下改成抽屉,抽屉里仍可切换课件
fullscreen: {
enabled: true,
// 可视宽度不超过该值时启用(手机/窄屏)
maxWidth: 820,
},
};
/* =================================================================== */
const TODO_CARD_SEL = '.latest-todo-list';
const COURSE_LIST_SEL = 'ol.semester-list';
const APPLY_DELAY = 120;
// 课程封面统一是一张占位图,去掉后把空间还给文字。
// 列表里 .course-content 原本是 float:right + width: calc(100% - 150px),
// 隐藏封面后要改回整行宽度。
const COVER_CSS = !CONFIG.courseCover.enabled
? ''
: `
.user-courses .course .item .course-cover { display: none !important; }
.user-courses .course .item .course-content {
width: 100% !important;
margin-left: 0 !important;
float: none !important;
}
${
CONFIG.courseCover.hideOnDashboard
? `.latest-visit-course .visited-course .course-cover { display: none !important; }
.latest-visit-course .visited-course .course-info {
width: 100% !important;
padding-left: 0 !important;
}`
: ''
}
`;
// 全屏课件页:站点把章节侧边栏做成 .activity-area 的表格列(min-width: 221px),
// 课件区宽度还写死成 100vw - 261px。手机上 390px 的视口里课件只剩 129px 宽,
// 连同侧边栏一起把课件挤没了。窄屏下改成:侧边栏收进抽屉,课件独占整屏。
// 这些规则都挂在 html.zy-fs-mobile 上,宽屏(桌面)完全不受影响。
const FULLSCREEN_CSS = !CONFIG.fullscreen.enabled
? ''
: `
/* body 有 min-width: 1080px,不解除的话整页仍按 1080 排版,课件要横向拖动才看得全 */
html.zy-fs-mobile body.fullscreen-activity { min-width: 0 !important; }
html.zy-fs-mobile .fullscreen-activity .activity-area {
display: flex !important;
flex-direction: column;
width: 100% !important;
/* 原来这 844px 是侧边栏的 height:100vh 撑出来的,侧边栏变成浮层后要自己撑 */
height: 100vh !important;
}
html.zy-fs-mobile .fullscreen-activity .activity-area .fullscreen-right {
flex: 1 1 0;
width: auto !important;
min-height: 0;
}
html.zy-fs-mobile .fullscreen-activity .activity-area .activity-content-box {
width: auto !important;
/* 站点按「100vh - 84px」给它定高,那个 84 是桌面页头的高度;这里让内容自己决定,
由外层 .fullscreen-right 统一滚动。 */
height: auto !important;
}
/* 站点按 1000px 排课件正文,手机上会横向溢出 */
html.zy-fs-mobile .fullscreen-activity .activity-area .activity-content-box .activity-content-bd {
min-width: 0 !important;
}
html.zy-fs-mobile .fullscreen-activity .activity-area .activity-content-box img {
max-width: 100% !important;
height: auto !important;
}
html.zy-fs-mobile .zy-fs-bar {
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
height: 44px;
padding: 0 8px;
box-sizing: border-box;
border-bottom: 1px solid #e8eaec;
background-color: #fff;
}
html.zy-fs-mobile .zy-fs-btn {
display: inline-flex;
align-items: center;
gap: 4px;
height: 32px;
padding: 0 12px;
border: 1px solid #dcdfe6;
border-radius: 6px;
background-color: #fff;
color: #232429;
font-family: inherit;
font-size: 14px;
line-height: 1;
cursor: pointer;
}
html.zy-fs-mobile .zy-fs-btn:active { background-color: #f2f3f5; }
html.zy-fs-mobile .fullscreen-activity .activity-area .module-syllabus-nav {
position: fixed !important;
top: 0;
left: 0;
bottom: 0;
width: min(84vw, 320px) !important;
min-width: 0 !important;
height: 100% !important;
display: block !important;
z-index: 2001;
visibility: hidden;
transform: translateX(-100%);
transition: transform .22s ease, visibility 0s linear .22s;
box-shadow: 2px 0 16px rgba(15, 23, 42, .28);
}
html.zy-fs-mobile.zy-fs-nav-open .fullscreen-activity .activity-area .module-syllabus-nav {
visibility: visible;
transform: none;
transition: transform .22s ease;
}
.zy-fs-backdrop {
display: none;
position: fixed;
inset: 0;
z-index: 2000;
background-color: rgba(15, 23, 42, .38);
}
html.zy-fs-nav-open .zy-fs-backdrop { display: block; }
`;
const CSS = `
${COVER_CSS}
${FULLSCREEN_CSS}
.zy-course-chip {
flex: 0 0 auto;
max-width: 55%;
padding: 0 6px;
border-radius: 9px;
background-color: #e8f0fd;
color: #2b6cb0;
font-size: 12px;
line-height: 18px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
${TODO_CARD_SEL} .todo-title.zy-has-course {
display: flex;
align-items: center;
column-gap: 6px;
min-height: 24px;
}
${TODO_CARD_SEL} .todo-title.zy-has-course .title-text {
flex: 0 1 auto;
min-width: 0;
max-width: none;
}
${TODO_CARD_SEL} .todo-title.zy-has-course.zy-has-type {
padding-right: 46px;
}
/* 站点把「作业/测试」角标做成了 left:auto 的绝对定位,会盖住标题开头(以及这里的课程标签),
所以有课程标签时把它摆到右侧 */
${TODO_CARD_SEL} .todo-title.zy-has-course.zy-has-type .activity-type {
left: auto;
right: 0;
}
.zy-course-hidden {
display: none !important;
}
.zy-hidden-notice {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin: 0 0 15px;
padding: 8px 12px;
border: 1px dashed #cbd5e1;
border-radius: 4px;
background-color: #f8fafc;
color: #64748b;
font-size: 13px;
line-height: 1.5;
}
.zy-toggle-hidden {
flex: 0 0 auto;
padding: 2px 10px;
border: 1px solid #cbd5e1;
border-radius: 4px;
background-color: #fff;
color: #2b6cb0;
font-family: inherit;
font-size: 12px;
line-height: 18px;
cursor: pointer;
}
.zy-toggle-hidden:hover {
border-color: #93c5fd;
background-color: #eff6ff;
}
.zy-toggle-hidden:focus-visible {
outline: 2px solid #93c5fd;
outline-offset: 1px;
}
/* 移动端弹窗:只把外层浮层限制在“可视区”并允许滚动。
不要改 .popup-content / .main-area 的高度和宽度:站点内部大量使用
height: calc(100% - Npx) 的百分比布局(如附件选择、文件列表),
一旦把父级高度改成 auto,这些区域会塌缩成 0 高,上传框等控件就“消失”了。 */
.zy-mobile-popup.popup-area {
width: var(--zy-vw) !important;
height: var(--zy-vh) !important;
left: var(--zy-vx, 0px) !important;
top: var(--zy-vy, 0px) !important;
overflow: auto !important;
-webkit-overflow-scrolling: touch;
}
/* 站点用内联 margin-top 按 layout viewport 垂直居中,这里改为贴近可视区顶部 */
.zy-mobile-popup > .popup-content {
margin: 8px auto !important;
}
/* 弹窗比屏幕宽时(如附件选择 900px),底部按钮组按可视宽度排布并拉长,
这样不用横向拖动就能点到 */
.zy-mobile-popup > .popup-content > .popup-footer {
width: min(100%, var(--zy-vw)) !important;
box-sizing: border-box !important;
}
.zy-mobile-popup > .popup-content > .popup-footer .form-buttons {
display: flex !important;
align-items: center;
justify-content: center;
gap: 8px;
padding: 0 8px !important;
box-sizing: border-box;
}
.zy-mobile-popup > .popup-content > .popup-footer .form-buttons > .button,
.zy-mobile-popup > .popup-content > .popup-footer > .button {
flex: 1 1 0 !important;
width: calc(100% - 16px) !important;
max-width: 220px !important;
margin: 0 !important;
}
/* 本地上传页只有一个居中的上传框,收窄到可视宽度即可;
个人资源库等文件列表保持原宽度,避免表格列被压乱 */
.zy-mobile-popup .select-local-content {
width: var(--zy-vw) !important;
box-sizing: border-box !important;
}
`;
/* ============================== 工具函数 ============================== */
function injectStyle() {
if (document.getElementById('zy-enhancer-style')) return;
const style = document.createElement('style');
style.id = 'zy-enhancer-style';
style.textContent = CSS;
(document.head || document.documentElement).appendChild(style);
}
// 该页面是 AngularJS 应用,数据挂在 scope 上(DOM 里只渲染了一部分字段)
function scopeOf(el) {
try {
const ng = window.angular;
return ng && ng.element ? ng.element(el).scope() : null;
} catch (e) {
return null;
}
}
function setClass(el, name, on) {
if (el.classList.contains(name) === on) return; // 避免无意义的 DOM 变更
el.classList.toggle(name, on);
}
/* ========================= 1. 首页 - 待办事项 ========================= */
function enhanceTodos() {
const card = document.querySelector(TODO_CARD_SEL);
if (!card) return;
const cardScope = scopeOf(card);
const todoList = cardScope && Array.isArray(cardScope.todoList) ? cardScope.todoList : [];
card.querySelectorAll('.todo-list').forEach((item, index) => {
if (item.dataset.zyTodo === 'done') return;
const itemScope = scopeOf(item);
const data = (itemScope && itemScope.todoData) || todoList[index] || null;
const courseName = data && data.course_name ? String(data.course_name).trim() : '';
if (!courseName) return;
const title = item.querySelector('.todo-title');
if (!title) return;
const chip = document.createElement('span');
chip.className = 'zy-course-chip';
chip.textContent = courseName;
chip.title = '课程:' + courseName;
title.insertBefore(chip, title.firstChild);
setClass(title, 'zy-has-course', true);
setClass(title, 'zy-has-type', !!item.querySelector('.activity-type'));
item.dataset.zyTodo = 'done';
});
}
/* ========================= 2. 我的课程 - 隐藏课程 ========================= */
function courseNameOf(item) {
const scope = scopeOf(item);
const course = scope && scope.course;
if (course) {
const name = course.display_name || course.name || course.second_name;
if (name) return String(name).trim();
}
const el = item.querySelector('.course-name');
return el ? el.textContent.trim() : '';
}
function isHiddenKeywordCourse(name) {
if (!name) return false;
return CONFIG.courses.hideKeywords.some((keyword) => keyword && name.includes(keyword));
}
// 仅在当前标签页记住「已临时取消隐藏」;关闭标签页/浏览器后回到默认隐藏。
// 隐私模式等场景下 sessionStorage 可能不可用,此时退化为内存状态。
const REVEAL_KEY = 'zy-eclass-enhancer:showHiddenCourses:v1';
let revealed = false;
try {
revealed = sessionStorage.getItem(REVEAL_KEY) === '1';
} catch (e) {
revealed = false;
}
function setRevealed(value) {
revealed = value;
try {
sessionStorage.setItem(REVEAL_KEY, value ? '1' : '0');
} catch (e) {
// 存不了就只在内存里生效
}
}
function hiddenNoticeText(count) {
const template = revealed ? CONFIG.courses.noticeRevealed : CONFIG.courses.notice;
return template
.replace('{n}', String(count))
.replace('{keywords}', CONFIG.courses.hideKeywords.join('、'));
}
function updateHiddenNotice(host, hiddenCount) {
const container = host.parentElement;
let notice = document.querySelector('.zy-hidden-notice');
if (!container || hiddenCount <= 0) {
if (notice) notice.remove();
return;
}
if (!notice) {
notice = document.createElement('div');
notice.className = 'zy-hidden-notice';
const text = document.createElement('span');
text.className = 'zy-hidden-text';
const button = document.createElement('button');
button.type = 'button';
button.className = 'zy-toggle-hidden';
notice.appendChild(text);
notice.appendChild(button);
}
if (notice.parentElement !== container) container.insertBefore(notice, host);
// 只更新文本/按钮,不整块替换节点,避免按钮失焦
const text = notice.querySelector('.zy-hidden-text');
const button = notice.querySelector('.zy-toggle-hidden');
const noticeText = hiddenNoticeText(hiddenCount);
if (text && text.textContent !== noticeText) text.textContent = noticeText;
if (button) {
const label = revealed ? '隐藏' : '显示';
if (button.textContent !== label) button.textContent = label;
button.setAttribute('aria-expanded', revealed ? 'true' : 'false');
button.setAttribute(
'aria-label',
revealed ? '重新隐藏这些课程' : '临时显示这些被隐藏的课程'
);
}
const tip = revealed
? '仅当前标签页有效,关闭浏览器后会自动恢复隐藏'
: '点击「显示」可临时查看;仅当前标签页有效,关闭浏览器后自动恢复隐藏';
if (notice.title !== tip) notice.title = tip;
}
function enhanceCourses() {
const host = document.querySelector('.user-courses ' + COURSE_LIST_SEL);
const notice = document.querySelector('.zy-hidden-notice');
if (!host) {
if (notice) notice.remove(); // 列表还没渲染/被过滤为空时不要留下孤立的提示
return;
}
let hiddenCount = 0;
host.querySelectorAll('li.course').forEach((item) => {
const matched = isHiddenKeywordCourse(courseNameOf(item));
if (matched) hiddenCount++;
setClass(item, 'zy-course-hidden', matched && !revealed);
});
updateHiddenNotice(host, hiddenCount);
}
// 用事件委托,提示条被 Angular 重建时无需重新绑定
function onCourseToggleClick(event) {
const target = event.target instanceof Element ? event.target.closest('.zy-toggle-hidden') : null;
if (!target) return;
event.preventDefault();
event.stopPropagation();
setRevealed(!revealed);
applyAll();
}
/* ===================== 3. 课程页「返回」链接 ===================== */
// 站点把「返回」硬编码成了 https://aita.yanhekt.cn/... ,而页面里
// #courseListUrl(服务端渲染)才是真正的课程列表地址
function courseListUrl() {
const input = document.getElementById('courseListUrl');
const value = input && input.value ? input.value.trim() : '';
// 只接受站内相对地址,避免再次跳到站外
const isRelative = value && !/^[a-z][a-z0-9+.-]*:/i.test(value) && !value.startsWith('//');
return isRelative ? value : CONFIG.backLink.fallbackUrl;
}
function enhanceBackLink() {
const target = courseListUrl();
document.querySelectorAll('a.go-back-link[href]').forEach((link) => {
let host = null;
try {
host = new URL(link.getAttribute('href'), location.href).host;
} catch (e) {
host = null;
}
if (!host || host === location.host) return; // 站内(含相对地址)的一律不动
if (link.getAttribute('href') === target) return;
link.setAttribute('href', target);
if (!link.title) link.title = '返回我的课程';
});
}
/* ===================== 4. 移动端弹窗高度 ===================== */
// 手机上 layout viewport 可以远大于可视区(本站 body 有 min-width: 1080px,
// 而 .popup-area 是 position:fixed + height:100%,100% 算的是 layout viewport),
// 于是弹窗按“屏幕外”的高度排版,底部的确认/提交按钮点不到。
function visibleViewport() {
const de = document.documentElement;
const vv = window.visualViewport;
const w = Math.min(de.clientWidth || window.innerWidth, vv ? Math.round(vv.width) : Infinity);
const h = Math.min(de.clientHeight || window.innerHeight, vv ? Math.round(vv.height) : Infinity);
return {
w: w || window.innerWidth,
h: h || window.innerHeight,
x: vv ? Math.round(vv.offsetLeft) : 0,
y: vv ? Math.round(vv.offsetTop) : 0,
};
}
function enhanceMobilePopups() {
const de = document.documentElement;
const view = visibleViewport();
// 只在真正出问题时才接管:弹窗按 layout viewport(100%)排版,
// 而它比可视区高(手机上的典型情况)。桌面窗口即使窄,两者也基本一致。
const layoutGap = window.innerHeight - de.clientHeight > 40;
const coarse = window.matchMedia('(pointer: coarse)').matches;
const fitNeeded = layoutGap || (coarse && view.w <= CONFIG.mobilePopup.maxWidth);
document.querySelectorAll('.popup-area').forEach((modal) => {
const fit = CONFIG.mobilePopup.enabled && fitNeeded && modal.classList.contains('open');
setClass(modal, 'zy-mobile-popup', fit);
if (!fit) return;
// 让浮层正好覆盖可视区(含视觉视口的偏移)
modal.style.setProperty('--zy-vw', view.w + 'px');
modal.style.setProperty('--zy-vh', view.h + 'px');
modal.style.setProperty('--zy-vx', view.x + 'px');
modal.style.setProperty('--zy-vy', view.y + 'px');
});
}
/* ===================== 5. 本地上传后刷新个人资源库 ===================== */
// 站点把「正在上传」的临时项插进列表(progress < 99),上传完成后再从服务端
// 重新取列表;此时服务端可能还没收录这个文件,于是新文件从「个人资源库」消失,
// 要来回切 tab 等它同步好才会出现。这里在检测到上传完成后按需重试刷新。
const FILE_SELECT_SEL = '#file-select';
const RESOURCE_UPLOAD_TTL = 2 * 60 * 1000;
const RESOURCE_REFRESH_DELAYS = [700, 1600, 3200, 6000];
const resourceState = {
scope: null,
pending: new Map(), // 文件名 -> 上传完成时间
timer: 0,
attempt: 0,
};
function openFileSelect() {
const popup = document.querySelector(FILE_SELECT_SEL);
return popup && popup.classList.contains('open') ? popup : null;
}
function fileSelectScope(popup) {
const node = popup.querySelector('.popup-content') || popup;
const scope = scopeOf(node);
return scope && scope.model ? scope : null;
}
// 只有「个人资源库」真正显示出来时才做判断:上传成功后站点会把 model.tab
// 提前设成 repository,但界面上还停在「本地上传」
function showingRepository(popup) {
const active = popup.querySelector('.tabs .tab-title a.active');
const click = active && active.getAttribute('ng-click');
return !!click && click.indexOf("changeTab('repository')") !== -1;
}
function repositoryHas(scope, names) {
const list = (scope.model.pagination && scope.model.pagination.currentList) || [];
const present = new Set();
for (const item of list) {
// 带 progress 的是站点塞进来的本地上传项(含上传中),不代表服务端已有该文件
if (item.progress != null) continue;
if (item.name) present.add(String(item.name));
}
for (const name of names) if (!present.has(name)) return false;
return true;
}
function bindResourceRefresh(scope) {
if (resourceState.scope === scope) return;
resourceState.scope = scope;
resourceState.pending.clear();
resourceState.attempt = 0;
const remember = (item) => {
// 文件夹里的文件不单独检查:根目录里出现的是文件夹本身(由 addFolder 记录)
if (!item || !item.name || item.isFolderFile || !openFileSelect()) return;
resourceState.pending.set(String(item.name), Date.now());
resourceState.attempt = 0;
scheduleResourceCheck();
};
scope.$on('fileUploadSuccess', (event, file) => remember(file));
scope.$on('addFolder', (event, folder) => remember(folder));
}
function scheduleResourceCheck(delay = 1000) {
if (resourceState.timer) return;
resourceState.timer = setTimeout(checkResourceRefresh, delay);
}
function checkResourceRefresh() {
resourceState.timer = 0;
const popup = openFileSelect();
if (!popup) return;
const scope = fileSelectScope(popup);
if (!scope || resourceState.scope !== scope) return;
const model = scope.model;
const now = Date.now();
for (const [name, at] of resourceState.pending) {
if (now - at > RESOURCE_UPLOAD_TTL) resourceState.pending.delete(name);
}
if (!resourceState.pending.size) return;
// 新上传的文件只会出现在资源库根目录的第一页,且不带筛选/搜索条件
if (
model.tab !== 'repository' ||
(model.path && model.path.length) ||
model.searching ||
(model.fileType && model.fileType !== 'all') ||
!showingRepository(popup)
) {
return;
}
if (repositoryHas(scope, resourceState.pending.keys())) {
resourceState.pending.clear();
resourceState.attempt = 0;
return;
}
if (resourceState.attempt >= RESOURCE_REFRESH_DELAYS.length) return;
const delay = RESOURCE_REFRESH_DELAYS[resourceState.attempt++];
try {
// 走和站点一样的取数方法,但不弹加载遮罩、也不重置分页/筛选
const noop = (window.angular && window.angular.noop) || function () {};
scope.$evalAsync(() => model.changePage(1, noop));
} catch (e) {
console.error('[zy-eclass-enhancer] 刷新个人资源库失败:', e);
return;
}
resourceState.timer = setTimeout(checkResourceRefresh, delay);
}
function enhanceResourceLibrary() {
const popup = openFileSelect();
if (!popup) return;
const scope = fileSelectScope(popup);
if (!scope) return;
bindResourceRefresh(scope);
// 用户切到「个人资源库」(tab 的 class 变化会触发本轮 applyAll)时,
// 先等站点自己的请求回来,再判断新文件是否缺失
if (resourceState.pending.size && showingRepository(popup)) {
scheduleResourceCheck(1200);
}
}
/* ===================== 6. 全屏课件页:侧边栏挤掉课件 ===================== */
// 结构:.activity-area 是 display:table,章节侧边栏(.module-syllabus-nav)
// 和课件区(.fullscreen-right)各占一个单元格;课件区里的 .activity-content-box
// 宽度写死成 calc(100vw - 261px)。窄屏下把侧边栏抽成抽屉,课件区吃满整屏。
function fullscreenParts() {
const area = document.querySelector('.fullscreen-activity .activity-area');
const nav = area && area.querySelector('.module-syllabus-nav');
return area && nav ? { area, nav } : null;
}
function buildFullscreenBar(nav) {
const back = document.createElement('button');
back.type = 'button';
back.className = 'zy-fs-btn';
back.textContent = '‹ 返回';
// 侧边栏顶部原本就有「返回课程」,现在被收进抽屉了,这里代它点一下
back.addEventListener('click', () => {
const link = nav.querySelector('.return-link a');
if (link) link.click();
else history.back();
});
const menu = document.createElement('button');
menu.type = 'button';
menu.className = 'zy-fs-btn';
menu.textContent = '☰ 章节';
menu.addEventListener('click', () => {
const root = document.documentElement;
setClass(root, 'zy-fs-nav-open', !root.classList.contains('zy-fs-nav-open'));
});
const bar = document.createElement('div');
bar.className = 'zy-fs-bar';
bar.appendChild(back);
bar.appendChild(menu);
return bar;
}
function enhanceFullscreenReader() {
const root = document.documentElement;
const parts = fullscreenParts();
const narrow = window.matchMedia(`(max-width: ${CONFIG.fullscreen.maxWidth}px)`).matches;
const active = CONFIG.fullscreen.enabled && !!parts && narrow;
setClass(root, 'zy-fs-mobile', active);
if (!active) {
setClass(root, 'zy-fs-nav-open', false);
document.querySelectorAll('.zy-fs-bar, .zy-fs-backdrop').forEach((node) => node.remove());
return;
}
const { area, nav } = parts;
if (!area.querySelector('.zy-fs-bar')) {
area.insertBefore(buildFullscreenBar(nav), area.firstChild);
}
if (!document.querySelector('.zy-fs-backdrop') && document.body) {
const backdrop = document.createElement('div');
backdrop.className = 'zy-fs-backdrop';
backdrop.addEventListener('click', () => setClass(root, 'zy-fs-nav-open', false));
document.body.appendChild(backdrop);
}
// 选中某个课件后收起抽屉(站点随后会重载路由)
if (!nav.hasAttribute('data-zy-fs-bound')) {
nav.setAttribute('data-zy-fs-bound', '1');
nav.addEventListener('click', (event) => {
if (event.target.closest('a, li')) setClass(root, 'zy-fs-nav-open', false);
});
}
}
/* ============================== 调度 ============================== */
function applyAll() {
injectStyle();
if (CONFIG.todo.enabled) {
try {
enhanceTodos();
} catch (e) {
console.error('[zy-eclass-enhancer] 待办事项处理失败:', e);
}
}
if (CONFIG.courses.enabled) {
try {
enhanceCourses();
} catch (e) {
console.error('[zy-eclass-enhancer] 课程列表处理失败:', e);
}
}
if (CONFIG.backLink.enabled) {
try {
enhanceBackLink();
} catch (e) {
console.error('[zy-eclass-enhancer] 返回链接处理失败:', e);
}
}
if (CONFIG.mobilePopup.enabled) {
try {
enhanceMobilePopups();
} catch (e) {
console.error('[zy-eclass-enhancer] 移动端弹窗处理失败:', e);
}
}
if (CONFIG.resourceLibrary.enabled) {
try {
enhanceResourceLibrary();
} catch (e) {
console.error('[zy-eclass-enhancer] 个人资源库处理失败:', e);
}
}
if (CONFIG.fullscreen.enabled) {
try {
enhanceFullscreenReader();
} catch (e) {
console.error('[zy-eclass-enhancer] 全屏课件页处理失败:', e);
}
}
}
let scheduled = false;
function scheduleApply() {
if (scheduled) return;
scheduled = true;
setTimeout(() => {
scheduled = false;
applyAll();
}, APPLY_DELAY);
}
// Angular 会在筛选、翻页、展开时重建节点,所以持续监听 DOM 变化;
// class 变化用于捕捉弹窗打开/关闭(站点给 .popup-area 加 .open)
new MutationObserver(scheduleApply).observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class'],
});
window.addEventListener('resize', scheduleApply);
window.addEventListener('orientationchange', scheduleApply);
if (window.visualViewport) {
// 手机浏览器地址栏收起/展开时可视高度会变化
window.visualViewport.addEventListener('resize', scheduleApply);
}
if (CONFIG.courses.enabled) {
// 捕获阶段拦截,避免站点自己的点击处理也响应
document.addEventListener('click', onCourseToggleClick, true);
}
applyAll();
setTimeout(applyAll, 500);
setTimeout(applyAll, 1500);
window.addEventListener('load', applyAll);
})();