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

+7 -7
View File
@@ -8,20 +8,20 @@ import { computed } from "vue"
const styleTheme = EditorView.baseTheme({
"& .cm-scroller": {
"font-family": "Monaco",
"font-family": "var(--wp-font-mono)",
},
"&.cm-editor.cm-focused": {
outline: "none",
},
"&.cm-editor .cm-tooltip.cm-tooltip-autocomplete ul": {
"font-family": "Monaco",
"font-family": "var(--wp-font-mono)",
},
})
const bgColors: Record<string, string> = {
html: "#fff6f3",
css: "#f3f6ff",
js: "#fffdf0",
html: "var(--wp-html-soft)",
css: "var(--wp-css-soft)",
js: "var(--wp-js-soft)",
}
interface Props {
@@ -31,7 +31,7 @@ interface Props {
const props = withDefaults(defineProps<Props>(), {
language: "html",
fontSize: 20,
fontSize: 16,
})
const code = defineModel<string>("value")
@@ -43,7 +43,7 @@ const lang = computed(() => {
})
const bgTheme = computed(() => {
const bg = bgColors[props.language] ?? "#ffffff"
const bg = bgColors[props.language] ?? "var(--wp-surface)"
return EditorView.theme({
"&": { backgroundColor: bg },
".cm-gutters": { backgroundColor: bg },
+47 -24
View File
@@ -10,37 +10,25 @@
>
<n-tab-pane name="html" tab="HTML">
<template #tab>
<n-flex align="center">
<Icon :width="20" icon="skill-icons:html"></Icon>
<span>HTML</span>
</n-flex>
<span class="lang-tab lang-html">HTML</span>
</template>
<Editor v-model:value="html" :font-size="size" language="html" />
</n-tab-pane>
<n-tab-pane name="css" tab="CSS">
<template #tab>
<n-flex align="center">
<Icon :width="20" icon="skill-icons:css"></Icon>
<span>CSS</span>
</n-flex>
<span class="lang-tab lang-css">CSS</span>
</template>
<Editor v-model:value="css" :font-size="size" language="css" />
</n-tab-pane>
<n-tab-pane name="js" tab="JS">
<template #tab>
<n-flex align="center">
<Icon :width="20" icon="skill-icons:javascript"></Icon>
<span>JS</span>
</n-flex>
<span class="lang-tab lang-js">JS</span>
</template>
<Editor v-model:value="js" :font-size="size" language="js" />
</n-tab-pane>
<n-tab-pane name="actions" tab="选项">
<template #tab>
<n-flex align="center">
<Icon :width="20" icon="skill-icons:actix-dark"></Icon>
<span>选项</span>
</n-flex>
<span class="lang-tab">选项</span>
</template>
<n-flex class="wrapper" vertical>
<n-flex align="center">
@@ -53,7 +41,7 @@
<span class="label">字号</span>
<n-flex align="center">
<span :style="{ 'font-size': size + 'px' }">{{ size }}</span>
<n-button :disabled="size === 20" @click="changeSize(size - 2)">
<n-button :disabled="size <= 12" @click="changeSize(size - 2)">
调小
</n-button>
<n-button :disabled="size === 40" @click="changeSize(size + 2)">
@@ -78,7 +66,6 @@
</div>
</template>
<script lang="ts" setup>
import { Icon } from "@iconify/vue"
import Editor from "./Editor.vue"
import Toolbar from "./Toolbar.vue"
import { html, css, js, tab, size, reset } from "../../store/editors"
@@ -193,14 +180,50 @@ async function formatAndSubmit() {
font-size: 16px;
}
/* HTML / CSS / JS 各有固定颜色,见 theme.ts */
.lang-tab {
display: inline-flex;
align-items: center;
gap: 6px;
font-family: var(--wp-font-mono);
font-weight: 600;
font-size: 13px;
}
.lang-tab.lang-html::before,
.lang-tab.lang-css::before,
.lang-tab.lang-js::before {
content: "";
width: 8px;
height: 8px;
border-radius: 50%;
}
.lang-html {
color: var(--wp-html);
}
.lang-html::before {
background: var(--wp-html);
}
.lang-css {
color: var(--wp-css);
}
.lang-css::before {
background: var(--wp-css);
}
.lang-js {
color: var(--wp-js);
}
.lang-js::before {
background: var(--wp-js);
}
:deep(.n-tabs-nav .n-tabs-tab[data-name="html"]) {
background-color: #fff6f3 !important;
background-color: var(--wp-html-soft) !important;
}
:deep(.n-tabs-nav .n-tabs-tab[data-name="css"]) {
background-color: #f3f6ff !important;
background-color: var(--wp-css-soft) !important;
}
:deep(.n-tabs-nav .n-tabs-tab[data-name="js"]) {
background-color: #fffdf0 !important;
background-color: var(--wp-js-soft) !important;
}
.editors-root {
@@ -208,12 +231,12 @@ async function formatAndSubmit() {
}
:deep(.tab-active-html .n-tab-pane) {
background-color: #fff6f3;
background-color: var(--wp-html-soft);
}
:deep(.tab-active-css .n-tab-pane) {
background-color: #f3f6ff;
background-color: var(--wp-css-soft);
}
:deep(.tab-active-js .n-tab-pane) {
background-color: #fffdf0;
background-color: var(--wp-js-soft);
}
</style>
+1 -1
View File
@@ -57,7 +57,7 @@ import { useRouter } from "vue-router"
import { Submission } from "../../api"
import { submission } from "../../store/submission"
import { useMessage } from "naive-ui"
import { Icon } from "@iconify/vue"
import { Icon } from "../../icons"
import copy from "copy-text-to-clipboard"
import {
buildPreviewDocument,
+11 -88
View File
@@ -1,43 +1,31 @@
<template>
<n-flex align="center" class="corner">
<n-button quaternary v-if="!show" @click="showTutorial"> 教程 </n-button>
<n-button quaternary v-if="!show" @click="showTutorial">显示教程</n-button>
<template v-if="user.loaded && authed">
<n-button quaternary @click="emit('format')">整理</n-button>
<n-tooltip>
<template #trigger>
<n-button quaternary @click="emit('format')">整理代码</n-button>
</template>
自动整理缩进和换行
</n-tooltip>
<n-button
type="primary"
secondary
:disabled="submitDisabled"
:loading="submitLoading"
@click="emit('submit')"
>
提交
</n-button>
<n-dropdown :options="menu" @select="clickMenu">
<n-button>{{ user.username }}</n-button>
</n-dropdown>
</template>
<n-button
v-if="user.loaded && !authed"
@click="handleLogin"
secondary
type="primary"
>
登录
</n-button>
</n-flex>
</template>
<script lang="ts" setup>
import { computed, h } from "vue"
import { Icon } from "@iconify/vue"
import { authed, roleAdmin, roleNormal, user } from "../../store/user"
import { loginModal } from "../../store/modal"
import { computed } from "vue"
import { authed, user } from "../../store/user"
import { show, panelSize } from "../../store/panel"
import { taskId } from "../../store/task"
import { Account } from "../../api"
import { Role } from "../../utils/type"
import { router } from "../../router"
const props = defineProps<{
defineProps<{
submitLoading: boolean
}>()
const emit = defineEmits(["format", "submit"])
@@ -46,78 +34,13 @@ const submitDisabled = computed(() => {
return taskId.value === 0
})
const menu = computed(() => [
{
label: "后台管理",
key: "dashboard",
show: !roleNormal.value,
icon: () =>
h(Icon, {
icon: "streamline-emojis:robot-face-1",
width: 20,
}),
},
{
label: "我的提交",
key: "submissions",
icon: () =>
h(Icon, {
icon: "streamline-emojis:bar-chart",
width: 20,
}),
},
{
label: "退出账号",
key: "logout",
icon: () =>
h(Icon, {
icon: "streamline-emojis:hot-beverage-2",
width: 20,
}),
},
])
function showTutorial() {
show.value = true
panelSize.value = 2 / 5
}
function clickMenu(name: string) {
switch (name) {
case "dashboard": {
const route = roleAdmin.value ? "challenge-editor" : "tutorial-editor"
router.push({ name: route, params: { display: 0 } })
break
}
case "submissions":
router.push({
name: "submissions",
params: { page: 1 },
query: { username: user.username },
})
break
case "logout":
handleLogout()
break
}
}
function handleLogin() {
loginModal.value = true
}
async function handleLogout() {
// token 已经先清掉了,退出请求失败(比如断网)界面也要显示为已退出
try {
await Account.logout()
} finally {
user.username = ""
user.role = Role.Normal
}
}
</script>
<style scoped>
.corner {
margin-right: 20px;
margin-right: 12px;
}
</style>