feat(后台): 侧栏分组带图标、可收起,加顶栏;所有后台页面统一页头
Deploy / deploy (push) Canceled after 0s

- 侧栏按 概览 / 教学内容 / 学情分析 / 站点管理 分组,去掉超管菜单里的重复项;
  菜单项的角色与路由守卫一一对应,没有题目权限的教师不再看到点了就被踢回首页的「题目」
- 顶栏:面包屑、回到前台、明暗切换、用户菜单(退出登录);窄屏侧栏自动收起
- 新增 PageHeader:标题 + 右侧主操作 + 下方筛选栏,编辑页带返回按钮
- 卡点分析 / 年度趋势 / 标签管理从题目列表的按钮挪进侧栏
- 首页改成统计卡片 + 班级排名卡片;设置页按钮移到卡片右上角
- 题目、比赛、题单、用户列表的「选项」列固定在右侧,不再被挤出屏幕

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-09-27 19:54:42 +08:00
1 parent d25b5dbe6d
commit d3823430f9
25 files changed
+764 -587

No files matched your search

+13 -26
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
import PageHeader from "admin/components/PageHeader.vue"
import { errorMessage } from "utils/api"
import { formatISO } from "date-fns"
import TextEditor from "shared/components/TextEditor.vue"
@@ -112,23 +113,19 @@ onMounted(getContestDetail)
</script>
<template>
<n-flex class="titleWrapper" align="center">
<h2 class="title">
{{ route.name === "admin contest create" ? "新建比赛" : "编辑比赛" }}
</h2>
<template v-if="!props.contestID">
<n-alert type="success">
<template #header>
开始时间 {{ parseTime(contest.startTime, "YYYY年M月D日 HH:mm:ss") }}
</template>
</n-alert>
<n-alert type="warning">
<template #header>
结束时间 {{ parseTime(contest.endTime, "YYYY年M月D日 HH:mm:ss") }}
</template>
</n-alert>
<PageHeader
:title="route.name === 'admin contest create' ? '新建比赛' : '编辑比赛'"
:back="{ name: 'admin contest list' }"
>
<template v-if="!props.contestID" #extra>
<n-tag type="success" round>
开始 {{ parseTime(contest.startTime, "YYYY年M月D日 HH:mm:ss") }}
</n-tag>
<n-tag type="warning" round>
结束 {{ parseTime(contest.endTime, "YYYY年M月D日 HH:mm:ss") }}
</n-tag>
</template>
</n-flex>
</PageHeader>
<n-form inline>
<n-form-item label="标题">
<n-input style="width: 300px" v-model:value="contest.title" />
@@ -164,13 +161,3 @@ onMounted(getContestDetail)
<n-button type="primary" @click="submit">保存</n-button>
</n-flex>
</template>
<style scoped>
.titleWrapper {
margin-bottom: 16px;
}
.title {
margin: 0;
}
</style>
+32 -32
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
import PageHeader from "admin/components/PageHeader.vue"
import { errorMessage } from "utils/api"
import type { AcmHelperItem, ContestSubmissionInfo } from "utils/types"
import { NButton, NCheckbox, NSelect, NTag } from "naive-ui"
@@ -232,38 +233,37 @@ onMounted(loadData)
<template>
<n-flex vertical>
<n-flex justify="space-between" align="center">
<n-flex align="center">
<h2 style="margin: 0">比赛辅助检查</h2>
<n-tag type="info" size="large"> 总计: {{ stats.total }} </n-tag>
<n-tag type="success" size="large"> 已检查: {{ stats.checked }} </n-tag>
<n-tag type="warning" size="large"> 未检查: {{ stats.unchecked }} </n-tag>
</n-flex>
<n-button type="primary" :disabled="stats.unchecked === 0" @click="markAllAsChecked">
标记全部为已检查
</n-button>
</n-flex>
<n-alert type="info" style="margin-bottom: 16px">
<template #header>使用说明</template>
此工具用于赛后人工审核代码,检查是否存在抄袭、作弊等行为。请逐个查看通过(AC)的提交代码,检查完成后勾选"已检查"。
</n-alert>
<n-flex align="center" style="margin-bottom: 16px">
<n-input
v-model:value="query.username"
placeholder="筛选用户名"
style="width: 150px"
clearable
/>
<n-input
v-model:value="query.problemId"
placeholder="筛选题目"
style="width: 150px"
clearable
/>
<n-select v-model:value="query.checked" :options="checkedOptions" style="width: 120px" />
</n-flex>
<PageHeader
title="比赛辅助检查"
description="赛后人工审核代码,检查是否存在抄袭、作弊等行为。请逐个查看通过(AC)的提交代码,检查完成后勾选「已检查」。"
:back="{ name: 'admin contest list' }"
>
<template #extra>
<n-tag type="info" round>总计 {{ stats.total }}</n-tag>
<n-tag type="success" round>已检查 {{ stats.checked }}</n-tag>
<n-tag type="warning" round>未检查 {{ stats.unchecked }}</n-tag>
</template>
<template #actions>
<n-button type="primary" :disabled="stats.unchecked === 0" @click="markAllAsChecked">
标记全部为已检查
</n-button>
</template>
<template #filters>
<n-input
v-model:value="query.username"
placeholder="筛选用户名"
style="width: 150px"
clearable
/>
<n-input
v-model:value="query.problemId"
placeholder="筛选题目"
style="width: 150px"
clearable
/>
<n-select v-model:value="query.checked" :options="checkedOptions" style="width: 120px" />
</template>
</PageHeader>
<n-data-table
:columns="columns"
+15 -19
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
import PageHeader from "admin/components/PageHeader.vue"
import { NSwitch, NTag } from "naive-ui"
import ContestTitle from "shared/components/ContestTitle.vue"
import ContestAccessTag from "shared/components/ContestAccessTag.vue"
@@ -79,6 +80,7 @@ const columns: DataTableColumn<AdminContest>[] = [
{
title: "选项",
key: "actions",
fixed: "right",
width: 300,
render: (row) => h(Actions, { contest: row }),
},
@@ -99,27 +101,21 @@ watchDebounced(() => query.keyword, listContests, {
</script>
<template>
<n-flex justify="space-between" class="titleWrapper">
<n-flex align="center">
<h2 class="title">比赛列表</h2>
<PageHeader title="比赛列表">
<template #actions>
<n-button type="primary" @click="$router.push({ name: 'admin contest create' })">
新建
新建比赛
</n-button>
</n-flex>
<div>
<n-input v-model:value="query.keyword" placeholder="输入标题关键字" />
</div>
</n-flex>
</template>
<template #filters>
<n-input
v-model:value="query.keyword"
placeholder="输入标题关键字"
clearable
style="width: 220px"
/>
</template>
</PageHeader>
<n-data-table :columns="columns" :data="contests" />
<Pagination :total="total" v-model:limit="query.limit" v-model:page="query.page" />
</template>
<style scoped>
.titleWrapper {
margin-bottom: 16px;
}
.title {
margin: 0;
}
</style>