Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 29db1b75ff | |||
| 398ee9acfc | |||
| 766f511e11 | |||
| 78c89cd2cc | |||
| 1bca764beb | |||
| 3373c29506 | |||
| 4dafdf0bb1 | |||
| df66e768f3 | |||
| e93f844a61 | |||
| fc8d7de10b | |||
| 80f4d4af46 | |||
| ecdf15e26f | |||
| e2c9f41fd7 | |||
| 513f52fcb3 | |||
| c44d763e63 | |||
| 8d4ed8099c | |||
| b02fa37350 |
@@ -0,0 +1,7 @@
|
|||||||
|
# huike-front 项目指南
|
||||||
|
|
||||||
|
## vConsole 显示规则
|
||||||
|
|
||||||
|
vConsole 仅在当前用户有 `SYS_CAN_SUDO` tag 时显示。判断逻辑在 `src/App.vue:updateVConsoleVisibility()`。
|
||||||
|
|
||||||
|
Tags 通过 `get_all_tags_of_the_user` API 异步加载(`src/store/user.ts:getTags()`),在 `setCurrentUser()` 之后执行。vConsole 的 watch 需要 `{ deep: true }` 以及独立的 `store.current.tags` watch 来覆盖异步加载完成后的触发。
|
||||||
+12
-1
@@ -77,6 +77,10 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
|
|
||||||
watch(() => store.current, () => {
|
watch(() => store.current, () => {
|
||||||
|
updateVConsoleVisibility();
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
function updateVConsoleVisibility() {
|
||||||
// vConsole 仅对 sudoer(有 SYS_CAN_SUDO tag)用户可见
|
// vConsole 仅对 sudoer(有 SYS_CAN_SUDO tag)用户可见
|
||||||
let showVConsole = store.current.tags?.some(t => t.tag_name === 'SYS_CAN_SUDO') ?? false;
|
let showVConsole = store.current.tags?.some(t => t.tag_name === 'SYS_CAN_SUDO') ?? false;
|
||||||
console.log('vconsole visibility (sudoer only)...', { showVConsole, tags: store.current.tags });
|
console.log('vconsole visibility (sudoer only)...', { showVConsole, tags: store.current.tags });
|
||||||
@@ -84,6 +88,13 @@ export default defineComponent({
|
|||||||
if (vconsole) {
|
if (vconsole) {
|
||||||
vconsole.hidden = !showVConsole;
|
vconsole.hidden = !showVConsole;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同时监听 tags 的独立变化(getTags 异步加载完成后触发)
|
||||||
|
watch(() => store.current.tags, (val) => {
|
||||||
|
if (val && val.length > 0) {
|
||||||
|
updateVConsoleVisibility();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const tab_change_check = (name: number | string) => {
|
const tab_change_check = (name: number | string) => {
|
||||||
@@ -106,7 +117,7 @@ export default defineComponent({
|
|||||||
if (role === HtyBaseRoles.TEACHER) return '/teacher/home'
|
if (role === HtyBaseRoles.TEACHER) return '/teacher/home'
|
||||||
if (role === HtySuperRoles.ADMIN) return '/admin/teachers'
|
if (role === HtySuperRoles.ADMIN) return '/admin/teachers'
|
||||||
if (role === HtySuperRoles.TESTER) return '/tester'
|
if (role === HtySuperRoles.TESTER) return '/tester'
|
||||||
if (role === HtySuperRoles.SUPERVISOR) return '/supervisor/subsidiaries'
|
if (role === HtySuperRoles.SUPERVISOR) return '/teacher/home'
|
||||||
return '/'
|
return '/'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,259 @@
|
|||||||
|
# 横屏矩阵视图架构说明
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
- [核心问题](#核心问题)
|
||||||
|
- [架构总览](#架构总览)
|
||||||
|
- [CSS 旋转原理](#css-旋转原理)
|
||||||
|
- [布局策略详解](#布局策略详解)
|
||||||
|
- [滚动方案](#滚动方案)
|
||||||
|
- [修改指南](#修改指南)
|
||||||
|
- [常见陷阱](#常见陷阱)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心问题
|
||||||
|
|
||||||
|
横屏模式下,矩阵视图需要:
|
||||||
|
|
||||||
|
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 旋转原理
|
||||||
|
|
||||||
|
### 旋转公式
|
||||||
|
|
||||||
|
```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 = 视觉左侧)和用户期望的滚动方向(视觉下方)不一致。所以需要手动接管。
|
||||||
|
|
||||||
|
### 手动滚动实现
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 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 为什么能用
|
||||||
|
|
||||||
|
```css
|
||||||
|
.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 链,确认改动不会破坏上下游。
|
||||||
@@ -270,6 +270,8 @@ export default defineComponent({
|
|||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
@sidebar-width: 1.2rem;
|
@sidebar-width: 1.2rem;
|
||||||
@cell-width: 1.8rem;
|
@cell-width: 1.8rem;
|
||||||
|
@portrait-header-height: 0.72rem;
|
||||||
|
@portrait-day-row-min-height: 1.16rem;
|
||||||
|
|
||||||
.matrix-wrapper {
|
.matrix-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -300,9 +302,12 @@ export default defineComponent({
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
pointer-events: none; /* allow click-through to cells underneath */
|
pointer-events: none; /* allow click-through to cells underneath */
|
||||||
|
|
||||||
.sidebar-corner {
|
.sidebar-corner {
|
||||||
|
height: @portrait-header-height;
|
||||||
|
box-sizing: border-box;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: #f7f8fa;
|
background: #f7f8fa;
|
||||||
border-bottom: 1px solid #e8e8e8;
|
border-bottom: 1px solid #e8e8e8;
|
||||||
@@ -312,7 +317,8 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-row {
|
.sidebar-row {
|
||||||
flex: 1;
|
flex: 1 0 @portrait-day-row-min-height;
|
||||||
|
min-height: @portrait-day-row-min-height;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -353,18 +359,20 @@ export default defineComponent({
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
min-width: fit-content;
|
min-width: fit-content;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── 行:共用 ─── */
|
/* ─── 行:共用 ─── */
|
||||||
.sr-header {
|
.sr-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
height: @portrait-header-height;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sr-row {
|
.sr-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1 0 @portrait-day-row-min-height;
|
||||||
min-height: 0;
|
min-height: @portrait-day-row-min-height;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Spacer(占位与 sidebar 同宽) ─── */
|
/* ─── Spacer(占位与 sidebar 同宽) ─── */
|
||||||
@@ -484,6 +492,8 @@ export default defineComponent({
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ml-header {
|
.ml-header {
|
||||||
@@ -491,6 +501,9 @@ export default defineComponent({
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: #f7f8fa;
|
background: #f7f8fa;
|
||||||
border-bottom: 1px solid #e8e8e8;
|
border-bottom: 1px solid #e8e8e8;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ml-corner {
|
.ml-corner {
|
||||||
@@ -521,13 +534,18 @@ export default defineComponent({
|
|||||||
|
|
||||||
.ml-body {
|
.ml-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
padding-bottom: 0.08rem;
|
padding-bottom: 0.08rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ml-row {
|
.ml-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-height: 0;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,7 +564,7 @@ export default defineComponent({
|
|||||||
.ml-cell {
|
.ml-cell {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0.6rem;
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 0.02rem;
|
gap: 0.02rem;
|
||||||
|
|||||||
+135
-108
@@ -2,16 +2,15 @@
|
|||||||
<div class="clazz-page">
|
<div class="clazz-page">
|
||||||
<div class="view-toolbar" :class="{ 'view-toolbar--landscape': isLandscape }">
|
<div class="view-toolbar" :class="{ 'view-toolbar--landscape': isLandscape }">
|
||||||
<div class="view-toolbar__main">
|
<div class="view-toolbar__main">
|
||||||
<div class="view-toolbar__nav" v-if="viewMode === 'matrix'">
|
<div class="view-toolbar__nav">
|
||||||
<van-button size="small" @click="matrixGoPrev">‹</van-button>
|
<van-button size="small" @click="navGoPrev">‹</van-button>
|
||||||
<van-button size="small" @click="matrixGoToday">本周</van-button>
|
<van-button size="small" @click="navGoToday">本周</van-button>
|
||||||
<van-button size="small" @click="matrixGoNext">›</van-button>
|
<van-button size="small" @click="navGoNext">›</van-button>
|
||||||
</div>
|
</div>
|
||||||
<span class="view-toolbar__range" v-if="viewMode === 'matrix'">{{ matrixWeekRange }}</span>
|
<span class="view-toolbar__range">{{ weekRange }}</span>
|
||||||
<span class="view-toolbar__range" v-else>日历周视图</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="view-toolbar__actions">
|
<div class="view-toolbar__actions">
|
||||||
<div class="view-toolbar__orient">
|
<div v-if="viewMode === 'matrix'" class="view-toolbar__orient">
|
||||||
<van-button size="small" plain @click="isLandscape = !isLandscape">
|
<van-button size="small" plain @click="isLandscape = !isLandscape">
|
||||||
{{ isLandscape ? '退出横屏' : '横屏' }}
|
{{ isLandscape ? '退出横屏' : '横屏' }}
|
||||||
</van-button>
|
</van-button>
|
||||||
@@ -33,24 +32,14 @@
|
|||||||
|
|
||||||
<div v-if="isLandscape" class="landscape-shell">
|
<div v-if="isLandscape" class="landscape-shell">
|
||||||
<div class="landscape-shell__toolbar">
|
<div class="landscape-shell__toolbar">
|
||||||
<div class="landscape-shell__left">
|
<div class="view-toolbar__nav">
|
||||||
<van-button size="small" plain @click="isLandscape = false">退出横屏</van-button>
|
|
||||||
<van-button v-if="viewMode === 'matrix'" size="small" @click="matrixGoToday">本周</van-button>
|
|
||||||
</div>
|
|
||||||
<div class="landscape-shell__title">
|
|
||||||
<template v-if="viewMode === 'matrix'">
|
|
||||||
<van-button size="small" @click="matrixGoPrev">‹</van-button>
|
<van-button size="small" @click="matrixGoPrev">‹</van-button>
|
||||||
<span>{{ matrixWeekRange }}</span>
|
<van-button size="small" @click="matrixGoToday">本周</van-button>
|
||||||
<van-button size="small" @click="matrixGoNext">›</van-button>
|
<van-button size="small" @click="matrixGoNext">›</van-button>
|
||||||
</template>
|
|
||||||
<template v-else>日历周视图</template>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span class="view-toolbar__range">{{ matrixWeekRange }}</span>
|
||||||
<div class="landscape-shell__toggle">
|
<div class="landscape-shell__toggle">
|
||||||
<van-button
|
<van-button size="small" plain @click="isLandscape = false">退出横屏</van-button>
|
||||||
:type="viewMode === 'calendar' ? 'primary' : 'default'"
|
|
||||||
size="small" plain
|
|
||||||
@click="setViewMode('calendar')"
|
|
||||||
>日历</van-button>
|
|
||||||
<van-button
|
<van-button
|
||||||
:type="viewMode === 'matrix' ? 'primary' : 'default'"
|
:type="viewMode === 'matrix' ? 'primary' : 'default'"
|
||||||
size="small" plain
|
size="small" plain
|
||||||
@@ -59,16 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="landscape-shell__body" :class="{ 'landscape-shell__body--matrix': viewMode === 'matrix' }">
|
<div class="landscape-shell__body" :class="{ 'landscape-shell__body--matrix': viewMode === 'matrix' }">
|
||||||
<template v-if="viewMode === 'calendar'">
|
<template v-if="viewMode === 'matrix'">
|
||||||
<div class="landscape-stage-viewport">
|
|
||||||
<div class="landscape-stage">
|
|
||||||
<div class="calendar-wrapper calendar-wrapper--landscape">
|
|
||||||
<FullCalendar ref="calendar" :options="options" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div class="landscape-stage-viewport landscape-stage-viewport--matrix">
|
<div class="landscape-stage-viewport landscape-stage-viewport--matrix">
|
||||||
<div class="landscape-stage">
|
<div class="landscape-stage">
|
||||||
<div class="matrix-container matrix-container--landscape">
|
<div class="matrix-container matrix-container--landscape">
|
||||||
@@ -101,7 +81,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div v-show="viewMode === 'calendar'" class="calendar-wrapper">
|
<div v-if="viewMode === 'calendar'" class="calendar-wrapper">
|
||||||
<div class="calendar">
|
<div class="calendar">
|
||||||
<FullCalendar ref="calendar" :options="options" />
|
<FullCalendar ref="calendar" :options="options" />
|
||||||
</div>
|
</div>
|
||||||
@@ -380,6 +360,10 @@ export default defineComponent({
|
|||||||
if (cachedViewMode === 'matrix' || cachedViewMode === 'calendar') {
|
if (cachedViewMode === 'matrix' || cachedViewMode === 'calendar') {
|
||||||
viewMode.value = cachedViewMode;
|
viewMode.value = cachedViewMode;
|
||||||
}
|
}
|
||||||
|
// 如果初始视图是矩阵,在组件挂载后加载矩阵视图数据
|
||||||
|
if (viewMode.value === 'matrix') {
|
||||||
|
nextTick(() => triggerMatrixSearch());
|
||||||
|
}
|
||||||
|
|
||||||
const isLandscape = ref(false);
|
const isLandscape = ref(false);
|
||||||
|
|
||||||
@@ -424,17 +408,44 @@ export default defineComponent({
|
|||||||
const matrixWeekStart = computed(() => {
|
const matrixWeekStart = computed(() => {
|
||||||
const d = new Date(matrixWeekCursor.value);
|
const d = new Date(matrixWeekCursor.value);
|
||||||
const day = d.getDay();
|
const day = d.getDay();
|
||||||
d.setDate(d.getDate() - day);
|
const diff = day === 0 ? 6 : day - 1; // Monday as first day of week
|
||||||
|
d.setDate(d.getDate() - diff);
|
||||||
return formatDate(d, DateFormatter.Date);
|
return formatDate(d, DateFormatter.Date);
|
||||||
});
|
});
|
||||||
const matrixWeekEnd = computed(() => {
|
const matrixWeekEnd = computed(() => {
|
||||||
const d = new Date(matrixWeekCursor.value);
|
const d = new Date(matrixWeekCursor.value);
|
||||||
const day = d.getDay();
|
const day = d.getDay();
|
||||||
d.setDate(d.getDate() + (6 - day));
|
const diff = day === 0 ? 6 : day - 1; // Monday as first day of week
|
||||||
|
d.setDate(d.getDate() + (6 - diff));
|
||||||
return formatDate(d, DateFormatter.Date);
|
return formatDate(d, DateFormatter.Date);
|
||||||
});
|
});
|
||||||
const matrixWeekRange = computed(() => `${matrixWeekStart.value} ~ ${matrixWeekEnd.value}`);
|
const matrixWeekRange = computed(() => `${matrixWeekStart.value} ~ ${matrixWeekEnd.value}`);
|
||||||
|
|
||||||
|
// 日历视图日期范围(由 datesSet 更新)
|
||||||
|
const calendarDateRange = ref('');
|
||||||
|
|
||||||
|
// 统一切换:矩阵用 matrixWeekRange,日历时用 calendarDateRange
|
||||||
|
const weekRange = computed(() =>
|
||||||
|
viewMode.value === 'matrix' ? matrixWeekRange.value : calendarDateRange.value
|
||||||
|
);
|
||||||
|
|
||||||
|
// 导航按钮分发(日历 / 矩阵)
|
||||||
|
const navGoPrev = () => {
|
||||||
|
if (viewMode.value === 'matrix') return matrixGoPrev();
|
||||||
|
const api = calendar.value?.getApi?.();
|
||||||
|
api?.prev();
|
||||||
|
// datesSet 会自动更新 calendarDateRange
|
||||||
|
};
|
||||||
|
const navGoToday = () => {
|
||||||
|
if (viewMode.value === 'matrix') return matrixGoToday();
|
||||||
|
calendar.value?.getApi?.().gotoDate(new Date());
|
||||||
|
};
|
||||||
|
const navGoNext = () => {
|
||||||
|
if (viewMode.value === 'matrix') return matrixGoNext();
|
||||||
|
const api = calendar.value?.getApi?.();
|
||||||
|
api?.next();
|
||||||
|
};
|
||||||
|
|
||||||
// 标准化事件模型(供矩阵视图消费)
|
// 标准化事件模型(供矩阵视图消费)
|
||||||
const {normalizedEvents} = useClazzViewModel({
|
const {normalizedEvents} = useClazzViewModel({
|
||||||
list: computed(() => store.list),
|
list: computed(() => store.list),
|
||||||
@@ -481,7 +492,21 @@ export default defineComponent({
|
|||||||
// 切换视图时缓存
|
// 切换视图时缓存
|
||||||
const setViewMode = (mode: 'calendar' | 'matrix') => {
|
const setViewMode = (mode: 'calendar' | 'matrix') => {
|
||||||
viewMode.value = mode;
|
viewMode.value = mode;
|
||||||
|
if (mode === 'calendar') {
|
||||||
|
isLandscape.value = false;
|
||||||
|
// 从矩阵切回日历时,按日历当前可视范围加载数据
|
||||||
|
const api = calendar.value?.getApi?.();
|
||||||
|
if (api) {
|
||||||
|
const view = api.view;
|
||||||
|
state.start_date = formatDate(view.activeStart, DateFormatter.Date);
|
||||||
|
state.end_date = formatDate(view.activeEnd, DateFormatter.Date);
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
}
|
||||||
setKey('clazz_view_mode', mode);
|
setKey('clazz_view_mode', mode);
|
||||||
|
if (mode === 'matrix') {
|
||||||
|
triggerMatrixSearch();
|
||||||
|
}
|
||||||
updateCalendarSize();
|
updateCalendarSize();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -538,6 +563,7 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const draw = () => {
|
const draw = () => {
|
||||||
|
if (!calendar.value) { console.debug('[clazz draw] skip, calendar null'); return; }
|
||||||
let api = calendar.value.getApi()
|
let api = calendar.value.getApi()
|
||||||
api.removeAllEvents();
|
api.removeAllEvents();
|
||||||
store.list.forEach(item => {
|
store.list.forEach(item => {
|
||||||
@@ -599,6 +625,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const redraw = () => {
|
const redraw = () => {
|
||||||
|
if (!calendar.value) { console.debug('[clazz redraw] skip, calendar null'); return; }
|
||||||
let events = calendar.value.getApi().getEvents();
|
let events = calendar.value.getApi().getEvents();
|
||||||
events.forEach((event, i) => {
|
events.forEach((event, i) => {
|
||||||
let subsidiary_id = event.extendedProps.subsidiary_id;
|
let subsidiary_id = event.extendedProps.subsidiary_id;
|
||||||
@@ -662,7 +689,10 @@ export default defineComponent({
|
|||||||
|
|
||||||
const comments_count = computed(() => count_comments(store.current.id))
|
const comments_count = computed(() => count_comments(store.current.id))
|
||||||
|
|
||||||
const searchForSubsidiaries = async () => {
|
// 相同日期范围跳过重复 API 调用
|
||||||
|
const lastQueryKey = ref('');
|
||||||
|
|
||||||
|
const searchForSubsidiaries = async (start_date: string, end_date: string) => {
|
||||||
// 主管老师角色下才加载下属老师的排课
|
// 主管老师角色下才加载下属老师的排课
|
||||||
if (usingUser.store.currentRole !== HtyBaseRoles.TEACHER && usingUser.store.current.roles.some(r => r.role_key === HtySuperRoles.SUPERVISOR)) {
|
if (usingUser.store.currentRole !== HtyBaseRoles.TEACHER && usingUser.store.current.roles.some(r => r.role_key === HtySuperRoles.SUPERVISOR)) {
|
||||||
subsidiaries.value = usingSupervisor.store.subsidiaries;
|
subsidiaries.value = usingSupervisor.store.subsidiaries;
|
||||||
@@ -679,19 +709,31 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (subsidiaries.value.length) {
|
if (subsidiaries.value.length) {
|
||||||
let { start_date, end_date } = state;
|
const htyIds = subsidiaries.value.map(x => x.to_user_id);
|
||||||
await query_for_subsidiaries(start_date, end_date, subsidiaries.value.map(x => x.to_user_id))
|
return Promise.all([
|
||||||
await query_repeats_for_subsidiaries(start_date, end_date, subsidiaries.value.map(x => x.to_user_id))
|
query_for_subsidiaries(start_date, end_date, htyIds),
|
||||||
|
query_repeats_for_subsidiaries(start_date, end_date, htyIds),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const search = async () => {
|
const search = async (force = false) => {
|
||||||
let { start_date, end_date } = state;
|
let { start_date, end_date } = state;
|
||||||
await query(start_date, end_date)
|
const key = `${start_date}|${end_date}|${usingUser.store.currentRole}`;
|
||||||
await query_repeats(start_date, end_date)
|
|
||||||
|
|
||||||
await searchForSubsidiaries();
|
// 相同范围且非强制刷新 → 跳过 API,仅重绘
|
||||||
|
if (!force && lastQueryKey.value === key) {
|
||||||
|
draw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
query(start_date, end_date),
|
||||||
|
query_repeats(start_date, end_date),
|
||||||
|
searchForSubsidiaries(start_date, end_date),
|
||||||
|
]);
|
||||||
|
lastQueryKey.value = key;
|
||||||
draw();
|
draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -745,37 +787,14 @@ export default defineComponent({
|
|||||||
const options: CalendarOptions = {
|
const options: CalendarOptions = {
|
||||||
locale: zhLocale,
|
locale: zhLocale,
|
||||||
// themeSystem: 'bootstrap5',
|
// themeSystem: 'bootstrap5',
|
||||||
customButtons: {
|
headerToolbar: false,
|
||||||
current: {
|
|
||||||
text: '本周',
|
|
||||||
click: function() {
|
|
||||||
calendar.value.getApi().gotoDate(new Date())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
createClazz: {
|
|
||||||
text: '创建排课',
|
|
||||||
click: function() {
|
|
||||||
if (!is_teacher.value) return;
|
|
||||||
const now = new Date();
|
|
||||||
store.current.start_from = formatDate(now, DateFormatter.DateTimeSave);
|
|
||||||
now.setMinutes(now.getMinutes() + 45);
|
|
||||||
store.current.end_by = formatDate(now, DateFormatter.DateTimeSave);
|
|
||||||
state.editing = true;
|
|
||||||
state.readonly = false;
|
|
||||||
state.title = "新增排课"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
headerToolbar: {
|
|
||||||
left: 'prev,current,next',
|
|
||||||
center: 'title',
|
|
||||||
right: 'createClazz'
|
|
||||||
},
|
|
||||||
plugins: [ dayGridPlugin, bootstrap5Plugin, timeGridPlugin, interactionPlugin ],
|
plugins: [ dayGridPlugin, bootstrap5Plugin, timeGridPlugin, interactionPlugin ],
|
||||||
initialView: 'timeGridWeek',
|
initialView: 'timeGridWeek',
|
||||||
initialDate: initialDate,
|
initialDate: initialDate,
|
||||||
slotMinTime: "06:00:00",
|
slotMinTime: "00:00:00",
|
||||||
slotMaxTime: "22:00:00",
|
slotMaxTime: "24:00:00",
|
||||||
|
scrollTime: "06:00:00",
|
||||||
|
allDaySlot: false,
|
||||||
nowIndicator: true,
|
nowIndicator: true,
|
||||||
droppable: true,
|
droppable: true,
|
||||||
editable: true,
|
editable: true,
|
||||||
@@ -809,6 +828,12 @@ export default defineComponent({
|
|||||||
view(event.id)
|
view(event.id)
|
||||||
},
|
},
|
||||||
datesSet: function({start, end}) {
|
datesSet: function({start, end}) {
|
||||||
|
// 矩阵视图有自己的日期范围和数据加载逻辑
|
||||||
|
if (viewMode.value === 'matrix') return;
|
||||||
|
// 更新日历日期范围显示
|
||||||
|
const endDate = new Date(end);
|
||||||
|
endDate.setDate(endDate.getDate() - 1);
|
||||||
|
calendarDateRange.value = `${formatDate(start, DateFormatter.Date)} ~ ${formatDate(endDate, DateFormatter.Date)}`;
|
||||||
if (store.hanging) {
|
if (store.hanging) {
|
||||||
store.hanging = false;
|
store.hanging = false;
|
||||||
let {state: cachedState} = getKey('clazz_state');
|
let {state: cachedState} = getKey('clazz_state');
|
||||||
@@ -855,7 +880,8 @@ export default defineComponent({
|
|||||||
if (await createInstance(store.current.instance)) {
|
if (await createInstance(store.current.instance)) {
|
||||||
state.editing = false;
|
state.editing = false;
|
||||||
store.current = {} as Clazz;
|
store.current = {} as Clazz;
|
||||||
await search()
|
// 数据已变更,强制刷新
|
||||||
|
await search(true)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -863,11 +889,13 @@ export default defineComponent({
|
|||||||
if (await createOrUpdate()) {
|
if (await createOrUpdate()) {
|
||||||
state.editing = false;
|
state.editing = false;
|
||||||
store.current = {} as Clazz;
|
store.current = {} as Clazz;
|
||||||
await search()
|
// 数据已变更,强制刷新
|
||||||
|
await search(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const freeze = () => {
|
const freeze = () => {
|
||||||
|
if (!calendar.value) return;
|
||||||
store.hanging = true;
|
store.hanging = true;
|
||||||
let date = calendar.value.getApi().getDate();
|
let date = calendar.value.getApi().getDate();
|
||||||
setKey('clazz_state', {state, date})
|
setKey('clazz_state', {state, date})
|
||||||
@@ -982,7 +1010,8 @@ export default defineComponent({
|
|||||||
}).then(async () => {
|
}).then(async () => {
|
||||||
if (await removeCurrent(is_root)) {
|
if (await removeCurrent(is_root)) {
|
||||||
cancel();
|
cancel();
|
||||||
await search();
|
// 数据已变更,强制刷新
|
||||||
|
await search(true);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1080,6 +1109,7 @@ export default defineComponent({
|
|||||||
isLandscape, matrixWeekStart, matrixWeekEnd, matrixWeekRange, matrixWeekDays,
|
isLandscape, matrixWeekStart, matrixWeekEnd, matrixWeekRange, matrixWeekDays,
|
||||||
matrixGoPrev, matrixGoNext, matrixGoToday,
|
matrixGoPrev, matrixGoNext, matrixGoToday,
|
||||||
onMatrixCellClick, onMatrixEventClick,
|
onMatrixCellClick, onMatrixEventClick,
|
||||||
|
weekRange, calendarDateRange, navGoPrev, navGoToday, navGoNext,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1095,13 +1125,15 @@ export default defineComponent({
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|
||||||
:deep(.fc-toolbar-title) {
|
|
||||||
font-size: 1.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.fc) {
|
:deep(.fc) {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.fc-scroller) {
|
||||||
|
overflow-y: auto !important;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-wrapper {
|
.calendar-wrapper {
|
||||||
@@ -1127,6 +1159,15 @@ export default defineComponent({
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 日历按钮与矩阵按钮一致,去掉焦点阴影
|
||||||
|
:deep(.fc .fc-button-primary:focus),
|
||||||
|
:deep(.fc .fc-button:focus) {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
:deep(.fc .fc-button-primary:not(:disabled).fc-button-active:focus) {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.fc-view-harness) {
|
:deep(.fc-view-harness) {
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -1265,7 +1306,15 @@ export default defineComponent({
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.matrix-sidebar) {
|
||||||
|
padding-bottom: calc(0.72rem + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.matrix-scroll) {
|
:deep(.matrix-scroll) {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.scroll-inner) {
|
||||||
padding-bottom: calc(0.72rem + env(safe-area-inset-bottom));
|
padding-bottom: calc(0.72rem + env(safe-area-inset-bottom));
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
@@ -1331,11 +1380,14 @@ export default defineComponent({
|
|||||||
|
|
||||||
&__range {
|
&__range {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
font-size: 0.22rem;
|
font-size: 0.2rem;
|
||||||
color: #666;
|
color: #666;
|
||||||
white-space: nowrap;
|
line-height: 1.3;
|
||||||
overflow: hidden;
|
white-space: normal;
|
||||||
text-overflow: ellipsis;
|
overflow: visible;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__nav {
|
&__nav {
|
||||||
@@ -1350,7 +1402,6 @@ export default defineComponent({
|
|||||||
font-size: 0.24rem;
|
font-size: 0.24rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__orient {
|
&__orient {
|
||||||
@@ -1397,8 +1448,7 @@ export default defineComponent({
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
min-height: var(--landscape-toolbar-height);
|
min-height: var(--landscape-toolbar-height);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.08rem;
|
gap: 0.08rem;
|
||||||
padding: 0.08rem 0.12rem;
|
padding: 0.08rem 0.12rem;
|
||||||
@@ -1406,29 +1456,6 @@ export default defineComponent({
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.landscape-shell__left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.08rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landscape-shell__title {
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.08rem;
|
|
||||||
color: #333;
|
|
||||||
font-size: 0.24rem;
|
|
||||||
font-weight: 600;
|
|
||||||
|
|
||||||
span {
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.landscape-shell__toggle {
|
.landscape-shell__toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1473,7 +1500,7 @@ export default defineComponent({
|
|||||||
.landscape-stage-viewport--matrix {
|
.landscape-stage-viewport--matrix {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.landscape-stage {
|
.landscape-stage {
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
const usingUser = useUser();
|
const usingUser = useUser();
|
||||||
const {store, query, reset} = useDaka();
|
const {store, query, reset} = useDaka();
|
||||||
|
console.debug('[daka] setup, currentRole:', usingUser.store.currentRole);
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
keyword: '',
|
keyword: '',
|
||||||
|
|||||||
+1
-1
@@ -159,7 +159,7 @@ export default defineComponent({
|
|||||||
if (role === HtyBaseRoles.TEACHER) return '/teacher/home'
|
if (role === HtyBaseRoles.TEACHER) return '/teacher/home'
|
||||||
if (role === HtySuperRoles.ADMIN) return '/admin/teachers'
|
if (role === HtySuperRoles.ADMIN) return '/admin/teachers'
|
||||||
if (role === HtySuperRoles.TESTER) return '/tester'
|
if (role === HtySuperRoles.TESTER) return '/tester'
|
||||||
if (role === HtySuperRoles.SUPERVISOR) return '/supervisor/subsidiaries'
|
if (role === HtySuperRoles.SUPERVISOR) return '/teacher/home'
|
||||||
return '/guest/profile'
|
return '/guest/profile'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -8,6 +8,7 @@ import {
|
|||||||
Daka,
|
Daka,
|
||||||
DakaScope,
|
DakaScope,
|
||||||
HtyBaseRoles,
|
HtyBaseRoles,
|
||||||
|
HtySuperRoles,
|
||||||
HtyRoles,
|
HtyRoles,
|
||||||
Clazz,
|
Clazz,
|
||||||
JihuaQueryParam,
|
JihuaQueryParam,
|
||||||
@@ -82,7 +83,8 @@ export default function useDaka() {
|
|||||||
|
|
||||||
async function query(params: JihuaQueryParam): Promise<boolean> {
|
async function query(params: JihuaQueryParam): Promise<boolean> {
|
||||||
let {current: {hty_id}, currentRole} = usingUser.store;
|
let {current: {hty_id}, currentRole} = usingUser.store;
|
||||||
if (currentRole === HtyBaseRoles.TEACHER) {
|
console.debug('[daka query] currentRole:', currentRole, 'hty_id:', hty_id);
|
||||||
|
if (currentRole === HtyBaseRoles.TEACHER || currentRole === HtySuperRoles.SUPERVISOR) {
|
||||||
params.teacher_id = hty_id;
|
params.teacher_id = hty_id;
|
||||||
params.scope = DakaScope.ALL;
|
params.scope = DakaScope.ALL;
|
||||||
} else if (currentRole === HtyBaseRoles.STUDENT) {
|
} else if (currentRole === HtyBaseRoles.STUDENT) {
|
||||||
@@ -92,10 +94,11 @@ export default function useDaka() {
|
|||||||
params.teacher_id = teacherStore.currentTeacherId;
|
params.teacher_id = teacherStore.currentTeacherId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.debug('[daka query] params after role setup:', params);
|
||||||
store.query_cache = params;
|
store.query_cache = params;
|
||||||
load_start()
|
load_start()
|
||||||
const {r, d, e} = await request({url: '/api/v1/ws/find_dakas_with_sections_by_user_id', params});
|
const {r, d, e} = await request({url: '/api/v1/ws/find_dakas_with_sections_by_user_id', params});
|
||||||
load_done()
|
console.debug('[daka query] response:', {r, d, e});
|
||||||
if (r) {
|
if (r) {
|
||||||
if (params.page === 1) {
|
if (params.page === 1) {
|
||||||
store.list = [];
|
store.list = [];
|
||||||
|
|||||||
@@ -399,7 +399,13 @@ export default function useUser(router?: Router) {
|
|||||||
if (currentOrgId) {
|
if (currentOrgId) {
|
||||||
window.localStorage.setItem(HtySudoToken, authToken as string);
|
window.localStorage.setItem(HtySudoToken, authToken as string);
|
||||||
} else {
|
} else {
|
||||||
|
try {
|
||||||
await sudo2(userApp.id);
|
await sudo2(userApp.id);
|
||||||
|
} catch {
|
||||||
|
if (authToken) {
|
||||||
|
window.localStorage.setItem(HtySudoToken, authToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await getTags();
|
await getTags();
|
||||||
await getMyTeachers();
|
await getMyTeachers();
|
||||||
|
|||||||
Reference in New Issue
Block a user