1432 lines
61 KiB
HTML
1432 lines
61 KiB
HTML
<!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>
|