// ==UserScript==
// @name TG任务助手前台面板
// @namespace tg-task-monitor-ui
// @version 2.0.4
// @description 读取 TG任务状态后台扫描器 的共享结果,在 TG 页面右下角显示任务助手抽屉
// @match https://tg.zcst.edu.cn/*
// @match https://www.educoder.net/*
// @match http://172.16.36.150/*
// @storageName tg-exam-monitor-shared
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant unsafeWindow
// @run-at document-idle
// ==/UserScript==
(function () {// ==UserScript==
// @name TG任务助手前台面板
// @namespace tg-task-monitor-ui
// @version 2.0.4
// @description 读取 TG任务状态后台扫描器 的共享结果,在 TG 页面右下角显示任务助手抽屉
// @match https://tg.zcst.edu.cn/*
// @match https://www.educoder.net/*
// @match http://172.16.36.150/*
// @storageName tg-exam-monitor-shared
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant unsafeWindow
// @run-at document-idle
// ==/UserScript==
(function () {
"use strict";
const MANUAL_LOGIN = "";
const TG_INTRANET_HOST = "172.16.36.150";
const TG_INTRANET_ORIGIN = "http://172.16.36.150";
const TG_EXTERNAL_ORIGIN = "https://tg.zcst.edu.cn";
const STORE_KEY = "TG_EXAM_MONITOR_RESULT";
const STORE_KEY_TG_RESULT = "TG_EXAM_MONITOR_TG_RESULT";
const STORE_KEY_EDUCODER_RESULT = "TG_EXAM_MONITOR_EDUCODER_RESULT";
const STORE_KEY_LAST_ERROR = "TG_EXAM_MONITOR_LAST_ERROR";
const STORE_KEY_LAST_RUNNING = "TG_EXAM_MONITOR_LAST_RUNNING";
const STORE_KEY_FILTER_YEAR_MONTH = "TG_TASK_FILTER_YEAR_MONTH";
const STORE_KEY_AUTO_LOGIN = "TG_TASK_MONITOR_AUTO_LOGIN";
const STORE_KEY_COURSE_COLLAPSE = "TG_TASK_COURSE_COLLAPSE";
const STORE_KEY_SECTION_COLLAPSE = "TG_TASK_SECTION_COLLAPSE";
const STORE_KEY_PANEL_STATE = "TG_TASK_PANEL_STATE";
const STORE_KEY_REFRESH_REQUEST = "TG_TASK_ASSISTANT_REFRESH_REQUEST";
const STORE_KEY_REFRESH_STATUS = "TG_TASK_ASSISTANT_REFRESH_STATUS";
const STORE_KEY_REFRESH_HANDLED = "TG_TASK_REFRESH_HANDLED";
const STORE_KEY_AUTO_REFRESH_DONE_AT = "TG_TASK_ASSISTANT_AUTO_REFRESH_DONE_AT";
const STORE_KEY_IGNORED_TASKS = "TG_TASK_ASSISTANT_IGNORED_TASKS";
const DANGER_DAYS_THRESHOLD = 10;
const DANGER_NOTIFY_KEY = "TG_TASK_ASSISTANT_DANGER_NOTIFY_KEY";
const ROOT_ID = "__tg_task_assistant_root__";
const BUTTON_ID = "__tg_task_assistant_button__";
const DRAWER_ID = "__tg_task_assistant_drawer__";
const SVG_FILTER_ID = "__tg_liquid_glass_filter__";
const OPEN_KEY = "TG_TASK_ASSISTANT_OPEN";
const FILTER_KEY = "TG_TASK_ASSISTANT_FILTER";
let countdownIntervalId = null;
let pollTimerId = null;
let resizeObserverAttached = false;
let cacheDeletedNotice = "";
let latestResultLogin = "";
let jumpTaskRegistry = new Map();
let jumpTaskSeq = 0;
let pollWasRunning = false;
let latestRenderedScanTimestamp = 0;
let resizeApplyTimer = null;
let activeRefreshRequestId = "";
let activeRefreshStartedAt = 0;
let lastLoggedRefreshStatusKey = "";
let panelAutoRefreshTimerId = null;
let panelAutoRefreshRequested = false;
let latestTaskRegistry = new Map();
let hasLoggedTaskKeySample = false;
let hasLoggedDeadlineSample = false;
const PANEL_AUTO_REFRESH_DELAY_MS = 2000;
const css = `
#${BUTTON_ID}, #${DRAWER_ID} {
--tg-cyan: #67e8f9;
--tg-cyan-soft: rgba(103, 232, 249, .22);
--tg-violet: #8b5cf6;
--tg-violet-soft: rgba(139, 92, 246, .2);
--tg-pink: #f0abfc;
--tg-pink-soft: rgba(240, 171, 252, .18);
--tg-red: #fb7185;
--tg-amber: #fbbf24;
--tg-green: #86efac;
--tg-text: rgba(244, 247, 251, .94);
--tg-muted: rgba(203, 213, 225, .68);
--tg-faint: rgba(148, 163, 184, .48);
--tg-glass: rgba(12, 18, 30, .64);
--tg-glass-strong: rgba(15, 23, 42, .76);
--tg-line: rgba(148, 227, 255, .18);
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, "Microsoft YaHei", sans-serif;
color: var(--tg-text);
box-sizing: border-box;
letter-spacing: 0;
}
#${BUTTON_ID} *, #${DRAWER_ID} * {
box-sizing: border-box;
letter-spacing: 0;
}
#${BUTTON_ID} {
position: fixed;
right: 22px;
bottom: 24px;
z-index: 2147483646;
width: 62px;
height: 62px;
border: 1px solid rgba(103, 232, 249, .34);
border-radius: 50%;
background:
radial-gradient(circle at 32% 24%, rgba(255,255,255,.34), transparent 24%),
radial-gradient(circle at 70% 76%, rgba(139,92,246,.34), transparent 34%),
linear-gradient(145deg, rgba(12,18,30,.82), rgba(8,12,22,.66));
color: rgba(242, 251, 255, .96);
box-shadow:
0 22px 52px rgba(0, 0, 0, .44),
0 0 34px rgba(103, 232, 249, .18),
inset 0 1px 0 rgba(255,255,255,.24),
inset 0 -18px 42px rgba(139,92,246,.12);
backdrop-filter: blur(22px) saturate(145%);
-webkit-backdrop-filter: blur(22px) saturate(145%);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 760;
line-height: 1.16;
text-align: center;
overflow: visible;
user-select: none;
touch-action: none;
transition: transform .38s cubic-bezier(.16, 1, .3, 1), border-color .38s ease, box-shadow .38s ease;
animation: tgFloatButton 7s ease-in-out infinite;
}
#${BUTTON_ID}::before {
content: "";
position: absolute;
inset: -9px;
border-radius: inherit;
background: conic-gradient(from 140deg, transparent, rgba(103,232,249,.22), transparent, rgba(240,171,252,.16), transparent);
filter: blur(12px);
opacity: .7;
z-index: -1;
}
#${BUTTON_ID}:hover {
transform: translateY(-4px) scale(1.035);
border-color: rgba(103, 232, 249, .62);
box-shadow:
0 28px 70px rgba(0, 0, 0, .52),
0 0 48px rgba(103, 232, 249, .26),
0 0 58px rgba(139, 92, 246, .16),
inset 0 1px 0 rgba(255,255,255,.32);
}
#${BUTTON_ID} .tg-task-button-badge {
position: absolute;
right: -4px;
top: -5px;
min-width: 22px;
height: 22px;
padding: 0 6px;
border-radius: 999px;
border: 1px solid rgba(255,255,255,.44);
background: linear-gradient(135deg, rgba(251,113,133,.94), rgba(240,171,252,.74));
color: #fff;
font-size: 11px;
line-height: 20px;
box-shadow: 0 0 20px rgba(251,113,133,.34);
}
#${DRAWER_ID} {
position: fixed;
right: 18px;
top: 18px;
z-index: 2147483647;
width: min(520px, calc(100vw - 28px));
height: min(760px, calc(100vh - 48px));
min-width: 390px;
min-height: 480px;
max-width: calc(100vw - 40px);
max-height: calc(100vh - 40px);
resize: both;
display: flex;
flex-direction: column;
overflow: hidden;
border-radius: 30px;
color: var(--tg-text);
background:
linear-gradient(145deg, rgba(14, 21, 36, .72), rgba(6, 10, 18, .68)),
radial-gradient(circle at 18% 0%, rgba(103,232,249,.12), transparent 38%),
radial-gradient(circle at 92% 8%, rgba(139,92,246,.16), transparent 42%);
border: 1px solid rgba(160, 231, 255, .2);
box-shadow:
-34px 28px 90px rgba(0,0,0,.56),
0 0 0 1px rgba(255,255,255,.035) inset,
0 0 56px rgba(103,232,249,.12),
0 0 88px rgba(139,92,246,.1);
backdrop-filter: blur(34px) saturate(150%);
-webkit-backdrop-filter: blur(34px) saturate(150%);
transform: translateX(calc(100% + 38px)) scale(.985);
opacity: .42;
transition:
transform .58s cubic-bezier(.16, 1, .3, 1),
opacity .38s ease,
box-shadow .45s ease;
}
#${DRAWER_ID}.tg-open {
transform: translateX(0) scale(1);
opacity: 1;
}
#${DRAWER_ID}::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background-image:
radial-gradient(circle at 14% 16%, rgba(103,232,249,.22) 0 1px, transparent 2px),
radial-gradient(circle at 84% 22%, rgba(240,171,252,.18) 0 1px, transparent 2px),
radial-gradient(circle at 64% 74%, rgba(139,92,246,.16) 0 1px, transparent 2px),
linear-gradient(rgba(255,255,255,.035) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,.024) 1px, transparent 1px);
background-size: 128px 128px, 184px 184px, 148px 148px, 44px 44px, 44px 44px;
mask-image: linear-gradient(to bottom, rgba(0,0,0,.92), rgba(0,0,0,.26));
opacity: .42;
animation: tgParticleDrift 22s linear infinite;
}
#${DRAWER_ID}::after {
content: "";
position: absolute;
inset: 1px;
pointer-events: none;
border-radius: 29px;
background:
linear-gradient(135deg, rgba(255,255,255,.18), transparent 24%),
linear-gradient(315deg, rgba(103,232,249,.1), transparent 28%),
radial-gradient(circle at 78% 14%, rgba(240,171,252,.12), transparent 34%);
mix-blend-mode: screen;
opacity: .55;
}
#${DRAWER_ID} .tg-ambient {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
z-index: 0;
}
#${DRAWER_ID} .tg-orb,
#${DRAWER_ID} .tg-ring {
position: absolute;
display: block;
border-radius: 999px;
filter: blur(.2px);
opacity: .7;
transform: translate3d(0,0,0);
}
#${DRAWER_ID} .tg-orb-a {
width: 150px;
height: 150px;
right: -42px;
top: 70px;
background: radial-gradient(circle at 34% 30%, rgba(255,255,255,.38), rgba(103,232,249,.18) 28%, rgba(103,232,249,.04) 68%, transparent 74%);
filter: blur(1px);
animation: tgSlowFloatA 15s ease-in-out infinite;
}
#${DRAWER_ID} .tg-orb-b {
width: 92px;
height: 92px;
left: 32px;
bottom: 92px;
background: radial-gradient(circle at 34% 28%, rgba(255,255,255,.25), rgba(240,171,252,.18) 38%, transparent 72%);
filter: blur(1.5px);
animation: tgSlowFloatB 18s ease-in-out infinite;
}
#${DRAWER_ID} .tg-ring-a {
width: 178px;
height: 178px;
left: -78px;
top: 178px;
border: 1px solid rgba(103,232,249,.16);
box-shadow: inset 0 0 34px rgba(103,232,249,.08), 0 0 30px rgba(139,92,246,.08);
transform: rotate(-18deg);
animation: tgRingDrift 21s ease-in-out infinite;
}
#${DRAWER_ID} .tg-header,
#${DRAWER_ID} .tg-body,
#${DRAWER_ID} .tg-actions {
position: relative;
z-index: 1;
}
#${DRAWER_ID} .tg-header {
flex: 0 0 auto;
min-height: 78px;
padding: 18px 18px 14px;
border-bottom: 1px solid rgba(148, 227, 255, .13);
background: linear-gradient(180deg, rgba(255,255,255,.055), rgba(255,255,255,.018));
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
}
#${DRAWER_ID} .tg-title {
min-width: 0;
}
#${DRAWER_ID} .tg-title-main {
font-size: 22px;
font-weight: 760;
line-height: 1.15;
color: rgba(248, 250, 252, .98);
text-shadow: 0 0 22px rgba(103,232,249,.12);
}
#${DRAWER_ID} .tg-title-sub {
margin-top: 6px;
color: var(--tg-muted);
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#${DRAWER_ID} .tg-header-actions {
display: flex;
gap: 8px;
flex: 0 0 auto;
}
#${DRAWER_ID} .tg-icon-btn {
width: 34px;
height: 34px;
border: 1px solid rgba(148, 227, 255, .18);
background: rgba(255,255,255,.055);
color: rgba(235, 245, 255, .9);
border-radius: 12px;
cursor: pointer;
font-size: 13px;
line-height: 32px;
text-align: center;
box-shadow: inset 0 1px 0 rgba(255,255,255,.12);
transition: transform .28s cubic-bezier(.16, 1, .3, 1), border-color .28s ease, background .28s ease, box-shadow .28s ease;
}
#${DRAWER_ID} .tg-icon-btn:hover {
transform: translateY(-2px);
border-color: rgba(103,232,249,.44);
background: rgba(103,232,249,.09);
box-shadow: 0 0 22px rgba(103,232,249,.12), inset 0 1px 0 rgba(255,255,255,.18);
}
#${DRAWER_ID} .tg-top-refresh {
width: auto;
min-width: 74px;
padding: 0 11px;
font-size: 12px;
white-space: nowrap;
}
#${DRAWER_ID} .tg-body {
flex: 1 1 auto;
overflow-y: auto;
padding: 16px;
scrollbar-width: thin;
scrollbar-color: rgba(103,232,249,.28) transparent;
}
#${DRAWER_ID} .tg-body::-webkit-scrollbar {
width: 9px;
}
#${DRAWER_ID} .tg-body::-webkit-scrollbar-thumb {
background: rgba(103,232,249,.2);
border: 3px solid transparent;
border-radius: 999px;
background-clip: padding-box;
}
#${DRAWER_ID} .tg-meta,
#${DRAWER_ID} .tg-alert,
#${DRAWER_ID} .tg-empty,
#${DRAWER_ID} .tg-date-filter,
#${DRAWER_ID} .tg-summary-item,
#${DRAWER_ID} .tg-card {
position: relative;
overflow: hidden;
border: 1px solid rgba(148, 227, 255, .15);
background:
linear-gradient(145deg, rgba(255,255,255,.09), rgba(255,255,255,.035)),
rgba(8, 13, 24, .42);
box-shadow:
0 18px 42px rgba(0,0,0,.25),
inset 0 1px 0 rgba(255,255,255,.09),
inset 0 -1px 0 rgba(255,255,255,.035);
backdrop-filter: blur(20px) saturate(145%);
-webkit-backdrop-filter: blur(20px) saturate(145%);
}
#${DRAWER_ID} .tg-meta::before,
#${DRAWER_ID} .tg-alert::before,
#${DRAWER_ID} .tg-empty::before,
#${DRAWER_ID} .tg-date-filter::before,
#${DRAWER_ID} .tg-summary-item::before,
#${DRAWER_ID} .tg-card::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background-image:
radial-gradient(circle at 18% 0%, rgba(255,255,255,.12), transparent 34%),
repeating-linear-gradient(0deg, rgba(255,255,255,.025) 0 1px, transparent 1px 4px);
opacity: .38;
mix-blend-mode: screen;
}
#${DRAWER_ID} .tg-meta,
#${DRAWER_ID} .tg-alert,
#${DRAWER_ID} .tg-empty {
border-radius: 22px;
padding: 14px;
margin-bottom: 12px;
color: var(--tg-muted);
font-size: 12px;
line-height: 1.65;
}
#${DRAWER_ID} .tg-meta {
padding: 15px;
}
#${DRAWER_ID} .tg-refresh-status {
border-radius: 20px;
padding: 12px;
margin-bottom: 12px;
border: 1px solid rgba(148, 227, 255, .14);
background:
linear-gradient(145deg, rgba(255,255,255,.075), rgba(255,255,255,.025)),
rgba(8, 13, 24, .38);
box-shadow: 0 16px 36px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.08);
backdrop-filter: blur(18px) saturate(140%);
-webkit-backdrop-filter: blur(18px) saturate(140%);
}
#${DRAWER_ID} .tg-refresh-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
margin-bottom: 9px;
}
#${DRAWER_ID} .tg-refresh-title {
color: rgba(245,250,255,.92);
font-size: 12px;
font-weight: 760;
}
#${DRAWER_ID} .tg-refresh-stage {
color: var(--tg-muted);
font-size: 11px;
line-height: 1.5;
overflow-wrap: anywhere;
}
#${DRAWER_ID} .tg-progress-track {
height: 7px;
overflow: hidden;
border-radius: 999px;
border: 1px solid rgba(148, 227, 255, .12);
background: rgba(255,255,255,.045);
box-shadow: inset 0 1px 4px rgba(0,0,0,.28);
}
#${DRAWER_ID} .tg-progress-fill {
height: 100%;
width: 0%;
border-radius: inherit;
background: linear-gradient(90deg, rgba(103,232,249,.72), rgba(139,92,246,.62), rgba(240,171,252,.52));
box-shadow: 0 0 18px rgba(103,232,249,.22);
transition: width .42s cubic-bezier(.16, 1, .3, 1);
}
#${DRAWER_ID} .tg-meta-kicker {
color: rgba(103,232,249,.86);
font-size: 11px;
font-weight: 720;
margin-bottom: 12px;
}
#${DRAWER_ID} .tg-meta-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 9px;
}
#${DRAWER_ID} .tg-meta-chip {
border: 1px solid rgba(148, 227, 255, .12);
background: rgba(255,255,255,.045);
border-radius: 16px;
padding: 9px 10px;
min-width: 0;
}
#${DRAWER_ID} .tg-meta-label {
display: block;
color: var(--tg-faint);
font-size: 11px;
margin-bottom: 4px;
}
#${DRAWER_ID} .tg-meta-value {
display: block;
color: var(--tg-text);
font-size: 12px;
font-weight: 680;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#${DRAWER_ID} .tg-alert {
color: rgba(255, 228, 230, .95);
background:
linear-gradient(145deg, rgba(251,113,133,.16), rgba(255,255,255,.035)),
rgba(40, 9, 20, .34);
border-color: rgba(251,113,133,.26);
box-shadow: 0 18px 44px rgba(77, 10, 30, .25), inset 0 1px 0 rgba(255,255,255,.09);
}
#${DRAWER_ID} .tg-empty {
color: rgba(203, 213, 225, .74);
text-align: center;
padding: 18px;
}
#${DRAWER_ID} .tg-summary {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 9px;
margin-bottom: 12px;
}
#${DRAWER_ID} .tg-summary-item {
min-width: 0;
width: 100%;
border-radius: 20px;
padding: 12px 8px 11px;
text-align: left;
cursor: pointer;
color: inherit;
font: inherit;
appearance: none;
-webkit-appearance: none;
transition: transform .36s cubic-bezier(.16, 1, .3, 1), border-color .36s ease, box-shadow .36s ease, background .36s ease;
}
#${DRAWER_ID} .tg-summary-item:hover {
transform: translateY(-4px);
border-color: rgba(103,232,249,.34);
background:
linear-gradient(145deg, rgba(103,232,249,.1), rgba(255,255,255,.04)),
rgba(9, 16, 28, .54);
box-shadow: 0 24px 54px rgba(0,0,0,.3), 0 0 26px rgba(103,232,249,.1), inset 0 1px 0 rgba(255,255,255,.13);
}
#${DRAWER_ID} .tg-summary-item.tg-selected {
border-color: rgba(103,232,249,.48);
background:
linear-gradient(145deg, rgba(103,232,249,.16), rgba(139,92,246,.09)),
rgba(9, 16, 28, .62);
box-shadow:
0 24px 58px rgba(0,0,0,.34),
0 0 32px rgba(103,232,249,.18),
0 0 34px rgba(139,92,246,.11),
inset 0 1px 0 rgba(255,255,255,.15);
}
#${DRAWER_ID} .tg-summary-item.tg-selected .tg-summary-num {
color: rgba(165,243,252,.98);
text-shadow: 0 0 18px rgba(103,232,249,.18);
}
#${DRAWER_ID} .tg-summary-item[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
left: 50%;
bottom: calc(100% + 10px);
width: min(230px, 72vw);
transform: translate(-50%, 8px);
opacity: 0;
pointer-events: none;
color: rgba(235,245,255,.94);
background:
linear-gradient(145deg, rgba(18, 27, 44, .94), rgba(8, 13, 24, .9)),
rgba(8, 13, 24, .92);
border: 1px solid rgba(148, 227, 255, .18);
border-radius: 12px;
padding: 8px 10px;
font-size: 11px;
line-height: 1.45;
box-shadow: 0 16px 36px rgba(0,0,0,.38), 0 0 24px rgba(103,232,249,.1), inset 0 1px 0 rgba(255,255,255,.08);
backdrop-filter: blur(18px) saturate(145%);
-webkit-backdrop-filter: blur(18px) saturate(145%);
transition: opacity .22s ease, transform .22s cubic-bezier(.16, 1, .3, 1);
z-index: 6;
}
#${DRAWER_ID} .tg-summary-item[data-tooltip]:hover::after {
opacity: 1;
transform: translate(-50%, 0);
}
#${DRAWER_ID} .tg-summary-num {
position: relative;
font-size: 24px;
font-weight: 780;
line-height: 1;
margin-bottom: 8px;
color: rgba(248,250,252,.96);
}
#${DRAWER_ID} .tg-summary-label {
position: relative;
color: var(--tg-muted);
font-size: 11px;
white-space: nowrap;
}
#${DRAWER_ID} .tg-current-filter {
margin: -2px 2px 12px;
color: rgba(203, 213, 225, .76);
font-size: 12px;
font-weight: 650;
}
#${DRAWER_ID} .tg-current-filter strong {
color: rgba(165,243,252,.94);
font-weight: 760;
}
#${DRAWER_ID} .tg-date-filter {
border-radius: 22px;
padding: 13px;
margin-bottom: 12px;
}
#${DRAWER_ID} .tg-date-filter-row {
position: relative;
display: flex;
align-items: center;
gap: 8px;
color: rgba(235,245,255,.9);
font-size: 12px;
font-weight: 650;
}
#${DRAWER_ID} .tg-time-mark {
position: relative;
width: 28px;
height: 28px;
flex: 0 0 auto;
border-radius: 10px;
border: 1px solid rgba(103,232,249,.22);
background:
linear-gradient(180deg, rgba(103,232,249,.14), rgba(139,92,246,.08)),
rgba(255,255,255,.04);
box-shadow: 0 0 18px rgba(103,232,249,.1), inset 0 1px 0 rgba(255,255,255,.16);
}
#${DRAWER_ID} .tg-time-mark::before,
#${DRAWER_ID} .tg-time-mark::after {
content: "";
position: absolute;
display: block;
}
#${DRAWER_ID} .tg-time-mark::before {
left: 7px;
right: 7px;
top: 8px;
height: 2px;
border-radius: 999px;
background: rgba(103,232,249,.62);
box-shadow: 0 7px 0 rgba(103,232,249,.2);
}
#${DRAWER_ID} .tg-time-mark::after {
left: 8px;
top: 6px;
width: 12px;
height: 14px;
border: 1px solid rgba(245,250,255,.28);
border-radius: 4px;
}
#${DRAWER_ID} .tg-date-filter select {
height: 34px;
min-width: 84px;
border: 1px solid rgba(148, 227, 255, .18);
background: rgba(2, 6, 23, .42);
color: rgba(245, 250, 255, .94);
border-radius: 12px;
padding: 0 10px;
font-size: 12px;
outline: none;
box-shadow: inset 0 1px 0 rgba(255,255,255,.08);
transition: border-color .24s ease, box-shadow .24s ease, background .24s ease;
}
#${DRAWER_ID} .tg-date-filter select:focus,
#${DRAWER_ID} .tg-date-filter select:hover {
border-color: rgba(103,232,249,.42);
box-shadow: 0 0 24px rgba(103,232,249,.1), inset 0 1px 0 rgba(255,255,255,.12);
}
#${DRAWER_ID} .tg-date-filter select option {
background: #0f172a;
color: rgba(245,250,255,.94);
}
#${DRAWER_ID} .tg-date-filter-hint {
position: relative;
margin-top: 9px;
color: var(--tg-muted);
font-size: 12px;
}
#${DRAWER_ID} .tg-action-btn,
#${DRAWER_ID} .tg-detail-link {
min-height: 34px;
border: 1px solid rgba(148, 227, 255, .14);
background: rgba(255,255,255,.045);
color: rgba(226, 232, 240, .86);
border-radius: 14px;
cursor: pointer;
padding: 0 9px;
font-size: 12px;
font-weight: 650;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
box-shadow: inset 0 1px 0 rgba(255,255,255,.07);
transition: transform .28s cubic-bezier(.16, 1, .3, 1), border-color .28s ease, background .28s ease, box-shadow .28s ease, color .28s ease;
}
#${DRAWER_ID} .tg-action-btn:hover,
#${DRAWER_ID} .tg-detail-link:hover {
transform: translateY(-2px);
border-color: rgba(103,232,249,.42);
color: rgba(245,250,255,.98);
background:
linear-gradient(135deg, rgba(103,232,249,.14), rgba(139,92,246,.1)),
rgba(255,255,255,.055);
box-shadow: 0 0 24px rgba(103,232,249,.12), inset 0 1px 0 rgba(255,255,255,.12);
}
#${DRAWER_ID} .tg-section {
margin-bottom: 16px;
}
#${DRAWER_ID} .tg-course-group {
margin-bottom: 16px;
border: 1px solid rgba(148, 227, 255, .13);
background:
linear-gradient(145deg, rgba(255,255,255,.06), rgba(255,255,255,.025)),
rgba(8, 13, 24, .34);
border-radius: 24px;
padding: 12px;
box-shadow: 0 18px 42px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.07);
backdrop-filter: blur(18px) saturate(135%);
-webkit-backdrop-filter: blur(18px) saturate(135%);
}
#${DRAWER_ID} .tg-course-head {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 12px;
align-items: center;
color: rgba(248,250,252,.96);
font-size: 14px;
font-weight: 760;
margin: 2px 2px 10px;
overflow-wrap: anywhere;
cursor: pointer;
}
#${DRAWER_ID} .tg-course-title {
min-width: 0;
overflow-wrap: anywhere;
}
#${DRAWER_ID} .tg-course-stats {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
#${DRAWER_ID} .tg-course-stat {
color: rgba(203,213,225,.76);
background: rgba(255,255,255,.045);
border: 1px solid rgba(148, 227, 255, .1);
border-radius: 999px;
padding: 3px 7px;
font-size: 11px;
font-weight: 620;
}
#${DRAWER_ID} .tg-course-toggle {
width: 34px;
height: 34px;
border-radius: 13px;
border: 1px solid rgba(148, 227, 255, .16);
background: rgba(255,255,255,.045);
color: rgba(235,245,255,.9);
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255,255,255,.08);
transition: transform .28s cubic-bezier(.16, 1, .3, 1), border-color .28s ease, box-shadow .28s ease;
}
#${DRAWER_ID} .tg-course-toggle:hover {
transform: translateY(-2px);
border-color: rgba(103,232,249,.4);
box-shadow: 0 0 22px rgba(103,232,249,.12), inset 0 1px 0 rgba(255,255,255,.12);
}
#${DRAWER_ID} .tg-course-content {
display: grid;
grid-template-rows: 1fr;
transition: grid-template-rows .34s cubic-bezier(.16, 1, .3, 1), opacity .28s ease;
opacity: 1;
}
#${DRAWER_ID} .tg-course-content-inner {
overflow: hidden;
}
#${DRAWER_ID} .tg-course-group.tg-course-collapsed .tg-course-content {
grid-template-rows: 0fr;
opacity: 0;
}
#${DRAWER_ID} .tg-course-subgroup {
margin-top: 10px;
border: 1px solid rgba(148, 227, 255, .09);
border-radius: 18px;
background: rgba(255,255,255,.025);
padding: 9px;
}
#${DRAWER_ID} .tg-subgroup-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
color: rgba(203,213,225,.76);
font-size: 12px;
font-weight: 720;
margin: 0 2px;
cursor: pointer;
user-select: none;
}
#${DRAWER_ID} .tg-subgroup-title-main {
min-width: 0;
color: rgba(235,245,255,.88);
}
#${DRAWER_ID} .tg-subgroup-count {
flex: 0 0 auto;
color: var(--tg-muted);
border: 1px solid rgba(148, 227, 255, .1);
background: rgba(255,255,255,.04);
border-radius: 999px;
padding: 2px 7px;
font-size: 11px;
}
#${DRAWER_ID} .tg-subgroup-content {
display: grid;
grid-template-rows: 1fr;
margin-top: 8px;
transition: grid-template-rows .32s cubic-bezier(.16, 1, .3, 1), opacity .26s ease;
opacity: 1;
}
#${DRAWER_ID} .tg-subgroup-content-inner {
overflow: hidden;
}
#${DRAWER_ID} .tg-section-collapsed .tg-subgroup-content {
grid-template-rows: 0fr;
opacity: 0;
}
#${DRAWER_ID} .tg-section-title {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 8px;
color: rgba(245,250,255,.92);
font-size: 13px;
font-weight: 760;
margin: 15px 3px 9px;
}
#${DRAWER_ID} .tg-section-title::before {
content: "";
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--tg-cyan);
box-shadow: 0 0 14px rgba(103,232,249,.52);
margin-right: 1px;
}
#${DRAWER_ID} .tg-section-title span:first-child {
flex: 1 1 auto;
}
#${DRAWER_ID} .tg-section-count {
color: var(--tg-muted);
font-size: 12px;
font-weight: 560;
}
#${DRAWER_ID} .tg-section-urgent .tg-section-title::before {
background: var(--tg-red);
box-shadow: 0 0 16px rgba(251,113,133,.42);
}
#${DRAWER_ID} .tg-card {
border-radius: 22px;
padding: 13px;
margin-bottom: 10px;
line-height: 1.55;
transition: transform .36s cubic-bezier(.16, 1, .3, 1), border-color .36s ease, box-shadow .36s ease, background .36s ease;
}
#${DRAWER_ID} .tg-card:hover {
transform: translateY(-4px);
border-color: rgba(103,232,249,.32);
background:
linear-gradient(145deg, rgba(255,255,255,.105), rgba(255,255,255,.045)),
rgba(10, 17, 30, .54);
box-shadow: 0 28px 64px rgba(0,0,0,.34), 0 0 34px rgba(103,232,249,.1), inset 0 1px 0 rgba(255,255,255,.13);
}
#${DRAWER_ID} .tg-card.tg-state-ok {
border-color: rgba(134,239,172,.18);
}
#${DRAWER_ID} .tg-card.tg-state-warn {
border-color: rgba(251,191,36,.22);
}
#${DRAWER_ID} .tg-card.tg-state-bad {
border-color: rgba(251,113,133,.28);
box-shadow: 0 20px 48px rgba(54, 8, 24, .26), 0 0 32px rgba(251,113,133,.08), inset 0 1px 0 rgba(255,255,255,.1);
}
#${DRAWER_ID} .tg-card.tg-state-muted {
border-color: rgba(148,163,184,.14);
}
#${DRAWER_ID} .tg-card-head {
position: relative;
display: flex;
gap: 10px;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 9px;
}
#${DRAWER_ID} .tg-card-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
}
#${DRAWER_ID} .tg-card-top-actions {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
flex: 0 0 auto;
}
#${DRAWER_ID} .tg-card-title {
min-width: 0;
color: rgba(248,250,252,.96);
font-size: 14px;
font-weight: 720;
line-height: 1.45;
overflow-wrap: anywhere;
}
#${DRAWER_ID} .tg-card-title a {
color: inherit;
text-decoration: none;
}
#${DRAWER_ID} .tg-card-title a:hover {
color: rgba(165,243,252,.98);
}
#${DRAWER_ID} .tg-type {
flex: 0 0 auto;
border: 1px solid rgba(148, 227, 255, .18);
border-radius: 999px;
padding: 3px 8px;
color: rgba(225, 245, 255, .82);
font-size: 11px;
font-weight: 680;
background: rgba(255,255,255,.045);
box-shadow: inset 0 1px 0 rgba(255,255,255,.08);
}
#${DRAWER_ID} .tg-countdown {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 34px;
border-radius: 999px;
border: 1px solid rgba(251,191,36,.34);
background:
linear-gradient(135deg, rgba(251,191,36,.16), rgba(251,113,133,.1)),
rgba(255,255,255,.045);
color: rgba(255, 237, 213, .96);
padding: 6px 12px;
font-size: 16px;
font-weight: 780;
line-height: 1.2;
box-shadow: 0 0 24px rgba(251,191,36,.13), inset 0 1px 0 rgba(255,255,255,.1);
white-space: nowrap;
animation: tgCountdownPulse 2.8s ease-in-out infinite;
}
#${DRAWER_ID} .tg-ignored-flag {
display: inline-flex;
align-items: center;
min-height: 30px;
padding: 0 11px;
border-radius: 999px;
border: 1px solid rgba(148, 163, 184, .18);
background: rgba(255,255,255,.04);
color: rgba(203, 213, 225, .82);
font-size: 11px;
font-weight: 760;
white-space: nowrap;
}
#${DRAWER_ID} .tg-icon-action {
width: 34px;
height: 34px;
border: 1px solid rgba(148, 227, 255, .14);
background: rgba(255,255,255,.045);
color: rgba(226, 232, 240, .86);
border-radius: 12px;
cursor: pointer;
font-size: 16px;
line-height: 1;
box-shadow: inset 0 1px 0 rgba(255,255,255,.07);
transition: transform .28s cubic-bezier(.16, 1, .3, 1), border-color .28s ease, background .28s ease, box-shadow .28s ease, color .28s ease;
}
#${DRAWER_ID} .tg-icon-action:hover {
transform: translateY(-2px);
border-color: rgba(251,191,36,.42);
color: rgba(255, 245, 220, .98);
background:
linear-gradient(135deg, rgba(251,191,36,.14), rgba(251,113,133,.1)),
rgba(255,255,255,.055);
box-shadow: 0 0 24px rgba(251,191,36,.12), inset 0 1px 0 rgba(255,255,255,.12);
}
#${DRAWER_ID} .tg-restore-btn {
min-width: 64px;
}
#${DRAWER_ID} .tg-status {
position: relative;
font-size: 13px;
font-weight: 760;
margin: 7px 0 6px;
}
#${DRAWER_ID} .tg-status-row {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
flex-wrap: wrap;
}
#${DRAWER_ID} .tg-ok { color: var(--tg-green); }
#${DRAWER_ID} .tg-warn { color: var(--tg-amber); }
#${DRAWER_ID} .tg-bad { color: var(--tg-red); }
#${DRAWER_ID} .tg-muted { color: var(--tg-muted); }
#${DRAWER_ID} .tg-card-note {
margin-top: -2px;
margin-bottom: 8px;
color: rgba(203, 213, 225, .72);
font-size: 12px;
}
#${DRAWER_ID} .tg-small,
#${DRAWER_ID} .tg-task-meta {
position: relative;
color: var(--tg-muted);
font-size: 12px;
line-height: 1.7;
overflow-wrap: anywhere;
}
#${DRAWER_ID} .tg-task-meta {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 7px;
margin-top: 10px;
}
#${DRAWER_ID} .tg-task-meta span {
display: block;
border: 1px solid rgba(148, 227, 255, .1);
background: rgba(255,255,255,.035);
border-radius: 13px;
padding: 7px 8px;
}
#${DRAWER_ID} .tg-card-footer {
position: relative;
display: flex;
justify-content: flex-start;
margin-top: 11px;
}
#${DRAWER_ID} .tg-detail-link {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 30px;
text-decoration: none;
padding: 0 12px;
border: 1px solid rgba(103,232,249,.22);
border-radius: 999px;
color: rgba(225,245,255,.92);
background: rgba(255,255,255,.045);
cursor: pointer;
font: inherit;
font-size: 12px;
transition: transform .25s cubic-bezier(.16, 1, .3, 1), border-color .25s ease, box-shadow .25s ease;
}
#${DRAWER_ID} .tg-detail-link:hover {
transform: translateY(-1px);
border-color: rgba(103,232,249,.45);
box-shadow: 0 0 18px rgba(103,232,249,.12);
}
#${DRAWER_ID} .tg-actions {
flex: 0 0 auto;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr);
gap: 10px;
padding: 12px 16px 16px;
border-top: 1px solid rgba(148, 227, 255, .13);
background: linear-gradient(180deg, rgba(8,13,24,.44), rgba(8,13,24,.78));
backdrop-filter: blur(24px) saturate(145%);
-webkit-backdrop-filter: blur(24px) saturate(145%);
}
#${DRAWER_ID} .tg-action-btn {
min-height: 40px;
border-radius: 16px;
}
#${DRAWER_ID} [data-copy-all-json] {
border-color: rgba(103,232,249,.28);
background:
linear-gradient(135deg, rgba(103,232,249,.13), rgba(139,92,246,.11)),
rgba(255,255,255,.055);
color: rgba(245,250,255,.96);
}
#${DRAWER_ID} [data-delete-cache] {
border-color: rgba(251,113,133,.22);
background:
linear-gradient(135deg, rgba(251,113,133,.1), rgba(251,191,36,.06)),
rgba(255,255,255,.045);
color: rgba(255,228,230,.94);
}
#${DRAWER_ID} textarea.tg-json-box {
width: 100%;
height: 260px;
margin-top: 10px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, Monaco, monospace;
font-size: 12px;
line-height: 1.5;
border: 1px solid rgba(148, 227, 255, .16);
border-radius: 18px;
padding: 12px;
resize: vertical;
color: rgba(235,245,255,.94);
background: rgba(2,6,23,.58);
box-shadow: inset 0 1px 0 rgba(255,255,255,.08);
}
@keyframes tgFloatButton {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
@keyframes tgParticleDrift {
from { background-position: 0 0, 0 0, 0 0, 0 0, 0 0; }
to { background-position: 128px 70px, -184px 120px, 148px -90px, 44px 44px, -44px 44px; }
}
@keyframes tgSlowFloatA {
0%, 100% { transform: translate3d(0,0,0) scale(1); opacity: .58; }
50% { transform: translate3d(-18px,24px,0) scale(1.06); opacity: .76; }
}
@keyframes tgSlowFloatB {
0%, 100% { transform: translate3d(0,0,0) scale(1); opacity: .44; }
50% { transform: translate3d(22px,-18px,0) scale(1.08); opacity: .66; }
}
@keyframes tgRingDrift {
0%, 100% { transform: rotate(-18deg) translate3d(0,0,0); opacity: .36; }
50% { transform: rotate(8deg) translate3d(18px,10px,0); opacity: .52; }
}
@keyframes tgCountdownPulse {
0%, 100% { box-shadow: 0 0 20px rgba(251,191,36,.11), inset 0 1px 0 rgba(255,255,255,.1); }
50% { box-shadow: 0 0 30px rgba(251,191,36,.22), 0 0 18px rgba(251,113,133,.12), inset 0 1px 0 rgba(255,255,255,.14); }
}
/* iOS 26 Liquid Glass practical skin */
#${BUTTON_ID}, #${DRAWER_ID} {
--tg-liquid-bg: rgba(255,255,255,.16);
--tg-liquid-bg-strong: rgba(20, 25, 36, .72);
--tg-liquid-card: rgba(255,255,255,.13);
--tg-liquid-card-strong: rgba(255,255,255,.2);
--tg-liquid-border: rgba(255,255,255,.35);
--tg-liquid-border-soft: rgba(255,255,255,.22);
--tg-liquid-shadow: 0 20px 60px rgba(0,0,0,.25);
--tg-text: rgba(255,255,255,.96);
--tg-muted: rgba(232,238,247,.76);
--tg-faint: rgba(220,228,240,.58);
--tg-cyan: #8ee8ff;
--tg-violet: #b8a7ff;
--tg-pink: #ffc5e8;
--tg-red: #ff6b7f;
--tg-amber: #ffd166;
--tg-green: #8ce99a;
text-shadow: 0 1px 1px rgba(0,0,0,.24);
}
#${BUTTON_ID} {
width: auto;
min-width: 132px;
height: 48px;
padding: 0 18px;
border-radius: 999px;
border: 1px solid var(--tg-liquid-border);
background:
linear-gradient(135deg, rgba(255,255,255,.32), rgba(255,255,255,.12)),
rgba(255,255,255,.16);
color: rgba(255,255,255,.96);
box-shadow: var(--tg-liquid-shadow), inset 0 1px 0 rgba(255,255,255,.42), inset 0 -1px 0 rgba(255,255,255,.14);
backdrop-filter: blur(22px) saturate(180%);
-webkit-backdrop-filter: blur(22px) saturate(180%);
font-size: 13px;
font-weight: 780;
line-height: 1;
animation: none;
transition: transform .18s cubic-bezier(.2, .8, .2, 1), border-color .18s ease, box-shadow .18s ease, background .18s ease;
}
#${BUTTON_ID}::before {
inset: 1px;
border-radius: 999px;
background: linear-gradient(120deg, transparent 12%, rgba(255,255,255,.36) 32%, transparent 56%);
filter: none;
opacity: .34;
transform: translateX(-42%);
transition: transform .22s ease, opacity .18s ease;
}
#${BUTTON_ID}:hover {
transform: translateY(-2px);
border-color: rgba(255,255,255,.58);
background:
linear-gradient(135deg, rgba(255,255,255,.4), rgba(255,255,255,.18)),
rgba(255,255,255,.2);
box-shadow: 0 24px 64px rgba(0,0,0,.3), inset 0 1px 0 rgba(255,255,255,.5);
}
#${BUTTON_ID}:hover::before {
transform: translateX(36%);
opacity: .55;
}
#${BUTTON_ID} .tg-task-button-badge {
right: 4px;
top: -8px;
min-width: 24px;
height: 24px;
border-radius: 999px;
border: 1px solid rgba(255,255,255,.62);
background: rgba(255, 78, 112, .84);
color: #fff;
line-height: 22px;
box-shadow: 0 10px 24px rgba(255,78,112,.24), inset 0 1px 0 rgba(255,255,255,.38);
backdrop-filter: blur(14px) saturate(180%);
-webkit-backdrop-filter: blur(14px) saturate(180%);
}
#${DRAWER_ID} {
border-radius: 28px;
color: var(--tg-text);
background:
linear-gradient(180deg, rgba(22,27,38,.72), rgba(10,14,22,.68)),
rgba(255,255,255,.16);
border: 1px solid var(--tg-liquid-border);
box-shadow: var(--tg-liquid-shadow), inset 0 1px 0 rgba(255,255,255,.32), inset 0 0 0 1px rgba(255,255,255,.08);
backdrop-filter: blur(22px) saturate(180%);
-webkit-backdrop-filter: blur(22px) saturate(180%);
transition: transform .2s cubic-bezier(.2, .8, .2, 1), opacity .18s ease, box-shadow .18s ease;
}
#${DRAWER_ID}.tg-open {
background:
linear-gradient(180deg, rgba(20,25,36,.78), rgba(8,12,20,.74)),
rgba(255,255,255,.18);
}
#${DRAWER_ID}::before {
background:
linear-gradient(115deg, rgba(255,255,255,.2), transparent 24%),
radial-gradient(circle at 18% 0%, rgba(255,255,255,.16), transparent 30%);
opacity: .55;
animation: none;
mask-image: none;
}
#${DRAWER_ID}::after {
border-radius: 27px;
background:
linear-gradient(135deg, rgba(255,255,255,.2), transparent 28%),
linear-gradient(315deg, rgba(255,255,255,.08), transparent 34%);
opacity: .45;
}
#${DRAWER_ID} .tg-ambient {
display: none;
}
#${DRAWER_ID} .tg-header {
min-height: 76px;
border-bottom: 1px solid rgba(255,255,255,.18);
background: rgba(255,255,255,.08);
backdrop-filter: blur(16px) saturate(160%);
-webkit-backdrop-filter: blur(16px) saturate(160%);
}
#${DRAWER_ID} .tg-title-main,
#${DRAWER_ID} .tg-card-title,
#${DRAWER_ID} .tg-course-head,
#${DRAWER_ID} .tg-section-title {
color: rgba(255,255,255,.98);
text-shadow: 0 1px 2px rgba(0,0,0,.28);
}
#${DRAWER_ID} .tg-icon-btn,
#${DRAWER_ID} .tg-action-btn,
#${DRAWER_ID} .tg-detail-link,
#${DRAWER_ID} .tg-course-toggle,
#${DRAWER_ID} .tg-subgroup-count,
#${DRAWER_ID} .tg-course-stat,
#${DRAWER_ID} .tg-type,
#${DRAWER_ID} .tg-date-filter select {
border-radius: 999px;
border-color: var(--tg-liquid-border-soft);
background: rgba(255,255,255,.13);
color: rgba(255,255,255,.92);
box-shadow: inset 0 1px 0 rgba(255,255,255,.22);
backdrop-filter: blur(14px) saturate(170%);
-webkit-backdrop-filter: blur(14px) saturate(170%);
transition: transform .16s ease, border-color .16s ease, background .16s ease, box-shadow .16s ease;
}
#${DRAWER_ID} .tg-top-refresh {
min-width: 78px;
}
#${DRAWER_ID} .tg-icon-btn:hover,
#${DRAWER_ID} .tg-action-btn:hover,
#${DRAWER_ID} .tg-detail-link:hover,
#${DRAWER_ID} .tg-course-toggle:hover,
#${DRAWER_ID} .tg-date-filter select:hover,
#${DRAWER_ID} .tg-date-filter select:focus {
transform: translateY(-1px);
border-color: rgba(255,255,255,.48);
background: rgba(255,255,255,.2);
box-shadow: 0 10px 26px rgba(0,0,0,.18), inset 0 1px 0 rgba(255,255,255,.34);
}
#${DRAWER_ID} .tg-meta,
#${DRAWER_ID} .tg-alert,
#${DRAWER_ID} .tg-empty,
#${DRAWER_ID} .tg-date-filter,
#${DRAWER_ID} .tg-summary-item,
#${DRAWER_ID} .tg-refresh-status,
#${DRAWER_ID} .tg-course-group,
#${DRAWER_ID} .tg-course-subgroup,
#${DRAWER_ID} .tg-card {
border: 1px solid var(--tg-liquid-border-soft);
background:
linear-gradient(180deg, rgba(255,255,255,.16), rgba(255,255,255,.08)),
rgba(255,255,255,.1);
box-shadow: 0 12px 34px rgba(0,0,0,.18), inset 0 1px 0 rgba(255,255,255,.24);
backdrop-filter: blur(18px) saturate(170%);
-webkit-backdrop-filter: blur(18px) saturate(170%);
}
#${DRAWER_ID} .tg-meta::before,
#${DRAWER_ID} .tg-alert::before,
#${DRAWER_ID} .tg-empty::before,
#${DRAWER_ID} .tg-date-filter::before,
#${DRAWER_ID} .tg-summary-item::before,
#${DRAWER_ID} .tg-card::before {
background: linear-gradient(120deg, rgba(255,255,255,.18), transparent 38%);
opacity: .42;
mix-blend-mode: normal;
}
#${DRAWER_ID} .tg-summary-item,
#${DRAWER_ID} .tg-card {
transition: transform .18s cubic-bezier(.2, .8, .2, 1), border-color .18s ease, box-shadow .18s ease, background .18s ease;
}
#${DRAWER_ID} .tg-summary-item:hover,
#${DRAWER_ID} .tg-card:hover {
transform: translateY(-2px);
border-color: rgba(255,255,255,.46);
background:
linear-gradient(180deg, rgba(255,255,255,.22), rgba(255,255,255,.1)),
rgba(255,255,255,.14);
box-shadow: 0 16px 42px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.3);
}
#${DRAWER_ID} .tg-summary-item.tg-selected {
border-color: rgba(142,232,255,.62);
background:
linear-gradient(180deg, rgba(142,232,255,.2), rgba(255,255,255,.12)),
rgba(255,255,255,.16);
box-shadow: 0 16px 42px rgba(0,0,0,.22), 0 0 0 1px rgba(142,232,255,.18), inset 0 1px 0 rgba(255,255,255,.32);
}
#${DRAWER_ID} .tg-summary-item[data-tooltip]::after,
#${DRAWER_ID} .tg-status[data-tooltip]::after {
color: rgba(255,255,255,.96);
background:
linear-gradient(180deg, rgba(34,40,54,.86), rgba(12,16,25,.82)),
rgba(255,255,255,.16);
border: 1px solid rgba(255,255,255,.32);
box-shadow: 0 16px 42px rgba(0,0,0,.24), inset 0 1px 0 rgba(255,255,255,.22);
backdrop-filter: blur(18px) saturate(180%);
-webkit-backdrop-filter: blur(18px) saturate(180%);
}
#${DRAWER_ID} .tg-status[data-tooltip] {
cursor: help;
}
#${DRAWER_ID} .tg-status[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
left: 0;
bottom: calc(100% + 8px);
width: min(240px, 72vw);
transform: translateY(6px);
opacity: 0;
pointer-events: none;
border-radius: 14px;
padding: 8px 10px;
font-size: 11px;
line-height: 1.45;
font-weight: 560;
transition: opacity .16s ease, transform .16s ease;
z-index: 8;
}
#${DRAWER_ID} .tg-status[data-tooltip]:hover::after {
opacity: 1;
transform: translateY(0);
}
#${DRAWER_ID} .tg-progress-track {
height: 5px;
border-radius: 999px;
border: 1px solid rgba(255,255,255,.2);
background: rgba(255,255,255,.12);
}
#${DRAWER_ID} .tg-progress-fill {
background: linear-gradient(90deg, rgba(142,232,255,.92), rgba(184,167,255,.82), rgba(255,197,232,.76));
box-shadow: 0 0 16px rgba(142,232,255,.18);
transition: width .18s ease, opacity .18s ease;
}
#${DRAWER_ID} .tg-refresh-status.tg-refresh-done {
animation: tgProgressFade .9s ease .8s forwards;
}
#${DRAWER_ID} .tg-task-meta span {
border-color: rgba(255,255,255,.16);
background: rgba(255,255,255,.08);
color: rgba(238,244,252,.82);
backdrop-filter: blur(10px) saturate(150%);
-webkit-backdrop-filter: blur(10px) saturate(150%);
}
#${DRAWER_ID} .tg-actions {
border-top: 1px solid rgba(255,255,255,.18);
background: rgba(10,14,22,.52);
backdrop-filter: blur(18px) saturate(180%);
-webkit-backdrop-filter: blur(18px) saturate(180%);
}
#${DRAWER_ID} textarea.tg-json-box {
color: rgba(255,255,255,.94);
background: rgba(12,16,25,.68);
border-color: rgba(255,255,255,.22);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
#${BUTTON_ID} {
background: rgba(32, 38, 50, .94);
}
#${DRAWER_ID} {
background: rgba(20, 24, 34, .96);
}
#${DRAWER_ID} .tg-meta,
#${DRAWER_ID} .tg-alert,
#${DRAWER_ID} .tg-empty,
#${DRAWER_ID} .tg-date-filter,
#${DRAWER_ID} .tg-summary-item,
#${DRAWER_ID} .tg-refresh-status,
#${DRAWER_ID} .tg-course-group,
#${DRAWER_ID} .tg-course-subgroup,
#${DRAWER_ID} .tg-card {
background: rgba(35, 42, 56, .92);
}
}
@keyframes tgProgressFade {
to { opacity: .42; transform: translateY(-1px); }
}
/* Liquid Glass Pro light skin: CSS + SVG filter, optimized for white web pages */
#${BUTTON_ID}, #${DRAWER_ID} {
--tg-liquid-surface: rgba(255,255,255,.58);
--tg-liquid-card: rgba(255,255,255,.42);
--tg-liquid-card-hover: rgba(255,255,255,.58);
--tg-liquid-border: rgba(255,255,255,.75);
--tg-liquid-line: rgba(15,23,42,.08);
--tg-liquid-shadow: 0 20px 60px rgba(15,23,42,.18);
--tg-liquid-ease: cubic-bezier(.22, 1, .36, 1);
--tg-text: #111827;
--tg-muted: rgba(31,41,55,.72);
--tg-faint: rgba(75,85,99,.58);
--tg-cyan: #0891b2;
--tg-violet: #6d28d9;
--tg-pink: #be185d;
--tg-red: #dc2626;
--tg-amber: #b45309;
--tg-green: #15803d;
color: var(--tg-text);
text-shadow: none;
}
#${BUTTON_ID} {
min-width: 136px;
height: 50px;
border-radius: 999px;
border: 1px solid var(--tg-liquid-border);
background:
radial-gradient(circle at var(--tg-mouse-x, 28%) var(--tg-mouse-y, 18%), rgba(255,255,255,.96), transparent 36%),
linear-gradient(135deg, rgba(255,255,255,.78), rgba(255,255,255,.38)),
rgba(255,255,255,.58);
color: #111827;
box-shadow: 0 18px 46px rgba(15,23,42,.16), inset 0 1px 0 rgba(255,255,255,.86), inset 0 -1px 0 rgba(255,255,255,.42);
backdrop-filter: blur(26px) saturate(180%);
-webkit-backdrop-filter: blur(26px) saturate(180%);
transition: transform .18s var(--tg-liquid-ease), border-color .18s ease, box-shadow .18s ease, background .18s ease;
}
#${BUTTON_ID}::before {
inset: -1px;
border-radius: 999px;
background:
linear-gradient(120deg, transparent 18%, rgba(255,255,255,.82) 38%, transparent 58%),
radial-gradient(circle at 50% 0%, rgba(255,255,255,.74), transparent 44%);
filter: url(#${SVG_FILTER_ID});
opacity: .48;
transform: translateX(-28%);
transition: transform .22s var(--tg-liquid-ease), opacity .18s ease;
}
#${BUTTON_ID}:hover {
transform: translateY(-2px);
border-color: rgba(255,255,255,.92);
background:
radial-gradient(circle at var(--tg-mouse-x, 28%) var(--tg-mouse-y, 18%), rgba(255,255,255,1), transparent 38%),
linear-gradient(135deg, rgba(255,255,255,.86), rgba(255,255,255,.5)),
rgba(255,255,255,.66);
box-shadow: 0 22px 54px rgba(15,23,42,.2), inset 0 1px 0 rgba(255,255,255,.95);
}
#${BUTTON_ID}:active {
transform: translateY(0) scale(.985);
}
#${BUTTON_ID}:hover::before {
transform: translateX(22%);
opacity: .68;
}
#${BUTTON_ID} .tg-task-button-badge {
background: rgba(239, 68, 68, .88);
color: #fff;
border-color: rgba(255,255,255,.9);
box-shadow: 0 8px 20px rgba(239,68,68,.24), inset 0 1px 0 rgba(255,255,255,.42);
}
#${DRAWER_ID} {
color: #111827;
background:
radial-gradient(circle at var(--tg-mouse-x, 72%) var(--tg-mouse-y, 12%), rgba(255,255,255,.92), transparent 30%),
linear-gradient(180deg, rgba(255,255,255,.68), rgba(255,255,255,.48)),
rgba(255,255,255,.58);
border: 1px solid var(--tg-liquid-border);
box-shadow: var(--tg-liquid-shadow), inset 0 1px 0 rgba(255,255,255,.8), inset 0 -1px 0 rgba(255,255,255,.32);
backdrop-filter: blur(26px) saturate(180%);
-webkit-backdrop-filter: blur(26px) saturate(180%);
transition: transform .28s var(--tg-liquid-ease), opacity .22s ease, box-shadow .22s ease;
}
#${DRAWER_ID}.tg-open {
background:
radial-gradient(circle at var(--tg-mouse-x, 72%) var(--tg-mouse-y, 12%), rgba(255,255,255,.98), transparent 32%),
linear-gradient(180deg, rgba(255,255,255,.72), rgba(255,255,255,.54)),
rgba(255,255,255,.58);
}
#${DRAWER_ID}::before {
background:
linear-gradient(125deg, rgba(255,255,255,.92), transparent 24%, rgba(255,255,255,.22) 58%, transparent 76%),
radial-gradient(circle at var(--tg-mouse-x, 80%) var(--tg-mouse-y, 8%), rgba(255,255,255,.72), transparent 26%);
filter: url(#${SVG_FILTER_ID});
opacity: .48;
mix-blend-mode: screen;
animation: none;
mask-image: none;
}
#${DRAWER_ID}::after {
border-radius: 27px;
background:
linear-gradient(135deg, rgba(255,255,255,.72), transparent 26%),
linear-gradient(315deg, rgba(255,255,255,.32), transparent 34%);
opacity: .58;
mix-blend-mode: screen;
}
#${DRAWER_ID} .tg-header {
border-bottom: 1px solid rgba(15,23,42,.08);
background: rgba(255,255,255,.36);
backdrop-filter: blur(18px) saturate(180%);
-webkit-backdrop-filter: blur(18px) saturate(180%);
}
#${DRAWER_ID} .tg-title-main,
#${DRAWER_ID} .tg-card-title,
#${DRAWER_ID} .tg-course-head,
#${DRAWER_ID} .tg-section-title,
#${DRAWER_ID} .tg-meta-value,
#${DRAWER_ID} .tg-refresh-title {
color: #111827;
text-shadow: none;
}
#${DRAWER_ID} .tg-title-sub,
#${DRAWER_ID} .tg-meta-label,
#${DRAWER_ID} .tg-refresh-stage,
#${DRAWER_ID} .tg-small,
#${DRAWER_ID} .tg-task-meta,
#${DRAWER_ID} .tg-summary-label,
#${DRAWER_ID} .tg-date-filter-hint,
#${DRAWER_ID} .tg-current-filter,
#${DRAWER_ID} .tg-section-count,
#${DRAWER_ID} .tg-course-stat,
#${DRAWER_ID} .tg-subgroup-title,
#${DRAWER_ID} .tg-subgroup-count {
color: var(--tg-muted);
text-shadow: none;
}
#${DRAWER_ID} .tg-meta,
#${DRAWER_ID} .tg-alert,
#${DRAWER_ID} .tg-empty,
#${DRAWER_ID} .tg-date-filter,
#${DRAWER_ID} .tg-summary-item,
#${DRAWER_ID} .tg-refresh-status,
#${DRAWER_ID} .tg-course-group,
#${DRAWER_ID} .tg-course-subgroup,
#${DRAWER_ID} .tg-card {
color: #111827;
border: 1px solid var(--tg-liquid-line);
background: var(--tg-liquid-card);
box-shadow: 0 10px 30px rgba(15,23,42,.08), inset 0 1px 0 rgba(255,255,255,.72);
backdrop-filter: none;
-webkit-backdrop-filter: none;
transition: transform .18s var(--tg-liquid-ease), border-color .18s ease, box-shadow .18s ease, background .18s ease;
}
#${DRAWER_ID} .tg-meta,
#${DRAWER_ID} .tg-alert,
#${DRAWER_ID} .tg-empty,
#${DRAWER_ID} .tg-date-filter,
#${DRAWER_ID} .tg-summary-item,
#${DRAWER_ID} .tg-refresh-status,
#${DRAWER_ID} .tg-course-group {
backdrop-filter: blur(12px) saturate(160%);
-webkit-backdrop-filter: blur(12px) saturate(160%);
}
#${DRAWER_ID} .tg-meta::before,
#${DRAWER_ID} .tg-alert::before,
#${DRAWER_ID} .tg-empty::before,
#${DRAWER_ID} .tg-date-filter::before,
#${DRAWER_ID} .tg-summary-item::before,
#${DRAWER_ID} .tg-card::before {
background: linear-gradient(135deg, rgba(255,255,255,.68), transparent 38%);
opacity: .45;
mix-blend-mode: normal;
}
#${DRAWER_ID} .tg-summary-item:hover,
#${DRAWER_ID} .tg-card:hover,
#${DRAWER_ID} .tg-course-group:hover {
transform: translateY(-2px);
border-color: rgba(15,23,42,.13);
background: var(--tg-liquid-card-hover);
box-shadow: 0 14px 34px rgba(15,23,42,.12), inset 0 1px 0 rgba(255,255,255,.86);
}
#${DRAWER_ID} .tg-summary-item:active,
#${DRAWER_ID} .tg-card:active {
transform: translateY(0) scale(.995);
}
#${DRAWER_ID} .tg-summary-item.tg-selected {
border-color: rgba(8,145,178,.28);
background: rgba(236,254,255,.66);
box-shadow: 0 14px 34px rgba(8,145,178,.12), inset 0 1px 0 rgba(255,255,255,.88);
}
#${DRAWER_ID} .tg-summary-item.tg-selected .tg-summary-num {
color: #075985;
text-shadow: none;
}
#${DRAWER_ID} .tg-icon-btn,
#${DRAWER_ID} .tg-action-btn,
#${DRAWER_ID} .tg-detail-link,
#${DRAWER_ID} .tg-course-toggle,
#${DRAWER_ID} .tg-type,
#${DRAWER_ID} .tg-date-filter select {
position: relative;
overflow: hidden;
color: #111827;
border: 1px solid rgba(15,23,42,.1);
background: rgba(255,255,255,.48);
box-shadow: 0 8px 22px rgba(15,23,42,.07), inset 0 1px 0 rgba(255,255,255,.78);
backdrop-filter: blur(14px) saturate(170%);
-webkit-backdrop-filter: blur(14px) saturate(170%);
transition: transform .18s var(--tg-liquid-ease), border-color .18s ease, background .18s ease, box-shadow .18s ease;
}
#${DRAWER_ID} .tg-icon-btn::before,
#${DRAWER_ID} .tg-action-btn::before,
#${DRAWER_ID} .tg-detail-link::before,
#${DRAWER_ID} .tg-course-toggle::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(120deg, transparent, rgba(255,255,255,.72), transparent);
transform: translateX(-120%);
transition: transform .22s var(--tg-liquid-ease);
}
#${DRAWER_ID} .tg-icon-btn:hover,
#${DRAWER_ID} .tg-action-btn:hover,
#${DRAWER_ID} .tg-detail-link:hover,
#${DRAWER_ID} .tg-course-toggle:hover,
#${DRAWER_ID} .tg-date-filter select:hover,
#${DRAWER_ID} .tg-date-filter select:focus {
transform: translateY(-1px);
border-color: rgba(15,23,42,.16);
background: rgba(255,255,255,.68);
box-shadow: 0 12px 28px rgba(15,23,42,.11), inset 0 1px 0 rgba(255,255,255,.92);
}
#${DRAWER_ID} .tg-icon-btn:hover::before,
#${DRAWER_ID} .tg-action-btn:hover::before,
#${DRAWER_ID} .tg-detail-link:hover::before,
#${DRAWER_ID} .tg-course-toggle:hover::before {
transform: translateX(120%);
}
#${DRAWER_ID} .tg-icon-btn:active,
#${DRAWER_ID} .tg-action-btn:active,
#${DRAWER_ID} .tg-detail-link:active,
#${DRAWER_ID} .tg-course-toggle:active {
transform: translateY(0) scale(.98);
}
#${DRAWER_ID} .tg-liquid-ripple {
position: absolute;
width: 10px;
height: 10px;
border-radius: 999px;
pointer-events: none;
background: rgba(8,145,178,.16);
transform: translate(-50%, -50%) scale(1);
animation: tgLiquidRipple .55s var(--tg-liquid-ease) forwards;
z-index: 0;
}
#${DRAWER_ID} .tg-progress-track {
height: 5px;
border: 1px solid rgba(15,23,42,.08);
background: rgba(255,255,255,.48);
box-shadow: inset 0 1px 2px rgba(15,23,42,.05);
}
#${DRAWER_ID} .tg-progress-fill {
background: linear-gradient(90deg, rgba(8,145,178,.68), rgba(99,102,241,.58), rgba(190,24,93,.42));
box-shadow: 0 0 16px rgba(8,145,178,.16);
filter: url(#${SVG_FILTER_ID});
transition: width .18s ease, opacity .18s ease;
}
#${DRAWER_ID} .tg-summary-item[data-tooltip]::after,
#${DRAWER_ID} .tg-status[data-tooltip]::after {
color: #111827;
background:
linear-gradient(180deg, rgba(255,255,255,.88), rgba(255,255,255,.72)),
rgba(255,255,255,.78);
border: 1px solid rgba(15,23,42,.1);
box-shadow: 0 16px 40px rgba(15,23,42,.14), inset 0 1px 0 rgba(255,255,255,.88);
backdrop-filter: blur(18px) saturate(180%);
-webkit-backdrop-filter: blur(18px) saturate(180%);
text-shadow: none;
}
#${DRAWER_ID} .tg-summary-item[data-tooltip]::before,
#${DRAWER_ID} .tg-status[data-tooltip]::before {
content: "";
position: absolute;
width: 9px;
height: 9px;
background: rgba(255,255,255,.78);
border-right: 1px solid rgba(15,23,42,.08);
border-bottom: 1px solid rgba(15,23,42,.08);
transform: rotate(45deg);
opacity: 0;
pointer-events: none;
transition: opacity .16s ease;
z-index: 7;
}
#${DRAWER_ID} .tg-summary-item[data-tooltip]::before {
left: 50%;
bottom: calc(100% + 5px);
margin-left: -4px;
}
#${DRAWER_ID} .tg-status[data-tooltip]::before {
left: 18px;
bottom: calc(100% + 3px);
}
#${DRAWER_ID} .tg-summary-item[data-tooltip]:hover::before,
#${DRAWER_ID} .tg-status[data-tooltip]:hover::before {
opacity: 1;
}
#${DRAWER_ID} .tg-alert {
color: #7f1d1d;
background: rgba(254,242,242,.72);
border-color: rgba(220,38,38,.14);
}
#${DRAWER_ID} .tg-empty {
color: #374151;
}
#${DRAWER_ID} .tg-task-meta span,
#${DRAWER_ID} .tg-course-stat,
#${DRAWER_ID} .tg-subgroup-count {
border-color: rgba(15,23,42,.06);
background: rgba(255,255,255,.38);
color: rgba(31,41,55,.72);
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
#${DRAWER_ID} .tg-actions {
border-top: 1px solid rgba(15,23,42,.08);
background: rgba(255,255,255,.38);
backdrop-filter: blur(18px) saturate(180%);
-webkit-backdrop-filter: blur(18px) saturate(180%);
}
#${DRAWER_ID} textarea.tg-json-box {
color: #111827;
background: rgba(255,255,255,.72);
border-color: rgba(15,23,42,.1);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
#${BUTTON_ID},
#${DRAWER_ID} {
background: rgba(255,255,255,.94);
}
#${DRAWER_ID} .tg-meta,
#${DRAWER_ID} .tg-alert,
#${DRAWER_ID} .tg-empty,
#${DRAWER_ID} .tg-date-filter,
#${DRAWER_ID} .tg-summary-item,
#${DRAWER_ID} .tg-refresh-status,
#${DRAWER_ID} .tg-course-group,
#${DRAWER_ID} .tg-course-subgroup,
#${DRAWER_ID} .tg-card {
background: rgba(255,255,255,.9);
}
}
@keyframes tgLiquidRipple {
to {
opacity: 0;
transform: translate(-50%, -50%) scale(34);
}
}
/* Readable dark console skin: higher contrast, less white haze */
#${BUTTON_ID}, #${DRAWER_ID} {
--tg-liquid-surface: rgba(10, 14, 24, .78);
--tg-liquid-card: rgba(17, 24, 39, .74);
--tg-liquid-card-hover: rgba(24, 34, 52, .86);
--tg-liquid-border: rgba(125, 211, 252, .24);
--tg-liquid-line: rgba(148, 163, 184, .2);
--tg-liquid-shadow: 0 22px 70px rgba(0, 0, 0, .46);
--tg-text: rgba(248, 250, 252, .96);
--tg-muted: rgba(203, 213, 225, .78);
--tg-faint: rgba(148, 163, 184, .7);
--tg-cyan: #22d3ee;
--tg-violet: #8b5cf6;
--tg-pink: #f472b6;
--tg-red: #fb7185;
--tg-amber: #fbbf24;
--tg-green: #4ade80;
color: var(--tg-text);
}
#${BUTTON_ID} {
border: 1px solid rgba(125, 211, 252, .32);
background:
radial-gradient(circle at var(--tg-mouse-x, 28%) var(--tg-mouse-y, 18%), rgba(34,211,238,.18), transparent 36%),
linear-gradient(135deg, rgba(31,41,55,.92), rgba(8,13,24,.82)),
rgba(10,14,24,.82);
color: rgba(248,250,252,.96);
box-shadow: 0 18px 48px rgba(0,0,0,.42), 0 0 0 1px rgba(255,255,255,.04) inset, inset 0 1px 0 rgba(255,255,255,.16);
backdrop-filter: blur(24px) saturate(170%);
-webkit-backdrop-filter: blur(24px) saturate(170%);
}
#${BUTTON_ID}::before {
background:
linear-gradient(120deg, transparent 18%, rgba(125,211,252,.28) 38%, transparent 58%),
radial-gradient(circle at 50% 0%, rgba(167,139,250,.18), transparent 44%);
opacity: .36;
}
#${BUTTON_ID}:hover {
border-color: rgba(125, 211, 252, .56);
background:
radial-gradient(circle at var(--tg-mouse-x, 28%) var(--tg-mouse-y, 18%), rgba(34,211,238,.26), transparent 38%),
linear-gradient(135deg, rgba(38,50,72,.96), rgba(12,18,31,.9)),
rgba(12,18,31,.9);
box-shadow: 0 24px 60px rgba(0,0,0,.5), 0 0 28px rgba(34,211,238,.14), inset 0 1px 0 rgba(255,255,255,.2);
}
#${DRAWER_ID} {
color: var(--tg-text);
background:
radial-gradient(circle at var(--tg-mouse-x, 72%) var(--tg-mouse-y, 12%), rgba(34,211,238,.13), transparent 30%),
radial-gradient(circle at 100% 0%, rgba(139,92,246,.12), transparent 34%),
linear-gradient(145deg, rgba(17,24,39,.84), rgba(3,7,18,.82)),
rgba(10,14,24,.78);
border: 1px solid rgba(125, 211, 252, .26);
box-shadow: var(--tg-liquid-shadow), 0 0 42px rgba(34,211,238,.08), 0 0 74px rgba(139,92,246,.07), inset 0 1px 0 rgba(255,255,255,.12);
backdrop-filter: blur(24px) saturate(170%);
-webkit-backdrop-filter: blur(24px) saturate(170%);
}
#${DRAWER_ID}.tg-open {
background:
radial-gradient(circle at var(--tg-mouse-x, 72%) var(--tg-mouse-y, 12%), rgba(34,211,238,.15), transparent 32%),
radial-gradient(circle at 100% 0%, rgba(139,92,246,.14), transparent 35%),
linear-gradient(145deg, rgba(17,24,39,.88), rgba(3,7,18,.86)),
rgba(10,14,24,.84);
}
#${DRAWER_ID}::before {
background:
linear-gradient(125deg, rgba(255,255,255,.14), transparent 24%, rgba(34,211,238,.08) 58%, transparent 76%),
radial-gradient(circle at var(--tg-mouse-x, 80%) var(--tg-mouse-y, 8%), rgba(125,211,252,.16), transparent 26%);
opacity: .42;
mix-blend-mode: screen;
}
#${DRAWER_ID}::after {
background:
linear-gradient(135deg, rgba(255,255,255,.13), transparent 28%),
linear-gradient(315deg, rgba(34,211,238,.08), transparent 36%);
opacity: .42;
}
#${DRAWER_ID} .tg-header {
border-bottom: 1px solid rgba(148, 163, 184, .18);
background: rgba(15, 23, 42, .58);
backdrop-filter: blur(18px) saturate(160%);
-webkit-backdrop-filter: blur(18px) saturate(160%);
}
#${DRAWER_ID} .tg-title-main,
#${DRAWER_ID} .tg-card-title,
#${DRAWER_ID} .tg-course-head,
#${DRAWER_ID} .tg-section-title,
#${DRAWER_ID} .tg-meta-value,
#${DRAWER_ID} .tg-refresh-title,
#${DRAWER_ID} .tg-summary-num {
color: rgba(248,250,252,.98);
text-shadow: none;
}
#${DRAWER_ID} .tg-title-sub,
#${DRAWER_ID} .tg-meta-label,
#${DRAWER_ID} .tg-refresh-stage,
#${DRAWER_ID} .tg-small,
#${DRAWER_ID} .tg-task-meta,
#${DRAWER_ID} .tg-summary-label,
#${DRAWER_ID} .tg-date-filter-hint,
#${DRAWER_ID} .tg-current-filter,
#${DRAWER_ID} .tg-section-count,
#${DRAWER_ID} .tg-course-stat,
#${DRAWER_ID} .tg-subgroup-title,
#${DRAWER_ID} .tg-subgroup-count {
color: var(--tg-muted);
text-shadow: none;
}
#${DRAWER_ID} .tg-current-filter strong,
#${DRAWER_ID} .tg-meta-kicker {
color: #67e8f9;
}
#${DRAWER_ID} .tg-meta,
#${DRAWER_ID} .tg-alert,
#${DRAWER_ID} .tg-empty,
#${DRAWER_ID} .tg-date-filter,
#${DRAWER_ID} .tg-summary-item,
#${DRAWER_ID} .tg-refresh-status,
#${DRAWER_ID} .tg-course-group,
#${DRAWER_ID} .tg-course-subgroup,
#${DRAWER_ID} .tg-card {
color: var(--tg-text);
border: 1px solid var(--tg-liquid-line);
background:
linear-gradient(180deg, rgba(30,41,59,.72), rgba(15,23,42,.66)),
rgba(15, 23, 42, .72);
box-shadow: 0 12px 34px rgba(0,0,0,.28), inset 0 1px 0 rgba(255,255,255,.08);
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
#${DRAWER_ID} .tg-meta,
#${DRAWER_ID} .tg-alert,
#${DRAWER_ID} .tg-empty,
#${DRAWER_ID} .tg-date-filter,
#${DRAWER_ID} .tg-summary-item,
#${DRAWER_ID} .tg-refresh-status,
#${DRAWER_ID} .tg-course-group {
backdrop-filter: blur(10px) saturate(145%);
-webkit-backdrop-filter: blur(10px) saturate(145%);
}
#${DRAWER_ID} .tg-meta::before,
#${DRAWER_ID} .tg-alert::before,
#${DRAWER_ID} .tg-empty::before,
#${DRAWER_ID} .tg-date-filter::before,
#${DRAWER_ID} .tg-summary-item::before,
#${DRAWER_ID} .tg-card::before {
background: linear-gradient(135deg, rgba(255,255,255,.1), transparent 38%);
opacity: .32;
}
#${DRAWER_ID} .tg-summary-item:hover,
#${DRAWER_ID} .tg-card:hover,
#${DRAWER_ID} .tg-course-group:hover {
border-color: rgba(125,211,252,.34);
background:
linear-gradient(180deg, rgba(38,50,72,.84), rgba(17,24,39,.78)),
rgba(17,24,39,.82);
box-shadow: 0 16px 42px rgba(0,0,0,.36), 0 0 26px rgba(34,211,238,.08), inset 0 1px 0 rgba(255,255,255,.12);
}
#${DRAWER_ID} .tg-summary-item.tg-selected {
border-color: rgba(34,211,238,.5);
background:
linear-gradient(180deg, rgba(14,116,144,.32), rgba(17,24,39,.82)),
rgba(17,24,39,.86);
box-shadow: 0 16px 42px rgba(0,0,0,.36), 0 0 28px rgba(34,211,238,.16), inset 0 1px 0 rgba(255,255,255,.14);
}
#${DRAWER_ID} .tg-summary-item.tg-selected .tg-summary-num {
color: #67e8f9;
}
#${DRAWER_ID} .tg-icon-btn,
#${DRAWER_ID} .tg-action-btn,
#${DRAWER_ID} .tg-detail-link,
#${DRAWER_ID} .tg-course-toggle,
#${DRAWER_ID} .tg-type,
#${DRAWER_ID} .tg-date-filter select {
color: rgba(248,250,252,.94);
border: 1px solid rgba(148,163,184,.22);
background: rgba(15,23,42,.7);
box-shadow: 0 8px 22px rgba(0,0,0,.24), inset 0 1px 0 rgba(255,255,255,.08);
backdrop-filter: blur(12px) saturate(145%);
-webkit-backdrop-filter: blur(12px) saturate(145%);
}
#${DRAWER_ID} .tg-icon-btn::before,
#${DRAWER_ID} .tg-action-btn::before,
#${DRAWER_ID} .tg-detail-link::before,
#${DRAWER_ID} .tg-course-toggle::before {
background: linear-gradient(120deg, transparent, rgba(125,211,252,.2), transparent);
}
#${DRAWER_ID} .tg-icon-btn:hover,
#${DRAWER_ID} .tg-action-btn:hover,
#${DRAWER_ID} .tg-detail-link:hover,
#${DRAWER_ID} .tg-course-toggle:hover,
#${DRAWER_ID} .tg-date-filter select:hover,
#${DRAWER_ID} .tg-date-filter select:focus {
border-color: rgba(125,211,252,.42);
background: rgba(30,41,59,.82);
box-shadow: 0 12px 30px rgba(0,0,0,.32), 0 0 22px rgba(34,211,238,.08), inset 0 1px 0 rgba(255,255,255,.12);
}
#${DRAWER_ID} .tg-date-filter select option {
background: #0f172a;
color: rgba(248,250,252,.94);
}
#${DRAWER_ID} [data-copy-all-json] {
border-color: rgba(34,211,238,.36);
background: rgba(8,47,73,.72);
color: rgba(236,254,255,.98);
}
#${DRAWER_ID} [data-delete-cache] {
border-color: rgba(251,113,133,.36);
background: rgba(76, 29, 38, .72);
color: rgba(255,228,230,.96);
}
#${DRAWER_ID} .tg-task-meta span,
#${DRAWER_ID} .tg-course-stat,
#${DRAWER_ID} .tg-subgroup-count {
border-color: rgba(148,163,184,.14);
background: rgba(15,23,42,.48);
color: rgba(203,213,225,.82);
}
#${DRAWER_ID} .tg-progress-track {
border: 1px solid rgba(148,163,184,.18);
background: rgba(15,23,42,.72);
}
#${DRAWER_ID} .tg-progress-fill {
background: linear-gradient(90deg, rgba(34,211,238,.9), rgba(99,102,241,.78), rgba(139,92,246,.68));
box-shadow: 0 0 18px rgba(34,211,238,.18);
}
#${DRAWER_ID} .tg-summary-item[data-tooltip]::after,
#${DRAWER_ID} .tg-status[data-tooltip]::after {
color: rgba(248,250,252,.96);
background:
linear-gradient(180deg, rgba(30,41,59,.94), rgba(15,23,42,.92)),
rgba(15,23,42,.92);
border: 1px solid rgba(125,211,252,.22);
box-shadow: 0 16px 40px rgba(0,0,0,.34), inset 0 1px 0 rgba(255,255,255,.1);
}
#${DRAWER_ID} .tg-summary-item[data-tooltip]::before,
#${DRAWER_ID} .tg-status[data-tooltip]::before {
background: rgba(30,41,59,.94);
border-right: 1px solid rgba(125,211,252,.18);
border-bottom: 1px solid rgba(125,211,252,.18);
}
#${DRAWER_ID} .tg-alert {
color: #fecdd3;
background:
linear-gradient(180deg, rgba(127,29,29,.42), rgba(15,23,42,.72)),
rgba(76,29,38,.66);
border-color: rgba(251,113,133,.25);
}
#${DRAWER_ID} .tg-empty {
color: rgba(203,213,225,.84);
}
#${DRAWER_ID} .tg-actions {
border-top: 1px solid rgba(148,163,184,.18);
background: rgba(3,7,18,.72);
backdrop-filter: blur(18px) saturate(150%);
-webkit-backdrop-filter: blur(18px) saturate(150%);
}
#${DRAWER_ID} textarea.tg-json-box {
color: rgba(248,250,252,.94);
background: rgba(2,6,23,.82);
border-color: rgba(148,163,184,.2);
}
#${DRAWER_ID} .tg-ok { color: #4ade80; }
#${DRAWER_ID} .tg-warn { color: #fbbf24; }
#${DRAWER_ID} .tg-bad { color: #fb7185; }
#${DRAWER_ID} .tg-muted { color: rgba(203,213,225,.78); }
#${DRAWER_ID} .tg-state-ok {
border-color: rgba(74,222,128,.22);
}
#${DRAWER_ID} .tg-state-warn {
border-color: rgba(251,191,36,.28);
}
#${DRAWER_ID} .tg-state-bad {
border-color: rgba(251,113,133,.34);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
#${BUTTON_ID},
#${DRAWER_ID} {
background: rgba(15,23,42,.96);
}
#${DRAWER_ID} .tg-meta,
#${DRAWER_ID} .tg-alert,
#${DRAWER_ID} .tg-empty,
#${DRAWER_ID} .tg-date-filter,
#${DRAWER_ID} .tg-summary-item,
#${DRAWER_ID} .tg-refresh-status,
#${DRAWER_ID} .tg-course-group,
#${DRAWER_ID} .tg-course-subgroup,
#${DRAWER_ID} .tg-card {
background: rgba(17,24,39,.96);
}
}
@media (max-width: 520px) {
#${BUTTON_ID} {
right: 16px;
bottom: 18px;
}
#${DRAWER_ID} {
right: 8px;
top: 8px;
bottom: 8px;
width: calc(100vw - 16px);
border-radius: 24px;
}
#${DRAWER_ID} .tg-summary {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
#${DRAWER_ID} .tg-meta-grid,
#${DRAWER_ID} .tg-task-meta {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
#${DRAWER_ID} .tg-date-filter-row {
flex-wrap: wrap;
}
}
`;
if (typeof GM_addStyle === "function") {
GM_addStyle(css);
} else {
const style = document.createElement("style");
style.textContent = css;
document.head.appendChild(style);
}
async function getValue(key, defaultValue) {
if (typeof GM !== "undefined" && GM.getValue) {
return await GM.getValue(key, defaultValue);
}
return GM_getValue(key, defaultValue);
}
async function setValue(key, value) {
if (typeof GM !== "undefined" && GM.setValue) {
await GM.setValue(key, value);
return;
}
GM_setValue(key, value);
}
async function deleteValue(key) {
if (typeof GM !== "undefined" && GM.deleteValue) {
await GM.deleteValue(key);
return;
}
GM_deleteValue(key);
}
function extractLoginFromText(text) {
if (!text) return "";
const patterns = [
/user_\d+/i,
/"login"\s*:\s*"([^"]+)"/i,
/"username"\s*:\s*"([^"]+)"/i,
/zzud=([^&"'\s]+)/i,
/username=([^&"'\s]+)/i
];
for (const pattern of patterns) {
const match = String(text).match(pattern);
if (match) {
const value = match[1] || match[0];
if (value && /^user_\d+$/i.test(value)) {
return value;
}
}
}
return "";
}
function detectLoginFromPage() {
const manualLogin = String(MANUAL_LOGIN || "").trim();
if (manualLogin) return manualLogin;
const candidates = [];
candidates.push(location.href);
if (document && document.documentElement) {
candidates.push(document.documentElement.innerHTML);
}
try {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
candidates.push(key);
candidates.push(localStorage.getItem(key));
}
} catch (e) {}
try {
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
candidates.push(key);
candidates.push(sessionStorage.getItem(key));
}
} catch (e) {}
for (const text of candidates) {
const login = extractLoginFromText(text);
if (login) return login;
}
return "";
}
function saveAutoLoginIfDetected() {
if (!isTgHost()) {
return "";
}
const login = detectLoginFromPage();
if (login) {
GM_setValue(STORE_KEY_AUTO_LOGIN, login);
return login;
}
return "";
}
function escapeHtml(text) {
return String(text ?? "")
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
}
function cleanStatusMessage(message) {
return String(message || "").replace(/^\[DEBUG\]\s*/, "");
}
function isTgHost(hostname = location.hostname) {
return hostname === "tg.zcst.edu.cn" || hostname === TG_INTRANET_HOST;
}
function getTgOrigin(hostname = location.hostname) {
return hostname === TG_INTRANET_HOST ? TG_INTRANET_ORIGIN : TG_EXTERNAL_ORIGIN;
}
function getCurrentPageSite() {
if (location.hostname === "www.educoder.net") {
return {
siteKey: "educoder",
siteName: "Educoder主站",
pageOrigin: "https://www.educoder.net",
apiOrigin: "https://data.educoder.net",
resultKey: STORE_KEY_EDUCODER_RESULT,
login: "pchtkff9y",
courseId: 109348,
courseIdentifier: "MOAPGNLO",
courseName: "操作系统2026",
viewHint: "当前为 Educoder 主站视图,仅展示本平台任务。"
};
}
return {
siteKey: "tg-zcst",
backendSiteKey: "tg",
siteName: "TG校内站",
pageOrigin: getTgOrigin(),
apiOrigin: getTgOrigin(),
resultKey: STORE_KEY_TG_RESULT,
login: "",
viewHint: "当前为 TG 校内站视图,仅展示本平台任务。"
};
}
function getCurrentBackendSiteKey() {
const site = getCurrentPageSite();
return site.backendSiteKey || site.siteKey;
}
function isTaskForCurrentSite(task) {
const current = getCurrentPageSite();
return getTaskSiteKey(task) === current.siteKey || String(task?.siteKey || "") === getCurrentBackendSiteKey();
}
function isValueForCurrentSite(value) {
const current = getCurrentPageSite();
if (!value?.siteKey) return current.siteKey === "tg-zcst";
return value.siteKey === getCurrentBackendSiteKey() || value.siteKey === current.siteKey || (value.siteKey === "tg" && current.siteKey === "tg-zcst");
}
function filterResultForCurrentSite(result) {
if (!result || typeof result !== "object") return result;
const currentSite = getCurrentPageSite();
const backendSiteKey = getCurrentBackendSiteKey();
const matchesCurrentSite = item => {
const key = String(item?.siteKey || item?.key || "").trim();
return key === backendSiteKey || key === currentSite.siteKey || (key === "tg" && currentSite.siteKey === "tg-zcst");
};
const courses = Array.isArray(result.courses)
? result.courses.filter(course => {
const key = String(course?.siteKey || "").trim();
return key === backendSiteKey || (key === "tg" && currentSite.siteKey === "tg-zcst");
})
: [];
const tasks = Array.isArray(result.tasks) ? result.tasks.filter(isTaskForCurrentSite) : [];
const debug = result.debug && typeof result.debug === "object"
? {
...result.debug,
siteLogin: Array.isArray(result.debug.siteLogin) ? result.debug.siteLogin.filter(matchesCurrentSite) : result.debug.siteLogin,
siteScan: Array.isArray(result.debug.siteScan) ? result.debug.siteScan.filter(matchesCurrentSite) : result.debug.siteScan,
requestLog: Array.isArray(result.debug.requestLog) ? result.debug.requestLog.filter(matchesCurrentSite) : result.debug.requestLog,
courseScanFlow: Array.isArray(result.debug.courseScanFlow) ? result.debug.courseScanFlow.filter(matchesCurrentSite) : result.debug.courseScanFlow
}
: result.debug;
return {
...result,
courses,
tasks,
debug,
summary: {
...(result.summary || {}),
courseCount: courses.length,
taskCount: tasks.length
}
};
}
function isResultForCurrentSite(result) {
if (!result || typeof result !== "object") return false;
const current = getCurrentPageSite();
const backendSiteKey = getCurrentBackendSiteKey();
const keys = [
result.siteKey,
result.currentSite?.key,
...(Array.isArray(result.sites) ? result.sites.map(site => site?.key) : []),
...(Array.isArray(result.courses) ? result.courses.map(course => course?.siteKey) : []),
...(Array.isArray(result.tasks) ? result.tasks.map(task => task?.siteKey) : [])
].filter(Boolean).map(value => String(value).trim());
if (!keys.length) {
return current.siteKey === "tg-zcst";
}
return keys.some(key =>
key === current.siteKey ||
key === backendSiteKey ||
(key === "tg" && current.siteKey === "tg-zcst")
);
}
function getTaskSiteKey(task) {
const raw = String(task?.siteKey || "").trim();
if (!raw || raw === "tg") return "tg-zcst";
return raw;
}
function getTaskSiteName(task) {
return String(task?.siteName || (getTaskSiteKey(task) === "educoder" ? "Educoder主站" : "TG校内站")).trim();
}
function getCourseKey(task) {
const siteKey = getTaskSiteKey(task);
const courseId = task?.courseId || task?.courseIdentifier || task?.courseName || "unknown";
return `${siteKey}:${courseId}`;
}
function formatDateTime(value) {
if (!value) return "--";
try {
const normalized = String(value).replace(/-/g, "/");
const d = new Date(normalized);
if (Number.isNaN(d.getTime())) return String(value);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const h = String(d.getHours()).padStart(2, "0");
const min = String(d.getMinutes()).padStart(2, "0");
return `${y}-${m}-${day} ${h}:${min}`;
} catch (e) {
return String(value);
}
}
function normalizeTasks(data) {
if (Array.isArray(data?.tasks)) return data.tasks;
const tasks = [];
for (const course of data?.courses || []) {
for (const exam of course.exams || []) tasks.push({ ...exam, taskType: "exercise", taskTypeText: "考试/小测试" });
for (const homework of course.homeworks || []) tasks.push({ ...homework, taskType: "common_homework", taskTypeText: "图文作业" });
for (const experiment of course.experiments || []) tasks.push({ ...experiment, taskType: "classroom_experiment", taskTypeText: "课堂实验" });
}
return tasks;
}
function getTaskDate(task) {
const candidates = [
task?.endTime,
task?.publishTime,
task?.scanTime,
task?.startAt,
task?.endAt
];
for (const v of candidates) {
if (!v) continue;
const d = new Date(String(v).replace(/-/g, "/"));
if (!Number.isNaN(d.getTime())) return d;
}
return null;
}
function getTaskEndDate(task) {
const candidates = [
task?.endTime,
task?.endAt
];
for (const v of candidates) {
if (!v) continue;
const d = new Date(String(v).replace(/-/g, "/"));
if (!Number.isNaN(d.getTime())) return d;
}
return null;
}
function parseChineseRemainingToMs(text) {
if (!text) return null;
const str = String(text);
let ms = 0;
const day = str.match(/(\d+)\s*天/);
const hour = str.match(/(\d+)\s*小时/);
const minute = str.match(/(\d+)\s*分/);
const second = str.match(/(\d+)\s*秒/);
if (day) ms += Number(day[1]) * 24 * 60 * 60 * 1000;
if (hour) ms += Number(hour[1]) * 60 * 60 * 1000;
if (minute) ms += Number(minute[1]) * 60 * 1000;
if (second) ms += Number(second[1]) * 1000;
return ms || null;
}
function getTaskDeadlineDate(task) {
const timestamp = Number(task?.deadlineTimestamp ?? task?.raw?.deadlineTimestamp);
if (Number.isFinite(timestamp) && timestamp > 0) {
const normalizedTimestamp = timestamp < 10000000000 ? timestamp * 1000 : timestamp;
const d = new Date(normalizedTimestamp);
if (!Number.isNaN(d.getTime())) return d;
}
const raw = task?.raw || {};
const candidates = [
task?.endTime,
task?.endAt,
task?.deadline,
task?.deadlineTime,
task?.closeTime,
task?.end_time,
task?.end_time_s,
task?.deadline_time,
task?.close_time,
raw.end_time,
raw.end_time_s,
raw.endAt,
raw.deadline,
raw.deadlineTime,
raw.deadline_time,
raw.closeTime,
raw.close_time,
raw["截止时间"],
raw.exam?.end_time,
raw.exam?.end_time_s,
raw.exam?.deadline,
raw.exam?.deadlineTime,
raw.exam?.closeTime
];
for (const value of candidates) {
if (!value) continue;
const d = new Date(String(value).replace(/-/g, "/"));
if (!Number.isNaN(d.getTime())) return d;
}
return null;
}
function getTaskRemainingMs(task, now = new Date()) {
const remainingMs = Number(task?.remainingMs ?? task?.raw?.remainingMs);
if (Number.isFinite(remainingMs)) return remainingMs;
const deadline = getTaskDeadlineDate(task);
if (deadline) return deadline.getTime() - now.getTime();
const relativeCandidates = [
task?.deadlineRemaining,
task?.deadline,
task?.deadlineTime,
task?.closeTime,
task?.exerciseLeftTime,
task?.exercise_left_time,
task?.remainingTime,
task?.remainTime,
task?.status_time,
task?.raw?.deadlineRemaining,
task?.raw?.deadline,
task?.raw?.deadlineTime,
task?.raw?.deadline_time,
task?.raw?.closeTime,
task?.raw?.close_time,
task?.raw?.["截止时间"],
task?.raw?.exercise_left_time,
task?.raw?.status_time,
task?.raw?.exam?.exercise_left_time,
task?.raw?.exam?.deadline,
task?.raw?.exam?.deadlineTime,
task?.raw?.exam?.closeTime
];
for (const value of relativeCandidates) {
const ms = parseChineseRemainingToMs(value);
if (ms != null) return ms;
}
return null;
}
function parseTaskDeadline(task) {
if (task?.endTime) {
const d = new Date(String(task.endTime).replace(/-/g, "/"));
if (!Number.isNaN(d.getTime())) return d;
}
if (task?.deadlineAt) {
const d = new Date(String(task.deadlineAt).replace(/-/g, "/"));
if (!Number.isNaN(d.getTime())) return d;
}
if (task?.deadlineRemaining && task?.scanTime) {
const base = new Date(String(task.scanTime).replace(/-/g, "/"));
const ms = parseChineseRemainingToMs(task.deadlineRemaining);
if (!Number.isNaN(base.getTime()) && ms != null) {
return new Date(base.getTime() + ms);
}
}
return null;
}
function parseCountdownDeadline(value) {
if (!value) return null;
const d = new Date(String(value).replace(/-/g, "/"));
if (Number.isNaN(d.getTime())) return null;
return d;
}
function formatCountdown(deadlineValue) {
const deadline = deadlineValue instanceof Date ? deadlineValue : parseCountdownDeadline(deadlineValue);
if (!deadline) return "无截止时间";
const diff = deadline.getTime() - Date.now();
if (diff <= 0) return "已截止";
const totalSeconds = Math.floor(diff / 1000);
const totalMinutes = Math.floor(totalSeconds / 60);
const totalHours = Math.floor(totalSeconds / 3600);
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (diff >= 3 * 86400000) {
return `剩余 ${days}天 ${hours}小时`;
}
if (diff >= 86400000) {
return `仅剩 ${days}天 ${hours}小时`;
}
if (diff >= 3600000) {
return `仅剩 ${totalHours}小时 ${minutes}分`;
}
return `仅剩 ${totalMinutes}分 ${seconds}秒`;
}
function updateCountdowns() {
document.querySelectorAll(".tg-countdown[data-deadline]").forEach(el => {
const deadline = el.dataset.deadline || "";
el.textContent = formatCountdown(deadline);
});
}
function updateCountdownsOnly() {
updateCountdowns();
}
function ensureCountdownTimer() {
if (countdownIntervalId) return;
countdownIntervalId = setInterval(updateCountdownsOnly, 1000);
}
function stopCountdownTimer() {
if (!countdownIntervalId) return;
clearInterval(countdownIntervalId);
countdownIntervalId = null;
}
function getFilterStartDate(year, month) {
return new Date(year, month - 1, 1, 0, 0, 0);
}
function filterTasksByYearMonth(tasks, year, month) {
const startDate = getFilterStartDate(year, month);
return tasks.filter(task => {
const d = getTaskDate(task);
if (!d) return true;
return d >= startDate;
});
}
function getDefaultFilterYearMonth() {
const d = new Date();
d.setDate(1);
d.setHours(0, 0, 0, 0);
d.setMonth(d.getMonth() - 5);
return {
year: d.getFullYear(),
month: d.getMonth() + 1
};
}
async function loadFilterYearMonth() {
const saved = await getValue(STORE_KEY_FILTER_YEAR_MONTH, null);
const year = Number(saved?.year);
const month = Number(saved?.month);
if (year && month >= 1 && month <= 12) {
return { year, month };
}
return getDefaultFilterYearMonth();
}
async function loadCourseCollapseState() {
const saved = await getValue(STORE_KEY_COURSE_COLLAPSE, {});
return saved && typeof saved === "object" ? saved : {};
}
async function loadSectionCollapseState() {
const saved = await getValue(STORE_KEY_SECTION_COLLAPSE, {});
return saved && typeof saved === "object" ? saved : {};
}
async function loadIgnoredTaskMap() {
const saved = await getValue(STORE_KEY_IGNORED_TASKS, {});
return saved && typeof saved === "object" ? saved : {};
}
async function saveIgnoredTaskMap(map) {
const nextMap = map && typeof map === "object" ? map : {};
await setValue(STORE_KEY_IGNORED_TASKS, nextMap);
}
async function loadAutoRefreshDoneAt() {
return Number(await getValue(STORE_KEY_AUTO_REFRESH_DONE_AT, 0)) || 0;
}
async function markAutoRefreshDoneAt(timestamp = Date.now()) {
await setValue(STORE_KEY_AUTO_REFRESH_DONE_AT, Number(timestamp) || Date.now());
}
async function clearAutoRefreshDoneAt() {
await deleteValue(STORE_KEY_AUTO_REFRESH_DONE_AT);
}
async function isRefreshBusy() {
const refreshStatus = await getValue(STORE_KEY_REFRESH_STATUS, null);
if (refreshStatus?.status === "running" || refreshStatus?.status === "waiting") {
return true;
}
const runningState = await loadRunningState();
return runningState?.running === true;
}
function getTaskStableId(task) {
const raw = task?.raw || {};
const rawExam = raw?.exam || {};
const candidates = [
task?.exerciseId,
task?.examId,
task?.homeworkId,
task?.homework_id,
task?.shixunId,
task?.shixun_id,
task?.taskId,
task?.id,
task?.categoryId,
rawExam?.id,
raw?.exerciseId,
raw?.homework_id,
raw?.id,
raw?.category_id
];
for (const value of candidates) {
if (value === undefined || value === null || value === "") continue;
return String(value).trim();
}
return "";
}
function getTaskDeadlineLabel(task) {
const deadlineDate = getTaskDeadlineDate(task) || parseTaskDeadline(task);
const fallback = task?.deadline || task?.endTime || task?.endAt || task?.end_at || task?.dangerDeadline || task?.deadlineTime || "";
if (deadlineDate) {
return formatDateTime(deadlineDate);
}
return fallback ? String(fallback) : "无截止时间";
}
function getTaskKey(task) {
if (!hasLoggedTaskKeySample && task) {
console.log("[TG任务key检查] task=", task);
hasLoggedTaskKeySample = true;
}
const sourceKey = String(task?.siteKey || task?.source || task?.apiSource || "unknown").trim();
const courseKey = String(task?.courseIdentifier || task?.courseId || task?.courseName || "unknown").trim();
const taskType = String(task?.taskType || task?.type || task?.taskTypeText || "task").trim();
const stableId = getTaskStableId(task);
if (stableId) {
return `${sourceKey}|${courseKey}|${taskType}|${stableId}`;
}
const fallbackTitle = getTaskTitle(task);
const fallbackDeadline = getTaskDeadlineLabel(task);
return `${sourceKey}|${courseKey}|${taskType}|${fallbackTitle}|${fallbackDeadline}`;
}
function createIgnoredTaskSnapshot(task, taskKey = getTaskKey(task)) {
return {
key: taskKey,
title: getTaskTitle(task),
courseName: task?.courseName || "",
courseId: task?.courseId || "",
courseIdentifier: task?.courseIdentifier || "",
taskType: task?.taskType || task?.type || "",
taskTypeText: task?.taskTypeText || "",
source: task?.source || task?.apiSource || "",
siteKey: getTaskSiteKey(task),
siteName: getTaskSiteName(task),
deadline: getTaskDeadlineLabel(task),
detailUrl: task?.detailUrl || task?.url || "",
ignoredAt: new Date().toLocaleString()
};
}
async function ignoreTask(task) {
const taskKey = getTaskKey(task);
console.log("[TG忽略机制] taskKey=", taskKey, task?.title || task?.name);
const ignoredTaskMap = await loadIgnoredTaskMap();
ignoredTaskMap[taskKey] = createIgnoredTaskSnapshot(task, taskKey);
await saveIgnoredTaskMap(ignoredTaskMap);
console.log("[TG忽略机制] ignoredTaskMap=", ignoredTaskMap);
return ignoredTaskMap;
}
async function restoreTask(taskKey) {
const ignoredTaskMap = await loadIgnoredTaskMap();
delete ignoredTaskMap[taskKey];
await saveIgnoredTaskMap(ignoredTaskMap);
console.log("[TG忽略机制] ignoredTaskMap=", ignoredTaskMap);
return ignoredTaskMap;
}
function isDangerTask(task) {
if (task?.completed || task?.submitted) return false;
if (task?.ended) return false;
const remainingMs = getTaskRemainingMs(task);
if (remainingMs !== null) {
if (remainingMs <= 0) return false;
if (remainingMs <= DANGER_DAYS_THRESHOLD * 86400000) return true;
}
return false;
}
function taskPriority(task) {
if (isDangerTask(task)) return 1;
if (!task.completed && task.ended) return 2;
if (!task.completed) return 3;
return 5;
}
function sortTasks(tasks) {
return [...tasks].sort((a, b) => taskPriority(a) - taskPriority(b));
}
function calculateSummaryFromTasks(tasks) {
const siteCount = new Set(tasks.map(task => getTaskSiteKey(task)).filter(Boolean)).size;
const courseCount = new Set(tasks.map(task => getCourseKey(task)).filter(Boolean)).size;
const exerciseCount = tasks.filter(task => task.taskType === "exercise").length;
const homeworkCount = tasks.filter(task => task.taskType === "common_homework").length;
const experimentCount = tasks.filter(task => task.taskType === "classroom_experiment").length;
const unfinishedCount = tasks.filter(task => !task.completed).length;
const dangerCount = tasks.filter(task => !task.completed && isDangerTask(task)).length;
return {
siteCount,
courseCount,
taskCount: tasks.length,
exerciseCount,
homeworkCount,
experimentCount,
unfinishedCount,
dangerCount
};
}
function statusTone(task) {
const text = task.statusText || "";
if (task.completed) return { card: "tg-state-ok", text: "tg-ok" };
if (task.ended || text.includes("已截止")) return { card: "tg-state-bad", text: "tg-bad" };
if (isDangerTask(task) || text.includes("快截止") || text.includes("进行中")) return { card: "tg-state-warn", text: "tg-warn" };
return { card: "tg-state-muted", text: "tg-muted" };
}
function currentFilter() {
const filter = localStorage.getItem(FILTER_KEY) || "all";
if (filter === "site") return "all";
if (filter === "homework") return "common_homework";
if (filter === "experiment") return "classroom_experiment";
return filter;
}
function setCurrentFilter(filter) {
localStorage.setItem(FILTER_KEY, filter);
}
function isOpen() {
return localStorage.getItem(OPEN_KEY) === "1";
}
function setOpen(value) {
localStorage.setItem(OPEN_KEY, value ? "1" : "0");
}
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function getDefaultPanelState() {
return {
buttonX: Math.max(18, window.innerWidth - 84),
buttonY: Math.max(18, window.innerHeight - 86),
panelWidth: 520,
panelHeight: Math.min(760, Math.max(480, window.innerHeight - 48))
};
}
async function loadPanelState() {
const saved = await getValue(STORE_KEY_PANEL_STATE, {});
return {
...getDefaultPanelState(),
...(saved && typeof saved === "object" ? saved : {})
};
}
async function savePanelStatePatch(patch) {
const current = await loadPanelState();
await setValue(STORE_KEY_PANEL_STATE, { ...current, ...patch });
}
async function applyPanelState(panelState) {
const button = document.getElementById(BUTTON_ID);
const drawer = document.getElementById(DRAWER_ID);
const state = panelState || await loadPanelState();
if (button) {
const x = clamp(Number(state.buttonX) || 22, -42, window.innerWidth - 20);
const y = clamp(Number(state.buttonY) || 24, -42, window.innerHeight - 20);
button.style.left = `${x}px`;
button.style.top = `${y}px`;
button.style.right = "auto";
button.style.bottom = "auto";
}
if (drawer) {
const maxWidth = Math.min(760, window.innerWidth - 40);
const maxHeight = window.innerHeight - 40;
const width = clamp(Number(state.panelWidth) || 520, 390, maxWidth);
const height = clamp(Number(state.panelHeight) || Math.min(760, window.innerHeight - 48), 480, maxHeight);
drawer.style.width = `${width}px`;
drawer.style.height = `${height}px`;
drawer.style.maxWidth = `${maxWidth}px`;
drawer.style.maxHeight = `${maxHeight}px`;
drawer.style.right = "18px";
drawer.style.top = "18px";
}
}
async function loadData() {
const site = getCurrentPageSite();
const currentResult = await getValue(site.resultKey, null);
if (currentResult) {
if (site.siteKey === "educoder" && currentResult.source !== "educoder-page-frontend") {
return null;
}
return filterResultForCurrentSite(currentResult);
}
if (site.siteKey === "educoder") {
return null;
}
const fallback = await getValue(STORE_KEY, null);
return filterResultForCurrentSite(fallback);
}
async function loadRunningState() {
return await getValue(STORE_KEY_LAST_RUNNING, null);
}
async function loadLastError() {
return await getValue(STORE_KEY_LAST_ERROR, null);
}
async function loadRefreshRequest() {
return await getValue(STORE_KEY_REFRESH_REQUEST, null);
}
async function loadRefreshHandled() {
return await getValue(STORE_KEY_REFRESH_HANDLED, 0);
}
function normalizeFilterYearMonth(saved) {
const year = Number(saved?.year);
const month = Number(saved?.month);
if (year && month >= 1 && month <= 12) {
return { year, month };
}
return getDefaultFilterYearMonth();
}
function normalizeObject(value, fallback = {}) {
return value && typeof value === "object" ? value : fallback;
}
async function loadFrontendState() {
const [
result,
lastRunning,
lastError,
filterYearMonthRaw,
panelStateRaw,
courseCollapse,
sectionCollapse,
autoLogin,
refreshRequest,
refreshHandled,
refreshStatus,
ignoredTaskMap
] = await Promise.all([
loadData(),
getValue(STORE_KEY_LAST_RUNNING, null),
getValue(STORE_KEY_LAST_ERROR, null),
getValue(STORE_KEY_FILTER_YEAR_MONTH, null),
getValue(STORE_KEY_PANEL_STATE, null),
getValue(STORE_KEY_COURSE_COLLAPSE, {}),
getValue(STORE_KEY_SECTION_COLLAPSE, {}),
getValue(STORE_KEY_AUTO_LOGIN, ""),
getValue(STORE_KEY_REFRESH_REQUEST, null),
getValue(STORE_KEY_REFRESH_HANDLED, 0),
getValue(STORE_KEY_REFRESH_STATUS, null),
loadIgnoredTaskMap()
]);
return {
result,
lastRunning,
lastError: isValueForCurrentSite(lastError) ? lastError : null,
filterYearMonth: normalizeFilterYearMonth(filterYearMonthRaw),
panelState: {
...getDefaultPanelState(),
...normalizeObject(panelStateRaw)
},
courseCollapse: normalizeObject(courseCollapse),
sectionCollapse: normalizeObject(sectionCollapse),
autoLogin: String(autoLogin || "").trim(),
refreshRequest: isValueForCurrentSite(refreshRequest) ? refreshRequest : null,
refreshHandled,
refreshStatus: isValueForCurrentSite(refreshStatus) ? refreshStatus : null,
ignoredTaskMap: normalizeObject(ignoredTaskMap),
currentFilter: currentFilter()
};
}
function applyCurrentFilter(tasks, filter) {
return tasks.filter(task => taskMatchesFilter(task, filter));
}
function groupTasksByCourseAndType(tasks) {
const groups = new Map();
for (const task of tasks) {
const key = getCourseKey(task);
if (!groups.has(key)) {
groups.set(key, {
key,
siteKey: getTaskSiteKey(task),
siteName: getTaskSiteName(task),
courseName: task.courseName || "未识别课程",
tasks: [],
exams: [],
homeworks: [],
experiments: [],
unfinished: 0,
danger: 0
});
}
const group = groups.get(key);
group.tasks.push(task);
if (!task.completed) group.unfinished += 1;
if (isDangerTask(task)) group.danger += 1;
if (task.taskType === "exercise") {
group.exams.push(task);
} else if (task.taskType === "common_homework") {
group.homeworks.push(task);
} else if (task.taskType === "classroom_experiment") {
group.experiments.push(task);
}
}
return Array.from(groups.values());
}
function buildIgnoredDisplayTasks(allTasks, ignoredTaskMap) {
const matchedMap = new Map();
for (const task of allTasks) {
const key = getTaskKey(task);
if (ignoredTaskMap[key] && !matchedMap.has(key)) {
matchedMap.set(key, task);
}
}
return Object.keys(ignoredTaskMap).map(key => {
const snapshot = ignoredTaskMap[key] || {};
const matchedTask = matchedMap.get(key);
return matchedTask
? {
...snapshot,
...matchedTask,
__taskKey: key,
__ignoredView: true,
__snapshotOnly: false,
ignoredAt: snapshot.ignoredAt || matchedTask.ignoredAt || ""
}
: {
...snapshot,
__taskKey: key,
__ignoredView: true,
__snapshotOnly: true,
title: snapshot.title || "已忽略任务",
courseName: snapshot.courseName || "未识别课程",
taskType: snapshot.taskType || "task",
taskTypeText: snapshot.taskTypeText || "任务",
detailUrl: snapshot.detailUrl || "",
siteKey: snapshot.siteKey || "",
siteName: snapshot.siteName || "",
source: snapshot.source || "",
deadline: snapshot.deadline || "无截止时间"
};
});
}
function buildViewModel(result, state) {
const allTasks = sortTasks(normalizeTasks(result));
const filteredByMonth = filterTasksByYearMonth(allTasks, state.filterYearMonth.year, state.filterYearMonth.month);
const ignoredTaskMap = state.ignoredTaskMap || {};
const ignoredKeys = new Set(Object.keys(ignoredTaskMap));
const activeTasks = filteredByMonth.filter(task => !ignoredKeys.has(getTaskKey(task)));
const ignoredDisplayTasks = buildIgnoredDisplayTasks(allTasks, ignoredTaskMap);
const stats = {
...calculateSummaryFromTasks(activeTasks),
ignoredCount: Object.keys(ignoredTaskMap).length
};
const visibleTasks = state.currentFilter === "ignored"
? ignoredDisplayTasks
: applyCurrentFilter(activeTasks, state.currentFilter);
const dangerTasks = activeTasks.filter(isDangerTask);
const groupedTasks = state.currentFilter === "all"
? activeTasks.filter(task => !isDangerTask(task))
: state.currentFilter === "danger" || state.currentFilter === "ignored"
? []
: visibleTasks;
console.log("[TG忽略机制] ignoredTaskMap=", ignoredTaskMap);
console.log("[TG忽略机制] activeTasks=", activeTasks.length, "ignoredTasks=", ignoredDisplayTasks.length);
return {
allTasks,
filteredTasks: filteredByMonth,
activeTasks,
ignoredTaskMap,
ignoredKeys,
ignoredDisplayTasks,
visibleTasks,
dangerTasks,
stats,
courseGroups: groupTasksByCourseAndType(groupedTasks)
};
}
function getDangerTaskKey(task) {
return [
getTaskSiteKey(task),
task.courseId || task.courseIdentifier || task.courseName || "",
task.taskType || "",
task.exerciseId || task.homeworkId || task.id || task.title || ""
].join(":");
}
function getDangerNotifySignature(dangerList, result) {
const keys = dangerList.map(getDangerTaskKey).sort().join("|");
return `${result?.scanTimestamp || result?.scanTime || ""}:${keys}`;
}
function requestDangerNotification(dangerList, result) {
console.log("[TG任务助手] dangerList", dangerList);
if (!dangerList.length || typeof Notification === "undefined") return;
const signature = getDangerNotifySignature(dangerList, result);
if (localStorage.getItem(DANGER_NOTIFY_KEY) === signature) return;
const showNotification = () => {
if (Notification.permission !== "granted") return;
const first = dangerList[0];
const moreText = dangerList.length > 1 ? `等 ${dangerList.length} 项` : "";
new Notification("TG任务助手:发现危险任务", {
body: `${first.courseName || "课程"}:${getTaskTitle(first)} ${moreText}`,
tag: "tg-task-danger"
});
localStorage.setItem(DANGER_NOTIFY_KEY, signature);
};
if (Notification.permission === "granted") {
showNotification();
} else if (Notification.permission !== "denied") {
Notification.requestPermission().then(permission => {
if (permission === "granted") showNotification();
}).catch(() => {});
}
}
function createRoot() {
let root = document.getElementById(ROOT_ID);
if (root) return root;
root = document.createElement("div");
root.id = ROOT_ID;
root.innerHTML = `
`;
document.body.appendChild(root);
const button = root.querySelector(`#${BUTTON_ID}`);
root.addEventListener("click", handlePanelClick);
root.addEventListener("change", handlePanelChange);
root.addEventListener("mousemove", handleLiquidPointerMove, { passive: true });
attachButtonDrag(button);
attachDrawerResize(root.querySelector(`#${DRAWER_ID}`));
applyPanelState();
setDrawerOpen(isOpen());
return root;
}
async function handlePanelClick(event) {
const actionEl = event.target.closest("[data-action], [data-course-toggle], [data-section-toggle], [data-jump-id], [data-summary-filter]");
if (!actionEl) return;
const action = actionEl.dataset.action || "";
createLiquidRipple(actionEl, event);
if (action === "open-panel") {
const button = document.getElementById(BUTTON_ID);
if (button?.dataset.dragged === "1") {
button.dataset.dragged = "0";
return;
}
setDrawerOpen(true);
render();
return;
}
if (action === "close-panel") {
setDrawerOpen(false);
return;
}
if (action === "request-refresh") {
const started = await requestBackendRefresh("frontend-header-button", "manual");
if (started) {
startRunningPoll();
render();
}
return;
}
if (action === "delete-cache") {
deleteTaskCache();
return;
}
if (action === "copy-json") {
copyAllJson();
return;
}
if (action === "set-filter" || actionEl.dataset.summaryFilter) {
const nextFilter = actionEl.dataset.summaryFilter === "site" ? "all" : actionEl.dataset.summaryFilter;
setCurrentFilter(nextFilter);
render();
return;
}
if (action === "ignore-task") {
event.preventDefault();
event.stopPropagation();
const taskKey = String(actionEl.dataset.taskKey || "");
const task = latestTaskRegistry.get(taskKey);
if (!task) {
alert("忽略失败:当前任务数据已失效,请刷新后重试");
return;
}
if (!confirm("确认忽略这个任务?\n忽略后它不会出现在待办和危险提醒中,可在“忽略”分区恢复。")) {
return;
}
await ignoreTask(task);
render();
return;
}
if (action === "restore-task") {
event.preventDefault();
event.stopPropagation();
const taskKey = String(actionEl.dataset.taskKey || "");
if (!taskKey) return;
await restoreTask(taskKey);
render();
return;
}
if (action === "open-detail" || actionEl.dataset.jumpId) {
event.preventDefault();
event.stopPropagation();
const task = jumpTaskRegistry.get(actionEl.dataset.jumpId);
if (!task) {
alert("跳转失败:任务数据已失效,请刷新面板后重试");
return;
}
jumpToTask(task);
return;
}
if (action === "toggle-course" || actionEl.hasAttribute("data-course-toggle")) {
event.preventDefault();
event.stopPropagation();
const group = actionEl.closest(".tg-course-group");
if (!group) return;
const key = group.dataset.courseKey;
const collapseState = await loadCourseCollapseState();
collapseState[key] = !group.classList.contains("tg-course-collapsed");
await setValue(STORE_KEY_COURSE_COLLAPSE, collapseState);
render();
return;
}
if (action === "toggle-section" || actionEl.hasAttribute("data-section-toggle")) {
event.preventDefault();
event.stopPropagation();
const section = actionEl.closest(".tg-course-subgroup");
if (!section) return;
const key = section.dataset.sectionKey;
const collapseState = await loadSectionCollapseState();
collapseState[key] = !section.classList.contains("tg-section-collapsed");
await setValue(STORE_KEY_SECTION_COLLAPSE, collapseState);
render();
}
}
async function handlePanelChange(event) {
const target = event.target;
if (!target?.matches?.("[data-filter-year], [data-filter-month]")) return;
const root = document.getElementById(ROOT_ID);
const year = Number(root?.querySelector("[data-filter-year]")?.value);
const month = Number(root?.querySelector("[data-filter-month]")?.value);
if (!year || !month) return;
await setValue(STORE_KEY_FILTER_YEAR_MONTH, { year, month });
render();
}
function handleLiquidPointerMove(event) {
const target = event.target.closest(`#${BUTTON_ID}, #${DRAWER_ID}`);
if (!target) return;
const rect = target.getBoundingClientRect();
const x = clamp(((event.clientX - rect.left) / Math.max(rect.width, 1)) * 100, 0, 100);
const y = clamp(((event.clientY - rect.top) / Math.max(rect.height, 1)) * 100, 0, 100);
target.style.setProperty("--tg-mouse-x", `${x}%`);
target.style.setProperty("--tg-mouse-y", `${y}%`);
}
function createLiquidRipple(target, event) {
if (!target || !target.matches("button, .tg-detail-link, .tg-summary-item, .tg-course-toggle, .tg-action-btn, .tg-icon-btn")) return;
if (target.dataset.action === "open-panel") return;
const rect = target.getBoundingClientRect();
const ripple = document.createElement("span");
ripple.className = "tg-liquid-ripple";
ripple.style.left = `${event.clientX - rect.left}px`;
ripple.style.top = `${event.clientY - rect.top}px`;
target.appendChild(ripple);
setTimeout(() => ripple.remove(), 560);
}
function setDrawerOpen(open) {
const drawer = document.getElementById(DRAWER_ID);
if (!drawer) return;
drawer.classList.toggle("tg-open", open);
setOpen(open);
if (open) {
applyPanelState();
startRunningPoll();
} else {
if (panelAutoRefreshTimerId) {
clearTimeout(panelAutoRefreshTimerId);
panelAutoRefreshTimerId = null;
}
stopRunningPoll();
stopCountdownTimer();
}
}
async function maybeTriggerAutoRefreshOnce(source = "frontend-panel-open") {
if (!isOpen()) return false;
if (panelAutoRefreshRequested) return false;
const autoRefreshDoneAt = await loadAutoRefreshDoneAt();
if (autoRefreshDoneAt) return false;
panelAutoRefreshRequested = true;
await markAutoRefreshDoneAt(Date.now());
const started = await requestBackendRefresh(source, "auto", {
silentIfBusy: true
});
if (started) {
startRunningPoll();
render();
}
return started;
}
function attachButtonDrag(button) {
if (!button || button.dataset.dragReady === "1") return;
button.dataset.dragReady = "1";
let startX = 0;
let startY = 0;
let originX = 0;
let originY = 0;
let dragging = false;
const onMove = event => {
if (!dragging) return;
const nextX = clamp(originX + event.clientX - startX, -42, window.innerWidth - 20);
const nextY = clamp(originY + event.clientY - startY, -42, window.innerHeight - 20);
button.style.left = `${nextX}px`;
button.style.top = `${nextY}px`;
button.style.right = "auto";
button.style.bottom = "auto";
if (Math.abs(event.clientX - startX) + Math.abs(event.clientY - startY) > 5) {
button.dataset.dragged = "1";
}
};
const onUp = async () => {
if (!dragging) return;
dragging = false;
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp);
await savePanelStatePatch({
buttonX: Math.round(button.getBoundingClientRect().left),
buttonY: Math.round(button.getBoundingClientRect().top)
});
setTimeout(() => {
if (button.dataset.dragged === "1") button.dataset.dragged = "0";
}, 80);
};
button.addEventListener("mousedown", event => {
if (event.button !== 0 || isOpen()) return;
const rect = button.getBoundingClientRect();
startX = event.clientX;
startY = event.clientY;
originX = rect.left;
originY = rect.top;
dragging = true;
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
event.preventDefault();
});
}
function attachDrawerResize(drawer) {
if (!drawer || resizeObserverAttached || typeof ResizeObserver === "undefined") return;
resizeObserverAttached = true;
let saveTimer = null;
const observer = new ResizeObserver(entries => {
const entry = entries[0];
if (!entry || !isOpen()) return;
clearTimeout(saveTimer);
saveTimer = setTimeout(() => {
const rect = entry.target.getBoundingClientRect();
savePanelStatePatch({
panelWidth: Math.round(rect.width),
panelHeight: Math.round(rect.height)
});
}, 260);
});
observer.observe(drawer);
}
async function requestBackendRefresh(source, type = "manual", options = {}) {
const silentIfBusy = options?.silentIfBusy === true;
if (await isRefreshBusy()) {
if (!silentIfBusy) {
alert("正在刷新中,请稍后。");
}
return false;
}
// 刷新开始前先清空上一轮错误,避免成功刷新后面板和导出 JSON 仍残留旧报错。
await setValue(STORE_KEY_LAST_ERROR, null);
if (location.hostname === "www.educoder.net") {
try {
await scanEducoderInPage(source);
activeRefreshRequestId = `educoder:${Date.now()}`;
activeRefreshStartedAt = Date.now();
await markAutoRefreshDoneAt(activeRefreshStartedAt);
} catch (err) {
console.error("Educoder 页面内扫描失败:", err);
activeRefreshRequestId = "";
activeRefreshStartedAt = 0;
panelAutoRefreshRequested = false;
}
return true;
}
const requestId = Date.now() + "-" + Math.random().toString(36).slice(2, 8);
const requestedAt = Date.now();
const site = getCurrentPageSite();
const login = site.login || latestResultLogin || saveAutoLoginIfDetected() || parseLoginFromCurrentUrl() || "";
activeRefreshRequestId = requestId;
activeRefreshStartedAt = requestedAt;
await markAutoRefreshDoneAt(requestedAt);
await setValue(STORE_KEY_REFRESH_STATUS, {
status: "waiting",
requestId,
progress: 8,
message: "等待后台响应",
siteKey: getCurrentBackendSiteKey(),
siteName: site.siteName,
time: new Date().toLocaleString(),
timestamp: Date.now()
});
await setValue(STORE_KEY_REFRESH_REQUEST, {
type,
login,
siteKey: getCurrentBackendSiteKey(),
siteName: site.siteName,
pageHost: location.hostname,
requestId,
requestedAt,
requestedAtText: new Date(requestedAt).toLocaleString(),
source
});
return true;
}
function startRunningPoll() {
if (pollTimerId) return;
pollWasRunning = false;
pollTimerId = setInterval(async () => {
if (!isOpen()) {
stopRunningPoll();
return;
}
const refreshStatus = await getValue(STORE_KEY_REFRESH_STATUS, null);
if (refreshStatus?.status === "running" || refreshStatus?.status === "waiting") {
pollWasRunning = true;
render();
return;
}
if (refreshStatus?.status === "done" || refreshStatus?.status === "error") {
if (refreshStatus?.status === "done") {
await markAutoRefreshDoneAt(refreshStatus?.timestamp || Date.now());
}
activeRefreshRequestId = "";
activeRefreshStartedAt = 0;
render();
stopRunningPoll();
return;
}
const runningState = await loadRunningState();
const isRunning = runningState?.running === true;
if (isRunning) {
pollWasRunning = true;
render();
return;
}
if (pollWasRunning) {
pollWasRunning = false;
activeRefreshRequestId = "";
activeRefreshStartedAt = 0;
render();
stopRunningPoll();
return;
}
render();
stopRunningPoll();
}, 1000);
}
function stopRunningPoll() {
if (!pollTimerId) return;
clearInterval(pollTimerId);
pollTimerId = null;
}
async function deleteTaskCache() {
if (!confirm("确定要删除 TG任务助手缓存吗?这会清空当前扫描结果、运行状态、错误信息和复制用 JSON。")) return;
await deleteValue(getCurrentPageSite().resultKey);
await deleteValue(STORE_KEY);
await deleteValue(STORE_KEY_LAST_RUNNING);
await deleteValue(STORE_KEY_LAST_ERROR);
await clearAutoRefreshDoneAt();
panelAutoRefreshRequested = false;
activeRefreshRequestId = "";
activeRefreshStartedAt = 0;
cacheDeletedNotice = "缓存已删除";
render();
}
function updateFloatingBadge(summary) {
const badge = document.querySelector(`#${BUTTON_ID} .tg-task-button-badge`);
if (!badge) return;
const count = summary?.dangerCount || summary?.unfinishedCount || 0;
badge.textContent = String(count > 99 ? "99+" : count);
badge.style.display = count > 0 ? "block" : "none";
}
function getFilterLabel(filter) {
const labels = { ignored: "忽略", all: "总任务", unfinished: "未完成", danger: "危险", exercise: "考试", common_homework: "图文", homework: "图文", classroom_experiment: "实验", experiment: "实验" };
return labels[filter] || "全部任务";
}
function taskMatchesFilter(task, filter) {
if (filter === "unfinished") return !task.completed;
if (filter === "exercise") return task.taskType === "exercise";
if (filter === "common_homework" || filter === "homework") return task.taskType === "common_homework";
if (filter === "classroom_experiment" || filter === "experiment") return task.taskType === "classroom_experiment";
if (filter === "danger") return isDangerTask(task);
return true;
}
function renderSummaryCard(filter, numberHtml, label, selectedFilter) {
const selected = selectedFilter === filter;
return `
`;
}
function getEducoderPageFetch() {
if (typeof unsafeWindow !== "undefined" && unsafeWindow?.fetch) {
return unsafeWindow.fetch.bind(unsafeWindow);
}
return window.fetch.bind(window);
}
async function fetchEducoderPageJson(url) {
const pageFetch = getEducoderPageFetch();
const res = await pageFetch(url, {
method: "GET",
mode: "cors",
credentials: "include",
cache: "no-store",
headers: {
accept: "application/json",
"content-type": "application/json; charset=utf-8"
}
});
const text = await res.text();
let data;
try {
data = JSON.parse(text);
} catch (err) {
const error = new Error("Educoder JSON解析失败:" + text.slice(0, 300));
error.detail = {
url,
httpStatus: res.status,
statusText: res.statusText,
textPreview: text.slice(0, 500),
keys: [],
parseError: err?.message || String(err)
};
throw error;
}
return {
ok: res.ok,
status: res.status,
statusText: res.statusText,
data,
textPreview: text.slice(0, 500),
keys: data && typeof data === "object" ? Object.keys(data) : []
};
}
async function fetchAndLogEducoderPageJson(url, requestLog, type) {
try {
const resp = await fetchEducoderPageJson(url);
requestLog.push({
siteKey: "educoder",
siteName: "Educoder主站",
type,
url,
httpStatus: resp.status,
statusText: resp.statusText,
ok: resp.ok,
textPreview: resp.textPreview,
keys: resp.keys,
status: resp.data?.status,
message: resp.data?.message,
rawCount: Array.isArray(resp.data?.exercises)
? resp.data.exercises.length
: Array.isArray(resp.data?.homeworks)
? resp.data.homeworks.length
: undefined
});
return resp;
} catch (err) {
const detail = err?.detail || {};
requestLog.push({
siteKey: "educoder",
siteName: "Educoder主站",
type,
url,
httpStatus: detail.httpStatus || 0,
statusText: detail.statusText || "",
ok: false,
textPreview: detail.textPreview || "",
keys: detail.keys || [],
error: err?.message || String(err)
});
throw err;
}
}
function parseEducoderExerciseStatus(user) {
if (!user) {
return {
commitStatus: null,
statusText: "未提交",
completed: false
};
}
const commitStatus = user.commit_status;
if (commitStatus === 0) return { commitStatus, statusText: "未提交", completed: false };
if (commitStatus === 1) return { commitStatus, statusText: "提交中/待评阅", completed: false };
if (commitStatus === 2) return { commitStatus, statusText: "已完成/已提交", completed: true };
return {
commitStatus,
statusText: user.end_at || user.score != null ? "已完成/已提交" : "未知状态",
completed: !!user.end_at || user.score != null
};
}
function parseEducoderHomeworkItem(course, item) {
const homeworkStatus = Array.isArray(item?.status) ? item.status : [];
const workStatus = Array.isArray(item?.work_status) ? item.work_status : [];
const workStatusText = workStatus.join(" ");
const ended = homeworkStatus.includes("已截止") || item?.time_status === 5;
let notSubmitted = false;
let submitted = false;
if (typeof item?.un_commit_work === "boolean") {
notSubmitted = item.un_commit_work === true;
submitted = item.un_commit_work === false;
} else {
notSubmitted = workStatusText.includes("提交作品") || workStatusText.includes("未提交");
submitted = workStatusText.includes("查看作品") || workStatusText.includes("已提交") || workStatusText.includes("已完成");
}
let statusText = "未知状态";
if (notSubmitted) statusText = ended ? "已截止/未提交" : "未提交";
else if (submitted) statusText = ended ? "已截止/已提交" : "已提交";
const homeworkId = item?.homework_id || item?.id;
return {
taskType: "common_homework",
taskTypeText: "图文作业",
siteKey: "educoder",
siteName: "Educoder主站",
courseId: course.courseId,
courseIdentifier: course.courseIdentifier,
courseName: course.courseName,
homeworkId,
workId: item?.work_id || item?.student_work_id,
studentWorkId: item?.student_work_id,
title: item?.name || item?.homework_name || "未命名图文作业",
homeworkStatus,
workStatus,
timeStatus: item?.time_status,
statusText,
completed: submitted,
submitted,
notSubmitted,
ended,
publishTime: item?.publish_time || "",
endTime: item?.end_time || item?.end_time_s || "",
lateTime: item?.late_time || "",
score: "--",
source: "educoder-page-homework_commons_1",
detailUrl: `${course.pageOrigin}/classrooms/${course.courseIdentifier}/common_homework/${homeworkId}/detail?tabs=0`,
scanTime: new Date().toLocaleString(),
raw: item
};
}
function parseEducoderExperimentItem(course, item) {
const finishedStatus = Number(item?.shixun_finished_status);
const completed = finishedStatus === 1;
const ended = item?.time_status === 5 || (Array.isArray(item?.status) && item.status.includes("已截止"));
const homeworkId = item?.homework_id || item?.id;
return {
taskType: "classroom_experiment",
taskTypeText: "课堂实验",
siteKey: "educoder",
siteName: "Educoder主站",
courseId: course.courseId,
courseIdentifier: course.courseIdentifier,
courseName: course.courseName,
homeworkId,
title: item?.name || item?.shixun_name || "未命名课堂实验",
statusText: completed ? "已完成" : ended ? "已截止/未完成" : "进行中/未完成",
completed,
submitted: completed,
notSubmitted: !completed,
ended,
publishTime: item?.publish_time || "",
endTime: item?.end_time_s || item?.end_time || "",
lateTime: item?.late_time || "",
challengeCount: item?.challenge_count ?? null,
finishedChallengeCount: item?.finished_challenge_count ?? null,
passedTime: item?.student_passed_time || "",
detailUrl: `${course.pageOrigin}/classrooms/${course.courseIdentifier}/shixun_homework/${homeworkId}/detail?tabs=1`,
source: "educoder-page-homework_commons_4",
scanTime: new Date().toLocaleString(),
raw: item
};
}
async function scanEducoderInPage(source = "frontend-header-button") {
const site = getCurrentPageSite();
const requestId = Date.now() + "-" + Math.random().toString(36).slice(2, 8);
const requestLog = [];
const errors = [];
const scanTime = new Date().toLocaleString();
const scanTimestamp = Date.now();
const course = {
courseId: site.courseId,
courseIdentifier: site.courseIdentifier,
courseName: site.courseName,
pageOrigin: site.pageOrigin,
apiOrigin: site.apiOrigin
};
await setValue(STORE_KEY_LAST_ERROR, null);
await setValue(STORE_KEY_REFRESH_STATUS, {
status: "running",
requestId,
progress: 18,
message: "正在扫描 Educoder 主站任务",
siteKey: "educoder",
siteName: site.siteName,
source,
time: new Date().toLocaleString(),
timestamp: Date.now()
});
try {
function getEducoderActualLogin(site) {
// 1. 如果当前页面是 /users/xxx/classrooms,就优先取当前页面里的 xxx
const pageMatch = location.pathname.match(/\/users\/([^/]+)/);
if (pageMatch && pageMatch[1]) {
return decodeURIComponent(pageMatch[1]);
}
// 2. 如果当前页面不是用户页,比如 /paths,就回退使用 site.login
if (site && site.login) {
return site.login;
}
// 3. 两个都没有才报错
throw new Error(
"无法识别 Educoder 用户 ID:当前页面不是 /users/xxx 页面,且 site.login 为空。当前地址:" + location.href
);
}
const actualLogin = getEducoderActualLogin(site);
const homepageUrl =
`${site.apiOrigin}/api/users/${encodeURIComponent(actualLogin)}/homepage_info.json?zzud=${encodeURIComponent(actualLogin)}`;
console.log("[TG任务助手] homepage_info 实际请求地址:", homepageUrl);
const homepageResp = await fetchAndLogEducoderPageJson(
homepageUrl,
requestLog,
"homepage_info"
);
const homepage = homepageResp.data;
if (!homepage || homepage.is_logged_user !== true) {
throw new Error("Educoder 主站未登录或登录态失效:" + JSON.stringify({
status: homepage?.status,
message: homepage?.message,
keys: homepageResp.keys,
preview: homepageResp.textPreview
}));
}
// 下面原来的代码继续保留
const exerciseUrl = `${site.apiOrigin}/api/v2/courses/${course.courseIdentifier}/exercises.json?coursesId=${course.courseIdentifier}&limit=20&type=&id=${course.courseIdentifier}&zzud=${encodeURIComponent(site.login)}`;
const exerciseResp = await fetchAndLogEducoderPageJson(exerciseUrl, requestLog, "exercise_list");
if (!Array.isArray(exerciseResp.data?.exercises)) {
throw new Error("Educoder 考试列表返回结构异常:" + JSON.stringify({
status: exerciseResp.data?.status,
message: exerciseResp.data?.message,
keys: exerciseResp.keys,
preview: exerciseResp.textPreview
}));
}
const rawExams = exerciseResp.data.exercises;
const exams = [];
for (const exam of rawExams) {
const exerciseId = exam?.id;
let user = null;
let totalScore = "--";
let status = { commitStatus: null, statusText: "未提交", completed: false };
if (exerciseId) {
try {
const userUrl = `${site.apiOrigin}/api/exercises/${encodeURIComponent(exerciseId)}/exercise_users.json?page=1&limit=20&coursesId=${course.courseIdentifier}&categoryId=${encodeURIComponent(exerciseId)}&zzud=${encodeURIComponent(site.login)}`;
const userResp = await fetchAndLogEducoderPageJson(userUrl, requestLog, "exercise_user");
const userPayload = userResp.data;
user = userPayload?.current_answer_user || userPayload?.data?.current_answer_user || null;
totalScore = userPayload?.total_score ?? userPayload?.data?.total_score ?? "--";
status = parseEducoderExerciseStatus(user);
} catch (err) {
errors.push({
stage: "exercise_user",
siteKey: "educoder",
siteName: site.siteName,
courseName: course.courseName,
exerciseId,
message: err?.message || String(err)
});
}
}
exams.push({
taskType: "exercise",
taskTypeText: "考试/小测试",
siteKey: "educoder",
siteName: site.siteName,
courseId: course.courseId,
courseIdentifier: course.courseIdentifier,
courseName: course.courseName,
exerciseId,
title: exam?.exercise_name || exam?.name || exam?.title || "未命名考试",
detailUrl: `${site.pageOrigin}/classrooms/${course.courseIdentifier}/exercisenotice/${exerciseId}/users/${encodeURIComponent(site.login)}`,
deadlineRemaining: exam?.exercise_left_time || "--",
durationMinutes: exam?.time ?? null,
exerciseStatus: exam?.exercise_status ?? null,
currentStatus: exam?.current_status ?? null,
wholeExerciseStatus: exam?.whole_exercise_status ?? null,
exerciseUserId: exam?.exercise_user_id ?? user?.exercise_user_id ?? null,
...status,
startAt: user?.start_at || null,
endAt: user?.end_at || null,
score: user?.score ?? "--",
totalScore,
objectiveScore: user?.objective_score ?? "--",
subjectiveScore: user?.subjective_score ?? "--",
reviewStatus: user?.review_status ?? false,
userName: user?.user_name || "",
studentId: user?.student_id || "",
scanTime,
raw: { exam, user }
});
}
const homeworkUrl = `${site.apiOrigin}/api/courses/${course.courseIdentifier}/homework_commons.json?limit=100&status=0&id=${course.courseIdentifier}&type=1&sort_by=updated_at&sort_direction=asc&order=0&zzud=${encodeURIComponent(site.login)}`;
const homeworkResp = await fetchAndLogEducoderPageJson(homeworkUrl, requestLog, "homework_commons_1");
if (!Array.isArray(homeworkResp.data?.homeworks)) {
throw new Error("Educoder 图文作业返回结构异常:" + JSON.stringify({
status: homeworkResp.data?.status,
message: homeworkResp.data?.message,
keys: homeworkResp.keys,
preview: homeworkResp.textPreview
}));
}
const homeworks = homeworkResp.data.homeworks.map(item => parseEducoderHomeworkItem(course, item));
const experimentUrl = `${site.apiOrigin}/api/courses/${course.courseIdentifier}/homework_commons.json?limit=100&status=0&id=${course.courseIdentifier}&type=4&sort_by=updated_at&sort_direction=asc&order=0&zzud=${encodeURIComponent(site.login)}`;
const experimentResp = await fetchAndLogEducoderPageJson(experimentUrl, requestLog, "homework_commons_4");
if (!Array.isArray(experimentResp.data?.homeworks)) {
throw new Error("Educoder 课堂实验返回结构异常:" + JSON.stringify({
status: experimentResp.data?.status,
message: experimentResp.data?.message,
keys: experimentResp.keys,
preview: experimentResp.textPreview
}));
}
const experiments = experimentResp.data.homeworks.map(item => parseEducoderExperimentItem(course, item));
const tasks = sortTasks([...exams, ...homeworks, ...experiments]);
const result = {
login: site.login,
currentSite: {
key: "educoder",
name: site.siteName,
pageOrigin: site.pageOrigin,
apiOrigin: site.apiOrigin
},
scanTime,
scanTimestamp,
source: "educoder-page-frontend",
courses: [
{
courseId: course.courseId,
courseIdentifier: course.courseIdentifier,
courseName: course.courseName,
siteKey: "educoder",
siteName: site.siteName,
pageOrigin: site.pageOrigin,
apiOrigin: site.apiOrigin,
login: site.login,
exams,
homeworks,
experiments
}
],
tasks,
errors,
warnings: [],
debug: {
requestLog
},
summary: calculateSummaryFromTasks(tasks)
};
await setValue(STORE_KEY_EDUCODER_RESULT, result);
await setValue(STORE_KEY_LAST_ERROR, null);
await setValue(STORE_KEY_REFRESH_STATUS, {
status: "done",
requestId,
progress: 100,
message: "刷新完成",
siteKey: "educoder",
siteName: site.siteName,
time: new Date().toLocaleString(),
timestamp: Date.now()
});
render();
return result;
} catch (err) {
const message = err?.message || String(err);
await setValue(STORE_KEY_LAST_ERROR, {
siteKey: "educoder",
siteName: site.siteName,
time: new Date().toLocaleString(),
message: "Educoder 页面内刷新失败,请复制全部 JSON 排查。",
error: message,
stack: err?.stack || "",
stage: "educoder_page_frontend",
debug: {
requestLog,
errorDetail: err?.detail || null
}
});
await setValue(STORE_KEY_REFRESH_STATUS, {
status: "error",
requestId,
progress: 100,
message: "Educoder 页面内刷新失败,请复制全部 JSON 排查。",
error: message,
siteKey: "educoder",
siteName: site.siteName,
time: new Date().toLocaleString(),
timestamp: Date.now()
});
render();
throw err;
}
}
function renderYearMonthFilter(year, month) {
const now = new Date();
const currentYear = now.getFullYear();
const years = [];
for (let y = currentYear - 3; y <= currentYear + 1; y += 1) years.push(y);
const yearOptions = years.map(y => ``).join("");
const monthOptions = Array.from({ length: 12 }, (_, i) => i + 1).map(m => ``).join("");
return `
起始时间:
显示任务起点:${escapeHtml(year)} 年 ${escapeHtml(month)} 月之后
`;
}
function renderRefreshStatus(runningState, lastError, refreshRequest, refreshHandled, refreshStatus) {
const isRunning = runningState?.running === true || refreshStatus?.status === "running";
const hasError = !isRunning && refreshStatus?.status !== "running" && lastError?.message;
const percent = clamp(Number(refreshStatus?.progress ?? runningState?.percent) || (isRunning ? 12 : 100), 0, 100);
const requestedAt = Number(refreshRequest?.requestedAt) || 0;
const handledAt = Number(refreshHandled) || 0;
const pending = requestedAt && requestedAt > handledAt && !isRunning;
const statusClass = isRunning
? "tg-refresh-running"
: hasError || refreshStatus?.status === "error"
? "tg-refresh-failed"
: refreshStatus?.status === "done" || runningState?.stage === "done"
? "tg-refresh-done"
: pending
? "tg-refresh-pending"
: "";
let title = "刷新状态";
let stage = location.hostname === "www.educoder.net"
? "展开面板后会自动进行页面内刷新。"
: "展开面板后会自动请求后台刷新。";
let barPercent = pending ? 18 : 0;
if (isRunning) {
title = "正在刷新";
stage = cleanStatusMessage(refreshStatus?.message || runningState?.stageText || "后台正在扫描任务");
barPercent = percent;
} else if (refreshStatus?.status === "done") {
title = "最近刷新成功";
stage = cleanStatusMessage(refreshStatus.message || "刷新完成");
barPercent = 100;
} else if (refreshStatus?.status === "error") {
title = "最近刷新失败";
stage = cleanStatusMessage(refreshStatus.error || refreshStatus.message || "刷新失败");
barPercent = 100;
} else if (hasError) {
title = "最近刷新失败";
stage = cleanStatusMessage(lastError.message);
barPercent = 100;
} else if (runningState?.stage === "done") {
title = "最近刷新完成";
stage = cleanStatusMessage(runningState.endTime || runningState.stageText || "扫描完成");
barPercent = 100;
} else if (pending) {
title = "已请求刷新";
} else if (runningState?.stageText) {
title = "最近刷新状态";
stage = cleanStatusMessage(runningState.stageText);
barPercent = percent;
}
return `
${escapeHtml(title)}
${Math.round(barPercent)}%
${escapeHtml(stage)}
`;
}
function appendLog(text) {
const boxWrap = document.querySelector(`#${DRAWER_ID} .tg-json-container`);
const box = document.querySelector(`#${DRAWER_ID} .tg-json-box`);
if (!boxWrap || !box) return;
const line = `[${new Date().toLocaleString()}] ${text}`;
box.value = box.value ? `${box.value}\n${line}` : line;
boxWrap.style.display = "block";
}
function writeOutput(text) {
const boxWrap = document.querySelector(`#${DRAWER_ID} .tg-json-container`);
const box = document.querySelector(`#${DRAWER_ID} .tg-json-box`);
if (!boxWrap || !box) return;
box.value = text;
boxWrap.style.display = "block";
}
function parseCourseIdFromCurrentUrl() {
const match = location.href.match(/\/classrooms\/([^/?#]+)/);
return match && match[1] ? decodeURIComponent(match[1]) : "";
}
function parseLoginFromCurrentUrl() {
const userMatch = location.href.match(/\/users\/([^/?#]+)/);
if (userMatch && userMatch[1]) return decodeURIComponent(userMatch[1]);
const zzudMatch = location.href.match(/[?&]zzud=([^&]+)/);
if (zzudMatch && zzudMatch[1]) return decodeURIComponent(zzudMatch[1]);
const usernameMatch = location.href.match(/[?&]username=([^&]+)/);
if (usernameMatch && usernameMatch[1]) return decodeURIComponent(usernameMatch[1]);
return "";
}
function resolveJumpCourseId(task) {
const direct = task?.courseIdentifier || task?.course_id || task?.coursesId;
if (direct) return String(direct);
if (task?.courseId && !/^\d+$/.test(String(task.courseId))) {
return String(task.courseId);
}
const raw = task?.raw || {};
const rawCourse = raw.courseIdentifier || raw.course_identifier || raw.coursesId || raw.course_id;
if (rawCourse && !/^\d+$/.test(String(rawCourse))) {
return String(rawCourse);
}
return parseCourseIdFromCurrentUrl();
}
function resolveJumpLogin(task) {
return String(
task?.login ||
task?.userLogin ||
task?.raw?.user_login ||
latestResultLogin ||
parseLoginFromCurrentUrl() ||
""
).trim();
}
function getJumpType(task) {
if (task?.taskType === "classroom_experiment") return "shixun_homework";
if (task?.taskType === "exercise") return "exercise";
if (task?.taskType === "common_homework") return "common_homework";
return "";
}
function getJumpItem(task) {
if (task?.taskType === "exercise") {
return task?.raw?.exam || { id: task?.exerciseId || task?.id };
}
if (task?.taskType === "classroom_experiment" || task?.taskType === "common_homework") {
return task?.raw || { homework_id: task?.homeworkId || task?.id };
}
return null;
}
function buildJumpUrl(type, courseId, item, login) {
if (type === "shixun_homework") {
if (!item || !item.homework_id) throw new Error("课堂实验缺少 homework_id");
return `${location.origin}/classrooms/${encodeURIComponent(courseId)}/shixun_homework/${encodeURIComponent(item.homework_id)}/detail?tabs=1`;
}
if (type === "exercise") {
if (!item || !item.id) throw new Error("考试缺少 id");
if (!login) throw new Error("考试缺少 login");
return `${location.origin}/classrooms/${encodeURIComponent(courseId)}/exercisenotice/${encodeURIComponent(item.id)}/users/${encodeURIComponent(login)}`;
}
if (type === "common_homework") {
if (!item || !item.homework_id) throw new Error("图文作业缺少 homework_id");
return `${location.origin}/classrooms/${encodeURIComponent(courseId)}/common_homework/${encodeURIComponent(item.homework_id)}/detail?tabs=0`;
}
throw new Error(`未知跳转类型:${type}`);
}
function buildTaskJumpUrl(task) {
const type = getJumpType(task);
const courseId = resolveJumpCourseId(task);
const item = getJumpItem(task);
const login = resolveJumpLogin(task);
if (!courseId) throw new Error("缺少课程 ID");
return buildJumpUrl(type, courseId, item, login);
}
function jumpToTask(task) {
try {
const url = task?.detailUrl || buildTaskJumpUrl(task);
writeOutput(url);
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(url).catch(() => {});
}
window.open(url, "_blank");
} catch (err) {
console.error("跳转失败:", err, task);
alert(`跳转失败:${err.message}`);
}
}
function getTaskTitle(task) {
return task.title || task.name || task.exerciseName || task.homeworkName || task.experimentName || task.homeworkTitle || "未命名任务";
}
function getTaskTypeLabel(task) {
if (task.taskType === "exercise") return "考试 / 小测试";
if (task.taskType === "common_homework") return "图文作业";
if (task.taskType === "classroom_experiment") return "课堂实验";
return task.taskTypeText || "任务";
}
function renderDetailLink(task) {
const id = `task-${jumpTaskSeq++}`;
jumpTaskRegistry.set(id, task);
return ``;
}
function renderTaskActionButton(task, taskKey) {
if (task?.__ignoredView) {
return ``;
}
return ``;
}
function renderExerciseMeta(task) {
if (task?.taskType !== "exercise") return "";
const items = [];
const usedAttempts = Number(task?.usedAttempts);
const maxAttempts = Number(task?.maxAttempts);
const hasAttemptInfo = Number.isFinite(usedAttempts) && Number.isFinite(maxAttempts) && maxAttempts > 0;
const scoreText = task?.scoreText ?? task?.score;
const totalScoreText = task?.totalScoreText ?? task?.totalScore;
const hasScore = scoreText !== undefined && scoreText !== null && String(scoreText).trim() !== "" && String(scoreText).trim() !== "--";
const hasTotalScore = totalScoreText !== undefined && totalScoreText !== null && String(totalScoreText).trim() !== "";
if (hasAttemptInfo) {
items.push(`已考次数:${escapeHtml(String(usedAttempts))} / ${escapeHtml(String(maxAttempts))}`);
}
if (hasScore || hasTotalScore) {
items.push(`成绩:${escapeHtml(hasScore ? String(scoreText) : "--")} / ${escapeHtml(hasTotalScore ? String(totalScoreText) : "--")}`);
}
if (task?.exerciseLeftTime) {
items.push(`剩余时间:${escapeHtml(String(task.exerciseLeftTime))}`);
}
return items.join("");
}
function renderTaskCard(task) {
const taskKey = getTaskKey(task);
latestTaskRegistry.set(taskKey, task);
const tone = statusTone(task);
const status = task.examStatusLabel || task.submitStatusText || task.statusText || task.commitStatusText || (task.completed ? "已完成" : "未完成");
const deadline = getTaskDeadlineLabel(task);
const siteLine = `${getTaskSiteName(task)} · ${task.courseName || "未识别课程"}`;
const countdownText = formatCountdown(deadline);
const ignoredBadge = task?.__ignoredView
? `${task.__snapshotOnly ? "已忽略 / 未匹配" : "已忽略"}`
: "";
const snapshotNote = task?.__snapshotOnly
? `当前扫描未匹配到源任务
`
: "";
if (!hasLoggedDeadlineSample) {
console.log("[TG倒计时] deadline field=", deadline, "task=", task);
console.log("[TG忽略机制] taskKey=", taskKey, task.title || task.name);
hasLoggedDeadlineSample = true;
}
return `
${escapeHtml(countdownText)}
${ignoredBadge}
${renderTaskActionButton(task, taskKey)}
${escapeHtml(getTaskTitle(task))}
${escapeHtml(getTaskTypeLabel(task))}
${snapshotNote}
来源:${escapeHtml(siteLine)}
截止:${escapeHtml(deadline)}
${renderExerciseMeta(task)}
${task?.__ignoredView && task?.ignoredAt ? `忽略时间:${escapeHtml(task.ignoredAt)}` : ""}
`;
}
function renderTaskList(tasks) {
return tasks.map(renderTaskCard).join("");
}
function renderSection(title, tasks, className = "") {
if (!tasks.length) return "";
return `
${escapeHtml(title)}${tasks.length}
${renderTaskList(tasks)}
`;
}
function renderCourseSubgroup(groupKey, typeKey, title, tasks, sectionCollapseState) {
if (!tasks.length) return "";
const key = `${groupKey}:${typeKey}`;
const collapsed = sectionCollapseState[key] === true;
return `
${escapeHtml(title)}
${tasks.length}
`;
}
function renderCourseGroups(groups, courseCollapseState, sectionCollapseState) {
return groups.map(group => {
const collapsed = courseCollapseState[group.key] === true;
const stats = [
`站点 ${group.siteName || "TG校内站"}`,
`总任务 ${group.tasks?.length || 0}`,
`考试 ${group.exams?.length || 0}`,
`图文 ${group.homeworks?.length || 0}`,
`实验 ${group.experiments?.length || 0}`,
`危险 ${group.danger || 0}`
];
return `
${escapeHtml(group.courseName || "未识别课程")}
${escapeHtml(group.siteName || "TG校内站")}
${stats.map(x => `${escapeHtml(x)}`).join("")}
${renderCourseSubgroup(group.key, "exercise", "考试 / 小测试", group.exams || [], sectionCollapseState)}
${renderCourseSubgroup(group.key, "common_homework", "图文作业", group.homeworks || [], sectionCollapseState)}
${renderCourseSubgroup(group.key, "classroom_experiment", "课堂实验", group.experiments || [], sectionCollapseState)}
`;
}).join("");
}
async function render() {
createRoot();
const body = document.querySelector(`#${DRAWER_ID} .tg-body`);
if (!body) return;
try {
await renderPanelContent(body);
if (isOpen()) {
maybeTriggerAutoRefreshOnce().catch(err => {
console.error("TG任务助手自动刷新判断失败", err);
});
}
} catch (err) {
console.error("TG任务助手渲染失败", err);
body.innerHTML = `前台面板渲染失败:${escapeHtml(cleanStatusMessage(err?.message || String(err)))}
`;
}
}
async function renderPanelContent(body) {
jumpTaskRegistry = new Map();
jumpTaskSeq = 0;
latestTaskRegistry = new Map();
const state = await loadFrontendState();
const data = state.result;
const runningState = state.lastRunning;
const lastError = state.lastError;
const refreshRequest = state.refreshRequest;
const refreshHandled = state.refreshHandled;
const refreshStatus = state.refreshStatus;
const currentPageSite = getCurrentPageSite();
const isRunning = runningState?.running === true || refreshStatus?.status === "running";
const currentRefreshRunning = refreshStatus?.status === "running";
const currentRefreshError = refreshStatus?.status === "error";
const refreshStatusHtml = renderRefreshStatus(runningState, lastError, refreshRequest, refreshHandled, refreshStatus);
if (isRunning && !data) {
updateFloatingBadge({ dangerCount: 0, unfinishedCount: 0 });
body.innerHTML = `${refreshStatusHtml}后台正在运行
当前阶段:${escapeHtml(cleanStatusMessage(refreshStatus?.message || runningState?.stageText || "正在刷新"))}
`;
return;
}
if (!data && currentRefreshError) {
updateFloatingBadge({ dangerCount: 0, unfinishedCount: 0 });
body.innerHTML = `${refreshStatusHtml}最近刷新失败
错误:${escapeHtml(cleanStatusMessage(refreshStatus?.error || refreshStatus?.message || "未知错误"))}
`;
return;
}
if (!data && lastError?.message && !currentRefreshRunning) {
updateFloatingBadge({ dangerCount: 0, unfinishedCount: 0 });
body.innerHTML = `${refreshStatusHtml}上次后台错误:${escapeHtml(cleanStatusMessage(lastError.message))}
时间:${escapeHtml(lastError.time || "--")}
`;
return;
}
if (!data) {
updateFloatingBadge({ dangerCount: 0, unfinishedCount: 0 });
const emptyText = currentPageSite.siteKey === "educoder"
? "当前为 Educoder 主站视图,暂无本平台缓存。请点击“请求刷新”获取任务状态。"
: "当前为 TG 校内站视图,暂无本平台缓存。请点击“请求刷新”获取任务状态。";
body.innerHTML = `${refreshStatusHtml}${cacheDeletedNotice ? `${escapeHtml(cacheDeletedNotice)}
` : ""}${escapeHtml(emptyText)}
`;
return;
}
const manualLogin = String(MANUAL_LOGIN || "").trim();
const autoLogin = state.autoLogin;
const expectedLogin = currentPageSite.login || manualLogin || autoLogin;
const resultLogin = String(data?.login || "").trim();
latestResultLogin = resultLogin;
const accountText = expectedLogin || "未识别";
const accountSource = currentPageSite.login ? currentPageSite.siteName : manualLogin ? "手动配置" : autoLogin ? "自动识别" : "未识别";
let accountWarningHtml = "";
if (!isResultForCurrentSite(data)) {
updateFloatingBadge({ dangerCount: 0, unfinishedCount: 0 });
body.innerHTML = `当前缓存数据属于另一个账号,请重新运行后台扫描。
`;
return;
}
if (!expectedLogin) {
accountWarningHtml = `账号:未识别
提示:请先打开 TG 页面,或在后台脚本顶部填写 MANUAL_LOGIN。
`;
}
const filterYearMonth = state.filterYearMonth;
const courseCollapseState = state.courseCollapse;
const sectionCollapseState = state.sectionCollapse;
const viewModel = buildViewModel(data, state);
const summary = viewModel.stats;
updateFloatingBadge(summary);
const errors = data.errors || [];
const warnings = data.warnings || [];
const failedSiteLogins = Array.isArray(data?.debug?.siteLogin)
? data.debug.siteLogin.filter(item => item && item.ok === false)
: [];
const filter = state.currentFilter;
const filteredTasks = viewModel.visibleTasks;
const dangerTasks = viewModel.dangerTasks;
const dangerList = filter === "danger" ? filteredTasks : dangerTasks;
const ignoredTasks = viewModel.ignoredDisplayTasks;
requestDangerNotification(dangerList, data);
let html = accountWarningHtml + refreshStatusHtml;
if (isRunning) {
html += `后台正在运行
当前阶段:${escapeHtml(cleanStatusMessage(refreshStatus?.message || runningState?.stageText || "正在刷新"))}
`;
} else if (currentRefreshError) {
html += `最近刷新失败
错误:${escapeHtml(cleanStatusMessage(refreshStatus?.error || refreshStatus?.message || "未知错误"))}
`;
} else if (lastError && lastError.message && !currentRefreshRunning) {
html += `上次后台错误:${escapeHtml(cleanStatusMessage(lastError.message))}
时间:${escapeHtml(lastError.time || "--")}
`;
}
if (failedSiteLogins.length) {
html += failedSiteLogins.map(item => {
const siteName = item.siteName || "站点";
const loginUrl = item.siteKey === "educoder" ? "https://www.educoder.net" : getCurrentPageSite().pageOrigin;
return `${escapeHtml(siteName)}未登录或登录态失效,请先打开 ${escapeHtml(loginUrl)} 并登录后重新刷新。
原因:${escapeHtml(cleanStatusMessage(item.error || "登录态预检失败"))}
`;
}).join("");
}
html += `
${escapeHtml(currentPageSite.viewHint)}
${renderSummaryCard("ignored", `
${summary.ignoredCount || 0}
`, "忽略", filter)}
${renderSummaryCard("all", `
${summary.taskCount}
`, "总任务", filter)}
${renderSummaryCard("exercise", `
${summary.exerciseCount}
`, "考试", filter)}
${renderSummaryCard("common_homework", `
${summary.homeworkCount}
`, "图文", filter)}
${renderSummaryCard("classroom_experiment", `
${summary.experimentCount}
`, "实验", filter)}
${renderSummaryCard("danger", `
${summary.dangerCount}
`, "危险", filter)}
${filter !== "all" ? `正在查看:${escapeHtml(getFilterLabel(filter))}
` : ""}
`;
if (filter === "all" && dangerList.length) {
html += `危险提醒:发现 ${dangerList.length} 个 10 天内截止的未完成任务,请优先处理。
`;
html += renderSection("紧急任务", dangerList, "tg-section-urgent");
}
if (!filteredTasks.length) {
html += `当前筛选下没有任务。
`;
} else if (filter === "danger") {
html += renderSection("危险任务", filteredTasks, "tg-section-urgent");
} else if (filter === "ignored") {
html += renderSection("已忽略任务", ignoredTasks);
} else {
html += renderCourseGroups(viewModel.courseGroups, courseCollapseState, sectionCollapseState);
}
if (errors.length || warnings.length) {
html += `本次扫描:${errors.length} 个错误,${warnings.length} 个提醒。
需要排查时可以复制全部 JSON。
`;
}
html += ``;
body.innerHTML = html;
updateCountdowns();
ensureCountdownTimer();
latestRenderedScanTimestamp = Number(data.scanTimestamp) || latestRenderedScanTimestamp;
}
async function getAllJsonText() {
const state = await loadFrontendState();
return JSON.stringify(
{
result: state.result,
lastRunning: state.lastRunning,
lastError: state.lastError,
refreshRequest: state.refreshRequest,
refreshHandled: state.refreshHandled,
refreshStatus: state.refreshStatus,
panelState: state.panelState,
courseCollapse: state.courseCollapse,
sectionCollapse: state.sectionCollapse,
copiedAt: new Date().toLocaleString(),
pageUrl: location.href,
pageMode: isOpen() ? "panel-open" : "button-only"
},
null,
2
);
}
async function copyAllJson() {
const btn = document.querySelector("[data-copy-all-json]");
const text = await getAllJsonText();
try {
await navigator.clipboard.writeText(text);
btn.textContent = "已复制";
setTimeout(() => {
btn.textContent = "全部 JSON";
}, 1200);
} catch (e) {
showJsonBox(text);
btn.textContent = "手动复制";
setTimeout(() => {
btn.textContent = "全部 JSON";
}, 1500);
}
}
function showJsonBox(text) {
setDrawerOpen(true);
const boxWrap = document.querySelector(`#${DRAWER_ID} .tg-json-container`);
const box = document.querySelector(`#${DRAWER_ID} .tg-json-box`);
if (!boxWrap || !box) return;
box.value = text;
boxWrap.style.display = boxWrap.style.display === "none" ? "block" : "none";
box.focus();
box.select();
}
window.__tgTaskAssistant = {
render,
loadData,
loadRunningState,
loadLastError
};
saveAutoLoginIfDetected();
if (typeof GM_registerMenuCommand === "function") {
GM_registerMenuCommand("显示/请求刷新 TG任务助手", () => {
setDrawerOpen(true);
render();
});
}
window.addEventListener("resize", () => {
clearTimeout(resizeApplyTimer);
resizeApplyTimer = setTimeout(() => {
applyPanelState();
}, 120);
});
setTimeout(() => {
createRoot();
render();
}, 1000);
})();
"use strict";
const MANUAL_LOGIN = "";
const TG_INTRANET_HOST = "172.16.36.150";
const TG_INTRANET_ORIGIN = "http://172.16.36.150";
const TG_EXTERNAL_ORIGIN = "https://tg.zcst.edu.cn";
const STORE_KEY = "TG_EXAM_MONITOR_RESULT";
const STORE_KEY_TG_RESULT = "TG_EXAM_MONITOR_TG_RESULT";
const STORE_KEY_EDUCODER_RESULT = "TG_EXAM_MONITOR_EDUCODER_RESULT";
const STORE_KEY_LAST_ERROR = "TG_EXAM_MONITOR_LAST_ERROR";
const STORE_KEY_LAST_RUNNING = "TG_EXAM_MONITOR_LAST_RUNNING";
const STORE_KEY_FILTER_YEAR_MONTH = "TG_TASK_FILTER_YEAR_MONTH";
const STORE_KEY_AUTO_LOGIN = "TG_TASK_MONITOR_AUTO_LOGIN";
const STORE_KEY_COURSE_COLLAPSE = "TG_TASK_COURSE_COLLAPSE";
const STORE_KEY_SECTION_COLLAPSE = "TG_TASK_SECTION_COLLAPSE";
const STORE_KEY_PANEL_STATE = "TG_TASK_PANEL_STATE";
const STORE_KEY_REFRESH_REQUEST = "TG_TASK_ASSISTANT_REFRESH_REQUEST";
const STORE_KEY_REFRESH_STATUS = "TG_TASK_ASSISTANT_REFRESH_STATUS";
const STORE_KEY_REFRESH_HANDLED = "TG_TASK_REFRESH_HANDLED";
const STORE_KEY_AUTO_REFRESH_DONE_AT = "TG_TASK_ASSISTANT_AUTO_REFRESH_DONE_AT";
const STORE_KEY_IGNORED_TASKS = "TG_TASK_ASSISTANT_IGNORED_TASKS";
const DANGER_DAYS_THRESHOLD = 10;
const DANGER_NOTIFY_KEY = "TG_TASK_ASSISTANT_DANGER_NOTIFY_KEY";
const ROOT_ID = "__tg_task_assistant_root__";
const BUTTON_ID = "__tg_task_assistant_button__";
const DRAWER_ID = "__tg_task_assistant_drawer__";
const SVG_FILTER_ID = "__tg_liquid_glass_filter__";
const OPEN_KEY = "TG_TASK_ASSISTANT_OPEN";
const FILTER_KEY = "TG_TASK_ASSISTANT_FILTER";
let countdownIntervalId = null;
let pollTimerId = null;
let resizeObserverAttached = false;
let cacheDeletedNotice = "";
let latestResultLogin = "";
let jumpTaskRegistry = new Map();
let jumpTaskSeq = 0;
let pollWasRunning = false;
let latestRenderedScanTimestamp = 0;
let resizeApplyTimer = null;
let activeRefreshRequestId = "";
let activeRefreshStartedAt = 0;
let lastLoggedRefreshStatusKey = "";
let panelAutoRefreshTimerId = null;
let panelAutoRefreshRequested = false;
let latestTaskRegistry = new Map();
let hasLoggedTaskKeySample = false;
let hasLoggedDeadlineSample = false;
const PANEL_AUTO_REFRESH_DELAY_MS = 2000;
const css = `
#${BUTTON_ID}, #${DRAWER_ID} {
--tg-cyan: #67e8f9;
--tg-cyan-soft: rgba(103, 232, 249, .22);
--tg-violet: #8b5cf6;
--tg-violet-soft: rgba(139, 92, 246, .2);
--tg-pink: #f0abfc;
--tg-pink-soft: rgba(240, 171, 252, .18);
--tg-red: #fb7185;
--tg-amber: #fbbf24;
--tg-green: #86efac;
--tg-text: rgba(244, 247, 251, .94);
--tg-muted: rgba(203, 213, 225, .68);
--tg-faint: rgba(148, 163, 184, .48);
--tg-glass: rgba(12, 18, 30, .64);
--tg-glass-strong: rgba(15, 23, 42, .76);
--tg-line: rgba(148, 227, 255, .18);
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, "Microsoft YaHei", sans-serif;
color: var(--tg-text);
box-sizing: border-box;
letter-spacing: 0;
}
#${BUTTON_ID} *, #${DRAWER_ID} * {
box-sizing: border-box;
letter-spacing: 0;
}
#${BUTTON_ID} {
position: fixed;
right: 22px;
bottom: 24px;
z-index: 2147483646;
width: 62px;
height: 62px;
border: 1px solid rgba(103, 232, 249, .34);
border-radius: 50%;
background:
radial-gradient(circle at 32% 24%, rgba(255,255,255,.34), transparent 24%),
radial-gradient(circle at 70% 76%, rgba(139,92,246,.34), transparent 34%),
linear-gradient(145deg, rgba(12,18,30,.82), rgba(8,12,22,.66));
color: rgba(242, 251, 255, .96);
box-shadow:
0 22px 52px rgba(0, 0, 0, .44),
0 0 34px rgba(103, 232, 249, .18),
inset 0 1px 0 rgba(255,255,255,.24),
inset 0 -18px 42px rgba(139,92,246,.12);
backdrop-filter: blur(22px) saturate(145%);
-webkit-backdrop-filter: blur(22px) saturate(145%);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 760;
line-height: 1.16;
text-align: center;
overflow: visible;
user-select: none;
touch-action: none;
transition: transform .38s cubic-bezier(.16, 1, .3, 1), border-color .38s ease, box-shadow .38s ease;
animation: tgFloatButton 7s ease-in-out infinite;
}
#${BUTTON_ID}::before {
content: "";
position: absolute;
inset: -9px;
border-radius: inherit;
background: conic-gradient(from 140deg, transparent, rgba(103,232,249,.22), transparent, rgba(240,171,252,.16), transparent);
filter: blur(12px);
opacity: .7;
z-index: -1;
}
#${BUTTON_ID}:hover {
transform: translateY(-4px) scale(1.035);
border-color: rgba(103, 232, 249, .62);
box-shadow:
0 28px 70px rgba(0, 0, 0, .52),
0 0 48px rgba(103, 232, 249, .26),
0 0 58px rgba(139, 92, 246, .16),
inset 0 1px 0 rgba(255,255,255,.32);
}
#${BUTTON_ID} .tg-task-button-badge {
position: absolute;
right: -4px;
top: -5px;
min-width: 22px;
height: 22px;
padding: 0 6px;
border-radius: 999px;
border: 1px solid rgba(255,255,255,.44);
background: linear-gradient(135deg, rgba(251,113,133,.94), rgba(240,171,252,.74));
color: #fff;
font-size: 11px;
line-height: 20px;
box-shadow: 0 0 20px rgba(251,113,133,.34);
}
#${DRAWER_ID} {
position: fixed;
right: 18px;
top: 18px;
z-index: 2147483647;
width: min(520px, calc(100vw - 28px));
height: min(760px, calc(100vh - 48px));
min-width: 390px;
min-height: 480px;
max-width: calc(100vw - 40px);
max-height: calc(100vh - 40px);
resize: both;
display: flex;
flex-direction: column;
overflow: hidden;
border-radius: 30px;
color: var(--tg-text);
background:
linear-gradient(145deg, rgba(14, 21, 36, .72), rgba(6, 10, 18, .68)),
radial-gradient(circle at 18% 0%, rgba(103,232,249,.12), transparent 38%),
radial-gradient(circle at 92% 8%, rgba(139,92,246,.16), transparent 42%);
border: 1px solid rgba(160, 231, 255, .2);
box-shadow:
-34px 28px 90px rgba(0,0,0,.56),
0 0 0 1px rgba(255,255,255,.035) inset,
0 0 56px rgba(103,232,249,.12),
0 0 88px rgba(139,92,246,.1);
backdrop-filter: blur(34px) saturate(150%);
-webkit-backdrop-filter: blur(34px) saturate(150%);
transform: translateX(calc(100% + 38px)) scale(.985);
opacity: .42;
transition:
transform .58s cubic-bezier(.16, 1, .3, 1),
opacity .38s ease,
box-shadow .45s ease;
}
#${DRAWER_ID}.tg-open {
transform: translateX(0) scale(1);
opacity: 1;
}
#${DRAWER_ID}::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background-image:
radial-gradient(circle at 14% 16%, rgba(103,232,249,.22) 0 1px, transparent 2px),
radial-gradient(circle at 84% 22%, rgba(240,171,252,.18) 0 1px, transparent 2px),
radial-gradient(circle at 64% 74%, rgba(139,92,246,.16) 0 1px, transparent 2px),
linear-gradient(rgba(255,255,255,.035) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,.024) 1px, transparent 1px);
background-size: 128px 128px, 184px 184px, 148px 148px, 44px 44px, 44px 44px;
mask-image: linear-gradient(to bottom, rgba(0,0,0,.92), rgba(0,0,0,.26));
opacity: .42;
animation: tgParticleDrift 22s linear infinite;
}
#${DRAWER_ID}::after {
content: "";
position: absolute;
inset: 1px;
pointer-events: none;
border-radius: 29px;
background:
linear-gradient(135deg, rgba(255,255,255,.18), transparent 24%),
linear-gradient(315deg, rgba(103,232,249,.1), transparent 28%),
radial-gradient(circle at 78% 14%, rgba(240,171,252,.12), transparent 34%);
mix-blend-mode: screen;
opacity: .55;
}
#${DRAWER_ID} .tg-ambient {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
z-index: 0;
}
#${DRAWER_ID} .tg-orb,
#${DRAWER_ID} .tg-ring {
position: absolute;
display: block;
border-radius: 999px;
filter: blur(.2px);
opacity: .7;
transform: translate3d(0,0,0);
}
#${DRAWER_ID} .tg-orb-a {
width: 150px;
height: 150px;
right: -42px;
top: 70px;
background: radial-gradient(circle at 34% 30%, rgba(255,255,255,.38), rgba(103,232,249,.18) 28%, rgba(103,232,249,.04) 68%, transparent 74%);
filter: blur(1px);
animation: tgSlowFloatA 15s ease-in-out infinite;
}
#${DRAWER_ID} .tg-orb-b {
width: 92px;
height: 92px;
left: 32px;
bottom: 92px;
background: radial-gradient(circle at 34% 28%, rgba(255,255,255,.25), rgba(240,171,252,.18) 38%, transparent 72%);
filter: blur(1.5px);
animation: tgSlowFloatB 18s ease-in-out infinite;
}
#${DRAWER_ID} .tg-ring-a {
width: 178px;
height: 178px;
left: -78px;
top: 178px;
border: 1px solid rgba(103,232,249,.16);
box-shadow: inset 0 0 34px rgba(103,232,249,.08), 0 0 30px rgba(139,92,246,.08);
transform: rotate(-18deg);
animation: tgRingDrift 21s ease-in-out infinite;
}
#${DRAWER_ID} .tg-header,
#${DRAWER_ID} .tg-body,
#${DRAWER_ID} .tg-actions {
position: relative;
z-index: 1;
}
#${DRAWER_ID} .tg-header {
flex: 0 0 auto;
min-height: 78px;
padding: 18px 18px 14px;
border-bottom: 1px solid rgba(148, 227, 255, .13);
background: linear-gradient(180deg, rgba(255,255,255,.055), rgba(255,255,255,.018));
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
}
#${DRAWER_ID} .tg-title {
min-width: 0;
}
#${DRAWER_ID} .tg-title-main {
font-size: 22px;
font-weight: 760;
line-height: 1.15;
color: rgba(248, 250, 252, .98);
text-shadow: 0 0 22px rgba(103,232,249,.12);
}
#${DRAWER_ID} .tg-title-sub {
margin-top: 6px;
color: var(--tg-muted);
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#${DRAWER_ID} .tg-header-actions {
display: flex;
gap: 8px;
flex: 0 0 auto;
}
#${DRAWER_ID} .tg-icon-btn {
width: 34px;
height: 34px;
border: 1px solid rgba(148, 227, 255, .18);
background: rgba(255,255,255,.055);
color: rgba(235, 245, 255, .9);
border-radius: 12px;
cursor: pointer;
font-size: 13px;
line-height: 32px;
text-align: center;
box-shadow: inset 0 1px 0 rgba(255,255,255,.12);
transition: transform .28s cubic-bezier(.16, 1, .3, 1), border-color .28s ease, background .28s ease, box-shadow .28s ease;
}
#${DRAWER_ID} .tg-icon-btn:hover {
transform: translateY(-2px);
border-color: rgba(103,232,249,.44);
background: rgba(103,232,249,.09);
box-shadow: 0 0 22px rgba(103,232,249,.12), inset 0 1px 0 rgba(255,255,255,.18);
}
#${DRAWER_ID} .tg-top-refresh {
width: auto;
min-width: 74px;
padding: 0 11px;
font-size: 12px;
white-space: nowrap;
}
#${DRAWER_ID} .tg-body {
flex: 1 1 auto;
overflow-y: auto;
padding: 16px;
scrollbar-width: thin;
scrollbar-color: rgba(103,232,249,.28) transparent;
}
#${DRAWER_ID} .tg-body::-webkit-scrollbar {
width: 9px;
}
#${DRAWER_ID} .tg-body::-webkit-scrollbar-thumb {
background: rgba(103,232,249,.2);
border: 3px solid transparent;
border-radius: 999px;
background-clip: padding-box;
}
#${DRAWER_ID} .tg-meta,
#${DRAWER_ID} .tg-alert,
#${DRAWER_ID} .tg-empty,
#${DRAWER_ID} .tg-date-filter,
#${DRAWER_ID} .tg-summary-item,
#${DRAWER_ID} .tg-card {
position: relative;
overflow: hidden;
border: 1px solid rgba(148, 227, 255, .15);
background:
linear-gradient(145deg, rgba(255,255,255,.09), rgba(255,255,255,.035)),
rgba(8, 13, 24, .42);
box-shadow:
0 18px 42px rgba(0,0,0,.25),
inset 0 1px 0 rgba(255,255,255,.09),
inset 0 -1px 0 rgba(255,255,255,.035);
backdrop-filter: blur(20px) saturate(145%);
-webkit-backdrop-filter: blur(20px) saturate(145%);
}
#${DRAWER_ID} .tg-meta::before,
#${DRAWER_ID} .tg-alert::before,
#${DRAWER_ID} .tg-empty::before,
#${DRAWER_ID} .tg-date-filter::before,
#${DRAWER_ID} .tg-summary-item::before,
#${DRAWER_ID} .tg-card::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background-image:
radial-gradient(circle at 18% 0%, rgba(255,255,255,.12), transparent 34%),
repeating-linear-gradient(0deg, rgba(255,255,255,.025) 0 1px, transparent 1px 4px);
opacity: .38;
mix-blend-mode: screen;
}
#${DRAWER_ID} .tg-meta,
#${DRAWER_ID} .tg-alert,
#${DRAWER_ID} .tg-empty {
border-radius: 22px;
padding: 14px;
margin-bottom: 12px;
color: var(--tg-muted);
font-size: 12px;
line-height: 1.65;
}
#${DRAWER_ID} .tg-meta {
padding: 15px;
}
#${DRAWER_ID} .tg-refresh-status {
border-radius: 20px;
padding: 12px;
margin-bottom: 12px;
border: 1px solid rgba(148, 227, 255, .14);
background:
linear-gradient(145deg, rgba(255,255,255,.075), rgba(255,255,255,.025)),
rgba(8, 13, 24, .38);
box-shadow: 0 16px 36px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.08);
backdrop-filter: blur(18px) saturate(140%);
-webkit-backdrop-filter: blur(18px) saturate(140%);
}
#${DRAWER_ID} .tg-refresh-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
margin-bottom: 9px;
}
#${DRAWER_ID} .tg-refresh-title {
color: rgba(245,250,255,.92);
font-size: 12px;
font-weight: 760;
}
#${DRAWER_ID} .tg-refresh-stage {
color: var(--tg-muted);
font-size: 11px;
line-height: 1.5;
overflow-wrap: anywhere;
}
#${DRAWER_ID} .tg-progress-track {
height: 7px;
overflow: hidden;
border-radius: 999px;
border: 1px solid rgba(148, 227, 255, .12);
background: rgba(255,255,255,.045);
box-shadow: inset 0 1px 4px rgba(0,0,0,.28);
}
#${DRAWER_ID} .tg-progress-fill {
height: 100%;
width: 0%;
border-radius: inherit;
background: linear-gradient(90deg, rgba(103,232,249,.72), rgba(139,92,246,.62), rgba(240,171,252,.52));
box-shadow: 0 0 18px rgba(103,232,249,.22);
transition: width .42s cubic-bezier(.16, 1, .3, 1);
}
#${DRAWER_ID} .tg-meta-kicker {
color: rgba(103,232,249,.86);
font-size: 11px;
font-weight: 720;
margin-bottom: 12px;
}
#${DRAWER_ID} .tg-meta-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 9px;
}
#${DRAWER_ID} .tg-meta-chip {
border: 1px solid rgba(148, 227, 255, .12);
background: rgba(255,255,255,.045);
border-radius: 16px;
padding: 9px 10px;
min-width: 0;
}
#${DRAWER_ID} .tg-meta-label {
display: block;
color: var(--tg-faint);
font-size: 11px;
margin-bottom: 4px;
}
#${DRAWER_ID} .tg-meta-value {
display: block;
color: var(--tg-text);
font-size: 12px;
font-weight: 680;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#${DRAWER_ID} .tg-alert {
color: rgba(255, 228, 230, .95);
background:
linear-gradient(145deg, rgba(251,113,133,.16), rgba(255,255,255,.035)),
rgba(40, 9, 20, .34);
border-color: rgba(251,113,133,.26);
box-shadow: 0 18px 44px rgba(77, 10, 30, .25), inset 0 1px 0 rgba(255,255,255,.09);
}
#${DRAWER_ID} .tg-empty {
color: rgba(203, 213, 225, .74);
text-align: center;
padding: 18px;
}
#${DRAWER_ID} .tg-summary {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 9px;
margin-bottom: 12px;
}
#${DRAWER_ID} .tg-summary-item {
min-width: 0;
width: 100%;
border-radius: 20px;
padding: 12px 8px 11px;
text-align: left;
cursor: pointer;
color: inherit;
font: inherit;
appearance: none;
-webkit-appearance: none;
transition: transform .36s cubic-bezier(.16, 1, .3, 1), border-color .36s ease, box-shadow .36s ease, background .36s ease;
}
#${DRAWER_ID} .tg-summary-item:hover {
transform: translateY(-4px);
border-color: rgba(103,232,249,.34);
background:
linear-gradient(145deg, rgba(103,232,249,.1), rgba(255,255,255,.04)),
rgba(9, 16, 28, .54);
box-shadow: 0 24px 54px rgba(0,0,0,.3), 0 0 26px rgba(103,232,249,.1), inset 0 1px 0 rgba(255,255,255,.13);
}
#${DRAWER_ID} .tg-summary-item.tg-selected {
border-color: rgba(103,232,249,.48);
background:
linear-gradient(145deg, rgba(103,232,249,.16), rgba(139,92,246,.09)),
rgba(9, 16, 28, .62);
box-shadow:
0 24px 58px rgba(0,0,0,.34),
0 0 32px rgba(103,232,249,.18),
0 0 34px rgba(139,92,246,.11),
inset 0 1px 0 rgba(255,255,255,.15);
}
#${DRAWER_ID} .tg-summary-item.tg-selected .tg-summary-num {
color: rgba(165,243,252,.98);
text-shadow: 0 0 18px rgba(103,232,249,.18);
}
#${DRAWER_ID} .tg-summary-item[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
left: 50%;
bottom: calc(100% + 10px);
width: min(230px, 72vw);
transform: translate(-50%, 8px);
opacity: 0;
pointer-events: none;
color: rgba(235,245,255,.94);
background:
linear-gradient(145deg, rgba(18, 27, 44, .94), rgba(8, 13, 24, .9)),
rgba(8, 13, 24, .92);
border: 1px solid rgba(148, 227, 255, .18);
border-radius: 12px;
padding: 8px 10px;
font-size: 11px;
line-height: 1.45;
box-shadow: 0 16px 36px rgba(0,0,0,.38), 0 0 24px rgba(103,232,249,.1), inset 0 1px 0 rgba(255,255,255,.08);
backdrop-filter: blur(18px) saturate(145%);
-webkit-backdrop-filter: blur(18px) saturate(145%);
transition: opacity .22s ease, transform .22s cubic-bezier(.16, 1, .3, 1);
z-index: 6;
}
#${DRAWER_ID} .tg-summary-item[data-tooltip]:hover::after {
opacity: 1;
transform: translate(-50%, 0);
}
#${DRAWER_ID} .tg-summary-num {
position: relative;
font-size: 24px;
font-weight: 780;
line-height: 1;
margin-bottom: 8px;
color: rgba(248,250,252,.96);
}
#${DRAWER_ID} .tg-summary-label {
position: relative;
color: var(--tg-muted);
font-size: 11px;
white-space: nowrap;
}
#${DRAWER_ID} .tg-current-filter {
margin: -2px 2px 12px;
color: rgba(203, 213, 225, .76);
font-size: 12px;
font-weight: 650;
}
#${DRAWER_ID} .tg-current-filter strong {
color: rgba(165,243,252,.94);
font-weight: 760;
}
#${DRAWER_ID} .tg-date-filter {
border-radius: 22px;
padding: 13px;
margin-bottom: 12px;
}
#${DRAWER_ID} .tg-date-filter-row {
position: relative;
display: flex;
align-items: center;
gap: 8px;
color: rgba(235,245,255,.9);
font-size: 12px;
font-weight: 650;
}
#${DRAWER_ID} .tg-time-mark {
position: relative;
width: 28px;
height: 28px;
flex: 0 0 auto;
border-radius: 10px;
border: 1px solid rgba(103,232,249,.22);
background:
linear-gradient(180deg, rgba(103,232,249,.14), rgba(139,92,246,.08)),
rgba(255,255,255,.04);
box-shadow: 0 0 18px rgba(103,232,249,.1), inset 0 1px 0 rgba(255,255,255,.16);
}
#${DRAWER_ID} .tg-time-mark::before,
#${DRAWER_ID} .tg-time-mark::after {
content: "";
position: absolute;
display: block;
}
#${DRAWER_ID} .tg-time-mark::before {
left: 7px;
right: 7px;
top: 8px;
height: 2px;
border-radius: 999px;
background: rgba(103,232,249,.62);
box-shadow: 0 7px 0 rgba(103,232,249,.2);
}
#${DRAWER_ID} .tg-time-mark::after {
left: 8px;
top: 6px;
width: 12px;
height: 14px;
border: 1px solid rgba(245,250,255,.28);
border-radius: 4px;
}
#${DRAWER_ID} .tg-date-filter select {
height: 34px;
min-width: 84px;
border: 1px solid rgba(148, 227, 255, .18);
background: rgba(2, 6, 23, .42);
color: rgba(245, 250, 255, .94);
border-radius: 12px;
padding: 0 10px;
font-size: 12px;
outline: none;
box-shadow: inset 0 1px 0 rgba(255,255,255,.08);
transition: border-color .24s ease, box-shadow .24s ease, background .24s ease;
}
#${DRAWER_ID} .tg-date-filter select:focus,
#${DRAWER_ID} .tg-date-filter select:hover {
border-color: rgba(103,232,249,.42);
box-shadow: 0 0 24px rgba(103,232,249,.1), inset 0 1px 0 rgba(255,255,255,.12);
}
#${DRAWER_ID} .tg-date-filter select option {
background: #0f172a;
color: rgba(245,250,255,.94);
}
#${DRAWER_ID} .tg-date-filter-hint {
position: relative;
margin-top: 9px;
color: var(--tg-muted);
font-size: 12px;
}
#${DRAWER_ID} .tg-action-btn,
#${DRAWER_ID} .tg-detail-link {
min-height: 34px;
border: 1px solid rgba(148, 227, 255, .14);
background: rgba(255,255,255,.045);
color: rgba(226, 232, 240, .86);
border-radius: 14px;
cursor: pointer;
padding: 0 9px;
font-size: 12px;
font-weight: 650;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
box-shadow: inset 0 1px 0 rgba(255,255,255,.07);
transition: transform .28s cubic-bezier(.16, 1, .3, 1), border-color .28s ease, background .28s ease, box-shadow .28s ease, color .28s ease;
}
#${DRAWER_ID} .tg-action-btn:hover,
#${DRAWER_ID} .tg-detail-link:hover {
transform: translateY(-2px);
border-color: rgba(103,232,249,.42);
color: rgba(245,250,255,.98);
background:
linear-gradient(135deg, rgba(103,232,249,.14), rgba(139,92,246,.1)),
rgba(255,255,255,.055);
box-shadow: 0 0 24px rgba(103,232,249,.12), inset 0 1px 0 rgba(255,255,255,.12);
}
#${DRAWER_ID} .tg-section {
margin-bottom: 16px;
}
#${DRAWER_ID} .tg-course-group {
margin-bottom: 16px;
border: 1px solid rgba(148, 227, 255, .13);
background:
linear-gradient(145deg, rgba(255,255,255,.06), rgba(255,255,255,.025)),
rgba(8, 13, 24, .34);
border-radius: 24px;
padding: 12px;
box-shadow: 0 18px 42px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.07);
backdrop-filter: blur(18px) saturate(135%);
-webkit-backdrop-filter: blur(18px) saturate(135%);
}
#${DRAWER_ID} .tg-course-head {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 12px;
align-items: center;
color: rgba(248,250,252,.96);
font-size: 14px;
font-weight: 760;
margin: 2px 2px 10px;
overflow-wrap: anywhere;
cursor: pointer;
}
#${DRAWER_ID} .tg-course-title {
min-width: 0;
overflow-wrap: anywhere;
}
#${DRAWER_ID} .tg-course-stats {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
#${DRAWER_ID} .tg-course-stat {
color: rgba(203,213,225,.76);
background: rgba(255,255,255,.045);
border: 1px solid rgba(148, 227, 255, .1);
border-radius: 999px;
padding: 3px 7px;
font-size: 11px;
font-weight: 620;
}
#${DRAWER_ID} .tg-course-toggle {
width: 34px;
height: 34px;
border-radius: 13px;
border: 1px solid rgba(148, 227, 255, .16);
background: rgba(255,255,255,.045);
color: rgba(235,245,255,.9);
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255,255,255,.08);
transition: transform .28s cubic-bezier(.16, 1, .3, 1), border-color .28s ease, box-shadow .28s ease;
}
#${DRAWER_ID} .tg-course-toggle:hover {
transform: translateY(-2px);
border-color: rgba(103,232,249,.4);
box-shadow: 0 0 22px rgba(103,232,249,.12), inset 0 1px 0 rgba(255,255,255,.12);
}
#${DRAWER_ID} .tg-course-content {
display: grid;
grid-template-rows: 1fr;
transition: grid-template-rows .34s cubic-bezier(.16, 1, .3, 1), opacity .28s ease;
opacity: 1;
}
#${DRAWER_ID} .tg-course-content-inner {
overflow: hidden;
}
#${DRAWER_ID} .tg-course-group.tg-course-collapsed .tg-course-content {
grid-template-rows: 0fr;
opacity: 0;
}
#${DRAWER_ID} .tg-course-subgroup {
margin-top: 10px;
border: 1px solid rgba(148, 227, 255, .09);
border-radius: 18px;
background: rgba(255,255,255,.025);
padding: 9px;
}
#${DRAWER_ID} .tg-subgroup-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
color: rgba(203,213,225,.76);
font-size: 12px;
font-weight: 720;
margin: 0 2px;
cursor: pointer;
user-select: none;
}
#${DRAWER_ID} .tg-subgroup-title-main {
min-width: 0;
color: rgba(235,245,255,.88);
}
#${DRAWER_ID} .tg-subgroup-count {
flex: 0 0 auto;
color: var(--tg-muted);
border: 1px solid rgba(148, 227, 255, .1);
background: rgba(255,255,255,.04);
border-radius: 999px;
padding: 2px 7px;
font-size: 11px;
}
#${DRAWER_ID} .tg-subgroup-content {
display: grid;
grid-template-rows: 1fr;
margin-top: 8px;
transition: grid-template-rows .32s cubic-bezier(.16, 1, .3, 1), opacity .26s ease;
opacity: 1;
}
#${DRAWER_ID} .tg-subgroup-content-inner {
overflow: hidden;
}
#${DRAWER_ID} .tg-section-collapsed .tg-subgroup-content {
grid-template-rows: 0fr;
opacity: 0;
}
#${DRAWER_ID} .tg-section-title {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 8px;
color: rgba(245,250,255,.92);
font-size: 13px;
font-weight: 760;
margin: 15px 3px 9px;
}
#${DRAWER_ID} .tg-section-title::before {
content: "";
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--tg-cyan);
box-shadow: 0 0 14px rgba(103,232,249,.52);
margin-right: 1px;
}
#${DRAWER_ID} .tg-section-title span:first-child {
flex: 1 1 auto;
}
#${DRAWER_ID} .tg-section-count {
color: var(--tg-muted);
font-size: 12px;
font-weight: 560;
}
#${DRAWER_ID} .tg-section-urgent .tg-section-title::before {
background: var(--tg-red);
box-shadow: 0 0 16px rgba(251,113,133,.42);
}
#${DRAWER_ID} .tg-card {
border-radius: 22px;
padding: 13px;
margin-bottom: 10px;
line-height: 1.55;
transition: transform .36s cubic-bezier(.16, 1, .3, 1), border-color .36s ease, box-shadow .36s ease, background .36s ease;
}
#${DRAWER_ID} .tg-card:hover {
transform: translateY(-4px);
border-color: rgba(103,232,249,.32);
background:
linear-gradient(145deg, rgba(255,255,255,.105), rgba(255,255,255,.045)),
rgba(10, 17, 30, .54);
box-shadow: 0 28px 64px rgba(0,0,0,.34), 0 0 34px rgba(103,232,249,.1), inset 0 1px 0 rgba(255,255,255,.13);
}
#${DRAWER_ID} .tg-card.tg-state-ok {
border-color: rgba(134,239,172,.18);
}
#${DRAWER_ID} .tg-card.tg-state-warn {
border-color: rgba(251,191,36,.22);
}
#${DRAWER_ID} .tg-card.tg-state-bad {
border-color: rgba(251,113,133,.28);
box-shadow: 0 20px 48px rgba(54, 8, 24, .26), 0 0 32px rgba(251,113,133,.08), inset 0 1px 0 rgba(255,255,255,.1);
}
#${DRAWER_ID} .tg-card.tg-state-muted {
border-color: rgba(148,163,184,.14);
}
#${DRAWER_ID} .tg-card-head {
position: relative;
display: flex;
gap: 10px;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 9px;
}
#${DRAWER_ID} .tg-card-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
}
#${DRAWER_ID} .tg-card-top-actions {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
flex: 0 0 auto;
}
#${DRAWER_ID} .tg-card-title {
min-width: 0;
color: rgba(248,250,252,.96);
font-size: 14px;
font-weight: 720;
line-height: 1.45;
overflow-wrap: anywhere;
}
#${DRAWER_ID} .tg-card-title a {
color: inherit;
text-decoration: none;
}
#${DRAWER_ID} .tg-card-title a:hover {
color: rgba(165,243,252,.98);
}
#${DRAWER_ID} .tg-type {
flex: 0 0 auto;
border: 1px solid rgba(148, 227, 255, .18);
border-radius: 999px;
padding: 3px 8px;
color: rgba(225, 245, 255, .82);
font-size: 11px;
font-weight: 680;
background: rgba(255,255,255,.045);
box-shadow: inset 0 1px 0 rgba(255,255,255,.08);
}
#${DRAWER_ID} .tg-countdown {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 34px;
border-radius: 999px;
border: 1px solid rgba(251,191,36,.34);
background:
linear-gradient(135deg, rgba(251,191,36,.16), rgba(251,113,133,.1)),
rgba(255,255,255,.045);
color: rgba(255, 237, 213, .96);
padding: 6px 12px;
font-size: 16px;
font-weight: 780;
line-height: 1.2;
box-shadow: 0 0 24px rgba(251,191,36,.13), inset 0 1px 0 rgba(255,255,255,.1);
white-space: nowrap;
animation: tgCountdownPulse 2.8s ease-in-out infinite;
}
#${DRAWER_ID} .tg-ignored-flag {
display: inline-flex;
align-items: center;
min-height: 30px;
padding: 0 11px;
border-radius: 999px;
border: 1px solid rgba(148, 163, 184, .18);
background: rgba(255,255,255,.04);
color: rgba(203, 213, 225, .82);
font-size: 11px;
font-weight: 760;
white-space: nowrap;
}
#${DRAWER_ID} .tg-icon-action {
width: 34px;
height: 34px;
border: 1px solid rgba(148, 227, 255, .14);
background: rgba(255,255,255,.045);
color: rgba(226, 232, 240, .86);
border-radius: 12px;
cursor: pointer;
font-size: 16px;
line-height: 1;
box-shadow: inset 0 1px 0 rgba(255,255,255,.07);
transition: transform .28s cubic-bezier(.16, 1, .3, 1), border-color .28s ease, background .28s ease, box-shadow .28s ease, color .28s ease;
}
#${DRAWER_ID} .tg-icon-action:hover {
transform: translateY(-2px);
border-color: rgba(251,191,36,.42);
color: rgba(255, 245, 220, .98);
background:
linear-gradient(135deg, rgba(251,191,36,.14), rgba(251,113,133,.1)),
rgba(255,255,255,.055);
box-shadow: 0 0 24px rgba(251,191,36,.12), inset 0 1px 0 rgba(255,255,255,.12);
}
#${DRAWER_ID} .tg-restore-btn {
min-width: 64px;
}
#${DRAWER_ID} .tg-status {
position: relative;
font-size: 13px;
font-weight: 760;
margin: 7px 0 6px;
}
#${DRAWER_ID} .tg-status-row {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
flex-wrap: wrap;
}
#${DRAWER_ID} .tg-ok { color: var(--tg-green); }
#${DRAWER_ID} .tg-warn { color: var(--tg-amber); }
#${DRAWER_ID} .tg-bad { color: var(--tg-red); }
#${DRAWER_ID} .tg-muted { color: var(--tg-muted); }
#${DRAWER_ID} .tg-card-note {
margin-top: -2px;
margin-bottom: 8px;
color: rgba(203, 213, 225, .72);
font-size: 12px;
}
#${DRAWER_ID} .tg-small,
#${DRAWER_ID} .tg-task-meta {
position: relative;
color: var(--tg-muted);
font-size: 12px;
line-height: 1.7;
overflow-wrap: anywhere;
}
#${DRAWER_ID} .tg-task-meta {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 7px;
margin-top: 10px;
}
#${DRAWER_ID} .tg-task-meta span {
display: block;
border: 1px solid rgba(148, 227, 255, .1);
background: rgba(255,255,255,.035);
border-radius: 13px;
padding: 7px 8px;
}
#${DRAWER_ID} .tg-card-footer {
position: relative;
display: flex;
justify-content: flex-start;
margin-top: 11px;
}
#${DRAWER_ID} .tg-detail-link {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 30px;
text-decoration: none;
padding: 0 12px;
border: 1px solid rgba(103,232,249,.22);
border-radius: 999px;
color: rgba(225,245,255,.92);
background: rgba(255,255,255,.045);
cursor: pointer;
font: inherit;
font-size: 12px;
transition: transform .25s cubic-bezier(.16, 1, .3, 1), border-color .25s ease, box-shadow .25s ease;
}
#${DRAWER_ID} .tg-detail-link:hover {
transform: translateY(-1px);
border-color: rgba(103,232,249,.45);
box-shadow: 0 0 18px rgba(103,232,249,.12);
}
#${DRAWER_ID} .tg-actions {
flex: 0 0 auto;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr);
gap: 10px;
padding: 12px 16px 16px;
border-top: 1px solid rgba(148, 227, 255, .13);
background: linear-gradient(180deg, rgba(8,13,24,.44), rgba(8,13,24,.78));
backdrop-filter: blur(24px) saturate(145%);
-webkit-backdrop-filter: blur(24px) saturate(145%);
}
#${DRAWER_ID} .tg-action-btn {
min-height: 40px;
border-radius: 16px;
}
#${DRAWER_ID} [data-copy-all-json] {
border-color: rgba(103,232,249,.28);
background:
linear-gradient(135deg, rgba(103,232,249,.13), rgba(139,92,246,.11)),
rgba(255,255,255,.055);
color: rgba(245,250,255,.96);
}
#${DRAWER_ID} [data-delete-cache] {
border-color: rgba(251,113,133,.22);
background:
linear-gradient(135deg, rgba(251,113,133,.1), rgba(251,191,36,.06)),
rgba(255,255,255,.045);
color: rgba(255,228,230,.94);
}
#${DRAWER_ID} textarea.tg-json-box {
width: 100%;
height: 260px;
margin-top: 10px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, Monaco, monospace;
font-size: 12px;
line-height: 1.5;
border: 1px solid rgba(148, 227, 255, .16);
border-radius: 18px;
padding: 12px;
resize: vertical;
color: rgba(235,245,255,.94);
background: rgba(2,6,23,.58);
box-shadow: inset 0 1px 0 rgba(255,255,255,.08);
}
@keyframes tgFloatButton {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
@keyframes tgParticleDrift {
from { background-position: 0 0, 0 0, 0 0, 0 0, 0 0; }
to { background-position: 128px 70px, -184px 120px, 148px -90px, 44px 44px, -44px 44px; }
}
@keyframes tgSlowFloatA {
0%, 100% { transform: translate3d(0,0,0) scale(1); opacity: .58; }
50% { transform: translate3d(-18px,24px,0) scale(1.06); opacity: .76; }
}
@keyframes tgSlowFloatB {
0%, 100% { transform: translate3d(0,0,0) scale(1); opacity: .44; }
50% { transform: translate3d(22px,-18px,0) scale(1.08); opacity: .66; }
}
@keyframes tgRingDrift {
0%, 100% { transform: rotate(-18deg) translate3d(0,0,0); opacity: .36; }
50% { transform: rotate(8deg) translate3d(18px,10px,0); opacity: .52; }
}
@keyframes tgCountdownPulse {
0%, 100% { box-shadow: 0 0 20px rgba(251,191,36,.11), inset 0 1px 0 rgba(255,255,255,.1); }
50% { box-shadow: 0 0 30px rgba(251,191,36,.22), 0 0 18px rgba(251,113,133,.12), inset 0 1px 0 rgba(255,255,255,.14); }
}
/* iOS 26 Liquid Glass practical skin */
#${BUTTON_ID}, #${DRAWER_ID} {
--tg-liquid-bg: rgba(255,255,255,.16);
--tg-liquid-bg-strong: rgba(20, 25, 36, .72);
--tg-liquid-card: rgba(255,255,255,.13);
--tg-liquid-card-strong: rgba(255,255,255,.2);
--tg-liquid-border: rgba(255,255,255,.35);
--tg-liquid-border-soft: rgba(255,255,255,.22);
--tg-liquid-shadow: 0 20px 60px rgba(0,0,0,.25);
--tg-text: rgba(255,255,255,.96);
--tg-muted: rgba(232,238,247,.76);
--tg-faint: rgba(220,228,240,.58);
--tg-cyan: #8ee8ff;
--tg-violet: #b8a7ff;
--tg-pink: #ffc5e8;
--tg-red: #ff6b7f;
--tg-amber: #ffd166;
--tg-green: #8ce99a;
text-shadow: 0 1px 1px rgba(0,0,0,.24);
}
#${BUTTON_ID} {
width: auto;
min-width: 132px;
height: 48px;
padding: 0 18px;
border-radius: 999px;
border: 1px solid var(--tg-liquid-border);
background:
linear-gradient(135deg, rgba(255,255,255,.32), rgba(255,255,255,.12)),
rgba(255,255,255,.16);
color: rgba(255,255,255,.96);
box-shadow: var(--tg-liquid-shadow), inset 0 1px 0 rgba(255,255,255,.42), inset 0 -1px 0 rgba(255,255,255,.14);
backdrop-filter: blur(22px) saturate(180%);
-webkit-backdrop-filter: blur(22px) saturate(180%);
font-size: 13px;
font-weight: 780;
line-height: 1;
animation: none;
transition: transform .18s cubic-bezier(.2, .8, .2, 1), border-color .18s ease, box-shadow .18s ease, background .18s ease;
}
#${BUTTON_ID}::before {
inset: 1px;
border-radius: 999px;
background: linear-gradient(120deg, transparent 12%, rgba(255,255,255,.36) 32%, transparent 56%);
filter: none;
opacity: .34;
transform: translateX(-42%);
transition: transform .22s ease, opacity .18s ease;
}
#${BUTTON_ID}:hover {
transform: translateY(-2px);
border-color: rgba(255,255,255,.58);
background:
linear-gradient(135deg, rgba(255,255,255,.4), rgba(255,255,255,.18)),
rgba(255,255,255,.2);
box-shadow: 0 24px 64px rgba(0,0,0,.3), inset 0 1px 0 rgba(255,255,255,.5);
}
#${BUTTON_ID}:hover::before {
transform: translateX(36%);
opacity: .55;
}
#${BUTTON_ID} .tg-task-button-badge {
right: 4px;
top: -8px;
min-width: 24px;
height: 24px;
border-radius: 999px;
border: 1px solid rgba(255,255,255,.62);
background: rgba(255, 78, 112, .84);
color: #fff;
line-height: 22px;
box-shadow: 0 10px 24px rgba(255,78,112,.24), inset 0 1px 0 rgba(255,255,255,.38);
backdrop-filter: blur(14px) saturate(180%);
-webkit-backdrop-filter: blur(14px) saturate(180%);
}
#${DRAWER_ID} {
border-radius: 28px;
color: var(--tg-text);
background:
linear-gradient(180deg, rgba(22,27,38,.72), rgba(10,14,22,.68)),
rgba(255,255,255,.16);
border: 1px solid var(--tg-liquid-border);
box-shadow: var(--tg-liquid-shadow), inset 0 1px 0 rgba(255,255,255,.32), inset 0 0 0 1px rgba(255,255,255,.08);
backdrop-filter: blur(22px) saturate(180%);
-webkit-backdrop-filter: blur(22px) saturate(180%);
transition: transform .2s cubic-bezier(.2, .8, .2, 1), opacity .18s ease, box-shadow .18s ease;
}
#${DRAWER_ID}.tg-open {
background:
linear-gradient(180deg, rgba(20,25,36,.78), rgba(8,12,20,.74)),
rgba(255,255,255,.18);
}
#${DRAWER_ID}::before {
background:
linear-gradient(115deg, rgba(255,255,255,.2), transparent 24%),
radial-gradient(circle at 18% 0%, rgba(255,255,255,.16), transparent 30%);
opacity: .55;
animation: none;
mask-image: none;
}
#${DRAWER_ID}::after {
border-radius: 27px;
background:
linear-gradient(135deg, rgba(255,255,255,.2), transparent 28%),
linear-gradient(315deg, rgba(255,255,255,.08), transparent 34%);
opacity: .45;
}
#${DRAWER_ID} .tg-ambient {
display: none;
}
#${DRAWER_ID} .tg-header {
min-height: 76px;
border-bottom: 1px solid rgba(255,255,255,.18);
background: rgba(255,255,255,.08);
backdrop-filter: blur(16px) saturate(160%);
-webkit-backdrop-filter: blur(16px) saturate(160%);
}
#${DRAWER_ID} .tg-title-main,
#${DRAWER_ID} .tg-card-title,
#${DRAWER_ID} .tg-course-head,
#${DRAWER_ID} .tg-section-title {
color: rgba(255,255,255,.98);
text-shadow: 0 1px 2px rgba(0,0,0,.28);
}
#${DRAWER_ID} .tg-icon-btn,
#${DRAWER_ID} .tg-action-btn,
#${DRAWER_ID} .tg-detail-link,
#${DRAWER_ID} .tg-course-toggle,
#${DRAWER_ID} .tg-subgroup-count,
#${DRAWER_ID} .tg-course-stat,
#${DRAWER_ID} .tg-type,
#${DRAWER_ID} .tg-date-filter select {
border-radius: 999px;
border-color: var(--tg-liquid-border-soft);
background: rgba(255,255,255,.13);
color: rgba(255,255,255,.92);
box-shadow: inset 0 1px 0 rgba(255,255,255,.22);
backdrop-filter: blur(14px) saturate(170%);
-webkit-backdrop-filter: blur(14px) saturate(170%);
transition: transform .16s ease, border-color .16s ease, background .16s ease, box-shadow .16s ease;
}
#${DRAWER_ID} .tg-top-refresh {
min-width: 78px;
}
#${DRAWER_ID} .tg-icon-btn:hover,
#${DRAWER_ID} .tg-action-btn:hover,
#${DRAWER_ID} .tg-detail-link:hover,
#${DRAWER_ID} .tg-course-toggle:hover,
#${DRAWER_ID} .tg-date-filter select:hover,
#${DRAWER_ID} .tg-date-filter select:focus {
transform: translateY(-1px);
border-color: rgba(255,255,255,.48);
background: rgba(255,255,255,.2);
box-shadow: 0 10px 26px rgba(0,0,0,.18), inset 0 1px 0 rgba(255,255,255,.34);
}
#${DRAWER_ID} .tg-meta,
#${DRAWER_ID} .tg-alert,
#${DRAWER_ID} .tg-empty,
#${DRAWER_ID} .tg-date-filter,
#${DRAWER_ID} .tg-summary-item,
#${DRAWER_ID} .tg-refresh-status,
#${DRAWER_ID} .tg-course-group,
#${DRAWER_ID} .tg-course-subgroup,
#${DRAWER_ID} .tg-card {
border: 1px solid var(--tg-liquid-border-soft);
background:
linear-gradient(180deg, rgba(255,255,255,.16), rgba(255,255,255,.08)),
rgba(255,255,255,.1);
box-shadow: 0 12px 34px rgba(0,0,0,.18), inset 0 1px 0 rgba(255,255,255,.24);
backdrop-filter: blur(18px) saturate(170%);
-webkit-backdrop-filter: blur(18px) saturate(170%);
}
#${DRAWER_ID} .tg-meta::before,
#${DRAWER_ID} .tg-alert::before,
#${DRAWER_ID} .tg-empty::before,
#${DRAWER_ID} .tg-date-filter::before,
#${DRAWER_ID} .tg-summary-item::before,
#${DRAWER_ID} .tg-card::before {
background: linear-gradient(120deg, rgba(255,255,255,.18), transparent 38%);
opacity: .42;
mix-blend-mode: normal;
}
#${DRAWER_ID} .tg-summary-item,
#${DRAWER_ID} .tg-card {
transition: transform .18s cubic-bezier(.2, .8, .2, 1), border-color .18s ease, box-shadow .18s ease, background .18s ease;
}
#${DRAWER_ID} .tg-summary-item:hover,
#${DRAWER_ID} .tg-card:hover {
transform: translateY(-2px);
border-color: rgba(255,255,255,.46);
background:
linear-gradient(180deg, rgba(255,255,255,.22), rgba(255,255,255,.1)),
rgba(255,255,255,.14);
box-shadow: 0 16px 42px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.3);
}
#${DRAWER_ID} .tg-summary-item.tg-selected {
border-color: rgba(142,232,255,.62);
background:
linear-gradient(180deg, rgba(142,232,255,.2), rgba(255,255,255,.12)),
rgba(255,255,255,.16);
box-shadow: 0 16px 42px rgba(0,0,0,.22), 0 0 0 1px rgba(142,232,255,.18), inset 0 1px 0 rgba(255,255,255,.32);
}
#${DRAWER_ID} .tg-summary-item[data-tooltip]::after,
#${DRAWER_ID} .tg-status[data-tooltip]::after {
color: rgba(255,255,255,.96);
background:
linear-gradient(180deg, rgba(34,40,54,.86), rgba(12,16,25,.82)),
rgba(255,255,255,.16);
border: 1px solid rgba(255,255,255,.32);
box-shadow: 0 16px 42px rgba(0,0,0,.24), inset 0 1px 0 rgba(255,255,255,.22);
backdrop-filter: blur(18px) saturate(180%);
-webkit-backdrop-filter: blur(18px) saturate(180%);
}
#${DRAWER_ID} .tg-status[data-tooltip] {
cursor: help;
}
#${DRAWER_ID} .tg-status[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
left: 0;
bottom: calc(100% + 8px);
width: min(240px, 72vw);
transform: translateY(6px);
opacity: 0;
pointer-events: none;
border-radius: 14px;
padding: 8px 10px;
font-size: 11px;
line-height: 1.45;
font-weight: 560;
transition: opacity .16s ease, transform .16s ease;
z-index: 8;
}
#${DRAWER_ID} .tg-status[data-tooltip]:hover::after {
opacity: 1;
transform: translateY(0);
}
#${DRAWER_ID} .tg-progress-track {
height: 5px;
border-radius: 999px;
border: 1px solid rgba(255,255,255,.2);
background: rgba(255,255,255,.12);
}
#${DRAWER_ID} .tg-progress-fill {
background: linear-gradient(90deg, rgba(142,232,255,.92), rgba(184,167,255,.82), rgba(255,197,232,.76));
box-shadow: 0 0 16px rgba(142,232,255,.18);
transition: width .18s ease, opacity .18s ease;
}
#${DRAWER_ID} .tg-refresh-status.tg-refresh-done {
animation: tgProgressFade .9s ease .8s forwards;
}
#${DRAWER_ID} .tg-task-meta span {
border-color: rgba(255,255,255,.16);
background: rgba(255,255,255,.08);
color: rgba(238,244,252,.82);
backdrop-filter: blur(10px) saturate(150%);
-webkit-backdrop-filter: blur(10px) saturate(150%);
}
#${DRAWER_ID} .tg-actions {
border-top: 1px solid rgba(255,255,255,.18);
background: rgba(10,14,22,.52);
backdrop-filter: blur(18px) saturate(180%);
-webkit-backdrop-filter: blur(18px) saturate(180%);
}
#${DRAWER_ID} textarea.tg-json-box {
color: rgba(255,255,255,.94);
background: rgba(12,16,25,.68);
border-color: rgba(255,255,255,.22);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
#${BUTTON_ID} {
background: rgba(32, 38, 50, .94);
}
#${DRAWER_ID} {
background: rgba(20, 24, 34, .96);
}
#${DRAWER_ID} .tg-meta,
#${DRAWER_ID} .tg-alert,
#${DRAWER_ID} .tg-empty,
#${DRAWER_ID} .tg-date-filter,
#${DRAWER_ID} .tg-summary-item,
#${DRAWER_ID} .tg-refresh-status,
#${DRAWER_ID} .tg-course-group,
#${DRAWER_ID} .tg-course-subgroup,
#${DRAWER_ID} .tg-card {
background: rgba(35, 42, 56, .92);
}
}
@keyframes tgProgressFade {
to { opacity: .42; transform: translateY(-1px); }
}
/* Liquid Glass Pro light skin: CSS + SVG filter, optimized for white web pages */
#${BUTTON_ID}, #${DRAWER_ID} {
--tg-liquid-surface: rgba(255,255,255,.58);
--tg-liquid-card: rgba(255,255,255,.42);
--tg-liquid-card-hover: rgba(255,255,255,.58);
--tg-liquid-border: rgba(255,255,255,.75);
--tg-liquid-line: rgba(15,23,42,.08);
--tg-liquid-shadow: 0 20px 60px rgba(15,23,42,.18);
--tg-liquid-ease: cubic-bezier(.22, 1, .36, 1);
--tg-text: #111827;
--tg-muted: rgba(31,41,55,.72);
--tg-faint: rgba(75,85,99,.58);
--tg-cyan: #0891b2;
--tg-violet: #6d28d9;
--tg-pink: #be185d;
--tg-red: #dc2626;
--tg-amber: #b45309;
--tg-green: #15803d;
color: var(--tg-text);
text-shadow: none;
}
#${BUTTON_ID} {
min-width: 136px;
height: 50px;
border-radius: 999px;
border: 1px solid var(--tg-liquid-border);
background:
radial-gradient(circle at var(--tg-mouse-x, 28%) var(--tg-mouse-y, 18%), rgba(255,255,255,.96), transparent 36%),
linear-gradient(135deg, rgba(255,255,255,.78), rgba(255,255,255,.38)),
rgba(255,255,255,.58);
color: #111827;
box-shadow: 0 18px 46px rgba(15,23,42,.16), inset 0 1px 0 rgba(255,255,255,.86), inset 0 -1px 0 rgba(255,255,255,.42);
backdrop-filter: blur(26px) saturate(180%);
-webkit-backdrop-filter: blur(26px) saturate(180%);
transition: transform .18s var(--tg-liquid-ease), border-color .18s ease, box-shadow .18s ease, background .18s ease;
}
#${BUTTON_ID}::before {
inset: -1px;
border-radius: 999px;
background:
linear-gradient(120deg, transparent 18%, rgba(255,255,255,.82) 38%, transparent 58%),
radial-gradient(circle at 50% 0%, rgba(255,255,255,.74), transparent 44%);
filter: url(#${SVG_FILTER_ID});
opacity: .48;
transform: translateX(-28%);
transition: transform .22s var(--tg-liquid-ease), opacity .18s ease;
}
#${BUTTON_ID}:hover {
transform: translateY(-2px);
border-color: rgba(255,255,255,.92);
background:
radial-gradient(circle at var(--tg-mouse-x, 28%) var(--tg-mouse-y, 18%), rgba(255,255,255,1), transparent 38%),
linear-gradient(135deg, rgba(255,255,255,.86), rgba(255,255,255,.5)),
rgba(255,255,255,.66);
box-shadow: 0 22px 54px rgba(15,23,42,.2), inset 0 1px 0 rgba(255,255,255,.95);
}
#${BUTTON_ID}:active {
transform: translateY(0) scale(.985);
}
#${BUTTON_ID}:hover::before {
transform: translateX(22%);
opacity: .68;
}
#${BUTTON_ID} .tg-task-button-badge {
background: rgba(239, 68, 68, .88);
color: #fff;
border-color: rgba(255,255,255,.9);
box-shadow: 0 8px 20px rgba(239,68,68,.24), inset 0 1px 0 rgba(255,255,255,.42);
}
#${DRAWER_ID} {
color: #111827;
background:
radial-gradient(circle at var(--tg-mouse-x, 72%) var(--tg-mouse-y, 12%), rgba(255,255,255,.92), transparent 30%),
linear-gradient(180deg, rgba(255,255,255,.68), rgba(255,255,255,.48)),
rgba(255,255,255,.58);
border: 1px solid var(--tg-liquid-border);
box-shadow: var(--tg-liquid-shadow), inset 0 1px 0 rgba(255,255,255,.8), inset 0 -1px 0 rgba(255,255,255,.32);
backdrop-filter: blur(26px) saturate(180%);
-webkit-backdrop-filter: blur(26px) saturate(180%);
transition: transform .28s var(--tg-liquid-ease), opacity .22s ease, box-shadow .22s ease;
}
#${DRAWER_ID}.tg-open {
background:
radial-gradient(circle at var(--tg-mouse-x, 72%) var(--tg-mouse-y, 12%), rgba(255,255,255,.98), transparent 32%),
linear-gradient(180deg, rgba(255,255,255,.72), rgba(255,255,255,.54)),
rgba(255,255,255,.58);
}
#${DRAWER_ID}::before {
background:
linear-gradient(125deg, rgba(255,255,255,.92), transparent 24%, rgba(255,255,255,.22) 58%, transparent 76%),
radial-gradient(circle at var(--tg-mouse-x, 80%) var(--tg-mouse-y, 8%), rgba(255,255,255,.72), transparent 26%);
filter: url(#${SVG_FILTER_ID});
opacity: .48;
mix-blend-mode: screen;
animation: none;
mask-image: none;
}
#${DRAWER_ID}::after {
border-radius: 27px;
background:
linear-gradient(135deg, rgba(255,255,255,.72), transparent 26%),
linear-gradient(315deg, rgba(255,255,255,.32), transparent 34%);
opacity: .58;
mix-blend-mode: screen;
}
#${DRAWER_ID} .tg-header {
border-bottom: 1px solid rgba(15,23,42,.08);
background: rgba(255,255,255,.36);
backdrop-filter: blur(18px) saturate(180%);
-webkit-backdrop-filter: blur(18px) saturate(180%);
}
#${DRAWER_ID} .tg-title-main,
#${DRAWER_ID} .tg-card-title,
#${DRAWER_ID} .tg-course-head,
#${DRAWER_ID} .tg-section-title,
#${DRAWER_ID} .tg-meta-value,
#${DRAWER_ID} .tg-refresh-title {
color: #111827;
text-shadow: none;
}
#${DRAWER_ID} .tg-title-sub,
#${DRAWER_ID} .tg-meta-label,
#${DRAWER_ID} .tg-refresh-stage,
#${DRAWER_ID} .tg-small,
#${DRAWER_ID} .tg-task-meta,
#${DRAWER_ID} .tg-summary-label,
#${DRAWER_ID} .tg-date-filter-hint,
#${DRAWER_ID} .tg-current-filter,
#${DRAWER_ID} .tg-section-count,
#${DRAWER_ID} .tg-course-stat,
#${DRAWER_ID} .tg-subgroup-title,
#${DRAWER_ID} .tg-subgroup-count {
color: var(--tg-muted);
text-shadow: none;
}
#${DRAWER_ID} .tg-meta,
#${DRAWER_ID} .tg-alert,
#${DRAWER_ID} .tg-empty,
#${DRAWER_ID} .tg-date-filter,
#${DRAWER_ID} .tg-summary-item,
#${DRAWER_ID} .tg-refresh-status,
#${DRAWER_ID} .tg-course-group,
#${DRAWER_ID} .tg-course-subgroup,
#${DRAWER_ID} .tg-card {
color: #111827;
border: 1px solid var(--tg-liquid-line);
background: var(--tg-liquid-card);
box-shadow: 0 10px 30px rgba(15,23,42,.08), inset 0 1px 0 rgba(255,255,255,.72);
backdrop-filter: none;
-webkit-backdrop-filter: none;
transition: transform .18s var(--tg-liquid-ease), border-color .18s ease, box-shadow .18s ease, background .18s ease;
}
#${DRAWER_ID} .tg-meta,
#${DRAWER_ID} .tg-alert,
#${DRAWER_ID} .tg-empty,
#${DRAWER_ID} .tg-date-filter,
#${DRAWER_ID} .tg-summary-item,
#${DRAWER_ID} .tg-refresh-status,
#${DRAWER_ID} .tg-course-group {
backdrop-filter: blur(12px) saturate(160%);
-webkit-backdrop-filter: blur(12px) saturate(160%);
}
#${DRAWER_ID} .tg-meta::before,
#${DRAWER_ID} .tg-alert::before,
#${DRAWER_ID} .tg-empty::before,
#${DRAWER_ID} .tg-date-filter::before,
#${DRAWER_ID} .tg-summary-item::before,
#${DRAWER_ID} .tg-card::before {
background: linear-gradient(135deg, rgba(255,255,255,.68), transparent 38%);
opacity: .45;
mix-blend-mode: normal;
}
#${DRAWER_ID} .tg-summary-item:hover,
#${DRAWER_ID} .tg-card:hover,
#${DRAWER_ID} .tg-course-group:hover {
transform: translateY(-2px);
border-color: rgba(15,23,42,.13);
background: var(--tg-liquid-card-hover);
box-shadow: 0 14px 34px rgba(15,23,42,.12), inset 0 1px 0 rgba(255,255,255,.86);
}
#${DRAWER_ID} .tg-summary-item:active,
#${DRAWER_ID} .tg-card:active {
transform: translateY(0) scale(.995);
}
#${DRAWER_ID} .tg-summary-item.tg-selected {
border-color: rgba(8,145,178,.28);
background: rgba(236,254,255,.66);
box-shadow: 0 14px 34px rgba(8,145,178,.12), inset 0 1px 0 rgba(255,255,255,.88);
}
#${DRAWER_ID} .tg-summary-item.tg-selected .tg-summary-num {
color: #075985;
text-shadow: none;
}
#${DRAWER_ID} .tg-icon-btn,
#${DRAWER_ID} .tg-action-btn,
#${DRAWER_ID} .tg-detail-link,
#${DRAWER_ID} .tg-course-toggle,
#${DRAWER_ID} .tg-type,
#${DRAWER_ID} .tg-date-filter select {
position: relative;
overflow: hidden;
color: #111827;
border: 1px solid rgba(15,23,42,.1);
background: rgba(255,255,255,.48);
box-shadow: 0 8px 22px rgba(15,23,42,.07), inset 0 1px 0 rgba(255,255,255,.78);
backdrop-filter: blur(14px) saturate(170%);
-webkit-backdrop-filter: blur(14px) saturate(170%);
transition: transform .18s var(--tg-liquid-ease), border-color .18s ease, background .18s ease, box-shadow .18s ease;
}
#${DRAWER_ID} .tg-icon-btn::before,
#${DRAWER_ID} .tg-action-btn::before,
#${DRAWER_ID} .tg-detail-link::before,
#${DRAWER_ID} .tg-course-toggle::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(120deg, transparent, rgba(255,255,255,.72), transparent);
transform: translateX(-120%);
transition: transform .22s var(--tg-liquid-ease);
}
#${DRAWER_ID} .tg-icon-btn:hover,
#${DRAWER_ID} .tg-action-btn:hover,
#${DRAWER_ID} .tg-detail-link:hover,
#${DRAWER_ID} .tg-course-toggle:hover,
#${DRAWER_ID} .tg-date-filter select:hover,
#${DRAWER_ID} .tg-date-filter select:focus {
transform: translateY(-1px);
border-color: rgba(15,23,42,.16);
background: rgba(255,255,255,.68);
box-shadow: 0 12px 28px rgba(15,23,42,.11), inset 0 1px 0 rgba(255,255,255,.92);
}
#${DRAWER_ID} .tg-icon-btn:hover::before,
#${DRAWER_ID} .tg-action-btn:hover::before,
#${DRAWER_ID} .tg-detail-link:hover::before,
#${DRAWER_ID} .tg-course-toggle:hover::before {
transform: translateX(120%);
}
#${DRAWER_ID} .tg-icon-btn:active,
#${DRAWER_ID} .tg-action-btn:active,
#${DRAWER_ID} .tg-detail-link:active,
#${DRAWER_ID} .tg-course-toggle:active {
transform: translateY(0) scale(.98);
}
#${DRAWER_ID} .tg-liquid-ripple {
position: absolute;
width: 10px;
height: 10px;
border-radius: 999px;
pointer-events: none;
background: rgba(8,145,178,.16);
transform: translate(-50%, -50%) scale(1);
animation: tgLiquidRipple .55s var(--tg-liquid-ease) forwards;
z-index: 0;
}
#${DRAWER_ID} .tg-progress-track {
height: 5px;
border: 1px solid rgba(15,23,42,.08);
background: rgba(255,255,255,.48);
box-shadow: inset 0 1px 2px rgba(15,23,42,.05);
}
#${DRAWER_ID} .tg-progress-fill {
background: linear-gradient(90deg, rgba(8,145,178,.68), rgba(99,102,241,.58), rgba(190,24,93,.42));
box-shadow: 0 0 16px rgba(8,145,178,.16);
filter: url(#${SVG_FILTER_ID});
transition: width .18s ease, opacity .18s ease;
}
#${DRAWER_ID} .tg-summary-item[data-tooltip]::after,
#${DRAWER_ID} .tg-status[data-tooltip]::after {
color: #111827;
background:
linear-gradient(180deg, rgba(255,255,255,.88), rgba(255,255,255,.72)),
rgba(255,255,255,.78);
border: 1px solid rgba(15,23,42,.1);
box-shadow: 0 16px 40px rgba(15,23,42,.14), inset 0 1px 0 rgba(255,255,255,.88);
backdrop-filter: blur(18px) saturate(180%);
-webkit-backdrop-filter: blur(18px) saturate(180%);
text-shadow: none;
}
#${DRAWER_ID} .tg-summary-item[data-tooltip]::before,
#${DRAWER_ID} .tg-status[data-tooltip]::before {
content: "";
position: absolute;
width: 9px;
height: 9px;
background: rgba(255,255,255,.78);
border-right: 1px solid rgba(15,23,42,.08);
border-bottom: 1px solid rgba(15,23,42,.08);
transform: rotate(45deg);
opacity: 0;
pointer-events: none;
transition: opacity .16s ease;
z-index: 7;
}
#${DRAWER_ID} .tg-summary-item[data-tooltip]::before {
left: 50%;
bottom: calc(100% + 5px);
margin-left: -4px;
}
#${DRAWER_ID} .tg-status[data-tooltip]::before {
left: 18px;
bottom: calc(100% + 3px);
}
#${DRAWER_ID} .tg-summary-item[data-tooltip]:hover::before,
#${DRAWER_ID} .tg-status[data-tooltip]:hover::before {
opacity: 1;
}
#${DRAWER_ID} .tg-alert {
color: #7f1d1d;
background: rgba(254,242,242,.72);
border-color: rgba(220,38,38,.14);
}
#${DRAWER_ID} .tg-empty {
color: #374151;
}
#${DRAWER_ID} .tg-task-meta span,
#${DRAWER_ID} .tg-course-stat,
#${DRAWER_ID} .tg-subgroup-count {
border-color: rgba(15,23,42,.06);
background: rgba(255,255,255,.38);
color: rgba(31,41,55,.72);
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
#${DRAWER_ID} .tg-actions {
border-top: 1px solid rgba(15,23,42,.08);
background: rgba(255,255,255,.38);
backdrop-filter: blur(18px) saturate(180%);
-webkit-backdrop-filter: blur(18px) saturate(180%);
}
#${DRAWER_ID} textarea.tg-json-box {
color: #111827;
background: rgba(255,255,255,.72);
border-color: rgba(15,23,42,.1);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
#${BUTTON_ID},
#${DRAWER_ID} {
background: rgba(255,255,255,.94);
}
#${DRAWER_ID} .tg-meta,
#${DRAWER_ID} .tg-alert,
#${DRAWER_ID} .tg-empty,
#${DRAWER_ID} .tg-date-filter,
#${DRAWER_ID} .tg-summary-item,
#${DRAWER_ID} .tg-refresh-status,
#${DRAWER_ID} .tg-course-group,
#${DRAWER_ID} .tg-course-subgroup,
#${DRAWER_ID} .tg-card {
background: rgba(255,255,255,.9);
}
}
@keyframes tgLiquidRipple {
to {
opacity: 0;
transform: translate(-50%, -50%) scale(34);
}
}
/* Readable dark console skin: higher contrast, less white haze */
#${BUTTON_ID}, #${DRAWER_ID} {
--tg-liquid-surface: rgba(10, 14, 24, .78);
--tg-liquid-card: rgba(17, 24, 39, .74);
--tg-liquid-card-hover: rgba(24, 34, 52, .86);
--tg-liquid-border: rgba(125, 211, 252, .24);
--tg-liquid-line: rgba(148, 163, 184, .2);
--tg-liquid-shadow: 0 22px 70px rgba(0, 0, 0, .46);
--tg-text: rgba(248, 250, 252, .96);
--tg-muted: rgba(203, 213, 225, .78);
--tg-faint: rgba(148, 163, 184, .7);
--tg-cyan: #22d3ee;
--tg-violet: #8b5cf6;
--tg-pink: #f472b6;
--tg-red: #fb7185;
--tg-amber: #fbbf24;
--tg-green: #4ade80;
color: var(--tg-text);
}
#${BUTTON_ID} {
border: 1px solid rgba(125, 211, 252, .32);
background:
radial-gradient(circle at var(--tg-mouse-x, 28%) var(--tg-mouse-y, 18%), rgba(34,211,238,.18), transparent 36%),
linear-gradient(135deg, rgba(31,41,55,.92), rgba(8,13,24,.82)),
rgba(10,14,24,.82);
color: rgba(248,250,252,.96);
box-shadow: 0 18px 48px rgba(0,0,0,.42), 0 0 0 1px rgba(255,255,255,.04) inset, inset 0 1px 0 rgba(255,255,255,.16);
backdrop-filter: blur(24px) saturate(170%);
-webkit-backdrop-filter: blur(24px) saturate(170%);
}
#${BUTTON_ID}::before {
background:
linear-gradient(120deg, transparent 18%, rgba(125,211,252,.28) 38%, transparent 58%),
radial-gradient(circle at 50% 0%, rgba(167,139,250,.18), transparent 44%);
opacity: .36;
}
#${BUTTON_ID}:hover {
border-color: rgba(125, 211, 252, .56);
background:
radial-gradient(circle at var(--tg-mouse-x, 28%) var(--tg-mouse-y, 18%), rgba(34,211,238,.26), transparent 38%),
linear-gradient(135deg, rgba(38,50,72,.96), rgba(12,18,31,.9)),
rgba(12,18,31,.9);
box-shadow: 0 24px 60px rgba(0,0,0,.5), 0 0 28px rgba(34,211,238,.14), inset 0 1px 0 rgba(255,255,255,.2);
}
#${DRAWER_ID} {
color: var(--tg-text);
background:
radial-gradient(circle at var(--tg-mouse-x, 72%) var(--tg-mouse-y, 12%), rgba(34,211,238,.13), transparent 30%),
radial-gradient(circle at 100% 0%, rgba(139,92,246,.12), transparent 34%),
linear-gradient(145deg, rgba(17,24,39,.84), rgba(3,7,18,.82)),
rgba(10,14,24,.78);
border: 1px solid rgba(125, 211, 252, .26);
box-shadow: var(--tg-liquid-shadow), 0 0 42px rgba(34,211,238,.08), 0 0 74px rgba(139,92,246,.07), inset 0 1px 0 rgba(255,255,255,.12);
backdrop-filter: blur(24px) saturate(170%);
-webkit-backdrop-filter: blur(24px) saturate(170%);
}
#${DRAWER_ID}.tg-open {
background:
radial-gradient(circle at var(--tg-mouse-x, 72%) var(--tg-mouse-y, 12%), rgba(34,211,238,.15), transparent 32%),
radial-gradient(circle at 100% 0%, rgba(139,92,246,.14), transparent 35%),
linear-gradient(145deg, rgba(17,24,39,.88), rgba(3,7,18,.86)),
rgba(10,14,24,.84);
}
#${DRAWER_ID}::before {
background:
linear-gradient(125deg, rgba(255,255,255,.14), transparent 24%, rgba(34,211,238,.08) 58%, transparent 76%),
radial-gradient(circle at var(--tg-mouse-x, 80%) var(--tg-mouse-y, 8%), rgba(125,211,252,.16), transparent 26%);
opacity: .42;
mix-blend-mode: screen;
}
#${DRAWER_ID}::after {
background:
linear-gradient(135deg, rgba(255,255,255,.13), transparent 28%),
linear-gradient(315deg, rgba(34,211,238,.08), transparent 36%);
opacity: .42;
}
#${DRAWER_ID} .tg-header {
border-bottom: 1px solid rgba(148, 163, 184, .18);
background: rgba(15, 23, 42, .58);
backdrop-filter: blur(18px) saturate(160%);
-webkit-backdrop-filter: blur(18px) saturate(160%);
}
#${DRAWER_ID} .tg-title-main,
#${DRAWER_ID} .tg-card-title,
#${DRAWER_ID} .tg-course-head,
#${DRAWER_ID} .tg-section-title,
#${DRAWER_ID} .tg-meta-value,
#${DRAWER_ID} .tg-refresh-title,
#${DRAWER_ID} .tg-summary-num {
color: rgba(248,250,252,.98);
text-shadow: none;
}
#${DRAWER_ID} .tg-title-sub,
#${DRAWER_ID} .tg-meta-label,
#${DRAWER_ID} .tg-refresh-stage,
#${DRAWER_ID} .tg-small,
#${DRAWER_ID} .tg-task-meta,
#${DRAWER_ID} .tg-summary-label,
#${DRAWER_ID} .tg-date-filter-hint,
#${DRAWER_ID} .tg-current-filter,
#${DRAWER_ID} .tg-section-count,
#${DRAWER_ID} .tg-course-stat,
#${DRAWER_ID} .tg-subgroup-title,
#${DRAWER_ID} .tg-subgroup-count {
color: var(--tg-muted);
text-shadow: none;
}
#${DRAWER_ID} .tg-current-filter strong,
#${DRAWER_ID} .tg-meta-kicker {
color: #67e8f9;
}
#${DRAWER_ID} .tg-meta,
#${DRAWER_ID} .tg-alert,
#${DRAWER_ID} .tg-empty,
#${DRAWER_ID} .tg-date-filter,
#${DRAWER_ID} .tg-summary-item,
#${DRAWER_ID} .tg-refresh-status,
#${DRAWER_ID} .tg-course-group,
#${DRAWER_ID} .tg-course-subgroup,
#${DRAWER_ID} .tg-card {
color: var(--tg-text);
border: 1px solid var(--tg-liquid-line);
background:
linear-gradient(180deg, rgba(30,41,59,.72), rgba(15,23,42,.66)),
rgba(15, 23, 42, .72);
box-shadow: 0 12px 34px rgba(0,0,0,.28), inset 0 1px 0 rgba(255,255,255,.08);
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
#${DRAWER_ID} .tg-meta,
#${DRAWER_ID} .tg-alert,
#${DRAWER_ID} .tg-empty,
#${DRAWER_ID} .tg-date-filter,
#${DRAWER_ID} .tg-summary-item,
#${DRAWER_ID} .tg-refresh-status,
#${DRAWER_ID} .tg-course-group {
backdrop-filter: blur(10px) saturate(145%);
-webkit-backdrop-filter: blur(10px) saturate(145%);
}
#${DRAWER_ID} .tg-meta::before,
#${DRAWER_ID} .tg-alert::before,
#${DRAWER_ID} .tg-empty::before,
#${DRAWER_ID} .tg-date-filter::before,
#${DRAWER_ID} .tg-summary-item::before,
#${DRAWER_ID} .tg-card::before {
background: linear-gradient(135deg, rgba(255,255,255,.1), transparent 38%);
opacity: .32;
}
#${DRAWER_ID} .tg-summary-item:hover,
#${DRAWER_ID} .tg-card:hover,
#${DRAWER_ID} .tg-course-group:hover {
border-color: rgba(125,211,252,.34);
background:
linear-gradient(180deg, rgba(38,50,72,.84), rgba(17,24,39,.78)),
rgba(17,24,39,.82);
box-shadow: 0 16px 42px rgba(0,0,0,.36), 0 0 26px rgba(34,211,238,.08), inset 0 1px 0 rgba(255,255,255,.12);
}
#${DRAWER_ID} .tg-summary-item.tg-selected {
border-color: rgba(34,211,238,.5);
background:
linear-gradient(180deg, rgba(14,116,144,.32), rgba(17,24,39,.82)),
rgba(17,24,39,.86);
box-shadow: 0 16px 42px rgba(0,0,0,.36), 0 0 28px rgba(34,211,238,.16), inset 0 1px 0 rgba(255,255,255,.14);
}
#${DRAWER_ID} .tg-summary-item.tg-selected .tg-summary-num {
color: #67e8f9;
}
#${DRAWER_ID} .tg-icon-btn,
#${DRAWER_ID} .tg-action-btn,
#${DRAWER_ID} .tg-detail-link,
#${DRAWER_ID} .tg-course-toggle,
#${DRAWER_ID} .tg-type,
#${DRAWER_ID} .tg-date-filter select {
color: rgba(248,250,252,.94);
border: 1px solid rgba(148,163,184,.22);
background: rgba(15,23,42,.7);
box-shadow: 0 8px 22px rgba(0,0,0,.24), inset 0 1px 0 rgba(255,255,255,.08);
backdrop-filter: blur(12px) saturate(145%);
-webkit-backdrop-filter: blur(12px) saturate(145%);
}
#${DRAWER_ID} .tg-icon-btn::before,
#${DRAWER_ID} .tg-action-btn::before,
#${DRAWER_ID} .tg-detail-link::before,
#${DRAWER_ID} .tg-course-toggle::before {
background: linear-gradient(120deg, transparent, rgba(125,211,252,.2), transparent);
}
#${DRAWER_ID} .tg-icon-btn:hover,
#${DRAWER_ID} .tg-action-btn:hover,
#${DRAWER_ID} .tg-detail-link:hover,
#${DRAWER_ID} .tg-course-toggle:hover,
#${DRAWER_ID} .tg-date-filter select:hover,
#${DRAWER_ID} .tg-date-filter select:focus {
border-color: rgba(125,211,252,.42);
background: rgba(30,41,59,.82);
box-shadow: 0 12px 30px rgba(0,0,0,.32), 0 0 22px rgba(34,211,238,.08), inset 0 1px 0 rgba(255,255,255,.12);
}
#${DRAWER_ID} .tg-date-filter select option {
background: #0f172a;
color: rgba(248,250,252,.94);
}
#${DRAWER_ID} [data-copy-all-json] {
border-color: rgba(34,211,238,.36);
background: rgba(8,47,73,.72);
color: rgba(236,254,255,.98);
}
#${DRAWER_ID} [data-delete-cache] {
border-color: rgba(251,113,133,.36);
background: rgba(76, 29, 38, .72);
color: rgba(255,228,230,.96);
}
#${DRAWER_ID} .tg-task-meta span,
#${DRAWER_ID} .tg-course-stat,
#${DRAWER_ID} .tg-subgroup-count {
border-color: rgba(148,163,184,.14);
background: rgba(15,23,42,.48);
color: rgba(203,213,225,.82);
}
#${DRAWER_ID} .tg-progress-track {
border: 1px solid rgba(148,163,184,.18);
background: rgba(15,23,42,.72);
}
#${DRAWER_ID} .tg-progress-fill {
background: linear-gradient(90deg, rgba(34,211,238,.9), rgba(99,102,241,.78), rgba(139,92,246,.68));
box-shadow: 0 0 18px rgba(34,211,238,.18);
}
#${DRAWER_ID} .tg-summary-item[data-tooltip]::after,
#${DRAWER_ID} .tg-status[data-tooltip]::after {
color: rgba(248,250,252,.96);
background:
linear-gradient(180deg, rgba(30,41,59,.94), rgba(15,23,42,.92)),
rgba(15,23,42,.92);
border: 1px solid rgba(125,211,252,.22);
box-shadow: 0 16px 40px rgba(0,0,0,.34), inset 0 1px 0 rgba(255,255,255,.1);
}
#${DRAWER_ID} .tg-summary-item[data-tooltip]::before,
#${DRAWER_ID} .tg-status[data-tooltip]::before {
background: rgba(30,41,59,.94);
border-right: 1px solid rgba(125,211,252,.18);
border-bottom: 1px solid rgba(125,211,252,.18);
}
#${DRAWER_ID} .tg-alert {
color: #fecdd3;
background:
linear-gradient(180deg, rgba(127,29,29,.42), rgba(15,23,42,.72)),
rgba(76,29,38,.66);
border-color: rgba(251,113,133,.25);
}
#${DRAWER_ID} .tg-empty {
color: rgba(203,213,225,.84);
}
#${DRAWER_ID} .tg-actions {
border-top: 1px solid rgba(148,163,184,.18);
background: rgba(3,7,18,.72);
backdrop-filter: blur(18px) saturate(150%);
-webkit-backdrop-filter: blur(18px) saturate(150%);
}
#${DRAWER_ID} textarea.tg-json-box {
color: rgba(248,250,252,.94);
background: rgba(2,6,23,.82);
border-color: rgba(148,163,184,.2);
}
#${DRAWER_ID} .tg-ok { color: #4ade80; }
#${DRAWER_ID} .tg-warn { color: #fbbf24; }
#${DRAWER_ID} .tg-bad { color: #fb7185; }
#${DRAWER_ID} .tg-muted { color: rgba(203,213,225,.78); }
#${DRAWER_ID} .tg-state-ok {
border-color: rgba(74,222,128,.22);
}
#${DRAWER_ID} .tg-state-warn {
border-color: rgba(251,191,36,.28);
}
#${DRAWER_ID} .tg-state-bad {
border-color: rgba(251,113,133,.34);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
#${BUTTON_ID},
#${DRAWER_ID} {
background: rgba(15,23,42,.96);
}
#${DRAWER_ID} .tg-meta,
#${DRAWER_ID} .tg-alert,
#${DRAWER_ID} .tg-empty,
#${DRAWER_ID} .tg-date-filter,
#${DRAWER_ID} .tg-summary-item,
#${DRAWER_ID} .tg-refresh-status,
#${DRAWER_ID} .tg-course-group,
#${DRAWER_ID} .tg-course-subgroup,
#${DRAWER_ID} .tg-card {
background: rgba(17,24,39,.96);
}
}
@media (max-width: 520px) {
#${BUTTON_ID} {
right: 16px;
bottom: 18px;
}
#${DRAWER_ID} {
right: 8px;
top: 8px;
bottom: 8px;
width: calc(100vw - 16px);
border-radius: 24px;
}
#${DRAWER_ID} .tg-summary {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
#${DRAWER_ID} .tg-meta-grid,
#${DRAWER_ID} .tg-task-meta {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
#${DRAWER_ID} .tg-date-filter-row {
flex-wrap: wrap;
}
}
`;
if (typeof GM_addStyle === "function") {
GM_addStyle(css);
} else {
const style = document.createElement("style");
style.textContent = css;
document.head.appendChild(style);
}
async function getValue(key, defaultValue) {
if (typeof GM !== "undefined" && GM.getValue) {
return await GM.getValue(key, defaultValue);
}
return GM_getValue(key, defaultValue);
}
async function setValue(key, value) {
if (typeof GM !== "undefined" && GM.setValue) {
await GM.setValue(key, value);
return;
}
GM_setValue(key, value);
}
async function deleteValue(key) {
if (typeof GM !== "undefined" && GM.deleteValue) {
await GM.deleteValue(key);
return;
}
GM_deleteValue(key);
}
function extractLoginFromText(text) {
if (!text) return "";
const patterns = [
/user_\d+/i,
/"login"\s*:\s*"([^"]+)"/i,
/"username"\s*:\s*"([^"]+)"/i,
/zzud=([^&"'\s]+)/i,
/username=([^&"'\s]+)/i
];
for (const pattern of patterns) {
const match = String(text).match(pattern);
if (match) {
const value = match[1] || match[0];
if (value && /^user_\d+$/i.test(value)) {
return value;
}
}
}
return "";
}
function detectLoginFromPage() {
const manualLogin = String(MANUAL_LOGIN || "").trim();
if (manualLogin) return manualLogin;
const candidates = [];
candidates.push(location.href);
if (document && document.documentElement) {
candidates.push(document.documentElement.innerHTML);
}
try {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
candidates.push(key);
candidates.push(localStorage.getItem(key));
}
} catch (e) {}
try {
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
candidates.push(key);
candidates.push(sessionStorage.getItem(key));
}
} catch (e) {}
for (const text of candidates) {
const login = extractLoginFromText(text);
if (login) return login;
}
return "";
}
function saveAutoLoginIfDetected() {
if (!isTgHost()) {
return "";
}
const login = detectLoginFromPage();
if (login) {
GM_setValue(STORE_KEY_AUTO_LOGIN, login);
return login;
}
return "";
}
function escapeHtml(text) {
return String(text ?? "")
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
}
function cleanStatusMessage(message) {
return String(message || "").replace(/^\[DEBUG\]\s*/, "");
}
function isTgHost(hostname = location.hostname) {
return hostname === "tg.zcst.edu.cn" || hostname === TG_INTRANET_HOST;
}
function getTgOrigin(hostname = location.hostname) {
return hostname === TG_INTRANET_HOST ? TG_INTRANET_ORIGIN : TG_EXTERNAL_ORIGIN;
}
function getCurrentPageSite() {
if (location.hostname === "www.educoder.net") {
return {
siteKey: "educoder",
siteName: "Educoder主站",
pageOrigin: "https://www.educoder.net",
apiOrigin: "https://data.educoder.net",
resultKey: STORE_KEY_EDUCODER_RESULT,
login: "pchtkff9y",
courseId: 109348,
courseIdentifier: "MOAPGNLO",
courseName: "操作系统2026",
viewHint: "当前为 Educoder 主站视图,仅展示本平台任务。"
};
}
return {
siteKey: "tg-zcst",
backendSiteKey: "tg",
siteName: "TG校内站",
pageOrigin: getTgOrigin(),
apiOrigin: getTgOrigin(),
resultKey: STORE_KEY_TG_RESULT,
login: "",
viewHint: "当前为 TG 校内站视图,仅展示本平台任务。"
};
}
function getCurrentBackendSiteKey() {
const site = getCurrentPageSite();
return site.backendSiteKey || site.siteKey;
}
function isTaskForCurrentSite(task) {
const current = getCurrentPageSite();
return getTaskSiteKey(task) === current.siteKey || String(task?.siteKey || "") === getCurrentBackendSiteKey();
}
function isValueForCurrentSite(value) {
const current = getCurrentPageSite();
if (!value?.siteKey) return current.siteKey === "tg-zcst";
return value.siteKey === getCurrentBackendSiteKey() || value.siteKey === current.siteKey || (value.siteKey === "tg" && current.siteKey === "tg-zcst");
}
function filterResultForCurrentSite(result) {
if (!result || typeof result !== "object") return result;
const currentSite = getCurrentPageSite();
const backendSiteKey = getCurrentBackendSiteKey();
const matchesCurrentSite = item => {
const key = String(item?.siteKey || item?.key || "").trim();
return key === backendSiteKey || key === currentSite.siteKey || (key === "tg" && currentSite.siteKey === "tg-zcst");
};
const courses = Array.isArray(result.courses)
? result.courses.filter(course => {
const key = String(course?.siteKey || "").trim();
return key === backendSiteKey || (key === "tg" && currentSite.siteKey === "tg-zcst");
})
: [];
const tasks = Array.isArray(result.tasks) ? result.tasks.filter(isTaskForCurrentSite) : [];
const debug = result.debug && typeof result.debug === "object"
? {
...result.debug,
siteLogin: Array.isArray(result.debug.siteLogin) ? result.debug.siteLogin.filter(matchesCurrentSite) : result.debug.siteLogin,
siteScan: Array.isArray(result.debug.siteScan) ? result.debug.siteScan.filter(matchesCurrentSite) : result.debug.siteScan,
requestLog: Array.isArray(result.debug.requestLog) ? result.debug.requestLog.filter(matchesCurrentSite) : result.debug.requestLog,
courseScanFlow: Array.isArray(result.debug.courseScanFlow) ? result.debug.courseScanFlow.filter(matchesCurrentSite) : result.debug.courseScanFlow
}
: result.debug;
return {
...result,
courses,
tasks,
debug,
summary: {
...(result.summary || {}),
courseCount: courses.length,
taskCount: tasks.length
}
};
}
function isResultForCurrentSite(result) {
if (!result || typeof result !== "object") return false;
const current = getCurrentPageSite();
const backendSiteKey = getCurrentBackendSiteKey();
const keys = [
result.siteKey,
result.currentSite?.key,
...(Array.isArray(result.sites) ? result.sites.map(site => site?.key) : []),
...(Array.isArray(result.courses) ? result.courses.map(course => course?.siteKey) : []),
...(Array.isArray(result.tasks) ? result.tasks.map(task => task?.siteKey) : [])
].filter(Boolean).map(value => String(value).trim());
if (!keys.length) {
return current.siteKey === "tg-zcst";
}
return keys.some(key =>
key === current.siteKey ||
key === backendSiteKey ||
(key === "tg" && current.siteKey === "tg-zcst")
);
}
function getTaskSiteKey(task) {
const raw = String(task?.siteKey || "").trim();
if (!raw || raw === "tg") return "tg-zcst";
return raw;
}
function getTaskSiteName(task) {
return String(task?.siteName || (getTaskSiteKey(task) === "educoder" ? "Educoder主站" : "TG校内站")).trim();
}
function getCourseKey(task) {
const siteKey = getTaskSiteKey(task);
const courseId = task?.courseId || task?.courseIdentifier || task?.courseName || "unknown";
return `${siteKey}:${courseId}`;
}
function formatDateTime(value) {
if (!value) return "--";
try {
const normalized = String(value).replace(/-/g, "/");
const d = new Date(normalized);
if (Number.isNaN(d.getTime())) return String(value);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const h = String(d.getHours()).padStart(2, "0");
const min = String(d.getMinutes()).padStart(2, "0");
return `${y}-${m}-${day} ${h}:${min}`;
} catch (e) {
return String(value);
}
}
function normalizeTasks(data) {
if (Array.isArray(data?.tasks)) return data.tasks;
const tasks = [];
for (const course of data?.courses || []) {
for (const exam of course.exams || []) tasks.push({ ...exam, taskType: "exercise", taskTypeText: "考试/小测试" });
for (const homework of course.homeworks || []) tasks.push({ ...homework, taskType: "common_homework", taskTypeText: "图文作业" });
for (const experiment of course.experiments || []) tasks.push({ ...experiment, taskType: "classroom_experiment", taskTypeText: "课堂实验" });
}
return tasks;
}
function getTaskDate(task) {
const candidates = [
task?.endTime,
task?.publishTime,
task?.scanTime,
task?.startAt,
task?.endAt
];
for (const v of candidates) {
if (!v) continue;
const d = new Date(String(v).replace(/-/g, "/"));
if (!Number.isNaN(d.getTime())) return d;
}
return null;
}
function getTaskEndDate(task) {
const candidates = [
task?.endTime,
task?.endAt
];
for (const v of candidates) {
if (!v) continue;
const d = new Date(String(v).replace(/-/g, "/"));
if (!Number.isNaN(d.getTime())) return d;
}
return null;
}
function parseChineseRemainingToMs(text) {
if (!text) return null;
const str = String(text);
let ms = 0;
const day = str.match(/(\d+)\s*天/);
const hour = str.match(/(\d+)\s*小时/);
const minute = str.match(/(\d+)\s*分/);
const second = str.match(/(\d+)\s*秒/);
if (day) ms += Number(day[1]) * 24 * 60 * 60 * 1000;
if (hour) ms += Number(hour[1]) * 60 * 60 * 1000;
if (minute) ms += Number(minute[1]) * 60 * 1000;
if (second) ms += Number(second[1]) * 1000;
return ms || null;
}
function getTaskDeadlineDate(task) {
const timestamp = Number(task?.deadlineTimestamp ?? task?.raw?.deadlineTimestamp);
if (Number.isFinite(timestamp) && timestamp > 0) {
const normalizedTimestamp = timestamp < 10000000000 ? timestamp * 1000 : timestamp;
const d = new Date(normalizedTimestamp);
if (!Number.isNaN(d.getTime())) return d;
}
const raw = task?.raw || {};
const candidates = [
task?.endTime,
task?.endAt,
task?.deadline,
task?.deadlineTime,
task?.closeTime,
task?.end_time,
task?.end_time_s,
task?.deadline_time,
task?.close_time,
raw.end_time,
raw.end_time_s,
raw.endAt,
raw.deadline,
raw.deadlineTime,
raw.deadline_time,
raw.closeTime,
raw.close_time,
raw["截止时间"],
raw.exam?.end_time,
raw.exam?.end_time_s,
raw.exam?.deadline,
raw.exam?.deadlineTime,
raw.exam?.closeTime
];
for (const value of candidates) {
if (!value) continue;
const d = new Date(String(value).replace(/-/g, "/"));
if (!Number.isNaN(d.getTime())) return d;
}
return null;
}
function getTaskRemainingMs(task, now = new Date()) {
const remainingMs = Number(task?.remainingMs ?? task?.raw?.remainingMs);
if (Number.isFinite(remainingMs)) return remainingMs;
const deadline = getTaskDeadlineDate(task);
if (deadline) return deadline.getTime() - now.getTime();
const relativeCandidates = [
task?.deadlineRemaining,
task?.deadline,
task?.deadlineTime,
task?.closeTime,
task?.exerciseLeftTime,
task?.exercise_left_time,
task?.remainingTime,
task?.remainTime,
task?.status_time,
task?.raw?.deadlineRemaining,
task?.raw?.deadline,
task?.raw?.deadlineTime,
task?.raw?.deadline_time,
task?.raw?.closeTime,
task?.raw?.close_time,
task?.raw?.["截止时间"],
task?.raw?.exercise_left_time,
task?.raw?.status_time,
task?.raw?.exam?.exercise_left_time,
task?.raw?.exam?.deadline,
task?.raw?.exam?.deadlineTime,
task?.raw?.exam?.closeTime
];
for (const value of relativeCandidates) {
const ms = parseChineseRemainingToMs(value);
if (ms != null) return ms;
}
return null;
}
function parseTaskDeadline(task) {
if (task?.endTime) {
const d = new Date(String(task.endTime).replace(/-/g, "/"));
if (!Number.isNaN(d.getTime())) return d;
}
if (task?.deadlineAt) {
const d = new Date(String(task.deadlineAt).replace(/-/g, "/"));
if (!Number.isNaN(d.getTime())) return d;
}
if (task?.deadlineRemaining && task?.scanTime) {
const base = new Date(String(task.scanTime).replace(/-/g, "/"));
const ms = parseChineseRemainingToMs(task.deadlineRemaining);
if (!Number.isNaN(base.getTime()) && ms != null) {
return new Date(base.getTime() + ms);
}
}
return null;
}
function parseCountdownDeadline(value) {
if (!value) return null;
const d = new Date(String(value).replace(/-/g, "/"));
if (Number.isNaN(d.getTime())) return null;
return d;
}
function formatCountdown(deadlineValue) {
const deadline = deadlineValue instanceof Date ? deadlineValue : parseCountdownDeadline(deadlineValue);
if (!deadline) return "无截止时间";
const diff = deadline.getTime() - Date.now();
if (diff <= 0) return "已截止";
const totalSeconds = Math.floor(diff / 1000);
const totalMinutes = Math.floor(totalSeconds / 60);
const totalHours = Math.floor(totalSeconds / 3600);
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (diff >= 3 * 86400000) {
return `剩余 ${days}天 ${hours}小时`;
}
if (diff >= 86400000) {
return `仅剩 ${days}天 ${hours}小时`;
}
if (diff >= 3600000) {
return `仅剩 ${totalHours}小时 ${minutes}分`;
}
return `仅剩 ${totalMinutes}分 ${seconds}秒`;
}
function updateCountdowns() {
document.querySelectorAll(".tg-countdown[data-deadline]").forEach(el => {
const deadline = el.dataset.deadline || "";
el.textContent = formatCountdown(deadline);
});
}
function updateCountdownsOnly() {
updateCountdowns();
}
function ensureCountdownTimer() {
if (countdownIntervalId) return;
countdownIntervalId = setInterval(updateCountdownsOnly, 1000);
}
function stopCountdownTimer() {
if (!countdownIntervalId) return;
clearInterval(countdownIntervalId);
countdownIntervalId = null;
}
function getFilterStartDate(year, month) {
return new Date(year, month - 1, 1, 0, 0, 0);
}
function filterTasksByYearMonth(tasks, year, month) {
const startDate = getFilterStartDate(year, month);
return tasks.filter(task => {
const d = getTaskDate(task);
if (!d) return true;
return d >= startDate;
});
}
function getDefaultFilterYearMonth() {
const d = new Date();
d.setDate(1);
d.setHours(0, 0, 0, 0);
d.setMonth(d.getMonth() - 5);
return {
year: d.getFullYear(),
month: d.getMonth() + 1
};
}
async function loadFilterYearMonth() {
const saved = await getValue(STORE_KEY_FILTER_YEAR_MONTH, null);
const year = Number(saved?.year);
const month = Number(saved?.month);
if (year && month >= 1 && month <= 12) {
return { year, month };
}
return getDefaultFilterYearMonth();
}
async function loadCourseCollapseState() {
const saved = await getValue(STORE_KEY_COURSE_COLLAPSE, {});
return saved && typeof saved === "object" ? saved : {};
}
async function loadSectionCollapseState() {
const saved = await getValue(STORE_KEY_SECTION_COLLAPSE, {});
return saved && typeof saved === "object" ? saved : {};
}
async function loadIgnoredTaskMap() {
const saved = await getValue(STORE_KEY_IGNORED_TASKS, {});
return saved && typeof saved === "object" ? saved : {};
}
async function saveIgnoredTaskMap(map) {
const nextMap = map && typeof map === "object" ? map : {};
await setValue(STORE_KEY_IGNORED_TASKS, nextMap);
}
async function loadAutoRefreshDoneAt() {
return Number(await getValue(STORE_KEY_AUTO_REFRESH_DONE_AT, 0)) || 0;
}
async function markAutoRefreshDoneAt(timestamp = Date.now()) {
await setValue(STORE_KEY_AUTO_REFRESH_DONE_AT, Number(timestamp) || Date.now());
}
async function clearAutoRefreshDoneAt() {
await deleteValue(STORE_KEY_AUTO_REFRESH_DONE_AT);
}
async function isRefreshBusy() {
const refreshStatus = await getValue(STORE_KEY_REFRESH_STATUS, null);
if (refreshStatus?.status === "running" || refreshStatus?.status === "waiting") {
return true;
}
const runningState = await loadRunningState();
return runningState?.running === true;
}
function getTaskStableId(task) {
const raw = task?.raw || {};
const rawExam = raw?.exam || {};
const candidates = [
task?.exerciseId,
task?.examId,
task?.homeworkId,
task?.homework_id,
task?.shixunId,
task?.shixun_id,
task?.taskId,
task?.id,
task?.categoryId,
rawExam?.id,
raw?.exerciseId,
raw?.homework_id,
raw?.id,
raw?.category_id
];
for (const value of candidates) {
if (value === undefined || value === null || value === "") continue;
return String(value).trim();
}
return "";
}
function getTaskDeadlineLabel(task) {
const deadlineDate = getTaskDeadlineDate(task) || parseTaskDeadline(task);
const fallback = task?.deadline || task?.endTime || task?.endAt || task?.end_at || task?.dangerDeadline || task?.deadlineTime || "";
if (deadlineDate) {
return formatDateTime(deadlineDate);
}
return fallback ? String(fallback) : "无截止时间";
}
function getTaskKey(task) {
if (!hasLoggedTaskKeySample && task) {
console.log("[TG任务key检查] task=", task);
hasLoggedTaskKeySample = true;
}
const sourceKey = String(task?.siteKey || task?.source || task?.apiSource || "unknown").trim();
const courseKey = String(task?.courseIdentifier || task?.courseId || task?.courseName || "unknown").trim();
const taskType = String(task?.taskType || task?.type || task?.taskTypeText || "task").trim();
const stableId = getTaskStableId(task);
if (stableId) {
return `${sourceKey}|${courseKey}|${taskType}|${stableId}`;
}
const fallbackTitle = getTaskTitle(task);
const fallbackDeadline = getTaskDeadlineLabel(task);
return `${sourceKey}|${courseKey}|${taskType}|${fallbackTitle}|${fallbackDeadline}`;
}
function createIgnoredTaskSnapshot(task, taskKey = getTaskKey(task)) {
return {
key: taskKey,
title: getTaskTitle(task),
courseName: task?.courseName || "",
courseId: task?.courseId || "",
courseIdentifier: task?.courseIdentifier || "",
taskType: task?.taskType || task?.type || "",
taskTypeText: task?.taskTypeText || "",
source: task?.source || task?.apiSource || "",
siteKey: getTaskSiteKey(task),
siteName: getTaskSiteName(task),
deadline: getTaskDeadlineLabel(task),
detailUrl: task?.detailUrl || task?.url || "",
ignoredAt: new Date().toLocaleString()
};
}
async function ignoreTask(task) {
const taskKey = getTaskKey(task);
console.log("[TG忽略机制] taskKey=", taskKey, task?.title || task?.name);
const ignoredTaskMap = await loadIgnoredTaskMap();
ignoredTaskMap[taskKey] = createIgnoredTaskSnapshot(task, taskKey);
await saveIgnoredTaskMap(ignoredTaskMap);
console.log("[TG忽略机制] ignoredTaskMap=", ignoredTaskMap);
return ignoredTaskMap;
}
async function restoreTask(taskKey) {
const ignoredTaskMap = await loadIgnoredTaskMap();
delete ignoredTaskMap[taskKey];
await saveIgnoredTaskMap(ignoredTaskMap);
console.log("[TG忽略机制] ignoredTaskMap=", ignoredTaskMap);
return ignoredTaskMap;
}
function isDangerTask(task) {
if (task?.completed || task?.submitted) return false;
if (task?.ended) return false;
const remainingMs = getTaskRemainingMs(task);
if (remainingMs !== null) {
if (remainingMs <= 0) return false;
if (remainingMs <= DANGER_DAYS_THRESHOLD * 86400000) return true;
}
return false;
}
function taskPriority(task) {
if (isDangerTask(task)) return 1;
if (!task.completed && task.ended) return 2;
if (!task.completed) return 3;
return 5;
}
function sortTasks(tasks) {
return [...tasks].sort((a, b) => taskPriority(a) - taskPriority(b));
}
function calculateSummaryFromTasks(tasks) {
const siteCount = new Set(tasks.map(task => getTaskSiteKey(task)).filter(Boolean)).size;
const courseCount = new Set(tasks.map(task => getCourseKey(task)).filter(Boolean)).size;
const exerciseCount = tasks.filter(task => task.taskType === "exercise").length;
const homeworkCount = tasks.filter(task => task.taskType === "common_homework").length;
const experimentCount = tasks.filter(task => task.taskType === "classroom_experiment").length;
const unfinishedCount = tasks.filter(task => !task.completed).length;
const dangerCount = tasks.filter(task => !task.completed && isDangerTask(task)).length;
return {
siteCount,
courseCount,
taskCount: tasks.length,
exerciseCount,
homeworkCount,
experimentCount,
unfinishedCount,
dangerCount
};
}
function statusTone(task) {
const text = task.statusText || "";
if (task.completed) return { card: "tg-state-ok", text: "tg-ok" };
if (task.ended || text.includes("已截止")) return { card: "tg-state-bad", text: "tg-bad" };
if (isDangerTask(task) || text.includes("快截止") || text.includes("进行中")) return { card: "tg-state-warn", text: "tg-warn" };
return { card: "tg-state-muted", text: "tg-muted" };
}
function currentFilter() {
const filter = localStorage.getItem(FILTER_KEY) || "all";
if (filter === "site") return "all";
if (filter === "homework") return "common_homework";
if (filter === "experiment") return "classroom_experiment";
return filter;
}
function setCurrentFilter(filter) {
localStorage.setItem(FILTER_KEY, filter);
}
function isOpen() {
return localStorage.getItem(OPEN_KEY) === "1";
}
function setOpen(value) {
localStorage.setItem(OPEN_KEY, value ? "1" : "0");
}
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function getDefaultPanelState() {
return {
buttonX: Math.max(18, window.innerWidth - 84),
buttonY: Math.max(18, window.innerHeight - 86),
panelWidth: 520,
panelHeight: Math.min(760, Math.max(480, window.innerHeight - 48))
};
}
async function loadPanelState() {
const saved = await getValue(STORE_KEY_PANEL_STATE, {});
return {
...getDefaultPanelState(),
...(saved && typeof saved === "object" ? saved : {})
};
}
async function savePanelStatePatch(patch) {
const current = await loadPanelState();
await setValue(STORE_KEY_PANEL_STATE, { ...current, ...patch });
}
async function applyPanelState(panelState) {
const button = document.getElementById(BUTTON_ID);
const drawer = document.getElementById(DRAWER_ID);
const state = panelState || await loadPanelState();
if (button) {
const x = clamp(Number(state.buttonX) || 22, -42, window.innerWidth - 20);
const y = clamp(Number(state.buttonY) || 24, -42, window.innerHeight - 20);
button.style.left = `${x}px`;
button.style.top = `${y}px`;
button.style.right = "auto";
button.style.bottom = "auto";
}
if (drawer) {
const maxWidth = Math.min(760, window.innerWidth - 40);
const maxHeight = window.innerHeight - 40;
const width = clamp(Number(state.panelWidth) || 520, 390, maxWidth);
const height = clamp(Number(state.panelHeight) || Math.min(760, window.innerHeight - 48), 480, maxHeight);
drawer.style.width = `${width}px`;
drawer.style.height = `${height}px`;
drawer.style.maxWidth = `${maxWidth}px`;
drawer.style.maxHeight = `${maxHeight}px`;
drawer.style.right = "18px";
drawer.style.top = "18px";
}
}
async function loadData() {
const site = getCurrentPageSite();
const currentResult = await getValue(site.resultKey, null);
if (currentResult) {
if (site.siteKey === "educoder" && currentResult.source !== "educoder-page-frontend") {
return null;
}
return filterResultForCurrentSite(currentResult);
}
if (site.siteKey === "educoder") {
return null;
}
const fallback = await getValue(STORE_KEY, null);
return filterResultForCurrentSite(fallback);
}
async function loadRunningState() {
return await getValue(STORE_KEY_LAST_RUNNING, null);
}
async function loadLastError() {
return await getValue(STORE_KEY_LAST_ERROR, null);
}
async function loadRefreshRequest() {
return await getValue(STORE_KEY_REFRESH_REQUEST, null);
}
async function loadRefreshHandled() {
return await getValue(STORE_KEY_REFRESH_HANDLED, 0);
}
function normalizeFilterYearMonth(saved) {
const year = Number(saved?.year);
const month = Number(saved?.month);
if (year && month >= 1 && month <= 12) {
return { year, month };
}
return getDefaultFilterYearMonth();
}
function normalizeObject(value, fallback = {}) {
return value && typeof value === "object" ? value : fallback;
}
async function loadFrontendState() {
const [
result,
lastRunning,
lastError,
filterYearMonthRaw,
panelStateRaw,
courseCollapse,
sectionCollapse,
autoLogin,
refreshRequest,
refreshHandled,
refreshStatus,
ignoredTaskMap
] = await Promise.all([
loadData(),
getValue(STORE_KEY_LAST_RUNNING, null),
getValue(STORE_KEY_LAST_ERROR, null),
getValue(STORE_KEY_FILTER_YEAR_MONTH, null),
getValue(STORE_KEY_PANEL_STATE, null),
getValue(STORE_KEY_COURSE_COLLAPSE, {}),
getValue(STORE_KEY_SECTION_COLLAPSE, {}),
getValue(STORE_KEY_AUTO_LOGIN, ""),
getValue(STORE_KEY_REFRESH_REQUEST, null),
getValue(STORE_KEY_REFRESH_HANDLED, 0),
getValue(STORE_KEY_REFRESH_STATUS, null),
loadIgnoredTaskMap()
]);
return {
result,
lastRunning,
lastError: isValueForCurrentSite(lastError) ? lastError : null,
filterYearMonth: normalizeFilterYearMonth(filterYearMonthRaw),
panelState: {
...getDefaultPanelState(),
...normalizeObject(panelStateRaw)
},
courseCollapse: normalizeObject(courseCollapse),
sectionCollapse: normalizeObject(sectionCollapse),
autoLogin: String(autoLogin || "").trim(),
refreshRequest: isValueForCurrentSite(refreshRequest) ? refreshRequest : null,
refreshHandled,
refreshStatus: isValueForCurrentSite(refreshStatus) ? refreshStatus : null,
ignoredTaskMap: normalizeObject(ignoredTaskMap),
currentFilter: currentFilter()
};
}
function applyCurrentFilter(tasks, filter) {
return tasks.filter(task => taskMatchesFilter(task, filter));
}
function groupTasksByCourseAndType(tasks) {
const groups = new Map();
for (const task of tasks) {
const key = getCourseKey(task);
if (!groups.has(key)) {
groups.set(key, {
key,
siteKey: getTaskSiteKey(task),
siteName: getTaskSiteName(task),
courseName: task.courseName || "未识别课程",
tasks: [],
exams: [],
homeworks: [],
experiments: [],
unfinished: 0,
danger: 0
});
}
const group = groups.get(key);
group.tasks.push(task);
if (!task.completed) group.unfinished += 1;
if (isDangerTask(task)) group.danger += 1;
if (task.taskType === "exercise") {
group.exams.push(task);
} else if (task.taskType === "common_homework") {
group.homeworks.push(task);
} else if (task.taskType === "classroom_experiment") {
group.experiments.push(task);
}
}
return Array.from(groups.values());
}
function buildIgnoredDisplayTasks(allTasks, ignoredTaskMap) {
const matchedMap = new Map();
for (const task of allTasks) {
const key = getTaskKey(task);
if (ignoredTaskMap[key] && !matchedMap.has(key)) {
matchedMap.set(key, task);
}
}
return Object.keys(ignoredTaskMap).map(key => {
const snapshot = ignoredTaskMap[key] || {};
const matchedTask = matchedMap.get(key);
return matchedTask
? {
...snapshot,
...matchedTask,
__taskKey: key,
__ignoredView: true,
__snapshotOnly: false,
ignoredAt: snapshot.ignoredAt || matchedTask.ignoredAt || ""
}
: {
...snapshot,
__taskKey: key,
__ignoredView: true,
__snapshotOnly: true,
title: snapshot.title || "已忽略任务",
courseName: snapshot.courseName || "未识别课程",
taskType: snapshot.taskType || "task",
taskTypeText: snapshot.taskTypeText || "任务",
detailUrl: snapshot.detailUrl || "",
siteKey: snapshot.siteKey || "",
siteName: snapshot.siteName || "",
source: snapshot.source || "",
deadline: snapshot.deadline || "无截止时间"
};
});
}
function buildViewModel(result, state) {
const allTasks = sortTasks(normalizeTasks(result));
const filteredByMonth = filterTasksByYearMonth(allTasks, state.filterYearMonth.year, state.filterYearMonth.month);
const ignoredTaskMap = state.ignoredTaskMap || {};
const ignoredKeys = new Set(Object.keys(ignoredTaskMap));
const activeTasks = filteredByMonth.filter(task => !ignoredKeys.has(getTaskKey(task)));
const ignoredDisplayTasks = buildIgnoredDisplayTasks(allTasks, ignoredTaskMap);
const stats = {
...calculateSummaryFromTasks(activeTasks),
ignoredCount: Object.keys(ignoredTaskMap).length
};
const visibleTasks = state.currentFilter === "ignored"
? ignoredDisplayTasks
: applyCurrentFilter(activeTasks, state.currentFilter);
const dangerTasks = activeTasks.filter(isDangerTask);
const groupedTasks = state.currentFilter === "all"
? activeTasks.filter(task => !isDangerTask(task))
: state.currentFilter === "danger" || state.currentFilter === "ignored"
? []
: visibleTasks;
console.log("[TG忽略机制] ignoredTaskMap=", ignoredTaskMap);
console.log("[TG忽略机制] activeTasks=", activeTasks.length, "ignoredTasks=", ignoredDisplayTasks.length);
return {
allTasks,
filteredTasks: filteredByMonth,
activeTasks,
ignoredTaskMap,
ignoredKeys,
ignoredDisplayTasks,
visibleTasks,
dangerTasks,
stats,
courseGroups: groupTasksByCourseAndType(groupedTasks)
};
}
function getDangerTaskKey(task) {
return [
getTaskSiteKey(task),
task.courseId || task.courseIdentifier || task.courseName || "",
task.taskType || "",
task.exerciseId || task.homeworkId || task.id || task.title || ""
].join(":");
}
function getDangerNotifySignature(dangerList, result) {
const keys = dangerList.map(getDangerTaskKey).sort().join("|");
return `${result?.scanTimestamp || result?.scanTime || ""}:${keys}`;
}
function requestDangerNotification(dangerList, result) {
console.log("[TG任务助手] dangerList", dangerList);
if (!dangerList.length || typeof Notification === "undefined") return;
const signature = getDangerNotifySignature(dangerList, result);
if (localStorage.getItem(DANGER_NOTIFY_KEY) === signature) return;
const showNotification = () => {
if (Notification.permission !== "granted") return;
const first = dangerList[0];
const moreText = dangerList.length > 1 ? `等 ${dangerList.length} 项` : "";
new Notification("TG任务助手:发现危险任务", {
body: `${first.courseName || "课程"}:${getTaskTitle(first)} ${moreText}`,
tag: "tg-task-danger"
});
localStorage.setItem(DANGER_NOTIFY_KEY, signature);
};
if (Notification.permission === "granted") {
showNotification();
} else if (Notification.permission !== "denied") {
Notification.requestPermission().then(permission => {
if (permission === "granted") showNotification();
}).catch(() => {});
}
}
function createRoot() {
let root = document.getElementById(ROOT_ID);
if (root) return root;
root = document.createElement("div");
root.id = ROOT_ID;
root.innerHTML = `
`;
document.body.appendChild(root);
const button = root.querySelector(`#${BUTTON_ID}`);
root.addEventListener("click", handlePanelClick);
root.addEventListener("change", handlePanelChange);
root.addEventListener("mousemove", handleLiquidPointerMove, { passive: true });
attachButtonDrag(button);
attachDrawerResize(root.querySelector(`#${DRAWER_ID}`));
applyPanelState();
setDrawerOpen(isOpen());
return root;
}
async function handlePanelClick(event) {
const actionEl = event.target.closest("[data-action], [data-course-toggle], [data-section-toggle], [data-jump-id], [data-summary-filter]");
if (!actionEl) return;
const action = actionEl.dataset.action || "";
createLiquidRipple(actionEl, event);
if (action === "open-panel") {
const button = document.getElementById(BUTTON_ID);
if (button?.dataset.dragged === "1") {
button.dataset.dragged = "0";
return;
}
setDrawerOpen(true);
render();
return;
}
if (action === "close-panel") {
setDrawerOpen(false);
return;
}
if (action === "request-refresh") {
const started = await requestBackendRefresh("frontend-header-button", "manual");
if (started) {
startRunningPoll();
render();
}
return;
}
if (action === "delete-cache") {
deleteTaskCache();
return;
}
if (action === "copy-json") {
copyAllJson();
return;
}
if (action === "set-filter" || actionEl.dataset.summaryFilter) {
const nextFilter = actionEl.dataset.summaryFilter === "site" ? "all" : actionEl.dataset.summaryFilter;
setCurrentFilter(nextFilter);
render();
return;
}
if (action === "ignore-task") {
event.preventDefault();
event.stopPropagation();
const taskKey = String(actionEl.dataset.taskKey || "");
const task = latestTaskRegistry.get(taskKey);
if (!task) {
alert("忽略失败:当前任务数据已失效,请刷新后重试");
return;
}
if (!confirm("确认忽略这个任务?\n忽略后它不会出现在待办和危险提醒中,可在“忽略”分区恢复。")) {
return;
}
await ignoreTask(task);
render();
return;
}
if (action === "restore-task") {
event.preventDefault();
event.stopPropagation();
const taskKey = String(actionEl.dataset.taskKey || "");
if (!taskKey) return;
await restoreTask(taskKey);
render();
return;
}
if (action === "open-detail" || actionEl.dataset.jumpId) {
event.preventDefault();
event.stopPropagation();
const task = jumpTaskRegistry.get(actionEl.dataset.jumpId);
if (!task) {
alert("跳转失败:任务数据已失效,请刷新面板后重试");
return;
}
jumpToTask(task);
return;
}
if (action === "toggle-course" || actionEl.hasAttribute("data-course-toggle")) {
event.preventDefault();
event.stopPropagation();
const group = actionEl.closest(".tg-course-group");
if (!group) return;
const key = group.dataset.courseKey;
const collapseState = await loadCourseCollapseState();
collapseState[key] = !group.classList.contains("tg-course-collapsed");
await setValue(STORE_KEY_COURSE_COLLAPSE, collapseState);
render();
return;
}
if (action === "toggle-section" || actionEl.hasAttribute("data-section-toggle")) {
event.preventDefault();
event.stopPropagation();
const section = actionEl.closest(".tg-course-subgroup");
if (!section) return;
const key = section.dataset.sectionKey;
const collapseState = await loadSectionCollapseState();
collapseState[key] = !section.classList.contains("tg-section-collapsed");
await setValue(STORE_KEY_SECTION_COLLAPSE, collapseState);
render();
}
}
async function handlePanelChange(event) {
const target = event.target;
if (!target?.matches?.("[data-filter-year], [data-filter-month]")) return;
const root = document.getElementById(ROOT_ID);
const year = Number(root?.querySelector("[data-filter-year]")?.value);
const month = Number(root?.querySelector("[data-filter-month]")?.value);
if (!year || !month) return;
await setValue(STORE_KEY_FILTER_YEAR_MONTH, { year, month });
render();
}
function handleLiquidPointerMove(event) {
const target = event.target.closest(`#${BUTTON_ID}, #${DRAWER_ID}`);
if (!target) return;
const rect = target.getBoundingClientRect();
const x = clamp(((event.clientX - rect.left) / Math.max(rect.width, 1)) * 100, 0, 100);
const y = clamp(((event.clientY - rect.top) / Math.max(rect.height, 1)) * 100, 0, 100);
target.style.setProperty("--tg-mouse-x", `${x}%`);
target.style.setProperty("--tg-mouse-y", `${y}%`);
}
function createLiquidRipple(target, event) {
if (!target || !target.matches("button, .tg-detail-link, .tg-summary-item, .tg-course-toggle, .tg-action-btn, .tg-icon-btn")) return;
if (target.dataset.action === "open-panel") return;
const rect = target.getBoundingClientRect();
const ripple = document.createElement("span");
ripple.className = "tg-liquid-ripple";
ripple.style.left = `${event.clientX - rect.left}px`;
ripple.style.top = `${event.clientY - rect.top}px`;
target.appendChild(ripple);
setTimeout(() => ripple.remove(), 560);
}
function setDrawerOpen(open) {
const drawer = document.getElementById(DRAWER_ID);
if (!drawer) return;
drawer.classList.toggle("tg-open", open);
setOpen(open);
if (open) {
applyPanelState();
startRunningPoll();
} else {
if (panelAutoRefreshTimerId) {
clearTimeout(panelAutoRefreshTimerId);
panelAutoRefreshTimerId = null;
}
stopRunningPoll();
stopCountdownTimer();
}
}
async function maybeTriggerAutoRefreshOnce(source = "frontend-panel-open") {
if (!isOpen()) return false;
if (panelAutoRefreshRequested) return false;
const autoRefreshDoneAt = await loadAutoRefreshDoneAt();
if (autoRefreshDoneAt) return false;
panelAutoRefreshRequested = true;
await markAutoRefreshDoneAt(Date.now());
const started = await requestBackendRefresh(source, "auto", {
silentIfBusy: true
});
if (started) {
startRunningPoll();
render();
}
return started;
}
function attachButtonDrag(button) {
if (!button || button.dataset.dragReady === "1") return;
button.dataset.dragReady = "1";
let startX = 0;
let startY = 0;
let originX = 0;
let originY = 0;
let dragging = false;
const onMove = event => {
if (!dragging) return;
const nextX = clamp(originX + event.clientX - startX, -42, window.innerWidth - 20);
const nextY = clamp(originY + event.clientY - startY, -42, window.innerHeight - 20);
button.style.left = `${nextX}px`;
button.style.top = `${nextY}px`;
button.style.right = "auto";
button.style.bottom = "auto";
if (Math.abs(event.clientX - startX) + Math.abs(event.clientY - startY) > 5) {
button.dataset.dragged = "1";
}
};
const onUp = async () => {
if (!dragging) return;
dragging = false;
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp);
await savePanelStatePatch({
buttonX: Math.round(button.getBoundingClientRect().left),
buttonY: Math.round(button.getBoundingClientRect().top)
});
setTimeout(() => {
if (button.dataset.dragged === "1") button.dataset.dragged = "0";
}, 80);
};
button.addEventListener("mousedown", event => {
if (event.button !== 0 || isOpen()) return;
const rect = button.getBoundingClientRect();
startX = event.clientX;
startY = event.clientY;
originX = rect.left;
originY = rect.top;
dragging = true;
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
event.preventDefault();
});
}
function attachDrawerResize(drawer) {
if (!drawer || resizeObserverAttached || typeof ResizeObserver === "undefined") return;
resizeObserverAttached = true;
let saveTimer = null;
const observer = new ResizeObserver(entries => {
const entry = entries[0];
if (!entry || !isOpen()) return;
clearTimeout(saveTimer);
saveTimer = setTimeout(() => {
const rect = entry.target.getBoundingClientRect();
savePanelStatePatch({
panelWidth: Math.round(rect.width),
panelHeight: Math.round(rect.height)
});
}, 260);
});
observer.observe(drawer);
}
async function requestBackendRefresh(source, type = "manual", options = {}) {
const silentIfBusy = options?.silentIfBusy === true;
if (await isRefreshBusy()) {
if (!silentIfBusy) {
alert("正在刷新中,请稍后。");
}
return false;
}
// 刷新开始前先清空上一轮错误,避免成功刷新后面板和导出 JSON 仍残留旧报错。
await setValue(STORE_KEY_LAST_ERROR, null);
if (location.hostname === "www.educoder.net") {
try {
await scanEducoderInPage(source);
activeRefreshRequestId = `educoder:${Date.now()}`;
activeRefreshStartedAt = Date.now();
await markAutoRefreshDoneAt(activeRefreshStartedAt);
} catch (err) {
console.error("Educoder 页面内扫描失败:", err);
activeRefreshRequestId = "";
activeRefreshStartedAt = 0;
panelAutoRefreshRequested = false;
}
return true;
}
const requestId = Date.now() + "-" + Math.random().toString(36).slice(2, 8);
const requestedAt = Date.now();
const site = getCurrentPageSite();
const login = site.login || latestResultLogin || saveAutoLoginIfDetected() || parseLoginFromCurrentUrl() || "";
activeRefreshRequestId = requestId;
activeRefreshStartedAt = requestedAt;
await markAutoRefreshDoneAt(requestedAt);
await setValue(STORE_KEY_REFRESH_STATUS, {
status: "waiting",
requestId,
progress: 8,
message: "等待后台响应",
siteKey: getCurrentBackendSiteKey(),
siteName: site.siteName,
time: new Date().toLocaleString(),
timestamp: Date.now()
});
await setValue(STORE_KEY_REFRESH_REQUEST, {
type,
login,
siteKey: getCurrentBackendSiteKey(),
siteName: site.siteName,
pageHost: location.hostname,
requestId,
requestedAt,
requestedAtText: new Date(requestedAt).toLocaleString(),
source
});
return true;
}
function startRunningPoll() {
if (pollTimerId) return;
pollWasRunning = false;
pollTimerId = setInterval(async () => {
if (!isOpen()) {
stopRunningPoll();
return;
}
const refreshStatus = await getValue(STORE_KEY_REFRESH_STATUS, null);
if (refreshStatus?.status === "running" || refreshStatus?.status === "waiting") {
pollWasRunning = true;
render();
return;
}
if (refreshStatus?.status === "done" || refreshStatus?.status === "error") {
if (refreshStatus?.status === "done") {
await markAutoRefreshDoneAt(refreshStatus?.timestamp || Date.now());
}
activeRefreshRequestId = "";
activeRefreshStartedAt = 0;
render();
stopRunningPoll();
return;
}
const runningState = await loadRunningState();
const isRunning = runningState?.running === true;
if (isRunning) {
pollWasRunning = true;
render();
return;
}
if (pollWasRunning) {
pollWasRunning = false;
activeRefreshRequestId = "";
activeRefreshStartedAt = 0;
render();
stopRunningPoll();
return;
}
render();
stopRunningPoll();
}, 1000);
}
function stopRunningPoll() {
if (!pollTimerId) return;
clearInterval(pollTimerId);
pollTimerId = null;
}
async function deleteTaskCache() {
if (!confirm("确定要删除 TG任务助手缓存吗?这会清空当前扫描结果、运行状态、错误信息和复制用 JSON。")) return;
await deleteValue(getCurrentPageSite().resultKey);
await deleteValue(STORE_KEY);
await deleteValue(STORE_KEY_LAST_RUNNING);
await deleteValue(STORE_KEY_LAST_ERROR);
await clearAutoRefreshDoneAt();
panelAutoRefreshRequested = false;
activeRefreshRequestId = "";
activeRefreshStartedAt = 0;
cacheDeletedNotice = "缓存已删除";
render();
}
function updateFloatingBadge(summary) {
const badge = document.querySelector(`#${BUTTON_ID} .tg-task-button-badge`);
if (!badge) return;
const count = summary?.dangerCount || summary?.unfinishedCount || 0;
badge.textContent = String(count > 99 ? "99+" : count);
badge.style.display = count > 0 ? "block" : "none";
}
function getFilterLabel(filter) {
const labels = { ignored: "忽略", all: "总任务", unfinished: "未完成", danger: "危险", exercise: "考试", common_homework: "图文", homework: "图文", classroom_experiment: "实验", experiment: "实验" };
return labels[filter] || "全部任务";
}
function taskMatchesFilter(task, filter) {
if (filter === "unfinished") return !task.completed;
if (filter === "exercise") return task.taskType === "exercise";
if (filter === "common_homework" || filter === "homework") return task.taskType === "common_homework";
if (filter === "classroom_experiment" || filter === "experiment") return task.taskType === "classroom_experiment";
if (filter === "danger") return isDangerTask(task);
return true;
}
function renderSummaryCard(filter, numberHtml, label, selectedFilter) {
const selected = selectedFilter === filter;
return `
`;
}
function getEducoderPageFetch() {
if (typeof unsafeWindow !== "undefined" && unsafeWindow?.fetch) {
return unsafeWindow.fetch.bind(unsafeWindow);
}
return window.fetch.bind(window);
}
async function fetchEducoderPageJson(url) {
const pageFetch = getEducoderPageFetch();
const res = await pageFetch(url, {
method: "GET",
mode: "cors",
credentials: "include",
cache: "no-store",
headers: {
accept: "application/json",
"content-type": "application/json; charset=utf-8"
}
});
const text = await res.text();
let data;
try {
data = JSON.parse(text);
} catch (err) {
const error = new Error("Educoder JSON解析失败:" + text.slice(0, 300));
error.detail = {
url,
httpStatus: res.status,
statusText: res.statusText,
textPreview: text.slice(0, 500),
keys: [],
parseError: err?.message || String(err)
};
throw error;
}
return {
ok: res.ok,
status: res.status,
statusText: res.statusText,
data,
textPreview: text.slice(0, 500),
keys: data && typeof data === "object" ? Object.keys(data) : []
};
}
async function fetchAndLogEducoderPageJson(url, requestLog, type) {
try {
const resp = await fetchEducoderPageJson(url);
requestLog.push({
siteKey: "educoder",
siteName: "Educoder主站",
type,
url,
httpStatus: resp.status,
statusText: resp.statusText,
ok: resp.ok,
textPreview: resp.textPreview,
keys: resp.keys,
status: resp.data?.status,
message: resp.data?.message,
rawCount: Array.isArray(resp.data?.exercises)
? resp.data.exercises.length
: Array.isArray(resp.data?.homeworks)
? resp.data.homeworks.length
: undefined
});
return resp;
} catch (err) {
const detail = err?.detail || {};
requestLog.push({
siteKey: "educoder",
siteName: "Educoder主站",
type,
url,
httpStatus: detail.httpStatus || 0,
statusText: detail.statusText || "",
ok: false,
textPreview: detail.textPreview || "",
keys: detail.keys || [],
error: err?.message || String(err)
});
throw err;
}
}
function parseEducoderExerciseStatus(user) {
if (!user) {
return {
commitStatus: null,
statusText: "未提交",
completed: false
};
}
const commitStatus = user.commit_status;
if (commitStatus === 0) return { commitStatus, statusText: "未提交", completed: false };
if (commitStatus === 1) return { commitStatus, statusText: "提交中/待评阅", completed: false };
if (commitStatus === 2) return { commitStatus, statusText: "已完成/已提交", completed: true };
return {
commitStatus,
statusText: user.end_at || user.score != null ? "已完成/已提交" : "未知状态",
completed: !!user.end_at || user.score != null
};
}
function parseEducoderHomeworkItem(course, item) {
const homeworkStatus = Array.isArray(item?.status) ? item.status : [];
const workStatus = Array.isArray(item?.work_status) ? item.work_status : [];
const workStatusText = workStatus.join(" ");
const ended = homeworkStatus.includes("已截止") || item?.time_status === 5;
let notSubmitted = false;
let submitted = false;
if (typeof item?.un_commit_work === "boolean") {
notSubmitted = item.un_commit_work === true;
submitted = item.un_commit_work === false;
} else {
notSubmitted = workStatusText.includes("提交作品") || workStatusText.includes("未提交");
submitted = workStatusText.includes("查看作品") || workStatusText.includes("已提交") || workStatusText.includes("已完成");
}
let statusText = "未知状态";
if (notSubmitted) statusText = ended ? "已截止/未提交" : "未提交";
else if (submitted) statusText = ended ? "已截止/已提交" : "已提交";
const homeworkId = item?.homework_id || item?.id;
return {
taskType: "common_homework",
taskTypeText: "图文作业",
siteKey: "educoder",
siteName: "Educoder主站",
courseId: course.courseId,
courseIdentifier: course.courseIdentifier,
courseName: course.courseName,
homeworkId,
workId: item?.work_id || item?.student_work_id,
studentWorkId: item?.student_work_id,
title: item?.name || item?.homework_name || "未命名图文作业",
homeworkStatus,
workStatus,
timeStatus: item?.time_status,
statusText,
completed: submitted,
submitted,
notSubmitted,
ended,
publishTime: item?.publish_time || "",
endTime: item?.end_time || item?.end_time_s || "",
lateTime: item?.late_time || "",
score: "--",
source: "educoder-page-homework_commons_1",
detailUrl: `${course.pageOrigin}/classrooms/${course.courseIdentifier}/common_homework/${homeworkId}/detail?tabs=0`,
scanTime: new Date().toLocaleString(),
raw: item
};
}
function parseEducoderExperimentItem(course, item) {
const finishedStatus = Number(item?.shixun_finished_status);
const completed = finishedStatus === 1;
const ended = item?.time_status === 5 || (Array.isArray(item?.status) && item.status.includes("已截止"));
const homeworkId = item?.homework_id || item?.id;
return {
taskType: "classroom_experiment",
taskTypeText: "课堂实验",
siteKey: "educoder",
siteName: "Educoder主站",
courseId: course.courseId,
courseIdentifier: course.courseIdentifier,
courseName: course.courseName,
homeworkId,
title: item?.name || item?.shixun_name || "未命名课堂实验",
statusText: completed ? "已完成" : ended ? "已截止/未完成" : "进行中/未完成",
completed,
submitted: completed,
notSubmitted: !completed,
ended,
publishTime: item?.publish_time || "",
endTime: item?.end_time_s || item?.end_time || "",
lateTime: item?.late_time || "",
challengeCount: item?.challenge_count ?? null,
finishedChallengeCount: item?.finished_challenge_count ?? null,
passedTime: item?.student_passed_time || "",
detailUrl: `${course.pageOrigin}/classrooms/${course.courseIdentifier}/shixun_homework/${homeworkId}/detail?tabs=1`,
source: "educoder-page-homework_commons_4",
scanTime: new Date().toLocaleString(),
raw: item
};
}
async function scanEducoderInPage(source = "frontend-header-button") {
const site = getCurrentPageSite();
const requestId = Date.now() + "-" + Math.random().toString(36).slice(2, 8);
const requestLog = [];
const errors = [];
const scanTime = new Date().toLocaleString();
const scanTimestamp = Date.now();
const course = {
courseId: site.courseId,
courseIdentifier: site.courseIdentifier,
courseName: site.courseName,
pageOrigin: site.pageOrigin,
apiOrigin: site.apiOrigin
};
await setValue(STORE_KEY_LAST_ERROR, null);
await setValue(STORE_KEY_REFRESH_STATUS, {
status: "running",
requestId,
progress: 18,
message: "正在扫描 Educoder 主站任务",
siteKey: "educoder",
siteName: site.siteName,
source,
time: new Date().toLocaleString(),
timestamp: Date.now()
});
try {
function getEducoderActualLogin(site) {
// 1. 如果当前页面是 /users/xxx/classrooms,就优先取当前页面里的 xxx
const pageMatch = location.pathname.match(/\/users\/([^/]+)/);
if (pageMatch && pageMatch[1]) {
return decodeURIComponent(pageMatch[1]);
}
// 2. 如果当前页面不是用户页,比如 /paths,就回退使用 site.login
if (site && site.login) {
return site.login;
}
// 3. 两个都没有才报错
throw new Error(
"无法识别 Educoder 用户 ID:当前页面不是 /users/xxx 页面,且 site.login 为空。当前地址:" + location.href
);
}
const actualLogin = getEducoderActualLogin(site);
const homepageUrl =
`${site.apiOrigin}/api/users/${encodeURIComponent(actualLogin)}/homepage_info.json?zzud=${encodeURIComponent(actualLogin)}`;
console.log("[TG任务助手] homepage_info 实际请求地址:", homepageUrl);
const homepageResp = await fetchAndLogEducoderPageJson(
homepageUrl,
requestLog,
"homepage_info"
);
const homepage = homepageResp.data;
if (!homepage || homepage.is_logged_user !== true) {
throw new Error("Educoder 主站未登录或登录态失效:" + JSON.stringify({
status: homepage?.status,
message: homepage?.message,
keys: homepageResp.keys,
preview: homepageResp.textPreview
}));
}
// 下面原来的代码继续保留
const exerciseUrl = `${site.apiOrigin}/api/v2/courses/${course.courseIdentifier}/exercises.json?coursesId=${course.courseIdentifier}&limit=20&type=&id=${course.courseIdentifier}&zzud=${encodeURIComponent(site.login)}`;
const exerciseResp = await fetchAndLogEducoderPageJson(exerciseUrl, requestLog, "exercise_list");
if (!Array.isArray(exerciseResp.data?.exercises)) {
throw new Error("Educoder 考试列表返回结构异常:" + JSON.stringify({
status: exerciseResp.data?.status,
message: exerciseResp.data?.message,
keys: exerciseResp.keys,
preview: exerciseResp.textPreview
}));
}
const rawExams = exerciseResp.data.exercises;
const exams = [];
for (const exam of rawExams) {
const exerciseId = exam?.id;
let user = null;
let totalScore = "--";
let status = { commitStatus: null, statusText: "未提交", completed: false };
if (exerciseId) {
try {
const userUrl = `${site.apiOrigin}/api/exercises/${encodeURIComponent(exerciseId)}/exercise_users.json?page=1&limit=20&coursesId=${course.courseIdentifier}&categoryId=${encodeURIComponent(exerciseId)}&zzud=${encodeURIComponent(site.login)}`;
const userResp = await fetchAndLogEducoderPageJson(userUrl, requestLog, "exercise_user");
const userPayload = userResp.data;
user = userPayload?.current_answer_user || userPayload?.data?.current_answer_user || null;
totalScore = userPayload?.total_score ?? userPayload?.data?.total_score ?? "--";
status = parseEducoderExerciseStatus(user);
} catch (err) {
errors.push({
stage: "exercise_user",
siteKey: "educoder",
siteName: site.siteName,
courseName: course.courseName,
exerciseId,
message: err?.message || String(err)
});
}
}
exams.push({
taskType: "exercise",
taskTypeText: "考试/小测试",
siteKey: "educoder",
siteName: site.siteName,
courseId: course.courseId,
courseIdentifier: course.courseIdentifier,
courseName: course.courseName,
exerciseId,
title: exam?.exercise_name || exam?.name || exam?.title || "未命名考试",
detailUrl: `${site.pageOrigin}/classrooms/${course.courseIdentifier}/exercisenotice/${exerciseId}/users/${encodeURIComponent(site.login)}`,
deadlineRemaining: exam?.exercise_left_time || "--",
durationMinutes: exam?.time ?? null,
exerciseStatus: exam?.exercise_status ?? null,
currentStatus: exam?.current_status ?? null,
wholeExerciseStatus: exam?.whole_exercise_status ?? null,
exerciseUserId: exam?.exercise_user_id ?? user?.exercise_user_id ?? null,
...status,
startAt: user?.start_at || null,
endAt: user?.end_at || null,
score: user?.score ?? "--",
totalScore,
objectiveScore: user?.objective_score ?? "--",
subjectiveScore: user?.subjective_score ?? "--",
reviewStatus: user?.review_status ?? false,
userName: user?.user_name || "",
studentId: user?.student_id || "",
scanTime,
raw: { exam, user }
});
}
const homeworkUrl = `${site.apiOrigin}/api/courses/${course.courseIdentifier}/homework_commons.json?limit=100&status=0&id=${course.courseIdentifier}&type=1&sort_by=updated_at&sort_direction=asc&order=0&zzud=${encodeURIComponent(site.login)}`;
const homeworkResp = await fetchAndLogEducoderPageJson(homeworkUrl, requestLog, "homework_commons_1");
if (!Array.isArray(homeworkResp.data?.homeworks)) {
throw new Error("Educoder 图文作业返回结构异常:" + JSON.stringify({
status: homeworkResp.data?.status,
message: homeworkResp.data?.message,
keys: homeworkResp.keys,
preview: homeworkResp.textPreview
}));
}
const homeworks = homeworkResp.data.homeworks.map(item => parseEducoderHomeworkItem(course, item));
const experimentUrl = `${site.apiOrigin}/api/courses/${course.courseIdentifier}/homework_commons.json?limit=100&status=0&id=${course.courseIdentifier}&type=4&sort_by=updated_at&sort_direction=asc&order=0&zzud=${encodeURIComponent(site.login)}`;
const experimentResp = await fetchAndLogEducoderPageJson(experimentUrl, requestLog, "homework_commons_4");
if (!Array.isArray(experimentResp.data?.homeworks)) {
throw new Error("Educoder 课堂实验返回结构异常:" + JSON.stringify({
status: experimentResp.data?.status,
message: experimentResp.data?.message,
keys: experimentResp.keys,
preview: experimentResp.textPreview
}));
}
const experiments = experimentResp.data.homeworks.map(item => parseEducoderExperimentItem(course, item));
const tasks = sortTasks([...exams, ...homeworks, ...experiments]);
const result = {
login: site.login,
currentSite: {
key: "educoder",
name: site.siteName,
pageOrigin: site.pageOrigin,
apiOrigin: site.apiOrigin
},
scanTime,
scanTimestamp,
source: "educoder-page-frontend",
courses: [
{
courseId: course.courseId,
courseIdentifier: course.courseIdentifier,
courseName: course.courseName,
siteKey: "educoder",
siteName: site.siteName,
pageOrigin: site.pageOrigin,
apiOrigin: site.apiOrigin,
login: site.login,
exams,
homeworks,
experiments
}
],
tasks,
errors,
warnings: [],
debug: {
requestLog
},
summary: calculateSummaryFromTasks(tasks)
};
await setValue(STORE_KEY_EDUCODER_RESULT, result);
await setValue(STORE_KEY_LAST_ERROR, null);
await setValue(STORE_KEY_REFRESH_STATUS, {
status: "done",
requestId,
progress: 100,
message: "刷新完成",
siteKey: "educoder",
siteName: site.siteName,
time: new Date().toLocaleString(),
timestamp: Date.now()
});
render();
return result;
} catch (err) {
const message = err?.message || String(err);
await setValue(STORE_KEY_LAST_ERROR, {
siteKey: "educoder",
siteName: site.siteName,
time: new Date().toLocaleString(),
message: "Educoder 页面内刷新失败,请复制全部 JSON 排查。",
error: message,
stack: err?.stack || "",
stage: "educoder_page_frontend",
debug: {
requestLog,
errorDetail: err?.detail || null
}
});
await setValue(STORE_KEY_REFRESH_STATUS, {
status: "error",
requestId,
progress: 100,
message: "Educoder 页面内刷新失败,请复制全部 JSON 排查。",
error: message,
siteKey: "educoder",
siteName: site.siteName,
time: new Date().toLocaleString(),
timestamp: Date.now()
});
render();
throw err;
}
}
function renderYearMonthFilter(year, month) {
const now = new Date();
const currentYear = now.getFullYear();
const years = [];
for (let y = currentYear - 3; y <= currentYear + 1; y += 1) years.push(y);
const yearOptions = years.map(y => ``).join("");
const monthOptions = Array.from({ length: 12 }, (_, i) => i + 1).map(m => ``).join("");
return `
起始时间:
显示任务起点:${escapeHtml(year)} 年 ${escapeHtml(month)} 月之后
`;
}
function renderRefreshStatus(runningState, lastError, refreshRequest, refreshHandled, refreshStatus) {
const isRunning = runningState?.running === true || refreshStatus?.status === "running";
const hasError = !isRunning && refreshStatus?.status !== "running" && lastError?.message;
const percent = clamp(Number(refreshStatus?.progress ?? runningState?.percent) || (isRunning ? 12 : 100), 0, 100);
const requestedAt = Number(refreshRequest?.requestedAt) || 0;
const handledAt = Number(refreshHandled) || 0;
const pending = requestedAt && requestedAt > handledAt && !isRunning;
const statusClass = isRunning
? "tg-refresh-running"
: hasError || refreshStatus?.status === "error"
? "tg-refresh-failed"
: refreshStatus?.status === "done" || runningState?.stage === "done"
? "tg-refresh-done"
: pending
? "tg-refresh-pending"
: "";
let title = "刷新状态";
let stage = location.hostname === "www.educoder.net"
? "展开面板后会自动进行页面内刷新。"
: "展开面板后会自动请求后台刷新。";
let barPercent = pending ? 18 : 0;
if (isRunning) {
title = "正在刷新";
stage = cleanStatusMessage(refreshStatus?.message || runningState?.stageText || "后台正在扫描任务");
barPercent = percent;
} else if (refreshStatus?.status === "done") {
title = "最近刷新成功";
stage = cleanStatusMessage(refreshStatus.message || "刷新完成");
barPercent = 100;
} else if (refreshStatus?.status === "error") {
title = "最近刷新失败";
stage = cleanStatusMessage(refreshStatus.error || refreshStatus.message || "刷新失败");
barPercent = 100;
} else if (hasError) {
title = "最近刷新失败";
stage = cleanStatusMessage(lastError.message);
barPercent = 100;
} else if (runningState?.stage === "done") {
title = "最近刷新完成";
stage = cleanStatusMessage(runningState.endTime || runningState.stageText || "扫描完成");
barPercent = 100;
} else if (pending) {
title = "已请求刷新";
} else if (runningState?.stageText) {
title = "最近刷新状态";
stage = cleanStatusMessage(runningState.stageText);
barPercent = percent;
}
return `
${escapeHtml(title)}
${Math.round(barPercent)}%
${escapeHtml(stage)}
`;
}
function appendLog(text) {
const boxWrap = document.querySelector(`#${DRAWER_ID} .tg-json-container`);
const box = document.querySelector(`#${DRAWER_ID} .tg-json-box`);
if (!boxWrap || !box) return;
const line = `[${new Date().toLocaleString()}] ${text}`;
box.value = box.value ? `${box.value}\n${line}` : line;
boxWrap.style.display = "block";
}
function writeOutput(text) {
const boxWrap = document.querySelector(`#${DRAWER_ID} .tg-json-container`);
const box = document.querySelector(`#${DRAWER_ID} .tg-json-box`);
if (!boxWrap || !box) return;
box.value = text;
boxWrap.style.display = "block";
}
function parseCourseIdFromCurrentUrl() {
const match = location.href.match(/\/classrooms\/([^/?#]+)/);
return match && match[1] ? decodeURIComponent(match[1]) : "";
}
function parseLoginFromCurrentUrl() {
const userMatch = location.href.match(/\/users\/([^/?#]+)/);
if (userMatch && userMatch[1]) return decodeURIComponent(userMatch[1]);
const zzudMatch = location.href.match(/[?&]zzud=([^&]+)/);
if (zzudMatch && zzudMatch[1]) return decodeURIComponent(zzudMatch[1]);
const usernameMatch = location.href.match(/[?&]username=([^&]+)/);
if (usernameMatch && usernameMatch[1]) return decodeURIComponent(usernameMatch[1]);
return "";
}
function resolveJumpCourseId(task) {
const direct = task?.courseIdentifier || task?.course_id || task?.coursesId;
if (direct) return String(direct);
if (task?.courseId && !/^\d+$/.test(String(task.courseId))) {
return String(task.courseId);
}
const raw = task?.raw || {};
const rawCourse = raw.courseIdentifier || raw.course_identifier || raw.coursesId || raw.course_id;
if (rawCourse && !/^\d+$/.test(String(rawCourse))) {
return String(rawCourse);
}
return parseCourseIdFromCurrentUrl();
}
function resolveJumpLogin(task) {
return String(
task?.login ||
task?.userLogin ||
task?.raw?.user_login ||
latestResultLogin ||
parseLoginFromCurrentUrl() ||
""
).trim();
}
function getJumpType(task) {
if (task?.taskType === "classroom_experiment") return "shixun_homework";
if (task?.taskType === "exercise") return "exercise";
if (task?.taskType === "common_homework") return "common_homework";
return "";
}
function getJumpItem(task) {
if (task?.taskType === "exercise") {
return task?.raw?.exam || { id: task?.exerciseId || task?.id };
}
if (task?.taskType === "classroom_experiment" || task?.taskType === "common_homework") {
return task?.raw || { homework_id: task?.homeworkId || task?.id };
}
return null;
}
function buildJumpUrl(type, courseId, item, login) {
if (type === "shixun_homework") {
if (!item || !item.homework_id) throw new Error("课堂实验缺少 homework_id");
return `${location.origin}/classrooms/${encodeURIComponent(courseId)}/shixun_homework/${encodeURIComponent(item.homework_id)}/detail?tabs=1`;
}
if (type === "exercise") {
if (!item || !item.id) throw new Error("考试缺少 id");
if (!login) throw new Error("考试缺少 login");
return `${location.origin}/classrooms/${encodeURIComponent(courseId)}/exercisenotice/${encodeURIComponent(item.id)}/users/${encodeURIComponent(login)}`;
}
if (type === "common_homework") {
if (!item || !item.homework_id) throw new Error("图文作业缺少 homework_id");
return `${location.origin}/classrooms/${encodeURIComponent(courseId)}/common_homework/${encodeURIComponent(item.homework_id)}/detail?tabs=0`;
}
throw new Error(`未知跳转类型:${type}`);
}
function buildTaskJumpUrl(task) {
const type = getJumpType(task);
const courseId = resolveJumpCourseId(task);
const item = getJumpItem(task);
const login = resolveJumpLogin(task);
if (!courseId) throw new Error("缺少课程 ID");
return buildJumpUrl(type, courseId, item, login);
}
function jumpToTask(task) {
try {
const url = task?.detailUrl || buildTaskJumpUrl(task);
writeOutput(url);
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(url).catch(() => {});
}
window.open(url, "_blank");
} catch (err) {
console.error("跳转失败:", err, task);
alert(`跳转失败:${err.message}`);
}
}
function getTaskTitle(task) {
return task.title || task.name || task.exerciseName || task.homeworkName || task.experimentName || task.homeworkTitle || "未命名任务";
}
function getTaskTypeLabel(task) {
if (task.taskType === "exercise") return "考试 / 小测试";
if (task.taskType === "common_homework") return "图文作业";
if (task.taskType === "classroom_experiment") return "课堂实验";
return task.taskTypeText || "任务";
}
function renderDetailLink(task) {
const id = `task-${jumpTaskSeq++}`;
jumpTaskRegistry.set(id, task);
return ``;
}
function renderTaskActionButton(task, taskKey) {
if (task?.__ignoredView) {
return ``;
}
return ``;
}
function renderExerciseMeta(task) {
if (task?.taskType !== "exercise") return "";
const items = [];
const usedAttempts = Number(task?.usedAttempts);
const maxAttempts = Number(task?.maxAttempts);
const hasAttemptInfo = Number.isFinite(usedAttempts) && Number.isFinite(maxAttempts) && maxAttempts > 0;
const scoreText = task?.scoreText ?? task?.score;
const totalScoreText = task?.totalScoreText ?? task?.totalScore;
const hasScore = scoreText !== undefined && scoreText !== null && String(scoreText).trim() !== "" && String(scoreText).trim() !== "--";
const hasTotalScore = totalScoreText !== undefined && totalScoreText !== null && String(totalScoreText).trim() !== "";
if (hasAttemptInfo) {
items.push(`已考次数:${escapeHtml(String(usedAttempts))} / ${escapeHtml(String(maxAttempts))}`);
}
if (hasScore || hasTotalScore) {
items.push(`成绩:${escapeHtml(hasScore ? String(scoreText) : "--")} / ${escapeHtml(hasTotalScore ? String(totalScoreText) : "--")}`);
}
if (task?.exerciseLeftTime) {
items.push(`剩余时间:${escapeHtml(String(task.exerciseLeftTime))}`);
}
return items.join("");
}
function renderTaskCard(task) {
const taskKey = getTaskKey(task);
latestTaskRegistry.set(taskKey, task);
const tone = statusTone(task);
const status = task.examStatusLabel || task.submitStatusText || task.statusText || task.commitStatusText || (task.completed ? "已完成" : "未完成");
const deadline = getTaskDeadlineLabel(task);
const siteLine = `${getTaskSiteName(task)} · ${task.courseName || "未识别课程"}`;
const countdownText = formatCountdown(deadline);
const ignoredBadge = task?.__ignoredView
? `${task.__snapshotOnly ? "已忽略 / 未匹配" : "已忽略"}`
: "";
const snapshotNote = task?.__snapshotOnly
? `当前扫描未匹配到源任务
`
: "";
if (!hasLoggedDeadlineSample) {
console.log("[TG倒计时] deadline field=", deadline, "task=", task);
console.log("[TG忽略机制] taskKey=", taskKey, task.title || task.name);
hasLoggedDeadlineSample = true;
}
return `
${escapeHtml(countdownText)}
${ignoredBadge}
${renderTaskActionButton(task, taskKey)}
${escapeHtml(getTaskTitle(task))}
${escapeHtml(getTaskTypeLabel(task))}
${snapshotNote}
来源:${escapeHtml(siteLine)}
截止:${escapeHtml(deadline)}
${renderExerciseMeta(task)}
${task?.__ignoredView && task?.ignoredAt ? `忽略时间:${escapeHtml(task.ignoredAt)}` : ""}
`;
}
function renderTaskList(tasks) {
return tasks.map(renderTaskCard).join("");
}
function renderSection(title, tasks, className = "") {
if (!tasks.length) return "";
return `
${escapeHtml(title)}${tasks.length}
${renderTaskList(tasks)}
`;
}
function renderCourseSubgroup(groupKey, typeKey, title, tasks, sectionCollapseState) {
if (!tasks.length) return "";
const key = `${groupKey}:${typeKey}`;
const collapsed = sectionCollapseState[key] === true;
return `
${escapeHtml(title)}
${tasks.length}
`;
}
function renderCourseGroups(groups, courseCollapseState, sectionCollapseState) {
return groups.map(group => {
const collapsed = courseCollapseState[group.key] === true;
const stats = [
`站点 ${group.siteName || "TG校内站"}`,
`总任务 ${group.tasks?.length || 0}`,
`考试 ${group.exams?.length || 0}`,
`图文 ${group.homeworks?.length || 0}`,
`实验 ${group.experiments?.length || 0}`,
`危险 ${group.danger || 0}`
];
return `
${escapeHtml(group.courseName || "未识别课程")}
${escapeHtml(group.siteName || "TG校内站")}
${stats.map(x => `${escapeHtml(x)}`).join("")}
${renderCourseSubgroup(group.key, "exercise", "考试 / 小测试", group.exams || [], sectionCollapseState)}
${renderCourseSubgroup(group.key, "common_homework", "图文作业", group.homeworks || [], sectionCollapseState)}
${renderCourseSubgroup(group.key, "classroom_experiment", "课堂实验", group.experiments || [], sectionCollapseState)}
`;
}).join("");
}
async function render() {
createRoot();
const body = document.querySelector(`#${DRAWER_ID} .tg-body`);
if (!body) return;
try {
await renderPanelContent(body);
if (isOpen()) {
maybeTriggerAutoRefreshOnce().catch(err => {
console.error("TG任务助手自动刷新判断失败", err);
});
}
} catch (err) {
console.error("TG任务助手渲染失败", err);
body.innerHTML = `前台面板渲染失败:${escapeHtml(cleanStatusMessage(err?.message || String(err)))}
`;
}
}
async function renderPanelContent(body) {
jumpTaskRegistry = new Map();
jumpTaskSeq = 0;
latestTaskRegistry = new Map();
const state = await loadFrontendState();
const data = state.result;
const runningState = state.lastRunning;
const lastError = state.lastError;
const refreshRequest = state.refreshRequest;
const refreshHandled = state.refreshHandled;
const refreshStatus = state.refreshStatus;
const currentPageSite = getCurrentPageSite();
const isRunning = runningState?.running === true || refreshStatus?.status === "running";
const currentRefreshRunning = refreshStatus?.status === "running";
const currentRefreshError = refreshStatus?.status === "error";
const refreshStatusHtml = renderRefreshStatus(runningState, lastError, refreshRequest, refreshHandled, refreshStatus);
if (isRunning && !data) {
updateFloatingBadge({ dangerCount: 0, unfinishedCount: 0 });
body.innerHTML = `${refreshStatusHtml}后台正在运行
当前阶段:${escapeHtml(cleanStatusMessage(refreshStatus?.message || runningState?.stageText || "正在刷新"))}
`;
return;
}
if (!data && currentRefreshError) {
updateFloatingBadge({ dangerCount: 0, unfinishedCount: 0 });
body.innerHTML = `${refreshStatusHtml}最近刷新失败
错误:${escapeHtml(cleanStatusMessage(refreshStatus?.error || refreshStatus?.message || "未知错误"))}
`;
return;
}
if (!data && lastError?.message && !currentRefreshRunning) {
updateFloatingBadge({ dangerCount: 0, unfinishedCount: 0 });
body.innerHTML = `${refreshStatusHtml}上次后台错误:${escapeHtml(cleanStatusMessage(lastError.message))}
时间:${escapeHtml(lastError.time || "--")}
`;
return;
}
if (!data) {
updateFloatingBadge({ dangerCount: 0, unfinishedCount: 0 });
const emptyText = currentPageSite.siteKey === "educoder"
? "当前为 Educoder 主站视图,暂无本平台缓存。请点击“请求刷新”获取任务状态。"
: "当前为 TG 校内站视图,暂无本平台缓存。请点击“请求刷新”获取任务状态。";
body.innerHTML = `${refreshStatusHtml}${cacheDeletedNotice ? `${escapeHtml(cacheDeletedNotice)}
` : ""}${escapeHtml(emptyText)}
`;
return;
}
const manualLogin = String(MANUAL_LOGIN || "").trim();
const autoLogin = state.autoLogin;
const expectedLogin = currentPageSite.login || manualLogin || autoLogin;
const resultLogin = String(data?.login || "").trim();
latestResultLogin = resultLogin;
const accountText = expectedLogin || "未识别";
const accountSource = currentPageSite.login ? currentPageSite.siteName : manualLogin ? "手动配置" : autoLogin ? "自动识别" : "未识别";
let accountWarningHtml = "";
if (!isResultForCurrentSite(data)) {
updateFloatingBadge({ dangerCount: 0, unfinishedCount: 0 });
body.innerHTML = `当前缓存数据属于另一个账号,请重新运行后台扫描。
`;
return;
}
if (!expectedLogin) {
accountWarningHtml = `账号:未识别
提示:请先打开 TG 页面,或在后台脚本顶部填写 MANUAL_LOGIN。
`;
}
const filterYearMonth = state.filterYearMonth;
const courseCollapseState = state.courseCollapse;
const sectionCollapseState = state.sectionCollapse;
const viewModel = buildViewModel(data, state);
const summary = viewModel.stats;
updateFloatingBadge(summary);
const errors = data.errors || [];
const warnings = data.warnings || [];
const failedSiteLogins = Array.isArray(data?.debug?.siteLogin)
? data.debug.siteLogin.filter(item => item && item.ok === false)
: [];
const filter = state.currentFilter;
const filteredTasks = viewModel.visibleTasks;
const dangerTasks = viewModel.dangerTasks;
const dangerList = filter === "danger" ? filteredTasks : dangerTasks;
const ignoredTasks = viewModel.ignoredDisplayTasks;
requestDangerNotification(dangerList, data);
let html = accountWarningHtml + refreshStatusHtml;
if (isRunning) {
html += `后台正在运行
当前阶段:${escapeHtml(cleanStatusMessage(refreshStatus?.message || runningState?.stageText || "正在刷新"))}
`;
} else if (currentRefreshError) {
html += `最近刷新失败
错误:${escapeHtml(cleanStatusMessage(refreshStatus?.error || refreshStatus?.message || "未知错误"))}
`;
} else if (lastError && lastError.message && !currentRefreshRunning) {
html += `上次后台错误:${escapeHtml(cleanStatusMessage(lastError.message))}
时间:${escapeHtml(lastError.time || "--")}
`;
}
if (failedSiteLogins.length) {
html += failedSiteLogins.map(item => {
const siteName = item.siteName || "站点";
const loginUrl = item.siteKey === "educoder" ? "https://www.educoder.net" : getCurrentPageSite().pageOrigin;
return `${escapeHtml(siteName)}未登录或登录态失效,请先打开 ${escapeHtml(loginUrl)} 并登录后重新刷新。
原因:${escapeHtml(cleanStatusMessage(item.error || "登录态预检失败"))}
`;
}).join("");
}
html += `
${escapeHtml(currentPageSite.viewHint)}
${renderSummaryCard("ignored", `
${summary.ignoredCount || 0}
`, "忽略", filter)}
${renderSummaryCard("all", `
${summary.taskCount}
`, "总任务", filter)}
${renderSummaryCard("exercise", `
${summary.exerciseCount}
`, "考试", filter)}
${renderSummaryCard("common_homework", `
${summary.homeworkCount}
`, "图文", filter)}
${renderSummaryCard("classroom_experiment", `
${summary.experimentCount}
`, "实验", filter)}
${renderSummaryCard("danger", `
${summary.dangerCount}
`, "危险", filter)}
${filter !== "all" ? `正在查看:${escapeHtml(getFilterLabel(filter))}
` : ""}
`;
if (filter === "all" && dangerList.length) {
html += `危险提醒:发现 ${dangerList.length} 个 10 天内截止的未完成任务,请优先处理。
`;
html += renderSection("紧急任务", dangerList, "tg-section-urgent");
}
if (!filteredTasks.length) {
html += `当前筛选下没有任务。
`;
} else if (filter === "danger") {
html += renderSection("危险任务", filteredTasks, "tg-section-urgent");
} else if (filter === "ignored") {
html += renderSection("已忽略任务", ignoredTasks);
} else {
html += renderCourseGroups(viewModel.courseGroups, courseCollapseState, sectionCollapseState);
}
if (errors.length || warnings.length) {
html += `本次扫描:${errors.length} 个错误,${warnings.length} 个提醒。
需要排查时可以复制全部 JSON。
`;
}
html += ``;
body.innerHTML = html;
updateCountdowns();
ensureCountdownTimer();
latestRenderedScanTimestamp = Number(data.scanTimestamp) || latestRenderedScanTimestamp;
}
async function getAllJsonText() {
const state = await loadFrontendState();
return JSON.stringify(
{
result: state.result,
lastRunning: state.lastRunning,
lastError: state.lastError,
refreshRequest: state.refreshRequest,
refreshHandled: state.refreshHandled,
refreshStatus: state.refreshStatus,
panelState: state.panelState,
courseCollapse: state.courseCollapse,
sectionCollapse: state.sectionCollapse,
copiedAt: new Date().toLocaleString(),
pageUrl: location.href,
pageMode: isOpen() ? "panel-open" : "button-only"
},
null,
2
);
}
async function copyAllJson() {
const btn = document.querySelector("[data-copy-all-json]");
const text = await getAllJsonText();
try {
await navigator.clipboard.writeText(text);
btn.textContent = "已复制";
setTimeout(() => {
btn.textContent = "全部 JSON";
}, 1200);
} catch (e) {
showJsonBox(text);
btn.textContent = "手动复制";
setTimeout(() => {
btn.textContent = "全部 JSON";
}, 1500);
}
}
function showJsonBox(text) {
setDrawerOpen(true);
const boxWrap = document.querySelector(`#${DRAWER_ID} .tg-json-container`);
const box = document.querySelector(`#${DRAWER_ID} .tg-json-box`);
if (!boxWrap || !box) return;
box.value = text;
boxWrap.style.display = boxWrap.style.display === "none" ? "block" : "none";
box.focus();
box.select();
}
window.__tgTaskAssistant = {
render,
loadData,
loadRunningState,
loadLastError
};
saveAutoLoginIfDetected();
if (typeof GM_registerMenuCommand === "function") {
GM_registerMenuCommand("显示/请求刷新 TG任务助手", () => {
setDrawerOpen(true);
render();
});
}
window.addEventListener("resize", () => {
clearTimeout(resizeApplyTimer);
resizeApplyTimer = setTimeout(() => {
applyPanelState();
}, 120);
});
setTimeout(() => {
createRoot();
render();
}, 1000);
})();