Compare commits

...
6 Commits
Author SHA1 Message Date
xuyueandClaude Opus 5 6766c4dcd5 style(header): 演示模式菜单文案改为「进入演示 / 退出演示」
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
两端都是四字,动作对称。spec 与 plan 文档同步更新。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 07:51:11 -06:00
xuyueandClaude Opus 5 bd6c5d19f8 style(header): 演示模式退出文案改为四字
「退出学生视角」六字,与「学生视角」不齐。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 07:49:22 -06:00
xuyueandClaude Opus 5 8a343edbbb feat(header): 用户下拉菜单加入学生视角开关
仅超管可见。进入演示模式时若停在后台页面则跳回首页。
options 改为 computed,否则菜单文案与显示条件不会随状态更新。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 07:48:22 -06:00
xuyueandClaude Opus 5 07bbbce013 feat(user): store 层加入演示模式伪装开关
超管开启后所有角色 getter 降级为普通学生,全站权限判断自动跟随。
realIsSuperAdmin 保留真实身份,用于判断能否切换。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 07:47:27 -06:00
xuyueandClaude Opus 5 5fac75238d docs: 学生视角(演示模式)实施计划
两个任务:store 层伪装开关、Header 下拉菜单入口。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 07:46:23 -06:00
xuyueandClaude Opus 5 3509f340ff docs: 学生视角(演示模式)设计
超管一键把界面切换成普通学生的样子,方便上课演示。
纯前端伪装,在 userStore 层包装角色 getter,全站权限判断自动跟随。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 07:43:57 -06:00
6 changed files with 451 additions and 15 deletions
@@ -0,0 +1,296 @@
# 学生视角(演示模式)Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 超级管理员在右上角下拉菜单点一下,整站界面变成普通学生看到的样子,再点一下恢复。
**Architecture:** 纯前端伪装。全站 40 多处权限判断读的都是 `shared/store/user.ts` 里的 6 个角色 getter,没有一处直接读 `user.admin_type`。在 store 里加一个 `demoMode` 开关,给每个 getter 加 `!demoMode.value &&` 前缀,全站自动跟随。路由守卫(`src/main.ts`)、权限工具(`src/utils/permissions.ts`)、各页面 `v-if` 零改动。
**Tech Stack:** Vue 3 `<script setup>` + TypeScriptPinia setup storeNaive UI`n-dropdown` / `DropdownOption`),Vite。
**Spec:** `docs/superpowers/specs/2026-07-26-demo-student-view-design.md`
## Global Constraints
- **不写测试。** 项目根 CLAUDE.md 明确规定 "Do not write new tests",且 ojnext 无测试框架。本计划的验证步骤全部是 `npm run build` 冒烟 + 浏览器手工核对。
- **不改后端。** 演示模式是界面伪装,登录态仍是超管,接口权限不变。
- 仅超级管理员可见此开关。教师管理员、学生管理员不提供。
- 自动导入已配置:`ref` / `computed` / `useRouter` / `useRoute` / Naive UI 组件与类型(`DropdownOption`)均**不需要手写 import**。
- 存储 key 常量统一放 `src/utils/constants.ts``STORAGE_KEY`,读写走 `src/utils/storage.ts` 默认导出(内部做 JSON 序列化)。
- 提交前跑 `npm fmt`Prettier)。
- 中文注释、中文 UI 文案,与现有代码一致。
---
## File Structure
| 文件 | 职责 | 本次改动 |
|---|---|---|
| `src/utils/constants.ts` | 全局常量 | `STORAGE_KEY` 增加一个键 |
| `src/shared/store/user.ts` | 用户身份与角色判断的唯一来源 | 新增 `demoMode` 状态与伪装逻辑(改动主体) |
| `src/shared/components/Header.vue` | 顶栏与用户下拉菜单 | 新增菜单项与切换处理函数 |
不新建文件。`demoMode` 放进已有的 `user` store 而不是单开一个 store —— 它伪装的就是这个 store 的输出,分开会让两个 store 循环依赖。
---
### Task 1: store 层伪装开关
**Files:**
- Modify: `src/utils/constants.ts:147-153`
- Modify: `src/shared/store/user.ts`
**Interfaces:**
- Consumes: 无(第一个任务)
- Produces: `useUserStore()` 新增三个成员,供 Task 2 使用:
- `demoMode: boolean` — 当前是否处于演示模式(store 解包后为布尔值)
- `canToggleDemoMode: boolean` — 是否显示切换入口(真实超管身份,不受伪装影响)
- `toggleDemoMode(): void` — 翻转开关并写入 localStorage
- [ ] **Step 1: 在 `STORAGE_KEY` 增加常量**
打开 `src/utils/constants.ts`,把 `STORAGE_KEY` 改成:
```ts
export const STORAGE_KEY = {
AUTHED: "authed",
LANGUAGE: "problemLanguage",
LEARN_CURRENT_STEP: "learnStep",
ADMIN_PROBLEM: "adminProblem",
ADMIN_PROBLEM_TAGS: "adminProblemTags",
DEMO_MODE: "demoMode",
}
```
- [ ] **Step 2: 在 user store 加入 `demoMode` 与真实身份 getter**
打开 `src/shared/store/user.ts`。在 `const isAuthed = ...` 那一行之后、`const isAdminRole = ...` 之前,插入:
```ts
// 演示模式:超管临时把界面伪装成普通学生,方便上课投屏
const demoMode = ref<boolean>(storage.get(STORAGE_KEY.DEMO_MODE) ?? false)
// 不受伪装影响的真实身份,只用于判断能否切换演示模式。
// 若这里用被伪装后的 isSuperAdmin,一进入演示模式入口就消失了,退不出来。
const realIsSuperAdmin = computed(
() => user.value?.admin_type === USER_TYPE.SUPER_ADMIN,
)
```
`storage``STORAGE_KEY` 文件顶部已经 import 过,不需要新增 import。
- [ ] **Step 3: 给 6 个角色 getter 加上伪装前缀**
`src/shared/store/user.ts` 中原有的 6 个 getter`isAdminRole``isStudentAdmin``isTeacherAdmin``isTeacherOrAbove``isSuperAdmin``hasProblemPermission`)整段替换为:
```ts
const isAdminRole = computed(
() =>
!demoMode.value &&
(user.value?.admin_type === USER_TYPE.STUDENT_ADMIN ||
user.value?.admin_type === USER_TYPE.TEACHER_ADMIN ||
user.value?.admin_type === USER_TYPE.SUPER_ADMIN),
)
const isStudentAdmin = computed(
() => !demoMode.value && user.value?.admin_type === USER_TYPE.STUDENT_ADMIN,
)
const isTeacherAdmin = computed(
() => !demoMode.value && user.value?.admin_type === USER_TYPE.TEACHER_ADMIN,
)
const isTeacherOrAbove = computed(
() =>
!demoMode.value &&
(user.value?.admin_type === USER_TYPE.TEACHER_ADMIN ||
user.value?.admin_type === USER_TYPE.SUPER_ADMIN),
)
const isSuperAdmin = computed(() => !demoMode.value && realIsSuperAdmin.value)
const hasProblemPermission = computed(
() =>
!demoMode.value &&
user.value?.problem_permission !== PROBLEM_PERMISSION.NONE,
)
```
注意:`isAdminRole``isTeacherOrAbove` 原本是多个 `||` 连成的表达式,加前缀时**必须给原表达式套一层括号**,否则 `&&` 的优先级会让第一个 `||` 分支逃过伪装。
- [ ] **Step 4: 加入切换能力与切换函数**
紧接在 `hasProblemPermission` 之后插入:
```ts
const canToggleDemoMode = computed(() => realIsSuperAdmin.value)
function toggleDemoMode() {
demoMode.value = !demoMode.value
storage.set(STORAGE_KEY.DEMO_MODE, demoMode.value)
}
```
- [ ] **Step 5: 退出登录时重置内存中的开关**
`storage.clear()` 会清掉 localStorage 里的标记,但内存中的 ref 还留着,同一次会话里换账号登录会带过去。把 `clearProfile` 改成:
```ts
function clearProfile() {
profile.value = null
demoMode.value = false
storage.clear()
}
```
- [ ] **Step 6: 导出新成员**
在 store 末尾的 `return { ... }` 里加入三项(放在 `hasProblemPermission` 之后):
```ts
demoMode,
canToggleDemoMode,
toggleDemoMode,
```
- [ ] **Step 7: 格式化并冒烟构建**
```bash
cd ojnext
npm fmt
npm run build
```
Expected: 构建成功,无报错。
- [ ] **Step 8: 手工验证伪装生效(此时还没有 UI 入口,用 localStorage 模拟)**
启动 `npm start`,用超管账号登录,然后在浏览器 DevTools Console 执行:
```js
localStorage.setItem("demoMode", "true")
location.reload()
```
逐项核对:
- 顶栏「后台」菜单项消失
- 地址栏直接输入 `/admin` → 被弹回首页
- 题目详情页不再出现管理员专属按钮
再执行 `localStorage.setItem("demoMode", "false"); location.reload()`,确认上述内容全部恢复。
- [ ] **Step 9: 提交**
```bash
cd ojnext
git add src/utils/constants.ts src/shared/store/user.ts
git commit -m "feat(user): store 层加入演示模式伪装开关
超管开启后所有角色 getter 降级为普通学生,全站权限判断自动跟随。
realIsSuperAdmin 保留真实身份,用于判断能否切换。"
```
---
### Task 2: 下拉菜单切换入口
**Files:**
- Modify: `src/shared/components/Header.vue:109-113`(新增函数)、`:178-227``options` 改为 computed 并增加菜单项)
**Interfaces:**
- Consumes: Task 1 提供的 `userStore.demoMode``userStore.canToggleDemoMode``userStore.toggleDemoMode()`
- Produces: 无后续任务依赖
- [ ] **Step 1: 把 `options` 从普通数组改为 computed**
`src/shared/components/Header.vue` 第 178 行现在是:
```ts
const options: Array<DropdownOption | DropdownDividerOption> = [
```
改为:
```ts
const options = computed<Array<DropdownOption | DropdownDividerOption>>(() => [
```
并把第 227 行的结尾 `]` 改为 `])`
**这一步是必需的,不是风格偏好**:原来的 `options` 是普通数组,只在 setup 时求值一次。新菜单项的 `label`(「进入演示」/「退出演示」)和 `show` 都要跟随状态变化,留在普通数组里永远不会更新。同文件的 `menus`(第 119 行)本来就是 computed,改完两者一致。
模板里 `:options="options"`(第 280 行)不用动,computed 在模板中自动解包。
- [ ] **Step 2: 加入切换处理函数**
`handleLogout`(第 109-113 行)之后插入:
```ts
function handleToggleDemoMode() {
const entering = !userStore.demoMode
userStore.toggleDemoMode()
// 进入演示模式时若正停在后台页面,当前界面已经失去权限,必须主动退出去
if (entering && route.path.startsWith("/admin")) {
router.push("/")
}
}
```
`route``router` 在第 16-17 行已经拿到,`userStore` 在第 12 行已经拿到,无需新增。
- [ ] **Step 3: 在下拉菜单中加入菜单项**
`options` 数组里、`{ type: "divider" }`(第 220 行)**之前**插入:
```ts
{
label: userStore.demoMode ? "退出演示" : "进入演示",
key: "demo-mode",
show: userStore.canToggleDemoMode,
icon: renderIcon("fluent-emoji:graduation-cap"),
props: { onClick: handleToggleDemoMode },
},
```
文案本身就是状态指示器:看到「退出演示」说明当前正处于演示模式。按设计不额外加横幅。
- [ ] **Step 4: 格式化并冒烟构建**
```bash
cd ojnext
npm fmt
npm run build
```
Expected: 构建成功,无报错。
- [ ] **Step 5: 手工验证完整流程**
先清掉 Task 1 遗留的手工标记:DevTools Console 执行 `localStorage.removeItem("demoMode")`,刷新。
用超管账号登录,逐项核对:
1. 右上角用户名下拉菜单出现「进入演示」,图标正常显示(不是空白方块)
2. 点击 →「后台」菜单项消失,下拉菜单文案变为「退出演示」
3. 刷新页面 → 仍是学生界面,菜单仍显示「退出演示」
4. 地址栏直接输入 `/admin` → 弹回首页
5. 点「退出演示」→「后台」入口恢复,文案变回「进入演示」
6. 进入 `/admin/problem/list`,打开下拉菜单点「进入演示」→ 自动跳回首页
7. 退出登录后重新用超管登录 → 演示模式已重置为关闭状态
8. 用教师管理员账号登录 → 下拉菜单中**没有**这一项
- [ ] **Step 6: 提交**
```bash
cd ojnext
git add src/shared/components/Header.vue
git commit -m "feat(header): 用户下拉菜单加入学生视角开关
仅超管可见。进入演示模式时若停在后台页面则跳回首页。
options 改为 computed,否则菜单文案与显示条件不会随状态更新。"
```
---
## 完成后
两个任务都提交后,整个特性即完成。回读一遍 spec 的「连带影响」章节,确认这些变化在实机上都是预期的:
- 提交列表的教师筛选与额外列消失,`showSubmissions` 改为跟随站点配置
- 比赛失去超管免密码特权
- 协同代码编辑(`shared/composables/sync.ts`)的超管特殊颜色与提示一并变为学生行为 —— **已确认不做豁免**
@@ -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<boolean>(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. 用教师管理员账号登录 → 菜单中无此项
-2
View File
@@ -29,7 +29,6 @@ const hiddenICP = computed(() =>
["problem", "contest problem"].includes(route.name as string), ["problem", "contest problem"].includes(route.name as string),
) )
const currentYear = new Date().getFullYear() const currentYear = new Date().getFullYear()
const copyrightText = `© 2022 - ${currentYear} 判题狗 保留所有权利` const copyrightText = `© 2022 - ${currentYear} 判题狗 保留所有权利`
@@ -56,7 +55,6 @@ function goPublicSecurity() {
margin: 12px 0; margin: 12px 0;
} }
.beian--mobile { .beian--mobile {
font-size: 12px; font-size: 12px;
max-width: 100%; max-width: 100%;
+18 -2
View File
@@ -112,6 +112,15 @@ async function handleLogout() {
router.replace("/") router.replace("/")
} }
function handleToggleDemoMode() {
const entering = !userStore.demoMode
userStore.toggleDemoMode()
// 进入演示模式时若正停在后台页面,当前界面已经失去权限,必须主动退出去
if (entering && route.path.startsWith("/admin")) {
router.push("/")
}
}
function renderIcon(icon: string) { function renderIcon(icon: string) {
return () => h(Icon, { icon, width: 20 }) return () => h(Icon, { icon, width: 20 })
} }
@@ -175,7 +184,7 @@ const menus = computed<MenuOption[]>(() => [
}, },
]) ])
const options: Array<DropdownOption | DropdownDividerOption> = [ const options = computed<Array<DropdownOption | DropdownDividerOption>>(() => [
{ {
label: "我的主页", label: "我的主页",
key: "home", key: "home",
@@ -217,6 +226,13 @@ const options: Array<DropdownOption | DropdownDividerOption> = [
onClick: () => router.push("/ai-analysis"), onClick: () => router.push("/ai-analysis"),
}, },
}, },
{
label: userStore.demoMode ? "退出演示" : "进入演示",
key: "demo-mode",
show: userStore.canToggleDemoMode,
icon: renderIcon("fluent-emoji:graduation-cap"),
props: { onClick: handleToggleDemoMode },
},
{ type: "divider" }, { type: "divider" },
{ {
label: "退出", label: "退出",
@@ -224,7 +240,7 @@ const options: Array<DropdownOption | DropdownDividerOption> = [
icon: renderIcon("streamline-ultimate-color:coffee-cold"), icon: renderIcon("streamline-ultimate-color:coffee-cold"),
props: { onClick: handleLogout }, props: { onClick: handleLogout },
}, },
] ])
function goHome() { function goHome() {
router.push("/") router.push("/")
+34 -11
View File
@@ -11,30 +11,49 @@ export const useUserStore = defineStore("user", () => {
const [isFinished] = useToggle(false) const [isFinished] = useToggle(false)
const user = computed<User | null>(() => profile.value?.user ?? null) const user = computed<User | null>(() => profile.value?.user ?? null)
const isAuthed = computed(() => !!user.value?.email) const isAuthed = computed(() => !!user.value?.email)
// 演示模式:超管临时把界面伪装成普通学生,方便上课投屏
const demoMode = ref<boolean>(storage.get(STORAGE_KEY.DEMO_MODE) ?? false)
// 不受伪装影响的真实身份,只用于判断能否切换演示模式。
// 若这里用被伪装后的 isSuperAdmin,一进入演示模式入口就消失了,退不出来。
const realIsSuperAdmin = computed(
() => user.value?.admin_type === USER_TYPE.SUPER_ADMIN,
)
const isAdminRole = computed( const isAdminRole = computed(
() => () =>
user.value?.admin_type === USER_TYPE.STUDENT_ADMIN || !demoMode.value &&
user.value?.admin_type === USER_TYPE.TEACHER_ADMIN || (user.value?.admin_type === USER_TYPE.STUDENT_ADMIN ||
user.value?.admin_type === USER_TYPE.SUPER_ADMIN, user.value?.admin_type === USER_TYPE.TEACHER_ADMIN ||
user.value?.admin_type === USER_TYPE.SUPER_ADMIN),
) )
const isStudentAdmin = computed( const isStudentAdmin = computed(
() => user.value?.admin_type === USER_TYPE.STUDENT_ADMIN, () => !demoMode.value && user.value?.admin_type === USER_TYPE.STUDENT_ADMIN,
) )
const isTeacherAdmin = computed( const isTeacherAdmin = computed(
() => user.value?.admin_type === USER_TYPE.TEACHER_ADMIN, () => !demoMode.value && user.value?.admin_type === USER_TYPE.TEACHER_ADMIN,
) )
const isTeacherOrAbove = computed( const isTeacherOrAbove = computed(
() => () =>
user.value?.admin_type === USER_TYPE.TEACHER_ADMIN || !demoMode.value &&
user.value?.admin_type === USER_TYPE.SUPER_ADMIN, (user.value?.admin_type === USER_TYPE.TEACHER_ADMIN ||
) user.value?.admin_type === USER_TYPE.SUPER_ADMIN),
const isSuperAdmin = computed(
() => user.value?.admin_type === USER_TYPE.SUPER_ADMIN,
) )
const isSuperAdmin = computed(() => !demoMode.value && realIsSuperAdmin.value)
const hasProblemPermission = computed( const hasProblemPermission = computed(
() => user.value?.problem_permission !== PROBLEM_PERMISSION.NONE, () =>
!demoMode.value &&
user.value?.problem_permission !== PROBLEM_PERMISSION.NONE,
) )
const canToggleDemoMode = computed(() => realIsSuperAdmin.value)
function toggleDemoMode() {
demoMode.value = !demoMode.value
storage.set(STORAGE_KEY.DEMO_MODE, demoMode.value)
}
const showSubmissions = computed(() => { const showSubmissions = computed(() => {
let flag = configStore.config.submission_list_show_all let flag = configStore.config.submission_list_show_all
if (isAdminRole.value) flag = true if (isAdminRole.value) flag = true
@@ -51,6 +70,7 @@ export const useUserStore = defineStore("user", () => {
function clearProfile() { function clearProfile() {
profile.value = null profile.value = null
demoMode.value = false
storage.clear() storage.clear()
} }
return { return {
@@ -63,6 +83,9 @@ export const useUserStore = defineStore("user", () => {
isTeacherOrAbove, isTeacherOrAbove,
isSuperAdmin, isSuperAdmin,
hasProblemPermission, hasProblemPermission,
demoMode,
canToggleDemoMode,
toggleDemoMode,
isAuthed, isAuthed,
showSubmissions, showSubmissions,
getMyProfile, getMyProfile,
+1
View File
@@ -150,6 +150,7 @@ export const STORAGE_KEY = {
LEARN_CURRENT_STEP: "learnStep", LEARN_CURRENT_STEP: "learnStep",
ADMIN_PROBLEM: "adminProblem", ADMIN_PROBLEM: "adminProblem",
ADMIN_PROBLEM_TAGS: "adminProblemTags", ADMIN_PROBLEM_TAGS: "adminProblemTags",
DEMO_MODE: "demoMode",
} }
export const DIFFICULTY = { export const DIFFICULTY = {