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

+33 -54
View File
@@ -13,6 +13,7 @@ import { useRouteQuery } from "@vueuse/router"
import AuthorSelect from "shared/components/AuthorSelect.vue"
import type { DataTableRowKey } from "naive-ui"
import BatchTagModal from "./components/BatchTagModal.vue"
import PageHeader from "admin/components/PageHeader.vue"
interface Props {
contestID?: string
@@ -166,6 +167,7 @@ const baseColumns: DataTableColumn<AdminProblemRow>[] = [
{
title: "选项",
key: "actions",
fixed: "right",
width: 320,
render: (row) =>
h(Actions, {
@@ -232,54 +234,41 @@ watch(() => [query.page, query.limit, query.author], listProblems)
</script>
<template>
<n-flex class="titleWrapper" justify="space-between">
<n-flex align="center">
<h2 class="title">{{ title }}</h2>
<n-button
v-if="!isContestProblemList"
type="primary"
@click="$router.push({ name: 'admin problem create' })"
>
新建
</n-button>
<n-button
v-if="!isContestProblemList"
@click="$router.push({ name: 'admin stuck problems' })"
>
卡点分析
</n-button>
<n-button v-if="!isContestProblemList" @click="$router.push({ name: 'admin top ac trend' })">
年度趋势
</n-button>
<n-button v-if="!isContestProblemList" @click="$router.push({ name: 'admin tag list' })">
标签管理
</n-button>
</n-flex>
<n-flex>
<template v-if="!isContestProblemList && selectedProblemIds.length">
<n-button type="primary" @click="openBatchTag('add')">
添加标签({{ selectedProblemIds.length }})
<PageHeader
:title="title ?? '题目列表'"
:back="isContestProblemList ? { name: 'admin contest list' } : undefined"
>
<template #actions>
<template v-if="!isContestProblemList">
<template v-if="selectedProblemIds.length">
<n-button @click="openBatchTag('remove')">移除标签</n-button>
<n-button secondary type="primary" @click="openBatchTag('add')">
添加标签({{ selectedProblemIds.length }})
</n-button>
</template>
<n-button type="primary" @click="$router.push({ name: 'admin problem create' })">
新建题目
</n-button>
<n-button @click="openBatchTag('remove')">移除标签</n-button>
</template>
<n-button v-if="isContestProblemList" @click="createContestProblem"> 新建比赛题目 </n-button>
<n-button v-if="isContestProblemList" type="primary" @click="selectProblems">
从题目中选择
</n-button>
<n-flex align="center" v-if="!props.contestID">
<span>出题人</span>
<template v-else>
<n-button @click="createContestProblem">新建比赛题目</n-button>
<n-button type="primary" @click="selectProblems">从题库中选择</n-button>
</template>
</template>
<template #filters>
<n-input
v-model:value="query.keyword"
placeholder="输入标题关键字"
clearable
style="width: 220px"
@clear="clearQuery"
/>
<n-flex v-if="!props.contestID" align="center" :size="8">
<n-text depth="3">出题人</n-text>
<AuthorSelect v-model:value="query.author" all />
</n-flex>
<div>
<n-input
v-model:value="query.keyword"
placeholder="输入标题关键字"
clearable
@clear="clearQuery"
/>
</div>
</n-flex>
</n-flex>
</template>
</PageHeader>
<n-data-table
striped
:columns="columns"
@@ -301,13 +290,3 @@ watch(() => [query.page, query.limit, query.author], listProblems)
@done="onBatchTagDone"
/>
</template>
<style scoped>
.titleWrapper {
margin-bottom: 16px;
}
.title {
margin: 0;
}
</style>