Files
2026-10-14 17:25:35 +08:00

1432 lines
61 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN" data-page-node-id="iBRCKqOnf1XuoDj3pjXVgE">
<head data-page-node-id="kiAYJeD00uxkjFmY2beQpY">
<meta charset="UTF-8" data-page-node-id="SChAvSV9ibznYTivukK3X6">
<meta name="viewport" content="width=device-width, initial-scale=1" data-page-node-id="sIpgMlvOecH9LMR3Y1GMh8">
<title>课程表 · 智慧教育公共服务平台</title>
<style>
/* ============================================================
设计令牌(Design Tokens)— 与 AcaSyllabu 项目视觉语言对齐
============================================================ */
:root{
/* 品牌主色(对齐项目 $color-primary: #5979ea) */
--brand-50:#eef1fd; --brand-100:#dde4fb; --brand-400:#7d93ee;
--brand:#5979ea; --brand-600:#4560d4; --brand-800:#2f4191;
/* 中性色(对齐项目 $color-black / $color-dark / $color-pl 等) */
--ink-900:#2b2f38; --ink-700:#404042; --ink-600:#555;
--ink-400:#8c8f99; --ink-300:#a8abaf; --ink-200:#c9ccd2;
--line:#e9e9e9; --line-soft:#f0f1f3;
/* 背景 */
--bg-page:#f3f3f3; --bg-surface:#fff; --bg-subtle:#f9f9f9;
/* 语义色(对齐项目 $color-success / $color-warning / $color-error) */
--ok:#45b972; --warn:#e9b954; --err:#e8684a;
/* 学科色板 —— 每个色相 50 底色 / 200 描边 / 800 文字,全部通过
WCAG AA 4.5:1 正文对比度校验(见 design-notes.md 对比度表) */
--s-blue-50:#e6f1fb; --s-blue-200:#85b7eb; --s-blue-800:#0c447c;
--s-teal-50:#e1f5ee; --s-teal-200:#5dcaa5; --s-teal-800:#085041;
--s-purple-50:#eeedfe; --s-purple-200:#afa9ec; --s-purple-800:#3c3489;
--s-amber-50:#faeeda; --s-amber-200:#ef9f27; --s-amber-800:#633806;
--s-coral-50:#faece7; --s-coral-200:#f0997b; --s-coral-800:#712b13;
--s-green-50:#eaf3de; --s-green-200:#97c459; --s-green-800:#27500a;
--s-pink-50:#fbeaf0; --s-pink-200:#ed93b1; --s-pink-800:#72243e;
--s-cyan-50:#e4f4f8; --s-cyan-200:#6fc4d8; --s-cyan-800:#0b4c5c;
--s-gray-50:#f1efe8; --s-gray-200:#b4b2a9; --s-gray-800:#444441;
/* 间距(8pt 栅格 + 4px 半格) */
--sp-1:4px; --sp-2:8px; --sp-3:12px; --sp-4:16px;
--sp-5:20px; --sp-6:24px; --sp-8:32px; --sp-10:40px;
/* 圆角 */
--r-sm:6px; --r-md:8px; --r-lg:12px; --r-xl:16px;
/* 阴影(三级海拔体系) */
--sh-1:0 1px 2px rgba(43,47,56,.05);
--sh-2:0 4px 12px rgba(43,47,56,.08);
--sh-3:0 10px 28px rgba(43,47,56,.14);
/* 动效 */
--ease:cubic-bezier(.4,0,.2,1);
--dur-fast:150ms; --dur:220ms;
--header-h:56px;
--rail-w:60px;
--slot-h:76px;
--day-count:7;
--day-min:120px;
}
*,*::before,*::after{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{height:100%}
body{
margin:0;
min-width:1160px;
font-family:"PingFang SC","Microsoft YaHei","Helvetica Neue",Inter,system-ui,-apple-system,sans-serif;
font-size:14px;
line-height:1.6;
color:var(--ink-700);
background:var(--bg-page);
-webkit-font-smoothing:antialiased;
-moz-osx-font-smoothing:grayscale;
}
button{font-family:inherit;cursor:pointer}
ul{margin:0;padding:0;list-style:none}
/* 无障碍:键盘焦点环统一入口 */
:where(button,a,[tabindex],.course,.day-tab):focus-visible{
outline:2px solid var(--brand-600);
outline-offset:2px;
border-radius:var(--r-sm);
}
.sr-only{
position:absolute;width:1px;height:1px;margin:-1px;padding:0;
overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;
}
/* ============================================================
1. 顶部工具栏
============================================================ */
.app{display:flex;flex-direction:column;min-height:100%}
.topbar{
position:sticky;top:0;z-index:40;
display:flex;align-items:center;gap:var(--sp-3);
height:var(--header-h);padding:0 var(--sp-6);
background:var(--bg-surface);
border-bottom:1px solid var(--line);
}
.logo{display:flex;align-items:center;gap:var(--sp-3);flex:0 0 auto;padding-right:var(--sp-4);border-right:1px solid var(--line)}
.logo-mark{
display:grid;place-items:center;width:30px;height:30px;border-radius:var(--r-md);
background:var(--brand);color:#fff;font-weight:500;font-size:15px;letter-spacing:-.5px;
}
.logo-text{font-size:15px;font-weight:500;color:var(--ink-900);white-space:nowrap}
.toolbar{display:flex;align-items:center;gap:var(--sp-2);flex:1 1 auto;min-width:0}
.field{display:flex;align-items:center;gap:var(--sp-2);flex:0 0 auto}
.field > label{font-size:13px;color:var(--ink-400);white-space:nowrap}
.select{
height:32px;padding:0 30px 0 var(--sp-3);
font-size:13px;color:var(--ink-700);background:var(--bg-surface);
border:1px solid var(--ink-200);border-radius:var(--r-sm);
appearance:none;cursor:pointer;
background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' fill='none' stroke='%238c8f99' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat:no-repeat;background-position:right 10px center;
transition:border-color var(--dur-fast) var(--ease);
}
.select:hover{border-color:var(--brand-400)}
.toolbar-spacer{flex:1 1 auto;min-width:var(--sp-2)}
.segmented{display:inline-flex;padding:2px;background:var(--bg-subtle);border:1px solid var(--line);border-radius:var(--r-md);flex:0 0 auto}
.segmented button{
height:28px;padding:0 var(--sp-4);border:0;border-radius:var(--r-sm);
background:transparent;font-size:13px;color:var(--ink-600);
transition:background var(--dur-fast) var(--ease),color var(--dur-fast) var(--ease);
}
.segmented button:hover{color:var(--brand-600)}
.segmented button[aria-pressed="true"]{background:var(--bg-surface);color:var(--brand-600);font-weight:500;box-shadow:var(--sh-1)}
.icon-btn{
display:grid;place-items:center;width:32px;height:32px;flex:0 0 auto;
background:var(--bg-surface);border:1px solid var(--ink-200);border-radius:var(--r-sm);
color:var(--ink-600);
transition:border-color var(--dur-fast) var(--ease),color var(--dur-fast) var(--ease),
background var(--dur-fast) var(--ease);
}
.icon-btn:hover{border-color:var(--brand-400);color:var(--brand-600);background:var(--brand-50)}
/* ============================================================
2. 周次条
============================================================ */
.weekbar{
position:sticky;top:var(--header-h);z-index:30;
display:flex;align-items:center;gap:var(--sp-4);
padding:var(--sp-3) var(--sp-6);
background:var(--bg-surface);
border-bottom:1px solid var(--line);
}
.week-scroll{flex:1 1 auto;min-width:0;overflow-x:auto;scrollbar-width:thin}
.week-scroll::-webkit-scrollbar{height:4px}
.week-scroll::-webkit-scrollbar-thumb{background:var(--ink-200);border-radius:2px}
/* 20 周自适应铺满:flex 弹性 + min-width 下限;
窄屏先按 flex 收缩,触及下限后再由 .week-scroll 横向滚动,
保证 20 周始终完整可达、不出现被裁掉的“半截周” */
.week-list{display:flex;gap:var(--sp-1);padding:2px}
.week-list button{
flex:0 1 auto;min-width:50px;height:32px;padding:0 var(--sp-2);
border:1px solid transparent;border-radius:var(--r-sm);
background:transparent;font-size:13px;color:var(--ink-600);white-space:nowrap;
transition:background var(--dur-fast) var(--ease),color var(--dur-fast) var(--ease),
border-color var(--dur-fast) var(--ease);
}
.week-list button:hover{background:var(--brand-50);color:var(--brand-600)}
/* 选中态:实心品牌绿(周次是“导航”语义,与 brand 蓝的按钮态区分开) */
.week-list button[aria-current="true"]{
background:var(--brand);border-color:var(--brand);color:#fff;font-weight:500;
box-shadow:0 2px 6px rgba(89,121,234,.28);flex:0 0 auto;
}
.week-list button.is-today-week:not([aria-current="true"]){color:var(--ok);border-color:rgba(69,185,114,.45)}
.week-meta{flex:0 0 auto;font-size:13px;color:var(--ink-400);white-space:nowrap;font-variant-numeric:tabular-nums}
.week-meta b{color:var(--ink-700);font-weight:500}
/* ============================================================
3. 视图容器
============================================================ */
.views{flex:1 1 auto;padding:var(--sp-5) var(--sp-6) var(--sp-10)}
.view{display:none}
.view.is-active{display:block}
/* ============================================================
4. 周视图 · 课程网格
============================================================ */
/* 可用性下限:桌面端保证单日列 ≥ 104px(7 列 ≈ 780px),
小于该宽度时由 .grid-scroll 横向滚动,仍保持 7 天语义完整 */
.week-wrap{
display:grid;grid-template-columns:var(--rail-w) minmax(0,1fr);
min-width:calc(var(--rail-w) + var(--day-count) * var(--day-min) + var(--day-count) - 1);
background:var(--bg-surface);border:1px solid var(--line);
border-radius:var(--r-lg);box-shadow:var(--sh-1);overflow:hidden;
}
.grid-scroll{overflow-x:auto}
/* 表头行 */
.grid-head{display:grid;grid-template-columns:var(--rail-w) minmax(0,1fr);background:var(--bg-surface)}
.grid-head .corner{
display:grid;place-items:center;
height:68px;border-right:1px solid var(--line);border-bottom:1px solid var(--line);
background:var(--bg-surface);
}
.corner span{
font-size:12px;color:var(--ink-400);writing-mode:vertical-rl;letter-spacing:2px;
transform:rotate(180deg);
}
.day-heads{
display:grid;grid-template-columns:repeat(var(--day-count),1fr);
border-bottom:1px solid var(--line);
}
.day-head{
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;
height:68px;border-right:1px solid var(--line-soft);
}
.day-head:last-child{border-right:0}
.day-head .dow{font-size:13px;color:var(--ink-600)}
.day-head .dnum{font-size:19px;font-weight:500;color:var(--ink-900);font-variant-numeric:tabular-nums;line-height:1.2}
.day-head .dsub{font-size:11px;color:var(--ink-300)}
.day-head .dsub.is-today{
color:#fff;background:var(--brand);height:15px;line-height:15px;
padding:0 6px;border-radius:8px;font-weight:500;
}
.day-head.is-today{background:var(--brand-50)}
.day-head.is-today .dow,.day-head.is-today .dsub{color:var(--brand-600)}
.day-head.is-today .dnum{color:var(--brand-600)}
.day-head.is-weekend .dnum{color:var(--ink-400)}
.day-head.is-today .dnum::after{
content:"";display:block;width:20px;height:2px;margin:2px auto 0;
background:var(--brand);border-radius:1px;
}
/* 网格主体:左节次轴 + 右课程层,两列共用 --slot-h 行高 */
.grid-body{display:grid;grid-template-columns:var(--rail-w) minmax(0,1fr)}
.rail{border-right:1px solid var(--line);background:var(--bg-surface)}
.rail-item{
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;
height:var(--slot-h);border-bottom:1px solid var(--line-soft);
}
.rail-item:last-child{border-bottom:0}
.rail-item .n{font-size:14px;font-weight:500;color:var(--ink-700);font-variant-numeric:tabular-nums}
.rail-item .t{font-size:11px;color:var(--ink-300);font-variant-numeric:tabular-nums;letter-spacing:-.2px}
.rail-item.is-now{background:var(--brand-50)}
.rail-item.is-now .n{color:var(--brand-600)}
.rail-item.is-now .t{color:var(--brand-400)}
/* 用 7 等分比例代替 minmax(px,1fr):minmax 在「宽度由内容决定」的
父级里会让轨道取到最小像素值(7×min 撑爆容器→塌成 1 列)。
比例轨道只依赖容器宽度,再靠 .week-wrap 的 min-width 保证可用性。 */
.days{position:relative;min-width:0}
.days-bg{
display:grid;grid-template-columns:repeat(var(--day-count),1fr);
}
.days-bg .col{position:relative;border-right:1px solid var(--line-soft)}
.days-bg .col:last-child{border-right:0}
.days-bg .cell{height:var(--slot-h);border-bottom:1px solid var(--line-soft)}
.days-bg .col .cell:last-child{border-bottom:0}
.days-bg .col.is-today{background:rgba(89,121,234,.045)}
.days-bg .col.is-weekend .cell{background:rgba(43,47,56,.014)}
.days-bg .col.is-today .cell.is-now{background:rgba(89,121,234,.09)}
/* 课程层:与背景网格共用同一套行列尺寸,实现像素级对齐 */
.days-layer{
position:absolute;inset:0;
display:grid;grid-template-columns:repeat(var(--day-count),1fr);
pointer-events:none;
}
.day-track{position:relative;pointer-events:none}
/* 课程卡:绝对定位,top/height 由节次与节数换算 */
.course{
position:absolute;left:4px;right:4px;
display:flex;flex-direction:column;gap:1px;
padding:7px 9px;overflow:hidden;text-align:left;
border:1px solid var(--s-200,var(--ink-200));
border-left:3px solid var(--s-200,var(--ink-300));
border-radius:var(--r-sm);
background:var(--s-50,var(--bg-subtle));
color:var(--s-800,var(--ink-700));
pointer-events:auto;
transition:transform var(--dur-fast) var(--ease),box-shadow var(--dur-fast) var(--ease);
}
.course:hover{transform:translateY(-1px);box-shadow:var(--sh-2);z-index:5}
.course.is-selected{box-shadow:0 0 0 2px var(--brand),var(--sh-2);z-index:6}
.course .c-name{
font-size:13px;font-weight:500;line-height:1.25;color:var(--s-800,var(--ink-900));
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
}
.course .c-room,.course .c-teacher{
font-size:11px;line-height:1.35;color:var(--s-800,var(--ink-600));opacity:.78;
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.course .c-flag{
position:absolute;top:0;right:0;
height:16px;padding:0 6px;display:grid;place-items:center;
font-size:10px;font-weight:500;color:#fff;
border-bottom-left-radius:var(--r-sm);
background:var(--err);
}
.course[data-dense="1"] .c-teacher{display:none}
/* 当前时间指示线 */
.now-line{position:absolute;left:0;right:0;height:0;z-index:8;pointer-events:none;display:none}
.now-line.is-on{display:block}
.now-line::before{
content:"";position:absolute;left:0;right:0;top:0;height:2px;
background:var(--err);border-radius:1px;
}
.now-line::after{
content:"";position:absolute;left:-4px;top:-3px;width:8px;height:8px;
border-radius:50%;background:var(--err);
}
.now-line .now-tag{
position:absolute;right:0;top:-18px;height:16px;padding:0 6px;
display:grid;place-items:center;font-size:10px;font-weight:500;color:#fff;
background:var(--err);border-radius:var(--r-sm) var(--r-sm) 0 0;font-variant-numeric:tabular-nums;
}
/* 图例 */
.legend{
display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-2) var(--sp-5);
margin-top:var(--sp-4);padding:var(--sp-3) var(--sp-4);
background:var(--bg-surface);border:1px solid var(--line);border-radius:var(--r-md);
}
.legend .lg-title{font-size:13px;color:var(--ink-400)}
.legend .lg-item{display:inline-flex;align-items:center;gap:6px;font-size:13px;color:var(--ink-600)}
.legend .lg-dot{width:12px;height:12px;border-radius:3px;border:1px solid var(--s-200,var(--ink-200));background:var(--s-50,var(--bg-subtle))}
.legend .lg-sep{width:1px;height:14px;background:var(--line)}
/* ============================================================
5. 日视图
============================================================ */
/* 日视图:日期选择条 —— 与周视图共用同一周的日期,切换视图不丢上下文 */
.day-picker{
display:flex;align-items:center;gap:var(--sp-2);
margin-bottom:var(--sp-4);padding:var(--sp-3) var(--sp-4);
background:var(--bg-surface);border:1px solid var(--line);border-radius:var(--r-md);
}
.day-picker .dp-label{font-size:13px;color:var(--ink-400);margin-right:var(--sp-2)}
.day-picker .dp-list{display:flex;gap:var(--sp-2);flex:1 1 auto;min-width:0}
.day-tab{
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;
flex:1 1 0;min-width:64px;height:52px;
border:1px solid var(--line);border-radius:var(--r-sm);
background:var(--bg-surface);color:var(--ink-600);
transition:border-color var(--dur-fast) var(--ease),background var(--dur-fast) var(--ease),
color var(--dur-fast) var(--ease);
}
.day-tab:hover{border-color:var(--brand-400);color:var(--brand-600);background:var(--brand-50)}
.day-tab .dt-dow{font-size:12px;line-height:1.3}
.day-tab .dt-date{font-size:11px;color:var(--ink-300);font-variant-numeric:tabular-nums;line-height:1.3}
.day-tab[aria-selected="true"]{
background:var(--brand);border-color:var(--brand);color:#fff;font-weight:500;
}
.day-tab[aria-selected="true"] .dt-date{color:rgba(255,255,255,.82)}
.day-tab.is-today:not([aria-selected="true"]){border-color:rgba(89,121,234,.5);color:var(--brand-600)}
.day-tab.is-today:not([aria-selected="true"]) .dt-date{color:var(--brand-400)}
.day-wrap{
background:var(--bg-surface);border:1px solid var(--line);
border-radius:var(--r-lg);box-shadow:var(--sh-1);overflow:hidden;
}
.day-list{padding:var(--sp-2) 0}
.day-row{
display:grid;grid-template-columns:var(--rail-w) 78px minmax(0,1fr);
align-items:stretch;gap:var(--sp-4);padding:var(--sp-3) var(--sp-6);
border-bottom:1px solid var(--line-soft);
}
.day-row:last-child{border-bottom:0}
.day-row .dr-n{
display:grid;place-items:center;align-self:center;
width:36px;height:36px;justify-self:center;border-radius:50%;
background:var(--bg-subtle);color:var(--ink-600);
font-size:14px;font-weight:500;font-variant-numeric:tabular-nums;
}
.day-row .dr-time{
display:flex;flex-direction:column;justify-content:center;gap:2px;
font-size:12px;color:var(--ink-400);font-variant-numeric:tabular-nums;
}
.day-row .dr-time b{font-size:13px;font-weight:500;color:var(--ink-600)}
.day-row .dr-body{display:flex;align-items:center;gap:var(--sp-4);min-height:44px}
.day-row.is-empty .dr-body{color:var(--ink-300);font-size:13px}
.day-row.is-now{background:var(--brand-50)}
.day-row.is-now .dr-n{background:var(--brand);color:#fff}
.day-chip{
display:inline-flex;align-items:center;height:30px;padding:0 var(--sp-3);
border-radius:var(--r-sm);border:1px solid var(--s-200,var(--ink-200));
border-left:3px solid var(--s-200,var(--ink-300));
background:var(--s-50,var(--bg-subtle));color:var(--s-800,var(--ink-900));
font-size:13px;font-weight:500;
}
.day-meta{display:flex;flex-wrap:wrap;gap:var(--sp-4);font-size:13px;color:var(--ink-600)}
.day-meta span{display:inline-flex;align-items:center;gap:5px}
.day-meta .k{color:var(--ink-300)}
/* ============================================================
6. 课程详情侧栏
============================================================ */
.drawer{
position:fixed;top:0;right:0;z-index:60;
display:flex;flex-direction:column;
width:352px;height:100%;
background:var(--bg-surface);border-left:1px solid var(--line);
box-shadow:var(--sh-3);
transform:translateX(102%);
transition:transform var(--dur) var(--ease);
}
.drawer.is-open{transform:translateX(0)}
.drawer-head{
display:flex;align-items:flex-start;gap:var(--sp-3);
padding:var(--sp-5) var(--sp-6) var(--sp-4);
border-bottom:1px solid var(--line);
}
.drawer-head .dh-main{flex:1 1 auto;min-width:0}
.drawer-head .dh-kicker{font-size:12px;color:var(--ink-400);margin-bottom:2px}
.drawer-head h2{
margin:0;font-size:18px;font-weight:500;line-height:1.35;color:var(--ink-900);
word-break:break-word;
}
.drawer-body{flex:1 1 auto;overflow-y:auto;padding:var(--sp-5) var(--sp-6) var(--sp-8)}
.detail-swatch{
display:flex;align-items:center;gap:var(--sp-3);
padding:var(--sp-3) var(--sp-4);margin-bottom:var(--sp-5);
border-radius:var(--r-md);
border:1px solid var(--s-200,var(--ink-200));background:var(--s-50,var(--bg-subtle));
}
.detail-swatch .sw-bar{width:4px;height:34px;border-radius:2px;background:var(--s-200,var(--ink-300));flex:0 0 auto}
.detail-swatch .sw-txt{font-size:13px;color:var(--s-800,var(--ink-700));line-height:1.5}
.detail-swatch .sw-txt b{font-weight:500;display:block}
.dl{margin:0}
.dl .dl-row{
display:grid;grid-template-columns:74px minmax(0,1fr);gap:var(--sp-3);
padding:var(--sp-3) 0;border-bottom:1px dashed var(--line);
}
.dl .dl-row:last-child{border-bottom:0}
.dl dt{font-size:13px;color:var(--ink-400)}
.dl dd{margin:0;font-size:13px;color:var(--ink-700);word-break:break-word}
.dl dd .mono{font-family:ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;font-size:12.5px;letter-spacing:-.2px}
.pill{
display:inline-flex;align-items:center;height:20px;padding:0 8px;border-radius:10px;
font-size:11px;font-weight:500;
}
.pill.ok{background:rgba(69,185,114,.13);color:#2c7a4b}
.pill.warn{background:rgba(233,185,84,.2);color:#8a6412}
.pill.err{background:rgba(232,104,74,.14);color:#a8402a}
.pill.info{background:var(--brand-50);color:var(--brand-800)}
.drawer-foot{
display:flex;gap:var(--sp-3);padding:var(--sp-4) var(--sp-6);
border-top:1px solid var(--line);background:var(--bg-subtle);
}
.btn{
display:inline-flex;align-items:center;justify-content:center;gap:6px;
height:36px;padding:0 var(--sp-4);border-radius:var(--r-sm);
font-size:13px;font-weight:500;
border:1px solid var(--ink-200);background:var(--bg-surface);color:var(--ink-700);
transition:border-color var(--dur-fast) var(--ease),background var(--dur-fast) var(--ease),
color var(--dur-fast) var(--ease);
}
.btn:hover{border-color:var(--brand-400);color:var(--brand-600);background:var(--brand-50)}
.btn.primary{background:var(--brand);border-color:var(--brand);color:#fff}
.btn.primary:hover{background:var(--brand-600);border-color:var(--brand-600);color:#fff}
.btn.block{flex:1 1 auto}
.scrim{
position:fixed;inset:0;z-index:55;background:rgba(43,47,56,.28);
opacity:0;visibility:hidden;transition:opacity var(--dur) var(--ease),visibility var(--dur);
}
.scrim.is-open{opacity:1;visibility:visible}
/* ============================================================
7. 空状态 / 骨架 / 提示条
============================================================ */
.empty{
display:flex;flex-direction:column;align-items:center;gap:var(--sp-3);
padding:var(--sp-10) 0;color:var(--ink-400);text-align:center;
}
.empty .em-ico{
display:grid;place-items:center;width:64px;height:64px;border-radius:50%;
background:var(--bg-subtle);border:1px solid var(--line);color:var(--ink-300);
}
.empty .em-t{font-size:14px;color:var(--ink-600)}
.empty .em-s{font-size:13px}
.toast{
position:fixed;left:50%;bottom:36px;z-index:80;
transform:translate(-50%,10px);opacity:0;pointer-events:none;
padding:9px 18px;border-radius:var(--r-md);
background:rgba(43,47,56,.92);color:#fff;font-size:13px;
box-shadow:var(--sh-2);transition:opacity var(--dur) var(--ease),transform var(--dur) var(--ease);
}
.toast.is-on{opacity:1;transform:translate(-50%,0)}
/* ============================================================
8. 响应式
============================================================ */
@media (max-width:1320px){
:root{--slot-h:72px}
.views{padding-left:var(--sp-4);padding-right:var(--sp-4)}
.weekbar,.topbar{padding-left:var(--sp-4);padding-right:var(--sp-4)}
}
@media (max-width:1160px){
:root{--slot-h:68px}
.drawer{width:320px}
}
@media (max-width:960px){
.toolbar .field > label{display:none}
.week-meta{display:none}
}
@media (prefers-reduced-motion:reduce){
*{transition-duration:.01ms!important;animation-duration:.01ms!important}
}
/* ============================================================
9. 打印
============================================================ */
@media print{
body{background:#fff;min-width:0}
.topbar,.weekbar,.legend,.drawer,.scrim,.toast,.now-line{display:none!important}
.views{padding:0}
.week-wrap,.day-wrap{box-shadow:none;border-color:#ddd}
.app::before{
content:"课程表 · 大连女子高级中学 · 2026—2027 学年第一学期";
display:block;padding:0 0 12px;font-size:15px;font-weight:500;color:#000;
}
.course{transform:none!important;box-shadow:none!important;-webkit-print-color-adjust:exact;print-color-adjust:exact}
}
</style>
</head>
<body data-page-node-id="isXVivjeUuUNSaRF99cvrL">
<!-- 打印标题(仅打印可见) -->
<h1 class="sr-only" data-page-node-id="EIoEStPQeUZEVqIt485jsH">大连女子高级中学 2026—2027 学年第一学期课程表</h1>
<div class="app" data-page-node-id="jwhsloIIKJOprdE8sJoAMQ">
<!-- ========== 顶部工具栏 ========== -->
<header class="topbar" data-page-node-id="hwQ6PWJTZLCdCLKUP4hpzt">
<div class="logo" data-page-node-id="M8VtqaTjCGHJAwM2UxCcAv">
<span class="logo-mark" aria-hidden="true" data-page-node-id="IQ7Aw9EErXo3qnfyxYr8qu">课</span>
<span class="logo-text" data-page-node-id="9GQy2fAEmMQTzyWh6v61Ao">课程表</span>
</div>
<div class="toolbar" data-page-node-id="mkEVGjVuo5g7vVd1lImsNN">
<div class="field" data-page-node-id="qn9u3G9T7LyKGtu9Qz5Z3D">
<label for="sel-term" data-page-node-id="pUHxHj2Q30KYv1q3MGG7jO">学期</label>
<select class="select" id="sel-term" data-page-node-id="Nr0FSANInl2G1A14JpiATr">
<option value="2026-1" data-page-node-id="REdUBv5b2elc1SUwX9EEwd">2026—2027 学年第一学期</option>
<option value="2025-2" data-page-node-id="3MxYPO9D2pNaj4Hmz0RbQa">2025—2026 学年第二学期</option>
<option value="2025-1" data-page-node-id="3aXoGmpK3atWrHDus820Gy">2025—2026 学年第一学期</option>
</select>
</div>
<div class="field" data-page-node-id="qfRfr0kikd13gWEJMSn2R4">
<label for="sel-class" data-page-node-id="kv57266gLIw9uxVSCUn6lR">班级</label>
<select class="select" id="sel-class" data-page-node-id="gQ95cZ0y3XIGcZaMSYtGEG"></select>
</div>
<div class="toolbar-spacer" data-page-node-id="KTBaBY4ImBB7ZGJJ3UUUEp"></div>
<div class="segmented" role="group" aria-label="视图切换" data-page-node-id="nqbnJ5Y4vHBSKWFtMJgXR4">
<button type="button" data-view="week" aria-pressed="true" data-page-node-id="fCzTFPDUbHqGBeTChNdnYS">周视图</button>
<button type="button" data-view="day" aria-pressed="false" data-page-node-id="IEvBGMjMlLOkQc7CfR1KNA">日视图</button>
</div>
<button class="icon-btn" type="button" id="btn-today" title="回到本周" aria-label="回到本周" data-page-node-id="wXxvTPsQvMuewyKf4HqfBX">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" data-page-node-id="HIKaBT2aTK9X7YjqyENarY">
<rect x="2" y="3" width="12" height="11" rx="2" data-page-node-id="3f1vkG9rM4WjD0gQ8Uo69Y"/><path d="M2 6.5h12M5.5 1.8v2.4M10.5 1.8v2.4" data-page-node-id="RVGT2TWP4RyNp2L4HZijw8"/>
</svg>
</button>
<button class="icon-btn" type="button" id="btn-print" title="打印课程表" aria-label="打印课程表" data-page-node-id="Dnrj5OeZScnCv1Dmc5cFdm">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" data-page-node-id="BQksS3elJt6fOIzGoUfBKd">
<path d="M4.5 5.5V2h7v3.5M4.5 12H3a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-1.5" data-page-node-id="oOBND7aqjRzdkNeLamDUvJ"/>
<rect x="4.5" y="10" width="7" height="4" rx=".5" data-page-node-id="daY2TKJFyAo70fijFcVZ3W"/>
</svg>
</button>
</div>
</header>
<!-- ========== 周次条 ========== -->
<div class="weekbar" data-page-node-id="tYdea3BpTMMASnxkMJ0B9P">
<span class="week-meta" data-page-node-id="gZupEXM3cbGKQ1P1TYPQLM">当前:<b id="meta-class" data-page-node-id="8AVdp7SsD7eb76MPlwZoAD">高一(1)班</b> · <b id="meta-term" data-page-node-id="C3Ki0SmW86nIIufnJ1XqYp">2026—2027 学年第一学期</b></span>
<div class="week-scroll" data-page-node-id="hREahQuzomU3Z88U6trelf">
<div class="week-list" id="week-list" role="group" aria-label="切换周次" data-page-node-id="aXaCy3bK7orOolXCFYeJS0"></div>
</div>
<span class="week-meta" data-page-node-id="6lrlIvArqs8LkDhBA2Zkk3"><b id="meta-week" data-page-node-id="F0hYovxsomtTjmMjugzNTM">第 6 周</b> · <span id="meta-range" data-page-node-id="wA2SwaSbRlNvTfjRiakdSY">—</span></span>
</div>
<!-- ========== 视图区 ========== -->
<main class="views" data-page-node-id="TkqtPfm8Le8CDVOZwABJDp">
<!-- 周视图 -->
<section class="view is-active" id="view-week" aria-label="周课程表" data-page-node-id="EqNlvFPQp0RPXxh2rFShD1">
<div class="week-wrap" data-page-node-id="s7fHjGrb8Xt0r96qMn1n64">
<div class="grid-head" data-page-node-id="cO1zt9tjhUR9eV8kBczECU">
<div class="corner" data-page-node-id="kqMGwTMjntyPu9Luq8YKdu"><span data-page-node-id="uAT4N9cDuHZquD2QsT6H23">节次 / 星期</span></div>
<div class="day-heads" id="day-heads" data-page-node-id="YfVhvAlWj4HA1ehEeYJpfD"></div>
</div>
<div class="grid-scroll" data-page-node-id="2MXxhLHR5dWUZmBfODJca4">
<div class="grid-body" data-page-node-id="SUshbqUYuIWFw6Gg47g2pB">
<div class="rail" id="rail" data-page-node-id="jBn8cAJQagdgWYNlpABXKi"></div>
<div class="days" data-page-node-id="GJ5fmHgQL5Gf7o0CWcmmZa">
<div class="days-bg" id="days-bg" data-page-node-id="EcBLBu69xvWjTNu4iv9vYr"></div>
<div class="days-layer" id="days-layer" data-page-node-id="yIrrD7AIz7Md1WqCXRjKgI"></div>
<div class="now-line" id="now-line" data-page-node-id="mGDCbdNYljAp3mwLS32HeU"><span class="now-tag" id="now-tag" data-page-node-id="XQgiOK3Q4lBLuuDk4Bv70F"></span></div>
</div>
</div>
</div>
</div>
<div class="legend" id="legend" data-page-node-id="39lexbVQVqNDD6iHetBZfo">
<span class="lg-title" data-page-node-id="PEZLQESHmuTDp3nqCG96zG">学科</span>
</div>
</section>
<!-- 日视图 -->
<section class="view" id="view-day" aria-label="日课程表" data-page-node-id="tviiFTpcvSH9urwhC6qdMl">
<div class="day-picker" data-page-node-id="a3L3nO1oveDbQbGBPsxf6C">
<span class="dp-label" data-page-node-id="UClVaJ4Hi5vlRABW2Vc5xi">选择日期</span>
<div class="dp-list" id="day-picker" role="tablist" aria-label="选择星期" data-page-node-id="hlGdiZfKOrQFBt3kes3DO9"></div>
</div>
<div class="day-wrap" data-page-node-id="7LH8R6J0r8ALFo10qUghDv">
<div class="day-list" id="day-list" data-page-node-id="VD8KxJKgG10pmHRyjuDXy1"></div>
</div>
</section>
</main>
</div>
<!-- ========== 课程详情侧栏 ========== -->
<div class="scrim" id="scrim" data-page-node-id="VcfeEiU7TBgLIcIsDJlY4v"></div>
<aside class="drawer" id="drawer" role="dialog" aria-modal="true" aria-labelledby="drawer-title" aria-hidden="true" data-page-node-id="RUuHOKI3ZT3Nn8b5iqhCcc">
<div class="drawer-head" data-page-node-id="gI0RiHDUE1LRLKnyakCd4j">
<div class="dh-main" data-page-node-id="aBR2xAakdX35xubnYd4lgr">
<div class="dh-kicker" id="drawer-kicker" data-page-node-id="BwNGhcyMRz9xDZ7zLcFlwY">课程详情</div>
<h2 id="drawer-title" data-page-node-id="7Cd6jl0D1adXOuKp8YcQRc">—</h2>
</div>
<button class="icon-btn" type="button" id="drawer-close" aria-label="关闭详情" data-page-node-id="fednplGcJFuxjAzrER9FfS">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true" data-page-node-id="DN6XTeLhXJPFO3kKolU3kv">
<path d="M4 4l8 8M12 4l-8 8" data-page-node-id="nWaH6iwLTn0gWt1lutp8Hl"/>
</svg>
</button>
</div>
<div class="drawer-body" data-page-node-id="hFZZ2Gz3wi9NELBRyp0oH0">
<div class="detail-swatch" id="detail-swatch" data-page-node-id="JJnugvARXkGTrKQFeaZ7Ag">
<span class="sw-bar" aria-hidden="true" data-page-node-id="sYuic97DOiFey32oH8kfMZ"></span>
<span class="sw-txt" data-page-node-id="wvkpfd8iIsgOr8KqDLWwQ9"><b id="sw-title" data-page-node-id="E18FRKqbGT5hdmFrtfMv7A">—</b><span id="sw-sub" data-page-node-id="MX9FOe6UC5XSGbYjMQgvUQ">—</span></span>
</div>
<dl class="dl" id="detail-list" data-page-node-id="0OYRC4URXtuLP9VLdjmowm"></dl>
</div>
<div class="drawer-foot" data-page-node-id="isaxOriJrW8EVFoeegOI2D">
<button class="btn primary block" type="button" id="btn-swap" data-page-node-id="R0Rn8MkaFqwwOHC869Txyu">申请调课</button>
<button class="btn" type="button" id="btn-export" data-page-node-id="GFNWEoznImBeOHKG2LT5Fy">导出本周</button>
</div>
</aside>
<div class="toast" id="toast" role="status" aria-live="polite" data-page-node-id="Uyfbw6Zd4ElsRFqivSFVTt"></div>
<script>
"use strict";
/* ============================================================
课程表 · 演示数据层
真实环境由后端接口返回课程集合;此处内置一份可直接运行的
样例数据(高一/1 班、高二/1 班、高二/3 班),数据结构保持
与接口契约一致,替换 DATA 即可对接。
============================================================ */
var TERM_START = "2026-09-01"; // 学期第一周的周一
var TOTAL_WEEKS = 20;
var NOW = new Date(2026, 9, 10); // 演示基准日:2026-10-10(周六)
var SLOTS = [
{ n: 1, t: "08:00\n08:40" },
{ n: 2, t: "08:50\n09:30" },
{ n: 3, t: "09:55\n10:35" },
{ n: 4, t: "10:45\n11:25" },
{ n: 5, t: "13:30\n14:10" },
{ n: 6, t: "14:20\n15:00" },
{ n: 7, t: "15:25\n16:05" },
{ n: 8, t: "16:15\n16:55" }
];
/* 学科 → 色板令牌后缀。色相固定,保证同一学科跨周次配色一致 */
var SUBJECTS = {
"语文": "blue", "数学": "coral", "英语": "teal", "物理": "purple",
"化学": "amber", "生物": "green", "思想政治": "pink", "历史": "cyan",
"地理": "amber", "体育": "green", "信息技术": "cyan", "音乐": "pink",
"美术": "purple", "心理健康": "teal", "通用技术": "gray", "自习": "gray"
};
/* 课程记录:[星期索引0-6, 起始节次1-8, 连堂节数, 学科, 教师, 教室, 备注] */
var DATA = {
"2026-1": {
"高一(1)班": [
[0,1,1,"语文","马晓雯","实验楼 A203",""],
[0,2,1,"语文","马晓雯","实验楼 A203",""],
[0,3,1,"数学","陈立群","实验楼 A203",""],
[0,4,1,"数学","陈立群","实验楼 A203",""],
[0,5,1,"英语","李佩珊","实验楼 A203",""],
[0,6,1,"英语","李佩珊","实验楼 A203",""],
[0,7,1,"体育","孙立波","田径场",""],
[0,8,1,"自习","马晓雯","实验楼 A203","晚辅导"],
[1,1,1,"英语","李佩珊","实验楼 A203",""],
[1,2,1,"英语","李佩珊","实验楼 A203",""],
[1,3,1,"物理","周锦程","物理实验室 B102",""],
[1,4,1,"物理","周锦程","物理实验室 B102",""],
[1,5,1,"思想政治","王瑞琳","实验楼 A203",""],
[1,6,1,"历史","董书涵","实验楼 A203",""],
[1,7,2,"信息技术","张一鸣","机房 C301","上机"],
[2,1,1,"数学","陈立群","实验楼 A203",""],
[2,2,1,"数学","陈立群","实验楼 A203",""],
[2,3,1,"语文","马晓雯","实验楼 A203",""],
[2,4,1,"语文","马晓雯","实验楼 A203",""],
[2,5,1,"化学","何静雯","化学实验室 B105",""],
[2,6,1,"化学","何静雯","化学实验室 B105",""],
[2,7,1,"音乐","柳安然","音乐教室 D201",""],
[2,8,1,"自习","陈立群","实验楼 A203",""],
[3,1,1,"物理","周锦程","物理实验室 B102",""],
[3,2,1,"物理","周锦程","物理实验室 B102",""],
[3,3,1,"英语","李佩珊","实验楼 A203",""],
[3,4,1,"英语","李佩珊","实验楼 A203",""],
[3,5,1,"数学","陈立群","实验楼 A203",""],
[3,6,1,"语文","马晓雯","实验楼 A203",""],
[3,7,1,"体育","孙立波","体育馆",""],
[3,8,1,"自习","马晓雯","实验楼 A203",""],
[4,1,1,"语文","马晓雯","实验楼 A203",""],
[4,2,1,"语文","马晓雯","实验楼 A203",""],
[4,3,1,"数学","陈立群","实验楼 A203",""],
[4,4,1,"数学","陈立群","实验楼 A203",""],
[4,5,1,"生物","郑文清","生物实验室 B108",""],
[4,6,1,"生物","郑文清","生物实验室 B108",""],
[4,7,1,"美术","苏婉宁","美术教室 D203",""],
[4,8,1,"自习","李佩珊","实验楼 A203",""]
],
"高二(1)班": [
[0,1,1,"英语","赵晓彤","教学楼 A105",""],
[0,2,1,"英语","赵晓彤","教学楼 A105",""],
[0,3,2,"化学","何静雯","化学实验室 B105",""],
[0,5,1,"语文","唐雨柔","教学楼 A105",""],
[0,6,1,"语文","唐雨柔","教学楼 A105",""],
[0,7,1,"数学","严家伟","教学楼 A105",""],
[0,8,1,"自习","严家伟","教学楼 A105",""],
[1,1,1,"数学","严家伟","教学楼 A105",""],
[1,2,1,"数学","严家伟","教学楼 A105",""],
[1,3,1,"物理","周锦程","物理实验室 B102",""],
[1,4,1,"物理","周锦程","物理实验室 B102",""],
[1,5,1,"地理","白凌峰","教学楼 A105",""],
[1,6,1,"历史","董书涵","教学楼 A105",""],
[1,7,2,"通用技术","张一鸣","机房 C301",""],
[2,1,1,"语文","唐雨柔","教学楼 A105",""],
[2,2,1,"语文","唐雨柔","教学楼 A105",""],
[2,3,1,"英语","赵晓彤","教学楼 A105",""],
[2,4,1,"英语","赵晓彤","教学楼 A105",""],
[2,5,2,"生物","郑文清","生物实验室 B108",""],
[2,7,1,"体育","孙立波","田径场",""],
[2,8,1,"自习","唐雨柔","教学楼 A105",""],
[3,1,1,"数学","严家伟","教学楼 A105",""],
[3,2,1,"数学","严家伟","教学楼 A105",""],
[3,3,1,"化学","何静雯","化学实验室 B105",""],
[3,4,1,"化学","何静雯","化学实验室 B105",""],
[3,5,1,"思想政治","王瑞琳","教学楼 A105",""],
[3,6,1,"英语","赵晓彤","教学楼 A105",""],
[3,7,1,"体育","孙立波","体育馆",""],
[3,8,1,"自习","赵晓彤","教学楼 A105",""],
[4,1,1,"物理","周锦程","物理实验室 B102",""],
[4,2,1,"物理","周锦程","物理实验室 B102",""],
[4,3,1,"语文","唐雨柔","教学楼 A105",""],
[4,4,1,"数学","严家伟","教学楼 A105",""],
[4,5,1,"化学","何静雯","化学实验室 B105",""],
[4,6,1,"历史","董书涵","教学楼 A105",""],
[4,7,1,"心理健康","姜若彤","心理辅导室 D105",""],
[4,8,1,"自习","严家伟","教学楼 A105",""]
],
"高二(3)班": [
[0,1,1,"数学","严家伟","教学楼 A108",""],
[0,2,1,"数学","严家伟","教学楼 A108",""],
[0,3,1,"英语","赵晓彤","教学楼 A108",""],
[0,4,1,"英语","赵晓彤","教学楼 A108",""],
[0,5,2,"物理","周锦程","物理实验室 B102",""],
[0,7,1,"语文","唐雨柔","教学楼 A108",""],
[0,8,1,"自习","严家伟","教学楼 A108",""],
[1,1,1,"语文","唐雨柔","教学楼 A108",""],
[1,2,1,"语文","唐雨柔","教学楼 A108",""],
[1,3,1,"化学","何静雯","化学实验室 B105",""],
[1,4,1,"化学","何静雯","化学实验室 B105",""],
[1,5,1,"生物","郑文清","生物实验室 B108",""],
[1,6,1,"生物","郑文清","生物实验室 B108",""],
[1,7,1,"地理","白凌峰","教学楼 A108",""],
[1,8,1,"自习","赵晓彤","教学楼 A108",""],
[2,1,1,"英语","赵晓彤","教学楼 A108",""],
[2,2,1,"英语","赵晓彤","教学楼 A108",""],
[2,3,1,"数学","严家伟","教学楼 A108",""],
[2,4,1,"数学","严家伟","教学楼 A108",""],
[2,5,1,"语文","唐雨柔","教学楼 A108",""],
[2,6,1,"历史","董书涵","教学楼 A108",""],
[2,7,2,"体育","孙立波","田径场",""],
[3,1,1,"物理","周锦程","物理实验室 B102",""],
[3,2,1,"物理","周锦程","物理实验室 B102",""],
[3,3,1,"语文","唐雨柔","教学楼 A108",""],
[3,4,1,"语文","唐雨柔","教学楼 A108",""],
[3,5,1,"数学","严家伟","教学楼 A108",""],
[3,6,1,"英语","赵晓彤","教学楼 A108",""],
[3,7,1,"思想政治","王瑞琳","教学楼 A108",""],
[3,8,1,"自习","唐雨柔","教学楼 A108",""],
[4,1,1,"化学","何静雯","化学实验室 B105",""],
[4,2,1,"化学","何静雯","化学实验室 B105",""],
[4,3,1,"数学","严家伟","教学楼 A108",""],
[4,4,1,"英语","赵晓彤","教学楼 A108",""],
[4,5,1,"语文","唐雨柔","教学楼 A108",""],
[4,6,1,"生物","郑文清","生物实验室 B108",""],
[4,7,1,"体育","孙立波","体育馆",""],
[4,8,1,"自习","严家伟","教学楼 A108",""]
]
}
};
/* 各班级的每周课程(按周次覆盖示例:第 5 周有调课冲突) */
var OVERRIDES = {
"2026-1|高一(1)班|5": [
{ day: 2, slot: 3, status: "changed", note: "与数学互换", teacher: "周锦程" }
],
"2026-1|高一(1)班|11": [
{ day: 3, slot: 5, status: "cancel", note: "期中考试周,停课" }
],
"2026-1|高二(1)班|5": [
{ day: 1, slot: 7, status: "changed", note: "机房检修,调至周五 7 节" }
]
};
/* ============================================================
工具函数
============================================================ */
var DOW = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
var enc = encodeURIComponent;
function pad2(n) { return (n < 10 ? "0" : "") + n; }
function isSameDay(a, b) {
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
}
function startOfDay(d) { return new Date(d.getFullYear(), d.getMonth(), d.getDate()); }
function addDays(d, n) {
var x = new Date(d.getTime());
x.setDate(x.getDate() + n);
return x;
}
function parseDate(s) {
var p = s.split("-");
return new Date(+p[0], +p[1] - 1, +p[2]);
}
function fmtCN(d) { return (d.getMonth() + 1) + "月" + d.getDate() + "日"; }
function fmtMD(d) {
return pad2(d.getMonth() + 1) + "-" + pad2(d.getDate());
}
/* 第 n 周(1 起)的周一,一周固定 7 天 */
function weekMonday(week) {
return addDays(parseDate(TERM_START), (week - 1) * 7);
}
/* 依据基准日反推“当前是第几周”,超出范围时夹取到合法区间 */
function currentWeek() {
var diff = Math.floor((startOfDay(NOW) - startOfDay(parseDate(TERM_START))) / 86400000);
var w = Math.floor(diff / 7) + 1;
return Math.min(Math.max(w, 1), TOTAL_WEEKS);
}
/* 某一节的起止分钟数,用于定位当前时间指示线 */
function slotMinutes(slot, which) {
var t = SLOTS[slot - 1].t.split("\n")[which].split(":");
return (+t[0]) * 60 + (+t[1]);
}
/* 日视图默认落在有课的那天:
若“今天”在本周且当天有课 → 用今天;否则取本周第一个有课的星期,
一周内全无课时退回周一,避免日视图默认展示空列表 */
function resolveDay() {
var mon = weekMonday(state.week);
var todayIdx = NOW.getDay() === 0 ? 6 : NOW.getDay() - 1;
if (isSameDay(addDays(mon, todayIdx), NOW) &&
lessons().some(function (r) { return r[0] === todayIdx; })) {
state.day = todayIdx;
return;
}
for (var i = 0; i < 7; i++) {
if (lessons().some(function (r) { return r[0] === i; })) { state.day = i; return; }
}
state.day = 0;
}
/* ============================================================
状态
============================================================ */
var state = {
term: "2026-1",
klass: "高一(1)班",
week: currentWeek(),
view: "week",
day: 0, // 由 resolveDay() 在启动时落到「今天 / 第一个有课日」
selected: null
};
var el = {
selTerm: document.getElementById("sel-term"),
selClass: document.getElementById("sel-class"),
weekList: document.getElementById("week-list"),
weekScroll: document.querySelector(".week-scroll"),
metaClass: document.getElementById("meta-class"),
metaTerm: document.getElementById("meta-term"),
metaRange: document.getElementById("meta-range"),
metaWeek: document.getElementById("meta-week"),
dayHeads: document.getElementById("day-heads"),
rail: document.getElementById("rail"),
daysBg: document.getElementById("days-bg"),
daysLayer: document.getElementById("days-layer"),
dayList: document.getElementById("day-list"),
dayPicker: document.getElementById("day-picker"),
legend: document.getElementById("legend"),
nowLine: document.getElementById("now-line"),
nowTag: document.getElementById("now-tag"),
drawer: document.getElementById("drawer"),
scrim: document.getElementById("scrim"),
toast: document.getElementById("toast")
};
/* 取当前学期的班级列表 */
function classList() {
var t = DATA[state.term] || {};
return Object.keys(t);
}
/* 取当前学期 + 班级的课程数组 */
function lessons() {
var t = DATA[state.term] || {};
return t[state.klass] || [];
}
/* 取某班级包含的学科,用于图例 */
function subjectList() {
var seen = {};
var out = [];
lessons().forEach(function (r) {
if (!seen[r[3]]) { seen[r[3]] = 1; out.push(r[3]); }
});
return out;
}
/* 学科 → 色板变量(--s-blue-50 / --s-blue-200 / --s-blue-800) */
function paletteOf(subject) {
var hue = SUBJECTS[subject] || "gray";
return {
fill: "var(--s-" + hue + "-50)",
line: "var(--s-" + hue + "-200)",
text: "var(--s-" + hue + "-800)"
};
}
/* 应用周次覆盖(调课 / 停课) */
function overrideMap() {
var key = state.term + "|" + state.klass + "|" + state.week;
var list = OVERRIDES[key] || [];
var map = {};
list.forEach(function (o) { map[o.day + "-" + o.slot] = o; });
return map;
}
/* ============================================================
渲染 · 周次条
============================================================ */
function renderWeeks() {
var input = "";
for (var i = 1; i <= TOTAL_WEEKS; i++) {
input += '<button type="button" data-week="' + i + '" aria-current="false">第 ' + i + "周</button>";
}
el.weekList.innerHTML = input;
var cw = currentWeek();
Array.prototype.forEach.call(el.weekList.children, function (b) {
var n = +b.dataset.week;
b.setAttribute("aria-current", String(n === state.week));
b.setAttribute("aria-label", "第 " + n + " 周" + (n === cw ? "(本周)" : ""));
b.classList.toggle("is-today-week", n === cw);
});
var mon = weekMonday(state.week);
el.metaRange.textContent = fmtMD(mon) + " ~ " + fmtMD(addDays(mon, 6));
el.metaWeek.textContent = "第 " + state.week + " 周";
// 当前周自动滚入视野中部,避免 20 周条里“选中项在屏外”
var cur = el.weekList.children[state.week - 1];
if (cur && !el.weekList.dataset.scrolled) {
var sc = el.weekScroll;
sc.scrollLeft = Math.max(0, cur.offsetLeft - sc.clientWidth / 2 + cur.offsetWidth / 2);
el.weekList.dataset.scrolled = "1";
}
}
/* ============================================================
渲染 · 表头(星期 + 日期)
============================================================ */
function renderHeads() {
var mon = weekMonday(state.week);
var html = "";
for (var i = 0; i < 7; i++) {
var d = addDays(mon, i);
var today = isSameDay(d, NOW);
var cls = "day-head" +
(today ? " is-today" : "") +
(i >= 5 ? " is-weekend" : "");
html += '<div class="' + cls + '">' +
'<span class="dow">' + DOW[i] + '</span>' +
'<span class="dnum">' + pad2(d.getDate()) + '</span>' +
'<span class="dsub' + (today ? " is-today" : "") + '">' + (today ? "今天" : fmtCN(d)) + '</span>' +
'</div>';
}
el.dayHeads.innerHTML = html;
}
/* ============================================================
渲染 · 节次轴
============================================================ */
function renderRail() {
var nowSlot = nowSlotIndex();
var html = "";
for (var i = 0; i < SLOTS.length; i++) {
var s = SLOTS[i];
var t = s.t.split("\n");
html += '<div class="rail-item' + (i + 1 === nowSlot ? " is-now" : "") + '">' +
'<span class="n">' + s.n + '</span>' +
'<span class="t">' + t[0] + '</span>' +
'<span class="t">' + t[1] + '</span>' +
'</div>';
}
el.rail.innerHTML = html;
}
/* 判断基准日当天正处于第几节(不在任何节次内返回 0) */
function nowSlotIndex() {
var dayIdx = NOW.getDay() === 0 ? 6 : NOW.getDay() - 1;
var todayCol = addDays(weekMonday(state.week), dayIdx);
if (!isSameDay(todayCol, NOW)) return 0;
var m = NOW.getHours() * 60 + NOW.getMinutes();
for (var i = 0; i < SLOTS.length; i++) {
if (m >= slotMinutes(i + 1, 0) && m < slotMinutes(i + 1, 1)) return i + 1;
}
return 0;
}
/* ============================================================
渲染 · 背景网格 + 课程层
============================================================ */
function renderGrid() {
var mon = weekMonday(state.week);
var colHtml = "";
for (var d = 0; d < 7; d++) {
var date = addDays(mon, d);
var today = isSameDay(date, NOW);
var cls = "col" + (today ? " is-today" : "") + (d >= 5 ? " is-weekend" : "");
colHtml += '<div class="' + cls + '" data-day="' + d + '">';
for (var s = 0; s < SLOTS.length; s++) {
var isNow = today && nowSlotIndex() === s + 1;
colHtml += '<div class="cell' + (isNow ? " is-now" : "") + '"></div>';
}
colHtml += "</div>";
}
el.daysBg.innerHTML = colHtml;
var ov = overrideMap();
var layerHtml = "";
for (var dd = 0; dd < 7; dd++) {
layerHtml += '<div class="day-track">' + courseBlocks(dd, ov) + "</div>";
}
el.daysLayer.innerHTML = layerHtml;
// 当前时间指示线只在“本周”展示
var todayIdx = NOW.getDay() === 0 ? 6 : NOW.getDay() - 1;
var inWeek = isSameDay(addDays(mon, todayIdx), NOW);
if (inWeek) {
var gap = 8; // 课间 10 分钟按视觉比例折算
var parts = SLOTS[todayIdx >= 0 ? 0 : 0].t.split("\n");
var m0 = slotMinutes(1, 0);
var offsetTop = 0;
var m = NOW.getHours() * 60 + NOW.getMinutes();
var found = false;
for (var i2 = 0; i2 < SLOTS.length; i2++) {
var a = slotMinutes(i2 + 1, 0), b = slotMinutes(i2 + 1, 1);
if (m < a) {
var prevEnd = i2 === 0 ? a : slotMinutes(i2, 1);
var frac = Math.max(0, Math.min(1, (m - prevEnd) / Math.max(1, a - prevEnd)));
offsetTop = i2 * (slotHeightPx() + gap) + frac * gap;
found = true; break;
}
if (m >= a && m < b) {
var f = (m - a) / Math.max(1, b - a);
offsetTop = i2 * (slotHeightPx() + gap) + f * slotHeightPx();
found = true; break;
}
}
if (!found) offsetTop = SLOTS.length * (slotHeightPx() + gap);
el.nowLine.style.top = Math.round(offsetTop) + "px";
el.nowLine.style.left = "var(--rail-w)";
el.nowTag.textContent = pad2(NOW.getHours()) + ":" + pad2(NOW.getMinutes());
el.nowLine.classList.add("is-on");
} else {
el.nowLine.classList.remove("is-on");
}
}
/* 读取 CSS 变量中的节次高度,保证 JS 计算与实际渲染一致 */
function slotHeightPx() {
var v = getComputedStyle(document.documentElement).getPropertyValue("--slot-h").trim();
return parseFloat(v) || 76;
}
/* 一天内所有课程块 */
function courseBlocks(dayIdx, ov) {
var list = lessons().filter(function (r) { return r[0] === dayIdx; });
var html = "";
list.forEach(function (r) {
var slot = r[1], span = r[2], subject = r[3], teacher = r[4], room = r[5], note = r[6];
var o = ov[dayIdx + "-" + slot];
var p = paletteOf(subject);
var top = (slot - 1) * slotHeightPx();
var height = span * slotHeightPx() - 4;
var dense = height < 62 ? "1" : "0";
var flag = "";
if (o && o.status === "changed") flag = '<span class="c-flag">调课</span>';
if (o && o.status === "cancel") flag = '<span class="c-flag">停课</span>';
html += '<button type="button" class="course" data-dense="' + dense + '"' +
' style="--s-50:' + p.fill + ';--s-200:' + p.line + ';--s-800:' + p.text +
';top:' + top + "px;height:" + height + 'px"' +
' data-day="' + dayIdx + '" data-slot="' + slot + '"' +
' aria-label="' + subject + ',' + DOW[dayIdx] + '第 ' + slot + '节,' + teacher + ',' + room + '">' +
flag +
'<span class="c-name">' + subject + '</span>' +
'<span class="c-room">' + room + '</span>' +
'<span class="c-teacher">' + teacher + (span > 1 ? " · 连堂" + span + "节" : "") + '</span>' +
'</button>';
});
return html;
}
/* ============================================================
渲染 · 图例
============================================================ */
function renderLegend() {
var subs = subjectList();
var html = '<span class="lg-title">学科</span>';
subs.forEach(function (s) {
var p = paletteOf(s);
html += '<span class="lg-item"><span class="lg-dot" style="--s-50:' + p.fill +
';--s-200:' + p.line + '"></span>' + s + "</span>";
});
html += '<span class="lg-sep" aria-hidden="true"></span>' +
'<span class="lg-item"><span class="pill err">调课</span>本周有调整</span>' +
'<span class="lg-item"><span class="pill warn">停课</span>临时停课</span>';
el.legend.innerHTML = html;
}
/* ============================================================
渲染 · 日视图
============================================================ */
/* 日期选择条:与周视图共用同一周的日期,切换视图不丢上下文 */
function renderDayPicker() {
var mon = weekMonday(state.week);
var html = "";
for (var i = 0; i < 7; i++) {
var d = addDays(mon, i);
var has = lessons().some(function (r) { return r[0] === i; });
var cls = "day-tab" + (isSameDay(d, NOW) ? " is-today" : "");
html += '<button type="button" class="' + cls + '" role="tab" data-day-idx="' + i + '"' +
' aria-selected="' + (i === state.day) + '"' +
' aria-label="' + DOW[i] + " " + fmtCN(d) + (has ? "" : ",无课程") + '">' +
'<span class="dt-dow">' + DOW[i] + (isSameDay(d, NOW) ? " · 今" : "") + "</span>" +
'<span class="dt-date">' + fmtMD(d) + "</span>" +
"</button>";
}
el.dayPicker.innerHTML = html;
}
function renderDay() {
renderDayPicker();
var mon = weekMonday(state.week);
var date = addDays(mon, state.day);
var today = isSameDay(date, NOW);
var list = lessons().filter(function (r) { return r[0] === state.day; });
var bySlot = {};
list.forEach(function (r) {
for (var k = 0; k < r[2]; k++) bySlot[r[1] + k] = r;
});
var ov = overrideMap();
var nowS = nowSlotIndex();
var html = "";
for (var i = 1; i <= SLOTS.length; i++) {
var r = bySlot[i];
var isNow = today && nowS === i;
var t = SLOTS[i - 1].t.split("\n");
html += '<div class="day-row' + (r ? "" : " is-empty") + (isNow ? " is-now" : "") + '">';
html += '<span class="dr-n">' + i + '</span>';
html += '<span class="dr-time"><b>' + t[0] + "</b><span>" + t[1] + "</span></span>";
if (!r) {
html += '<div class="dr-body">— 本时段无课程 —</div>';
} else {
var p = paletteOf(r[3]);
var o = ov[state.day + "-" + r[1]];
var flag = "";
if (o && o.status === "changed") flag = '<span class="pill err">调课</span>';
if (o && o.status === "cancel") flag = '<span class="pill warn">停课</span>';
html += '<div class="dr-body">' +
'<span class="day-chip" style="--s-50:' + p.fill + ";--s-200:" + p.line + ";--s-800:" + p.text + '">' + r[3] + "</span>" +
'<span class="day-meta">' +
'<span><span class="k">教师</span>' + r[4] + "</span>" +
'<span><span class="k">教室</span>' + r[5] + "</span>" +
(r[2] > 1 ? '<span><span class="k">连堂</span>' + r[2] + " 节</span>" : "") +
(o && o.note ? '<span><span class="k">备注</span>' + o.note + "</span>" : (r[6] ? '<span><span class="k">备注</span>' + r[6] + "</span>" : "")) +
flag +
"</span></div>";
}
html += "</div>";
}
el.dayList.innerHTML = html;
}
/* ============================================================
渲染 · 详情侧栏
============================================================ */
function openDetail(dayIdx, slot) {
var r = lessons().filter(function (x) { return x[0] === dayIdx && slot >= x[1] && slot < x[1] + x[2]; })[0];
if (!r) return;
var p = paletteOf(r[3]);
var o = overrideMap()[dayIdx + "-" + r[1]];
var t = SLOTS[r[1] - 1].t.split("\n");
var endT = SLOTS[r[1] + r[2] - 2].t.split("\n");
var mon = weekMonday(state.week);
var date = addDays(mon, dayIdx);
document.getElementById("drawer-kicker").textContent =
"第 " + state.week + " 周 · " + DOW[dayIdx] + " · " + fmtCN(date);
document.getElementById("drawer-title").textContent = r[3];
var sw = document.getElementById("detail-swatch");
sw.style.setProperty("--s-50", p.fill);
sw.style.setProperty("--s-200", p.line);
sw.style.setProperty("--s-800", p.text);
document.getElementById("sw-title").textContent = r[4];
document.getElementById("sw-sub").textContent = r[5] + " · " + state.klass;
var rows = [
["节次", "第 " + r[1] + " 节" + (r[2] > 1 ? " — 第 " + (r[1] + r[2] - 1) + " 节(连堂 " + r[2] + " 节)" : "")],
["时间", '<span class="mono">' + t[0] + " — " + endT[1] + "</span>"],
["授课教师", r[4]],
["上课教室", r[5]],
["班级", state.klass],
["学期", el.selTerm.options[el.selTerm.selectedIndex].textContent],
["状态", o
? (o.status === "changed" ? '<span class="pill err">已调课</span>' : '<span class="pill warn">已停课</span>')
: '<span class="pill ok">正常</span>'],
["备注", (o && o.note) ? o.note : (r[6] ? r[6] : "—")]
];
document.getElementById("detail-list").innerHTML = rows.map(function (x) {
return '<div class="dl-row"><dt>' + x[0] + "</dt><dd>" + x[1] + "</dd></div>";
}).join("");
el.drawer.classList.add("is-open");
el.scrim.classList.add("is-open");
el.drawer.setAttribute("aria-hidden", "false");
document.getElementById("drawer-close").focus();
}
function closeDetail() {
el.drawer.classList.remove("is-open");
el.scrim.classList.remove("is-open");
el.drawer.setAttribute("aria-hidden", "true");
}
/* ============================================================
交互绑定
============================================================ */
function renderAll() {
renderWeeks();
renderHeads();
renderRail();
renderGrid();
renderLegend();
renderDay();
el.metaClass.textContent = state.klass;
el.metaTerm.textContent = el.selTerm.options[el.selTerm.selectedIndex].textContent;
}
function toast(msg) {
el.toast.textContent = msg;
el.toast.classList.add("is-on");
clearTimeout(toast._t);
toast._t = setTimeout(function () { el.toast.classList.remove("is-on"); }, 1900);
}
// 班级下拉初始化
classList().forEach(function (c) {
var op = document.createElement("option");
op.value = c; op.textContent = c;
el.selClass.appendChild(op);
});
el.selClass.value = state.klass;
// 日视图 · 切换日期(事件委托)
el.dayPicker.addEventListener("click", function (e) {
var b = e.target.closest(".day-tab");
if (!b) return;
state.day = +b.dataset.dayIdx;
renderDay();
});
// 周次切换(事件委托)
el.weekList.addEventListener("click", function (e) {
var b = e.target.closest("button[data-week]");
if (!b) return;
state.week = +b.dataset.week;
resolveDay();
closeDetail();
renderAll();
});
// 学期切换
el.selTerm.addEventListener("change", function () {
state.term = el.selTerm.value;
var list = classList();
el.selClass.innerHTML = "";
list.forEach(function (c) {
var op = document.createElement("option");
op.value = c; op.textContent = c;
el.selClass.appendChild(op);
});
state.klass = list[0] || "";
resolveDay();
closeDetail();
renderAll();
});
// 班级切换
el.selClass.addEventListener("change", function () {
state.klass = el.selClass.value;
resolveDay();
closeDetail();
renderAll();
});
// 视图切换
document.querySelectorAll(".segmented button").forEach(function (b) {
b.addEventListener("click", function () {
state.view = b.dataset.view;
document.querySelectorAll(".segmented button").forEach(function (x) {
x.setAttribute("aria-pressed", String(x === b));
});
document.getElementById("view-week").classList.toggle("is-active", state.view === "week");
document.getElementById("view-day").classList.toggle("is-active", state.view === "day");
if (state.view === "day") renderDay();
});
});
// 回到本周
document.getElementById("btn-today").addEventListener("click", function () {
state.week = currentWeek();
resolveDay();
closeDetail();
renderAll();
toast("已回到第 " + state.week + " 周");
});
// 打印
document.getElementById("btn-print").addEventListener("click", function () {
window.print();
});
// 课程块点击
el.daysLayer.addEventListener("click", function (e) {
var c = e.target.closest(".course");
if (!c) return;
document.querySelectorAll(".course.is-selected").forEach(function (x) { x.classList.remove("is-selected"); });
c.classList.add("is-selected");
state.selected = c;
openDetail(+c.dataset.day, +c.dataset.slot);
});
// 关闭详情
document.getElementById("drawer-close").addEventListener("click", closeDetail);
el.scrim.addEventListener("click", closeDetail);
// 键盘:Esc 关闭;方向键切换周次
document.addEventListener("keydown", function (e) {
if (e.key === "Escape") {
if (el.drawer.classList.contains("is-open")) closeDetail();
return;
}
var tag = (e.target.tagName || "").toLowerCase();
if (tag === "input" || tag === "select" || tag === "textarea") return;
if (e.key === "ArrowLeft" && state.week > 1) { state.week--; renderAll(); }
if (e.key === "ArrowRight" && state.week < TOTAL_WEEKS) { state.week++; renderAll(); }
});
// 调课 / 导出(演示反馈)
document.getElementById("btn-swap").addEventListener("click", function () {
toast("调课申请已提交班主任审核");
});
document.getElementById("btn-export").addEventListener("click", function () {
toast("已生成第 " + state.week + " 周课程表(演示)");
});
// 窗口尺寸变化时重算课程块位置(节次高度随断点变化)
var rt;
window.addEventListener("resize", function () {
clearTimeout(rt);
rt = setTimeout(function () { renderGrid(); renderRail(); }, 150);
});
/* 启动:先确定日视图落点,再整体渲染 */
resolveDay();
renderAll();
</script>
</body>