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>
12 KiB
横屏矩阵视图架构说明
目录
核心问题
横屏模式下,矩阵视图需要:
- 旋转 90° 来利用横屏空间:手机横屏时物理宽度大、高度小,课表需要填满这个空间
- 表头固定:周二~周日那一行在滚动时一直可见
- 纵向滚动:所有课时(第 1 节~第 N 节)都能通过滚动看到
- 无多余空白/截断
关键矛盾: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.vue 的 ml-header / ml-day-header 相关 CSS。不要在 index.vue 里加外部表头。
如果要改滚动行为
改 ClazzMatrixView.vue 的 ml-body CSS 和 onLandscapeBodyTouchMove / onLandscapeBodyWheel 函数。不要在 viewport 层设 overflow: auto。
如果要调整布局层次
记住这条规则:"谁包含内容,谁负责滚动"。内容(ml-row)在 ml-body 里 → ml-body 负责滚动。viewport 只负责裁剪(overflow: hidden)。不在 viewport 层滚动。
如果要改旋转角度
landscape-stage 的 rotate(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 链,确认改动不会破坏上下游。