feat(比赛): 比赛页一页看题目和自己的名次;排名竞争感做足、结束有领奖台;老师考试中看全班、考完看成绩;新建比赛一屏做完
Deploy / deploy (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
- 学生比赛页(设计稿 B):左边题目带几人做对、谁最先做对;右边我第几名、5 分钟里升了几名、再做对 1 道能到第几名、我前后的人 - 期中、期末考试中只有自己的进度,写明「排名考完公布」,题目页的排名入口也藏起来 - 排名页:按班筛、名次变化、最先做对的深绿格、自己那行钉住;结束后前三名领奖台 + 我的成绩卡;排名曲线和学生的「导出数据」去掉 - 老师的「全班情况」:卡在一道题、15 分钟没交的放最上面,没进来的人对上看板的请假;考完变「成绩」,点格子看代码、标看过了(代替后台审核页),导出成绩 - 比赛列表:进行中的一大块「接着比」,已结束的写我第几名,前三带奖杯 - 新建比赛:默认 10 分钟后开始,时长点按钮,搜题号把题加进来自动编号、拖动排顺序 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b94e028ff4
commit
151bbdcb50
19 files changed
+3768
-743
No files matched your search
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { isExamTag } from "@oj2/contract"
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import { useTone } from "oj/submission/composables/tone"
|
||||
import { CONTEST_STATUS, ContestStatus } from "utils/constants"
|
||||
|
||||
/**
|
||||
* 比赛的两个小标:状态(进行中 / 未开始 / 已结束)和类型(练习 / 期中 / 期末)。
|
||||
* 进行中绿、未开始琥珀、已结束灰;期中期末用浅红底 —— 学生一眼知道「这场是考试,排名考完才有」
|
||||
*/
|
||||
const props = defineProps<{ status?: ContestStatus; tag?: string }>()
|
||||
|
||||
const theme = useThemeVars()
|
||||
const tone = useTone()
|
||||
|
||||
const style = computed(() => {
|
||||
if (props.status !== undefined) {
|
||||
const kind =
|
||||
props.status === ContestStatus.underway
|
||||
? "success"
|
||||
: props.status === ContestStatus.not_started
|
||||
? "warning"
|
||||
: "default"
|
||||
const t = tone(kind)
|
||||
return { color: t.color, background: t.background, border: "1px solid transparent" }
|
||||
}
|
||||
if (props.tag && isExamTag(props.tag)) {
|
||||
const t = tone("error")
|
||||
return { color: t.color, background: t.background, border: "1px solid transparent" }
|
||||
}
|
||||
return {
|
||||
color: theme.value.textColor3,
|
||||
background: "transparent",
|
||||
border: `1px solid ${theme.value.borderColor}`,
|
||||
}
|
||||
})
|
||||
|
||||
const text = computed(() =>
|
||||
props.status !== undefined ? CONTEST_STATUS[props.status]?.name : props.tag,
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="badge" :class="{ strong: status === ContestStatus.underway }" :style="style">{{
|
||||
text
|
||||
}}</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 22px;
|
||||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { useContestStore } from "oj/store/contest"
|
||||
import { parseTime } from "utils/functions"
|
||||
import ContestAccessTag from "shared/components/ContestAccessTag.vue"
|
||||
@@ -10,12 +9,7 @@ const contestStore = useContestStore()
|
||||
<template>
|
||||
<n-popover v-if="contestStore.contest" placement="bottom-end" :show-arrow="false">
|
||||
<template #trigger>
|
||||
<n-button>
|
||||
<template #icon>
|
||||
<Icon icon="streamline-emojis:exclamation-mark"></Icon>
|
||||
</template>
|
||||
比赛信息
|
||||
</n-button>
|
||||
<n-button>比赛信息</n-button>
|
||||
</template>
|
||||
<div v-html="contestStore.contest.description"></div>
|
||||
<n-descriptions bordered label-placement="left" :column="1">
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContestScoreRow } from "@oj2/contract"
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import { useTone } from "oj/submission/composables/tone"
|
||||
import { secondsToDuration } from "utils/functions"
|
||||
import UserName from "./UserName.vue"
|
||||
|
||||
/**
|
||||
* 题目页右边的小榜:前三名,然后是我和我前后各两个人(中间隔一个「⋯」)。
|
||||
* 我在前八里、或者没上榜(老师、还没交题),就直接列前八
|
||||
*/
|
||||
const props = defineProps<{ rows: ContestScoreRow[]; me?: number }>()
|
||||
|
||||
const theme = useThemeVars()
|
||||
const tone = useTone()
|
||||
const success = computed(() => tone("success"))
|
||||
const danger = computed(() => tone("error"))
|
||||
|
||||
const shown = computed(() => {
|
||||
const rows = props.rows
|
||||
const index = rows.findIndex((row) => row.userId === props.me)
|
||||
if (index < 8) return { head: rows.slice(0, 8), tail: [] }
|
||||
return { head: rows.slice(0, 3), tail: rows.slice(index - 2, index + 3) }
|
||||
})
|
||||
|
||||
function move(row: ContestScoreRow) {
|
||||
return row.rank && row.prevRank ? row.prevRank - row.rank : 0
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mini">
|
||||
<template v-for="(group, gi) in [shown.head, shown.tail]" :key="gi">
|
||||
<div v-if="gi === 1 && group.length" class="gap">⋯</div>
|
||||
<div v-for="row in group" :key="row.userId" class="line" :class="{ me: row.userId === me }">
|
||||
<span class="rank">{{ row.rank }}</span>
|
||||
<span class="move">
|
||||
<template v-if="move(row) > 0"
|
||||
><span class="up">↑{{ move(row) }}</span></template
|
||||
>
|
||||
<template v-else-if="move(row) < 0"
|
||||
><span class="down">↓{{ -move(row) }}</span></template
|
||||
>
|
||||
</span>
|
||||
<UserName
|
||||
class="who"
|
||||
:username="row.username"
|
||||
:class-name="row.className"
|
||||
:strong="row.userId === me"
|
||||
/>
|
||||
<span class="solved">{{ row.solved }} 道</span>
|
||||
<span class="time">{{ secondsToDuration(row.totalTime) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mini {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.line {
|
||||
height: 30px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-radius: 4px;
|
||||
background: v-bind("theme.cardColor");
|
||||
}
|
||||
|
||||
.line.me {
|
||||
background: v-bind("success.background");
|
||||
}
|
||||
|
||||
.rank {
|
||||
width: 24px;
|
||||
text-align: right;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: v-bind("theme.textColor2");
|
||||
}
|
||||
|
||||
.me .rank {
|
||||
color: v-bind("success.color");
|
||||
}
|
||||
|
||||
.move {
|
||||
width: 28px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.up {
|
||||
color: v-bind("success.color");
|
||||
}
|
||||
|
||||
.down {
|
||||
color: v-bind("danger.color");
|
||||
}
|
||||
|
||||
.who {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.solved {
|
||||
width: 44px;
|
||||
text-align: right;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.time {
|
||||
width: 62px;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: v-bind("theme.textColor3");
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.gap {
|
||||
height: 14px;
|
||||
padding-left: 44px;
|
||||
font-size: 12px;
|
||||
line-height: 8px;
|
||||
color: v-bind("theme.textColor3");
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { useThemeVars } from "naive-ui"
|
||||
|
||||
/**
|
||||
* 榜单上的名字:`ks241` 前缀淡一点,后面的名字是重点(和提交列表一个写法)。
|
||||
* 前缀对不上班级号就整个照常显示
|
||||
*/
|
||||
const props = defineProps<{ username: string; className?: string | null; strong?: boolean }>()
|
||||
|
||||
const theme = useThemeVars()
|
||||
|
||||
const parts = computed(() => {
|
||||
const prefix = props.className ? `ks${props.className}` : ""
|
||||
if (prefix && props.username.startsWith(prefix) && props.username.length > prefix.length) {
|
||||
return [prefix, props.username.slice(prefix.length)]
|
||||
}
|
||||
return ["", props.username]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="user" :title="username"
|
||||
><span class="prefix">{{ parts[0] }}</span
|
||||
><span class="name" :class="{ strong }">{{ parts[1] }}</span></span
|
||||
>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.user {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.prefix {
|
||||
color: v-bind("theme.textColor3");
|
||||
}
|
||||
|
||||
.name {
|
||||
color: v-bind("theme.textColor1");
|
||||
}
|
||||
|
||||
.name.strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user