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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 19:54:42 +08:00

122 lines
3.0 KiB
Vue

<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"
import Pagination from "shared/components/Pagination.vue"
import { CONTEST_STATUS } from "utils/constants"
import { parseTime } from "utils/functions"
import type { AdminContest } from "utils/types"
import { editContest, getContestList } from "../api"
import Actions from "./components/Actions.vue"
const contests = ref<AdminContest[]>([])
const total = ref(0)
const query = reactive({
limit: 10,
page: 1,
keyword: "",
})
function toggleVisible(contest: AdminContest) {
contest.visible = !contest.visible
editContest(contest)
}
const columns: DataTableColumn<AdminContest>[] = [
{ title: "ID", key: "id", width: 60 },
{
title: "比赛",
key: "title",
minWidth: 200,
render: (row) => h(ContestTitle, { contest: row }),
},
{
title: "标签",
key: "tag",
width: 100,
},
{
title: "类型",
key: "contest_type",
width: 100,
render: (row) => h(ContestAccessTag, { contest: row, size: "small" }),
},
{
title: "状态",
key: "status",
width: 100,
render: (row) =>
h(
NTag,
{ type: CONTEST_STATUS[row.status]["type"], size: "small" },
() => CONTEST_STATUS[row.status]["name"],
),
},
{
title: "创建者",
key: "created_by",
width: 120,
render: (row) => row.createdBy.username,
},
{
title: "创建时间",
key: "create_time",
width: 160,
render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm"),
},
{
title: "可见",
key: "visible",
width: 100,
render: (row) =>
h(NSwitch, {
value: row.visible,
size: "small",
rubberBand: false,
onUpdateValue: () => toggleVisible(row),
}),
},
{
title: "选项",
key: "actions",
fixed: "right",
width: 300,
render: (row) => h(Actions, { contest: row }),
},
]
async function listContests() {
const offset = (query.page - 1) * query.limit
const res = await getContestList(offset, query.limit, query.keyword)
contests.value = res.results
total.value = res.total
}
onMounted(listContests)
watch(() => [query.page, query.limit], listContests)
watchDebounced(() => query.keyword, listContests, {
debounce: 500,
maxWait: 1000,
})
</script>
<template>
<PageHeader title="比赛列表">
<template #actions>
<n-button type="primary" @click="$router.push({ name: 'admin contest create' })">
新建比赛
</n-button>
</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>