Files
huike-front/src/pages/clazz/LANDSCAPE_MATRIX_ARCH.md
T
weli e2c9f41fd7 fix(read): handle sudo2 failure gracefully in read()
When sudo2() fails (e.g. CI user whose login JWT lacks current_org_id),
fall back to using the auth token as the sudo token instead of propagating
the error. This prevents the route guard from aborting org/department
loading, allowing subsequent API calls (loadMyDepartments, etc.) to proceed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 23:26:31 +08:00

12 KiB
Raw Blame History

横屏矩阵视图架构说明

目录


核心问题

横屏模式下,矩阵视图需要:

  1. 旋转 90° 来利用横屏空间:手机横屏时物理宽度大、高度小,课表需要填满这个空间
  2. 表头固定:周二~周日那一行在滚动时一直可见
  3. 纵向滚动:所有课时(第 1 节~第 N 节)都能通过滚动看到
  4. 无多余空白/截断

关键矛盾:CSS transform: rotate(90deg) 旋转后,元素的坐标方向和视觉方向不再一致。overflow、position: sticky、scrollTop 都在元素自身坐标系工作,而用户看到的是旋转后的视觉效果。处理不当就会出现"滚动不到底"、"表头跟着跑"、"出现多余空白"等问题。


架构总览

index.vue
┌──────────────────────────────────────────────────────┐
│  landscape-shell (position: fixed; inset: 0)          │
│  ┌──────────────────────────────────────────────────┐ │
│  │  landscape-shell__toolbar    ← 退出横屏 / 本周等   │ │
│  ├──────────────────────────────────────────────────┤ │
│  │  landscape-shell__body (overflow: hidden)          │ │
│  │  ┌──────────────────────────────────────────────┐ │ │
│  │  │  landscape-stage-viewport--matrix             │ │ │
│  │  │  (overflow: hidden — 只负责裁剪)              │ │ │
│  │  │  ┌──────────────────────────────────────────┐ │ │ │
│  │  │  │  landscape-stage (rotate 90°)             │ │ │
│  │  │  │  ┌──────────────────────────────────────┐ │ │ │
│  │  │  │  │  matrix-container--landscape          │ │ │ │
│  │  │  │  │  ┌──────────────────────────────────┐ │ │ │ │
│  │  │  │  │  │  ClazzMatrixView                  │ │ │ │ │
│  │  │  │  │  │  ┌──────────────────────────────┐ │ │ │ │ │
│  │  │  │  │  │  │  ml-wrap (flex column)        │ │ │ │ │ │
│  │  │  │  │  │  │  ├─ ml-header (sticky top)    │ │ │ │ │ │
│  │  │  │  │  │  │  └─ ml-body (overflow-y: auto) │ │ │ │ │ │
│  │  │  │  │  │  │     ├─ ml-row (第1节: A B C..)│ │ │ │ │ │
│  │  │  │  │  │  │     ├─ ml-row (第2节: A B C..)│ │ │ │ │ │
│  │  │  │  │  │  │     └─ ml-row (第N节: ...)    │ │ │ │ │ │
│  │  │  │  │  │  └──────────────────────────────┘ │ │ │ │ │
│  │  │  │  │  └──────────────────────────────────┘ │ │ │ │
│  │  │  │  └──────────────────────────────────────┘ │ │ │
│  │  │  └──────────────────────────────────────────┘ │ │
│  │  └──────────────────────────────────────────────┘ │
│  └──────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘

关键层次职责

CSS 职责
landscape-shell__body overflow: hidden 裁剪旋转容器,不滚动
landscape-stage-viewport--matrix overflow: hidden 裁剪旋转容器,不滚动
landscape-stage overflow: visible 旋转舞台,内容可见即可
ml-wrap overflow: hidden 包裹 header+body,裁剪
ml-header position: sticky; top: 0 表头固定在滚动容器顶部
ml-body overflow-y: auto 唯一滚动容器

CSS 旋转原理

旋转公式

.landscape-stage {
  width: var(--clazz-landscape-body-height);  /* 约 325px */
  height: 100dvw;                              /* 约 812px */
  transform: rotate(90deg) translateY(-100%);
  transform-origin: top left;
}
  • width = body-height(约 325px),height = 100dvw(约 812px
  • 旋转后变成视觉上 812×325,填满横屏
  • transform-origin: top left + translateY(-100%) 纠正旋转后的位置偏移

旋转后的坐标系变化

旋转前(元素自身坐标系 → 视觉方向):

元素方向 视觉方向
+X(向右) → 视觉右侧
+Y(向下) → 视觉下方

旋转 90° CW 后:

元素方向 视觉方向
+X(向右) → 视觉下方
+Y(向下) → 视觉左侧

关键理解overflow、scrollTop、position: sticky 全部在元素自身坐标系工作,不受 transform 影响。

ml-* 布局在旋转前后的映射

旋转前 ml-wrap 布局(元素坐标系):

┌──────┬──────────────────────────────┐  ← ml-header (元素顶部)
│ 时段  │ 周一   周二   周三   周四 ... │     flex row, 元素 X 方向排列
├──────┼──────────────────────────────┤
│ 第1节 │ A1     B1     C1     D1 ... │  ← ml-row
│ 第2节 │ A2     B2     C2     D2 ... │  ← ml-row
│ 第3节 │ A3     B3     C3     D3 ... │  ← ml-row
│ ...  │                              │
└──────┴──────────────────────────────┘
    ↑                        ↑
  元素 X 方向             元素 X 方向
  (label)                 (day cells)

旋转 90° CW 后的视觉效果:

              ←←← 元素 Y 方向 = 视觉左侧(更多课时往左延伸)
  ↓        ┌──────┬──────┬──────┬──────┐
  元        │ 时段  │ 周一  │ 周二  │ 周三  │ ...  → ml-header (sticky top)
  素 视觉   ├──────┼──────┼──────┼──────┤
  X  ↓     │ 第1节 │ A1   │ B1   │ C1   │ ...
  ↓ 下方   ├──────┼──────┼──────┼──────┤
           │ 第2节 │ A2   │ B2   │ C2   │ ...
           ├──────┼──────┼──────┼──────┤
           │ ...  │      │      │      │
           └──────┴──────┴──────┴──────┘

滚动方案

为什么浏览器原生滚动不够

旋转后,overflow-y: auto 的滚动方向(元素 Y = 视觉左侧)和用户期望的滚动方向(视觉下方)不一致。所以需要手动接管。

手动滚动实现

// ml-body 元素上绑定的事件
@touchstart="onLandscapeBodyTouchStart"
@touchmove.prevent="onLandscapeBodyTouchMove"
@wheel="onLandscapeBodyWheel"

// touchmove: 手指上下滑动 → 控制元素 scrollTop
function onLandscapeBodyTouchMove(e: TouchEvent) {
  const currentY = e.touches[0]?.clientY;
  const previousY = landscapeTouchLastY.value;
  const body = landscapeBodyRef.value;
  if (currentY == null || previousY == null || !body) return;
  body.scrollTop += currentY - previousY;  // 直接操作元素 scrollTop
  landscapeTouchLastY.value = currentY;
}

// wheel: 鼠标滚轮 → 控制元素 scrollTop
function onLandscapeBodyWheel(e: WheelEvent) {
  const body = landscapeBodyRef.value;
  if (!body) return;
  body.scrollTop += e.deltaY;  // 直接操作元素 scrollTop
}

关键点:

  • 直接操作 element.scrollTop,不走浏览器的 scroll 行为
  • scrollTop 在元素自身坐标系工作,不管视觉方向
  • touchmove.prevent 阻止浏览器默认行为(浏览器会尝试在 viewport 层滚动)
  • CSS touch-action: none + overscroll-behavior: contain 进一步禁止浏览器介入

sticky header 为什么能用

.ml-header {
  position: sticky;
  top: 0;    /* 元素自身坐标系的 top,不是视觉 top */
}

sticky 在滚动容器的坐标系中计算。ml-header 的滚动容器是 ml-body(最近的有 overflow 的父元素)。ml-body 的 scrollTop 在元素 Y 方向 = 旋转后的视觉左侧方向。sticky 的 top: 0 意味着"在元素 Y 方向离顶部 0px 处固定"——旋转后正好映射到视觉顶部。

这就是为什么 sticky header 必须在旋转内部,不能在外部。


修改指南

如果要改表头样式

ClazzMatrixView.vueml-header / ml-day-header 相关 CSS。不要index.vue 里加外部表头。

如果要改滚动行为

ClazzMatrixView.vueml-body CSS 和 onLandscapeBodyTouchMove / onLandscapeBodyWheel 函数。不要在 viewport 层设 overflow: auto

如果要调整布局层次

记住这条规则:"谁包含内容,谁负责滚动"。内容(ml-row)在 ml-body 里 → ml-body 负责滚动。viewport 只负责裁剪(overflow: hidden)。不在 viewport 层滚动。

如果要改旋转角度

landscape-stagerotate(90deg) 是整个策略的基础。如果要改成其他角度(例如 -90deg),坐标系映射方向全部会变,需要重新审视 scroll handler 的实现。


常见陷阱

陷阱 1:把表头放旋转容器外

症状:表头和内容宽度对不齐、位置偏移、横竖坐标不对应。

原因:旋转后的元素宽度不等于 CSS 上写的 width(因为 transform 不影响 layout box)。外部固定元素无法和旋转后内部元素对齐。

解法:表头放旋转容器内部,用 sticky。

陷阱 2:在 viewport 层设 overflow: auto

症状:出现大量空白可滚动区域、滚不到底。

原因:viewport 不在旋转容器内部,它看到的 stage flow box 尺寸是 width: 325px; height: 812px旋转前的尺寸)。812px 的可滚动高度远大于视觉上 325px 的显示区域,所以很多是空白。

解法viewport 固定 = overflow: hidden,滚动容器在内部 ml-body。

陷阱 3:试图去掉旋转改用其他方案

症状:布局显示方向完全不对。

原因:横屏模式的核心思想就是用旋转来利用物理横屏空间。去掉旋转 = 失去横屏适配。

解法:保留旋转,在旋转内部解决问题。

陷阱 4:用 CSS overflow 的自动滚动而不手动接管

症状:滚动方向错乱(上下滑变成左右滚动)。

原因:旋转后 overflow-y 对应视觉左侧方向。浏览器不会自动映射方向。

解法:手动接管 touch/wheel 事件,直接操作 scrollTop。

陷阱 5:修改 overflow 链中的某一层而不检查上下游

症状:内容被裁剪、滚动失效、sticky 不工作。

原因:overflow 链是层叠的:viewport(overflow:hidden) → stage(overflow:visible) → ml-wrap(overflow:hidden) → ml-body(overflow-y:auto)。任何一层改了,都会影响滚动和可见范围。

解法:改之前理解整个 overflow 链,确认改动不会破坏上下游。