From 3509f340ffdd10b6b763709d2f8d0caa1debe2ee Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Sun, 26 Jul 2026 07:43:57 -0600 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=AD=A6=E7=94=9F=E8=A7=86=E8=A7=92?= =?UTF-8?q?=EF=BC=88=E6=BC=94=E7=A4=BA=E6=A8=A1=E5=BC=8F=EF=BC=89=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 超管一键把界面切换成普通学生的样子,方便上课演示。 纯前端伪装,在 userStore 层包装角色 getter,全站权限判断自动跟随。 Co-Authored-By: Claude Opus 5 --- .../2026-07-26-demo-student-view-design.md | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-26-demo-student-view-design.md diff --git a/docs/superpowers/specs/2026-07-26-demo-student-view-design.md b/docs/superpowers/specs/2026-07-26-demo-student-view-design.md new file mode 100644 index 0000000..7716215 --- /dev/null +++ b/docs/superpowers/specs/2026-07-26-demo-student-view-design.md @@ -0,0 +1,102 @@ +# 学生视角(演示模式)设计 + +日期:2026-07-26 +范围:仅前端(ojnext) + +## 背景 + +超级管理员给学生上课演示时,界面上到处是管理员才可见的入口和按钮(后台菜单、题目编辑、提交列表的额外操作列等)。这些东西对学生是噪音,也容易误点。需要一个一键开关,把界面临时切换成普通学生看到的样子。 + +## 目标 + +- 超管点一下,全站界面变成普通学生的样子 +- 再点一下恢复 +- 刷新页面不丢状态 +- 改动集中,不散落到几十个页面 + +## 非目标 + +- 不改后端。演示模式是纯界面伪装,登录态仍然是超管,接口权限不变。目的是演示,不是权限隔离。 +- 不做审计日志、不做时长限制。 +- 教师管理员、学生管理员不提供此功能。 + +## 机制 + +全站所有权限判断都读 `shared/store/user.ts` 里的几个 getter,没有任何一处直接读 `user.admin_type`。因此在 store 层加一个开关,就能一次性覆盖全部调用点。 + +```ts +// shared/store/user.ts +const demoMode = ref(storage.get(STORAGE_KEY.DEMO_MODE) ?? false) + +// 不受伪装影响的真实身份,只用于决定是否显示切换入口 +const realIsSuperAdmin = computed( + () => user.value?.admin_type === USER_TYPE.SUPER_ADMIN, +) + +const isSuperAdmin = computed(() => !demoMode.value && realIsSuperAdmin.value) +const isAdminRole = computed(() => !demoMode.value && (/* 原逻辑 */)) +const isStudentAdmin = computed(() => !demoMode.value && (/* 原逻辑 */)) +const isTeacherAdmin = computed(() => !demoMode.value && (/* 原逻辑 */)) +const isTeacherOrAbove = computed(() => !demoMode.value && (/* 原逻辑 */)) +const hasProblemPermission = computed(() => !demoMode.value && (/* 原逻辑 */)) + +const canToggleDemoMode = computed(() => realIsSuperAdmin.value) + +function toggleDemoMode() { + demoMode.value = !demoMode.value + storage.set(STORAGE_KEY.DEMO_MODE, demoMode.value) +} +``` + +`realIsSuperAdmin` 是关键:如果切换入口的显示条件用被伪装后的 `isSuperAdmin`,一进入演示模式入口自己就消失了,退不出来。 + +## 改动清单 + +| 文件 | 改动 | +|---|---| +| `src/utils/constants.ts` | `STORAGE_KEY` 增加 `DEMO_MODE: "demoMode"` | +| `src/shared/store/user.ts` | 新增 `demoMode`、`realIsSuperAdmin`、`canToggleDemoMode`、`toggleDemoMode`;6 个角色 getter 加 `!demoMode.value &&` 前缀;导出新成员 | +| `src/shared/components/Header.vue` | 用户下拉菜单 `options` 增加一项「学生视角 / 退出学生视角」 | + +**不改**:`src/main.ts` 路由守卫、`src/utils/permissions.ts`、以及所有页面级的 `v-if` 判断。它们读的都是上述 getter,自动跟随。 + +## 交互 + +**入口**:右上角用户头像下拉菜单,与「我的主页」「我的提交」并列。 + +- 显示条件:`userStore.canToggleDemoMode` +- 文案随状态翻转:未开启显示「学生视角」,已开启显示「退出学生视角」 +- 该文案本身就是状态指示器,不额外加横幅或角标 + +**点击行为**: + +1. 调用 `toggleDemoMode()` +2. 如果是**进入**演示模式,且当前路由属于 `admins` 分支,执行 `router.push("/")`。否则页面会停在一个已失去权限的后台界面上。 +3. 退出演示模式不需要跳转,留在当前页即可。 + +## 连带影响(均为预期行为) + +- **后台入口消失**(`Header.vue:165-175`)。手动输入 `/admin/*` 地址也会被 `main.ts` 守卫弹回首页,因为守卫读的是被伪装后的 getter。 +- **提交列表**(`submission/list.vue`)的教师专属筛选、额外列隐藏;`showSubmissions` 改为跟随站点配置 `submission_list_show_all`,而不是无条件为 true。 +- **题目编辑表单**(`problem/components/Form.vue`)的管理员字段隐藏。 +- **比赛访问**(`oj/store/contest.ts:49`)失去超管免密码特权,需按学生流程输密码。演示时更真实。 +- **协同代码编辑**(`shared/composables/sync.ts`)的超管特殊颜色与提示一并变为学生行为。**确认不做豁免**——演示场景不涉及协同编辑功能。 + +## 持久化与清理 + +- 存 `localStorage`,key 为 `demoMode` +- store 初始化时从 storage 读取,刷新后保持 +- 退出登录时 `clearProfile()` 调用 `storage.clear()`,会一并清除,不会残留到下一个登录用户 + +## 验证方式 + +手工验证(本项目不写测试): + +1. 超管登录 → 下拉菜单出现「学生视角」 +2. 点击 → 顶栏「后台」消失,菜单文案变为「退出学生视角」 +3. 停在 `/admin/problem/list` 时点击 → 跳回首页 +4. 地址栏直接输 `/admin` → 弹回首页 +5. 刷新页面 → 仍是学生界面 +6. 点「退出学生视角」→ 后台入口恢复 +7. 退出登录再登录 → 演示模式已重置为关闭 +8. 用教师管理员账号登录 → 菜单中无此项