feat(后台比赛): 比赛列表一行看全——开始时间和多长、几道题、几人参加、学生看得到;期中期末挂标,操作只剩「编辑」「成绩」和 ⋯
Deploy / deploy (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
591c7bcab9
commit
32f7bcaf8b
2 files changed
+119
-47
No files matched your search
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { Icon } from "@iconify/vue"
|
||||
import type { AdminContest } from "utils/types"
|
||||
import { cloneContest } from "../../api"
|
||||
|
||||
@@ -21,6 +22,16 @@ function goClass() {
|
||||
window.open(`/contest/${props.contest.id}/class`, "_blank")
|
||||
}
|
||||
|
||||
const menuOptions: DropdownOption[] = [
|
||||
{ label: "复制一份", key: "clone" },
|
||||
{ label: "在前台看", key: "view" },
|
||||
]
|
||||
|
||||
function onMenu(key: string) {
|
||||
if (key === "clone") clone()
|
||||
else if (key === "view") window.open(`/contest/${props.contest.id}`, "_blank")
|
||||
}
|
||||
|
||||
async function clone() {
|
||||
try {
|
||||
const res = await cloneContest(props.contest.id)
|
||||
@@ -35,12 +46,16 @@ async function clone() {
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<n-flex>
|
||||
<n-button size="small" type="primary" secondary @click="goEdit"> 编辑 </n-button>
|
||||
<n-button size="small" type="info" secondary @click="goClass">
|
||||
<n-flex :size="4" justify="end" :wrap="false">
|
||||
<n-button size="small" @click="goEdit">编辑</n-button>
|
||||
<n-button size="small" @click="goClass">
|
||||
{{ contest.status === "-1" ? "成绩" : "全班情况" }}
|
||||
</n-button>
|
||||
<n-button size="small" secondary @click="clone"> 复制 </n-button>
|
||||
<n-dropdown trigger="click" placement="bottom-end" :options="menuOptions" @select="onMenu">
|
||||
<n-button size="small" quaternary aria-label="更多">
|
||||
<template #icon><Icon icon="ph:dots-three-bold" /></template>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</n-flex>
|
||||
</template>
|
||||
<style scoped></style>
|
||||
@@ -1,16 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { AdminContestListItem } from "@oj2/contract"
|
||||
import PageHeader from "admin/components/PageHeader.vue"
|
||||
import { NSwitch, NTag } from "naive-ui"
|
||||
import { NSwitch, useThemeVars } 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[]>([])
|
||||
/**
|
||||
* 后台比赛列表(设计稿「比赛重设计」CtAdminList)。一行一场:比赛名(期中 / 期末挂标,
|
||||
* 要密码的带锁)、开始时间和多长、状态、几道题、几人参加、出卷人、学生看得到;
|
||||
* 操作「编辑」「成绩 / 全班情况」+ ⋯。按开始时间排 —— 原来按创建时间,复制出来的旧比赛
|
||||
* 会跑到最前面。ID、标签、类型三列拿掉(标签只在期中 / 期末时挂出来,类型就是那把锁)
|
||||
*/
|
||||
const theme = useThemeVars()
|
||||
const contests = ref<AdminContestListItem[]>([])
|
||||
const total = ref(0)
|
||||
const query = reactive({
|
||||
limit: 10,
|
||||
@@ -18,57 +24,79 @@ const query = reactive({
|
||||
keyword: "",
|
||||
})
|
||||
|
||||
function toggleVisible(contest: AdminContest) {
|
||||
function toggleVisible(contest: AdminContestListItem) {
|
||||
contest.visible = !contest.visible
|
||||
editContest(contest)
|
||||
}
|
||||
|
||||
const columns: DataTableColumn<AdminContest>[] = [
|
||||
{ title: "ID", key: "id", width: 60 },
|
||||
/** 多长:一节课的写分钟 / 小时,挂好几天的练习写天 */
|
||||
function duration(row: AdminContestListItem) {
|
||||
const minutes = Math.round((Date.parse(row.endTime) - Date.parse(row.startTime)) / 60_000)
|
||||
if (minutes < 60) return `${minutes} 分钟`
|
||||
if (minutes < 48 * 60) {
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const rest = minutes % 60
|
||||
return rest ? `${hours} 小时 ${rest} 分` : `${hours} 小时`
|
||||
}
|
||||
return `${Math.round(minutes / 1440)} 天`
|
||||
}
|
||||
|
||||
const EXAM_TAGS = ["期中", "期末"]
|
||||
|
||||
const columns: DataTableColumn<AdminContestListItem>[] = [
|
||||
{
|
||||
title: "比赛",
|
||||
key: "title",
|
||||
minWidth: 200,
|
||||
render: (row) => h(ContestTitle, { contest: row }),
|
||||
minWidth: 220,
|
||||
render: (row) =>
|
||||
h("span", { class: "title-cell" }, [
|
||||
h(ContestTitle, { contest: row }),
|
||||
EXAM_TAGS.includes(row.tag) ? h("span", { class: "exam" }, row.tag) : null,
|
||||
]),
|
||||
},
|
||||
{
|
||||
title: "标签",
|
||||
key: "tag",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
key: "contest_type",
|
||||
width: 100,
|
||||
render: (row) => h(ContestAccessTag, { contest: row, size: "small" }),
|
||||
title: "开始 · 多长",
|
||||
key: "start_time",
|
||||
width: 220,
|
||||
render: (row) =>
|
||||
h("span", { class: "num" }, [
|
||||
parseTime(row.startTime, "YYYY-MM-DD HH:mm"),
|
||||
h("span", { class: "muted" }, ` · ${duration(row)}`),
|
||||
]),
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
key: "status",
|
||||
width: 100,
|
||||
width: 80,
|
||||
render: (row) =>
|
||||
h(
|
||||
NTag,
|
||||
{ type: CONTEST_STATUS[row.status]["type"], size: "small" },
|
||||
() => CONTEST_STATUS[row.status]["name"],
|
||||
"span",
|
||||
{ class: row.status === "0" ? "live" : row.status === "-1" ? "muted" : "" },
|
||||
row.status === "-1"
|
||||
? "结束了"
|
||||
: row.status === "0"
|
||||
? "进行中"
|
||||
: CONTEST_STATUS[row.status].name,
|
||||
),
|
||||
},
|
||||
{ title: "题", key: "problemCount", width: 56 },
|
||||
{
|
||||
title: "创建者",
|
||||
title: "参加",
|
||||
key: "participantCount",
|
||||
width: 64,
|
||||
render: (row) => (row.participantCount ? String(row.participantCount) : "—"),
|
||||
},
|
||||
{
|
||||
title: "出卷",
|
||||
key: "created_by",
|
||||
width: 120,
|
||||
width: 96,
|
||||
ellipsis: { tooltip: true },
|
||||
render: (row) => row.createdBy.username,
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
key: "create_time",
|
||||
width: 160,
|
||||
render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm"),
|
||||
},
|
||||
{
|
||||
title: "可见",
|
||||
title: "学生看得到",
|
||||
key: "visible",
|
||||
width: 100,
|
||||
width: 96,
|
||||
render: (row) =>
|
||||
h(NSwitch, {
|
||||
value: row.visible,
|
||||
@@ -78,10 +106,9 @@ const columns: DataTableColumn<AdminContest>[] = [
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: "选项",
|
||||
title: "",
|
||||
key: "actions",
|
||||
fixed: "right",
|
||||
width: 300,
|
||||
width: 190,
|
||||
render: (row) => h(Actions, { contest: row }),
|
||||
},
|
||||
]
|
||||
@@ -101,21 +128,51 @@ watchDebounced(() => query.keyword, listContests, {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeader title="比赛列表">
|
||||
<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"
|
||||
/>
|
||||
<n-input v-model:value="query.keyword" placeholder="比赛名" clearable style="width: 220px" />
|
||||
</template>
|
||||
</PageHeader>
|
||||
<n-data-table :columns="columns" :data="contests" />
|
||||
<n-data-table class="contest-table" :columns="columns" :data="contests" />
|
||||
<Pagination :total="total" v-model:limit="query.limit" v-model:page="query.page" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.contest-table :deep(.title-cell) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.contest-table :deep(.exam) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: v-bind("theme.errorColor");
|
||||
background: v-bind("theme.actionColor");
|
||||
}
|
||||
|
||||
.contest-table :deep(.num) {
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.contest-table :deep(.muted) {
|
||||
color: v-bind("theme.textColor3");
|
||||
}
|
||||
|
||||
.contest-table :deep(.live) {
|
||||
color: v-bind("theme.successColor");
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user