From ad2e4a34a2dad7c5bf21dfc2343e498e51940b04 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 29 Sep 2026 08:26:43 +0800 Subject: [PATCH] =?UTF-8?q?style(=E9=A1=B6=E6=A0=8F):=20=E6=9C=BA=E6=88=BF?= =?UTF-8?q?=201280=20=E5=AE=BD=E7=9A=84=E5=B1=8F=E4=B8=8A=E9=A1=B6?= =?UTF-8?q?=E6=A0=8F=E4=B8=8D=E5=86=8D=E6=8A=98=E6=88=90=E4=B8=A4=E8=A1=8C?= =?UTF-8?q?=E2=80=94=E2=80=94=E8=8F=9C=E5=8D=95=E6=94=B9=E6=88=90=E7=BA=AF?= =?UTF-8?q?=E6=96=87=E5=AD=97=EF=BC=8C=E3=80=8C=E5=90=8E=E5=8F=B0=E3=80=8D?= =?UTF-8?q?=E6=8C=AA=E5=88=B0=E5=8F=B3=E8=BE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- apps/web/src/oj/learn/index.vue | 2 +- .../oj/problem/components/ProblemEditor.vue | 4 +- apps/web/src/oj/problem/detail.vue | 2 +- .../components/FlowchartEditor/index.vue | 2 +- apps/web/src/shared/components/Header.vue | 336 +++++++++++------- apps/web/src/shared/layout/default.vue | 2 +- 6 files changed, 210 insertions(+), 138 deletions(-) diff --git a/apps/web/src/oj/learn/index.vue b/apps/web/src/oj/learn/index.vue index efe28cd..d412dce 100644 --- a/apps/web/src/oj/learn/index.vue +++ b/apps/web/src/oj/learn/index.vue @@ -169,7 +169,7 @@ watch(traced, loadProgress) /* 桌面端固定高度,目录/正文/代码各自内部滚动;移动端交给页面整体滚动 */ @media (min-width: 769px) { .learn-container { - height: calc(100vh - 138px); + height: calc(100vh - 134px); } } diff --git a/apps/web/src/oj/problem/components/ProblemEditor.vue b/apps/web/src/oj/problem/components/ProblemEditor.vue index ce40ee0..3849541 100644 --- a/apps/web/src/oj/problem/components/ProblemEditor.vue +++ b/apps/web/src/oj/problem/components/ProblemEditor.vue @@ -91,10 +91,10 @@ const contestID = route.params.contestID || null // 草稿存在哪个键下(题单入口单独一份),和「重置代码」共用一份算法 const storageKey = useDraftKey() -// 桌面:顶栏 60 + 工具栏 48,协作条再占 40(两栏顶满内容区,见 detail.vue 的 problem-split)。 +// 桌面:顶栏 56 + 工具栏 48,协作条再占 40(两栏顶满内容区,见 detail.vue 的 problem-split)。 // 手机:顶栏两行 + 课堂条 + 页签 + 工具栏约 236px,底部还有「运行例子 / 提交」那条约 64px const editorHeight = computed(() => { - const base = isDesktop.value ? 108 : 300 + const base = isDesktop.value ? 104 : 300 return `calc(100vh - ${base + (teacherCollab.value ? 40 : 0)}px)` }) diff --git a/apps/web/src/oj/problem/detail.vue b/apps/web/src/oj/problem/detail.vue index e6dc325..7db6019 100644 --- a/apps/web/src/oj/problem/detail.vue +++ b/apps/web/src/oj/problem/detail.vue @@ -376,7 +376,7 @@ onBeforeUnmount(() => { .problem-split { margin: -16px; width: calc(100% + 32px); - height: calc(100vh - 60px); + height: calc(100vh - 56px); } /* 分隔线:看上去 1px,拖的时候热区宽一点 */ diff --git a/apps/web/src/shared/components/FlowchartEditor/index.vue b/apps/web/src/shared/components/FlowchartEditor/index.vue index ff0ca97..8a7ace2 100644 --- a/apps/web/src/shared/components/FlowchartEditor/index.vue +++ b/apps/web/src/shared/components/FlowchartEditor/index.vue @@ -21,7 +21,7 @@ interface Props { height?: string } -const { height = "calc(100vh - 133px)" } = defineProps() +const { height = "calc(100vh - 134px)" } = defineProps() // Vue Flow 实例 const { addEdges, removeNodes, removeEdges, fitView, onNodesInitialized } = useVueFlow() diff --git a/apps/web/src/shared/components/Header.vue b/apps/web/src/shared/components/Header.vue index b02410b..d503046 100644 --- a/apps/web/src/shared/components/Header.vue +++ b/apps/web/src/shared/components/Header.vue @@ -9,6 +9,7 @@ import { useAuthModalStore } from "shared/store/authModal" import { useCollabStore } from "shared/store/collab" import { useConfigStore } from "../store/config" import { useUserStore } from "../store/user" +import { useThemeVars } from "naive-ui" const userStore = useUserStore() const configStore = useConfigStore() @@ -20,6 +21,7 @@ const router = useRouter() const { isMobile, isDesktop } = useBreakpoints() const { learnStep } = useLearnProgress() const { isDark, toggleDark } = useDarkTransition() +const theme = useThemeVars() /** * 求助的入口收进姓名下拉里,顶栏只留姓名按钮上的角标 —— 老师不用展开菜单 @@ -28,36 +30,6 @@ const { isDark, toggleDark } = useDarkTransition() */ const pendingHelpCount = computed(() => (collabStore.isTeacher ? collabStore.pendingCount : 0)) -const names = [ - "man-with-chinese-cap-1", - "cat-face", - "china", - "chicken", - "eyes", - "elephant", - "hear-no-evil-monkey", - "panda-face", - "penguin-1", - "rooster", - "star-struck-1", - "tomato", - "rocket", - "sparkles", - "money-bag", - "ghost", - "game-dice", - "ewe-1", - "artist-palette", - "baby-bottle", -] - -function getRandomAvatar() { - const name = names[Math.floor(Math.random() * names.length)] - return `streamline-emojis:${name}` -} - -const avatar = ref(getRandomAvatar()) - /** * 顶栏的题号框:老师报完题号,学生在哪一页都能直接敲,不用先回首页或题目列表。 * 只给桌面端 —— 窄屏顶栏已经挤满了,手机上从首页的搜索框走同一套逻辑。 @@ -70,6 +42,13 @@ async function handleJump() { jumpKeyword.value = "" } +/** + * 站名后面的小标签:环境名、演示中。 + * + * 演示模式除了下拉里那行「退出演示」再没有别的痕迹,而它是存在 localStorage 里 + * 的,刷新、关标签页都还在,只有退出登录才清 —— 不在这儿常驻标一下,很容易 + * 投屏完忘了退,第二天纳闷后台入口怎么没了。 + */ const envVersion = computed(() => { if (import.meta.env.PUBLIC_ENV === "test") { return "测试版" @@ -79,17 +58,6 @@ const envVersion = computed(() => { return "" }) -/** - * 站名后面括号里的东西:环境名 + 演示中。 - * - * 演示模式除了下拉里那行「退出演示」再没有别的痕迹,而它是存在 localStorage 里 - * 的,刷新、关标签页都还在,只有退出登录才清 —— 不在这儿常驻标一下,很容易 - * 投屏完忘了退,第二天纳闷后台入口怎么没了。 - */ -const titleTags = computed(() => - [envVersion.value, userStore.demoMode ? "演示中" : ""].filter(Boolean), -) - // 一级路径就是菜单 key,对不上的页面(/user、/setting、/achievement 等) // 自然没有一项亮着。根路径没登录时就是题目列表,登录了是个人首页,不亮任何一项 const active = computed(() => route.path.split("/")[1] || (userStore.isAuthed ? "" : "problem")) @@ -118,66 +86,77 @@ function learnLink(type: "python" | "c") { return `/learn/${type}/${learnStep.value[type].toString().padStart(2, "0")}` } -const menus = computed(() => [ +/** + * 顶栏导航的唯一一张表:桌面端渲染成一排纯文字链接,窄屏收进「菜单」下拉。 + * `key` 就是一级路径,用来判定高亮(见上面的 active)。 + * + * 桌面端不带图标、不用 n-menu:n-menu 横排每项固定占 100px,管理员 8 项在 + * 1280 宽的机房屏上直接把顶栏挤成两行。图标只留给下拉菜单。 + */ +interface NavLink { + key: string + label: string + to: string + icon: string + show?: boolean +} + +const navLinks = computed(() => + [ + { key: "problem", label: "题目", to: "/problem", icon: "fluent-emoji:memo" }, + { key: "problemset", label: "题单", to: "/problemset", icon: "fluent-emoji:clipboard" }, + { + key: "submission", + label: "提交", + to: "/submission", + icon: "fluent-emoji:inbox-tray", + show: userStore.showSubmissions, + }, + { key: "contest", label: "比赛", to: "/contest", icon: "fluent-emoji:chequered-flag" }, + { key: "rank", label: "排名", to: "/rank", icon: "fluent-emoji:trophy" }, + { key: "announcement", label: "公告", to: "/announcement", icon: "fluent-emoji:loudspeaker" }, + ].filter((link) => link.show !== false), +) + +const learnOptions: DropdownOption[] = [ + { label: "Python", key: "learn-python" }, + { label: "C语言", key: "learn-c" }, +] + +// 「后台」不和学生导航排在一起:桌面端放到右边那组的开头、弱化成灰字, +// 老师和学生看到的顶栏前半截就是同一排 +const adminPath = computed(() => (userStore.isSuperAdmin ? "/admin" : "/admin/problem/list")) + +const mobileMenus = computed(() => [ { label: "自学", key: "learn", icon: renderIcon("fluent-emoji:books"), - children: [ - { - label: () => h(RouterLink, { to: learnLink("python") }, { default: () => "Python" }), - key: "learn-python", - }, - { - label: () => h(RouterLink, { to: learnLink("c") }, { default: () => "C语言" }), - key: "learn-c", - }, - ], + children: learnOptions, }, + ...navLinks.value.map((link) => ({ + label: link.label, + key: link.key, + icon: renderIcon(link.icon), + })), { - label: () => h(RouterLink, { to: "/problem" }, { default: () => "题目" }), - key: "problem", - icon: renderIcon("fluent-emoji:memo"), - }, - { - label: () => h(RouterLink, { to: "/problemset" }, { default: () => "题单" }), - key: "problemset", - icon: renderIcon("fluent-emoji:clipboard"), - }, - { - label: () => h(RouterLink, { to: "/submission" }, { default: () => "提交" }), - key: "submission", - icon: renderIcon("fluent-emoji:inbox-tray"), - show: userStore.showSubmissions, - }, - { - label: () => h(RouterLink, { to: "/contest" }, { default: () => "比赛" }), - key: "contest", - icon: renderIcon("fluent-emoji:chequered-flag"), - }, - { - label: () => h(RouterLink, { to: "/rank" }, { default: () => "排名" }), - key: "rank", - icon: renderIcon("fluent-emoji:trophy"), - }, - { - label: () => h(RouterLink, { to: "/announcement" }, { default: () => "公告" }), - key: "announcement", - icon: renderIcon("fluent-emoji:loudspeaker"), - }, - { - label: () => - h( - RouterLink, - { to: userStore.isSuperAdmin ? "/admin" : "/admin/problem/list" }, - { default: () => "后台" }, - ), - show: userStore.isAdminRole, + label: "后台", key: "admin", icon: renderIcon("fluent-emoji:gear"), + show: userStore.isAdminRole, }, ]) +function handleNavSelect(key: string) { + if (key === "learn-python") router.push(learnLink("python")) + else if (key === "learn-c") router.push(learnLink("c")) + else if (key === "admin") router.push(adminPath.value) + else { + const link = navLinks.value.find((item) => item.key === key) + if (link) router.push(link.to) + } +} + const options = computed>(() => [ { label: pendingHelpCount.value ? `课堂求助(${pendingHelpCount.value})` : "课堂求助", @@ -252,33 +231,43 @@ const options = computed>(() => [ props: { onClick: handleLogout }, }, ]) - -function goHome() { - router.push("/") -} -
+ + - - +
+ diff --git a/apps/web/src/shared/layout/default.vue b/apps/web/src/shared/layout/default.vue index 78d9478..e06e3ba 100644 --- a/apps/web/src/shared/layout/default.vue +++ b/apps/web/src/shared/layout/default.vue @@ -30,7 +30,7 @@ watch( position="absolute" content-style="display: flex; flex-direction: column; min-height: 100%" > - +