feat: 改版阶段 1:统一顶栏和教学侧栏、配色和图标规范、路由角色守卫
Deploy / deploy (build, debian, 22) (push) Canceled after 0s

- 所有页面共用顶栏(教程 / 挑战 / 自由创作 / 作品展,右边“教学”和用户菜单);
  没登录时顶栏有“登录”按钮,关掉登录框不再白屏;/submission/:id 全屏页不带顶栏
- 后台改成“教学”侧栏,分出题 / 班级 / 管理三组;“全部提交”留在侧栏里打开;
  admin 也能进作品展管理(后端本来就允许)
- 路由按角色守卫:meta.role 为 staff / super,学生和 admin 进不了无权页面
- 配色只在 theme.ts 定义,同时生成 --wp-* CSS 变量和 Naive UI 主题,暗色值先定好
- 图标改用打包进来的 lucide(断网也能显示),去掉 @iconify/vue 和 PUBLIC_ICONIFY_URL;
  代码字体换成打包的 JetBrains Mono,编辑器默认字号 24 → 16
- 教程面板去掉和顶栏重复的切换和图标按钮,图标按钮都补上文字或提示

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-26 23:39:55 +08:00
1 parent e47fac1732
commit ff1eb2982e
43 files changed
+837 -429

No files matched your search

+106 -81
View File
@@ -1,126 +1,151 @@
<template>
<n-flex class="container" :wrap="false">
<div class="sidebar">
<div class="back-btn" @click="() => goHome($router, taskTab, step)">
← 返回
</div>
<n-divider style="margin: 8px 0" />
<div
v-for="item in menu"
:key="item.label"
:class="['nav-item', { active: $route.name === item.route.name }]"
@click="$router.push(item.route)"
>
{{ item.label }}
</div>
</div>
<div class="teach">
<nav class="sidebar">
<template v-for="group in groups" :key="group.label">
<div class="group-label">{{ group.label }}</div>
<router-link
v-for="item in group.items"
:key="item.label"
:to="item.route"
:class="['nav-item', { active: item.names.includes(String($route.name)) }]"
>
<Icon :icon="item.icon" :width="16" />
<span>{{ item.label }}</span>
</router-link>
</template>
</nav>
<div class="content">
<router-view></router-view>
</div>
</n-flex>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue"
import { taskTab } from "../store/task"
import { Icon } from "../icons"
import { step } from "../store/tutorial"
import { roleAdmin, roleSuper } from "../store/user"
import { goHome } from "../utils/helper"
import { roleSuper } from "../store/user"
const menu = computed(() =>
// admin 只能出挑战、看提交、管作品展;其余是超管的(和后端权限一致)
const groups = computed(() =>
[
{
label: "教程",
route: { name: "tutorial-editor", params: { display: step.value } },
show: roleSuper.value,
label: "出题",
items: [
{
label: "教程",
icon: "lucide:book-open",
route: { name: "tutorial-editor", params: { display: step.value } },
names: ["tutorial-editor"],
show: roleSuper.value,
},
{
label: "挑战",
icon: "lucide:swords",
route: { name: "challenge-editor", params: { display: 0 } },
names: ["challenge-editor"],
show: true,
},
],
},
{
label: "挑战",
route: { name: "challenge-editor", params: { display: 0 } },
show: roleAdmin.value || roleSuper.value,
label: "班级",
items: [
{
label: "成绩册",
icon: "lucide:graduation-cap",
route: { name: "gradebook" },
names: ["gradebook"],
show: roleSuper.value,
},
{
label: "全部提交",
icon: "lucide:list",
route: { name: "teach-submissions", params: { page: 1 } },
names: ["teach-submissions"],
show: true,
},
],
},
{
label: "用户",
route: { name: "user-manage", params: { page: 1 } },
show: roleSuper.value,
label: "管理",
items: [
{
label: "作品展",
icon: "lucide:award",
route: { name: "showcase-manage" },
names: ["showcase-manage"],
show: true,
},
{
label: "用户",
icon: "lucide:users",
route: { name: "user-manage", params: { page: 1 } },
names: ["user-manage"],
show: roleSuper.value,
},
],
},
{
label: "工坊",
route: { name: "showcase-manage" },
show: roleSuper.value,
},
{
label: "成绩",
route: { name: "gradebook" },
show: roleSuper.value,
},
{
label: "提交",
route: { name: "submissions", params: { page: 1 } },
show: roleAdmin.value || roleSuper.value,
},
].filter((item) => item.show),
]
.map((g) => ({ ...g, items: g.items.filter((i) => i.show) }))
.filter((g) => g.items.length),
)
</script>
<style scoped>
.container {
height: 100vh;
width: 100vw;
box-sizing: border-box;
.teach {
height: 100%;
display: flex;
}
.sidebar {
width: 110px;
min-width: 110px;
padding: 12px 8px;
border-right: 1px solid #efeff5;
background: #fafafa;
width: 148px;
flex: none;
padding: 8px;
border-right: 1px solid var(--wp-line);
background: var(--wp-surface);
display: flex;
flex-direction: column;
gap: 2px;
overflow-y: auto;
}
.back-btn {
padding: 7px 10px;
font-size: 13px;
color: #888;
cursor: pointer;
border-radius: 6px;
transition:
background-color 0.15s,
color 0.15s;
user-select: none;
}
.back-btn:hover {
background-color: #f0f0f0;
color: #444;
.group-label {
padding: 12px 10px 4px;
font-size: 12px;
color: var(--wp-faint);
letter-spacing: 0.04em;
}
.nav-item {
padding: 8px 10px;
display: flex;
align-items: center;
gap: 8px;
padding: 7px 10px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
color: #444;
transition:
background-color 0.15s,
color 0.15s;
user-select: none;
color: var(--wp-muted);
text-decoration: none;
}
.nav-item:hover {
background-color: #f0f0f0;
background: var(--wp-paper);
color: var(--wp-ink);
}
.nav-item.active {
background-color: #e8f8f0;
color: #18a058;
font-weight: 500;
background: var(--wp-brand-soft);
color: var(--wp-brand);
font-weight: 600;
}
.nav-item:focus-visible {
outline: 2px solid var(--wp-brand);
outline-offset: 1px;
}
.content {
flex: 1;
box-sizing: border-box;
min-width: 0;
overflow: hidden;
background: var(--wp-surface);
}
</style>