style(前台): 列表页去掉表头 emoji、已结束的比赛改成灰色;比赛列表置顶进行中的比赛,排名页先看榜单
- 前台 53 处表头改成纯文字,删掉只剩这一个用途的 utils/renders.ts - 「已结束」从红色改灰色(比赛列表 / 详情 / 后台 / 首页一起) - 比赛列表第一页、无筛选时顶部列出进行中和即将开始的比赛 - 内容少的页面页脚贴底,不再悬在半屏 - 排名页两张柱状图挪到最后,第一屏是 Top100、本周进步榜和我的排名 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
31aa5f6e74
commit
6bf8c6e1d9
12 files changed
+181
-105
No files matched your search
@@ -158,7 +158,6 @@ Naive 的日期选择器按浏览器本地时区渲染、没有 `timezone` 属
|
|||||||
- `utils/functions.ts` — `parseTime` / `zonedParts` / `zonedYear`(东八区时间口径,见上)、
|
- `utils/functions.ts` — `parseTime` / `zonedParts` / `zonedYear`(东八区时间口径,见上)、
|
||||||
`duration`、压缩与剪贴板等杂项
|
`duration`、压缩与剪贴板等杂项
|
||||||
- `utils/judge.ts` — Judge-related utilities
|
- `utils/judge.ts` — Judge-related utilities
|
||||||
- `utils/renders.ts` — Table column render helpers for Naive UI DataTable
|
|
||||||
|
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { getAnnouncement, getAnnouncementList } from "oj/api"
|
|||||||
import Pagination from "shared/components/Pagination.vue"
|
import Pagination from "shared/components/Pagination.vue"
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
import { parseTime } from "utils/functions"
|
import { parseTime } from "utils/functions"
|
||||||
import { renderTableTitle } from "utils/renders"
|
|
||||||
import type { AnnouncementListItem } from "utils/types"
|
import type { AnnouncementListItem } from "utils/types"
|
||||||
import TitleWithTag from "./components/TitleWithTag.vue"
|
import TitleWithTag from "./components/TitleWithTag.vue"
|
||||||
|
|
||||||
@@ -22,25 +21,25 @@ const query = reactive({
|
|||||||
const columns: DataTableColumn<AnnouncementListItem>[] = [
|
const columns: DataTableColumn<AnnouncementListItem>[] = [
|
||||||
{
|
{
|
||||||
key: "title",
|
key: "title",
|
||||||
title: renderTableTitle("公告标题", "streamline-emojis:fire"),
|
title: "公告标题",
|
||||||
render: (row) => h(TitleWithTag, { title: row.title, top: row.top }),
|
render: (row) => h(TitleWithTag, { title: row.title, top: row.top }),
|
||||||
minWidth: 300,
|
minWidth: 300,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "tag",
|
key: "tag",
|
||||||
title: renderTableTitle("标签", "fluent-emoji-flat:keycap-hashtag"),
|
title: "标签",
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (row) => h(NTag, () => row.tag || "公告"),
|
render: (row) => h(NTag, () => row.tag || "公告"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "createTime",
|
key: "createTime",
|
||||||
title: renderTableTitle("发布时间", "fluent-emoji-flat:eight-oclock"),
|
title: "发布时间",
|
||||||
render: (row) => parseTime(row.createTime),
|
render: (row) => parseTime(row.createTime),
|
||||||
width: 180,
|
width: 180,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "username",
|
key: "username",
|
||||||
title: renderTableTitle("发布人", "streamline-emojis:ghost"),
|
title: "发布人",
|
||||||
render: (row) => row.createdBy.username,
|
render: (row) => row.createdBy.username,
|
||||||
width: 120,
|
width: 120,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import Pagination from "shared/components/Pagination.vue"
|
|||||||
import { useAuthModalStore } from "shared/store/authModal"
|
import { useAuthModalStore } from "shared/store/authModal"
|
||||||
import { usePagination } from "shared/composables/pagination"
|
import { usePagination } from "shared/composables/pagination"
|
||||||
import { useUserStore } from "shared/store/user"
|
import { useUserStore } from "shared/store/user"
|
||||||
import { CONTEST_STATUS, ContestType } from "utils/constants"
|
import { CONTEST_STATUS, ContestStatus, ContestType } from "utils/constants"
|
||||||
import { renderTableTitle } from "utils/renders"
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
@@ -48,7 +47,7 @@ const tags: SelectOption[] = [
|
|||||||
|
|
||||||
const columns: DataTableColumn<Contest>[] = [
|
const columns: DataTableColumn<Contest>[] = [
|
||||||
{
|
{
|
||||||
title: renderTableTitle("状态", "streamline-emojis:collision"),
|
title: "状态",
|
||||||
key: "status",
|
key: "status",
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
@@ -59,25 +58,25 @@ const columns: DataTableColumn<Contest>[] = [
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("比赛", "streamline-emojis:bouquet"),
|
title: "比赛",
|
||||||
key: "title",
|
key: "title",
|
||||||
minWidth: 360,
|
minWidth: 360,
|
||||||
render: (row) => h(ContestTitle, { contest: row }),
|
render: (row) => h(ContestTitle, { contest: row }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("标签", "fluent-emoji-flat:keycap-hashtag"),
|
title: "标签",
|
||||||
key: "tag",
|
key: "tag",
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (row) => h(NTag, () => row.tag),
|
render: (row) => h(NTag, () => row.tag),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("开始时间", "fluent-emoji-flat:eleven-thirty"),
|
title: "开始时间",
|
||||||
key: "start_time",
|
key: "start_time",
|
||||||
width: 180,
|
width: 180,
|
||||||
render: (row) => parseTime(row.startTime),
|
render: (row) => parseTime(row.startTime),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("比赛时长", "streamline-emojis:fishing-pole"),
|
title: "比赛时长",
|
||||||
key: "duration",
|
key: "duration",
|
||||||
width: 180,
|
width: 180,
|
||||||
render: (row) => duration(row.startTime, row.endTime),
|
render: (row) => duration(row.startTime, row.endTime),
|
||||||
@@ -116,17 +115,45 @@ watchDebounced(() => query.keyword, listContests, {
|
|||||||
// 监听其他查询条件变化
|
// 监听其他查询条件变化
|
||||||
watch(() => [query.page, query.limit, query.status, query.tag], listContests)
|
watch(() => [query.page, query.limit, query.status, query.tag], listContests)
|
||||||
|
|
||||||
function rowProps(row: Contest) {
|
function openContest(row: Contest) {
|
||||||
return {
|
|
||||||
style: "cursor: pointer",
|
|
||||||
onClick() {
|
|
||||||
if (!userStore.isAuthed && row.contestType === ContestType.private) {
|
if (!userStore.isAuthed && row.contestType === ContestType.private) {
|
||||||
authStore.openLoginModal()
|
authStore.openLoginModal()
|
||||||
} else {
|
} else {
|
||||||
router.push("/contest/" + row.id)
|
router.push("/contest/" + row.id)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rowProps(row: Contest) {
|
||||||
|
return {
|
||||||
|
style: "cursor: pointer",
|
||||||
|
onClick: () => openContest(row),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进行中 / 即将开始的比赛单独拎到最上面:表格按开始时间倒序,一场开了一周的
|
||||||
|
// 练习赛会被后来建的比赛压到第二页去,学生找不着正在比的那一场
|
||||||
|
const active = ref<Contest[]>([])
|
||||||
|
const showActive = computed(
|
||||||
|
() =>
|
||||||
|
active.value.length > 0 && query.page === 1 && !query.keyword && !query.status && !query.tag,
|
||||||
|
)
|
||||||
|
|
||||||
|
async function listActive() {
|
||||||
|
const base = { offset: 0, limit: 6, keyword: "", tag: "" }
|
||||||
|
const [underway, upcoming] = await Promise.all([
|
||||||
|
getContestList({ ...base, status: ContestStatus.underway }),
|
||||||
|
getContestList({ ...base, status: ContestStatus.not_started }),
|
||||||
|
])
|
||||||
|
active.value = [...underway.results, ...upcoming.results].slice(0, 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(listActive)
|
||||||
|
|
||||||
|
function activeTime(row: Contest) {
|
||||||
|
if (row.status === ContestStatus.underway) {
|
||||||
|
return `${parseTime(row.endTime, "M月D日 HH:mm")} 结束`
|
||||||
|
}
|
||||||
|
return `${parseTime(row.startTime, "M月D日 HH:mm")} 开始`
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
@@ -157,7 +184,64 @@ function rowProps(row: Contest) {
|
|||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-space>
|
</n-space>
|
||||||
|
<div v-if="showActive" class="active-grid">
|
||||||
|
<div
|
||||||
|
v-for="contest in active"
|
||||||
|
:key="contest.id"
|
||||||
|
class="active-card"
|
||||||
|
:class="{ underway: contest.status === ContestStatus.underway }"
|
||||||
|
@click="openContest(contest)"
|
||||||
|
>
|
||||||
|
<n-flex align="center" :size="8">
|
||||||
|
<n-tag :type="CONTEST_STATUS[contest.status].type" size="small" :bordered="false">
|
||||||
|
{{ CONTEST_STATUS[contest.status].name }}
|
||||||
|
</n-tag>
|
||||||
|
<n-text depth="3" class="active-meta">{{ contest.tag }}</n-text>
|
||||||
|
</n-flex>
|
||||||
|
<ContestTitle :contest="contest" class="active-title" />
|
||||||
|
<n-text depth="3" class="active-meta">
|
||||||
|
{{ activeTime(contest) }} · 时长 {{ duration(contest.startTime, contest.endTime) }}
|
||||||
|
</n-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<n-data-table :bordered="false" :columns="columns" :data="data" :row-props="rowProps" />
|
<n-data-table :bordered="false" :columns="columns" :data="data" :row-props="rowProps" />
|
||||||
</n-flex>
|
</n-flex>
|
||||||
<Pagination v-model:limit="query.limit" v-model:page="query.page" :total="total" />
|
<Pagination v-model:limit="query.limit" v-model:page="query.page" :total="total" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.active-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(128, 128, 128, 0.2);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-card.underway {
|
||||||
|
border-color: rgba(24, 160, 88, 0.5);
|
||||||
|
background-color: rgba(24, 160, 88, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-card:hover {
|
||||||
|
border-color: #18a058;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
import type { ProblemRow } from "utils/types"
|
import type { ProblemRow } from "utils/types"
|
||||||
import ProblemStatus from "oj/problem/components/ProblemStatus.vue"
|
import ProblemStatus from "oj/problem/components/ProblemStatus.vue"
|
||||||
import { useContestStore } from "oj/store/contest"
|
import { useContestStore } from "oj/store/contest"
|
||||||
import { renderTableTitle } from "utils/renders"
|
|
||||||
|
|
||||||
const props = defineProps<{ contestID: string }>()
|
const props = defineProps<{ contestID: string }>()
|
||||||
|
|
||||||
@@ -10,29 +9,29 @@ const router = useRouter()
|
|||||||
const contestStore = useContestStore()
|
const contestStore = useContestStore()
|
||||||
const problemsColumns: DataTableColumn<ProblemRow>[] = [
|
const problemsColumns: DataTableColumn<ProblemRow>[] = [
|
||||||
{
|
{
|
||||||
title: renderTableTitle("状态", "streamline-ultimate-color:music-note-1"),
|
title: "状态",
|
||||||
key: "status",
|
key: "status",
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (row) => h(ProblemStatus, { status: row.status }),
|
render: (row) => h(ProblemStatus, { status: row.status }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("编号", "fluent-emoji-flat:input-numbers"),
|
title: "编号",
|
||||||
key: "_id",
|
key: "_id",
|
||||||
width: 100,
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("题目", "streamline-emojis:rice-ball"),
|
title: "题目",
|
||||||
key: "title",
|
key: "title",
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("提交数", "streamline-emojis:clinking-beer-mugs"),
|
title: "提交数",
|
||||||
key: "submissionCount",
|
key: "submissionCount",
|
||||||
align: "center",
|
align: "center",
|
||||||
width: 120,
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("通过率", "streamline-emojis:clapping-hands-1"),
|
title: "通过率",
|
||||||
key: "acRate",
|
key: "acRate",
|
||||||
align: "center",
|
align: "center",
|
||||||
width: 120,
|
width: 120,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { useContestStore } from "oj/store/contest"
|
|||||||
import Pagination from "shared/components/Pagination.vue"
|
import Pagination from "shared/components/Pagination.vue"
|
||||||
import { usePagination } from "shared/composables/pagination"
|
import { usePagination } from "shared/composables/pagination"
|
||||||
import { ContestStatus } from "utils/constants"
|
import { ContestStatus } from "utils/constants"
|
||||||
import { renderTableTitle } from "utils/renders"
|
|
||||||
import type { ContestRank, ProblemRow } from "utils/types"
|
import type { ContestRank, ProblemRow } from "utils/types"
|
||||||
import AcAndSubmission from "../components/AcAndSubmission.vue"
|
import AcAndSubmission from "../components/AcAndSubmission.vue"
|
||||||
import LineChart from "../components/LineChart.vue"
|
import LineChart from "../components/LineChart.vue"
|
||||||
@@ -45,7 +44,7 @@ const { query } = usePagination({}, { defaultLimit: 50 })
|
|||||||
|
|
||||||
const columns = ref<DataTableColumn<ContestRank>[]>([
|
const columns = ref<DataTableColumn<ContestRank>[]>([
|
||||||
{
|
{
|
||||||
title: renderTableTitle("编号", "fluent-emoji-flat:input-numbers"),
|
title: "编号",
|
||||||
key: "id",
|
key: "id",
|
||||||
width: 80,
|
width: 80,
|
||||||
fixed: "left",
|
fixed: "left",
|
||||||
@@ -53,7 +52,7 @@ const columns = ref<DataTableColumn<ContestRank>[]>([
|
|||||||
render: (_, index) => index + (query.page - 1) * query.limit + 1,
|
render: (_, index) => index + (query.page - 1) * query.limit + 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("用户", "streamline-emojis:smiling-face-with-sunglasses"),
|
title: "用户",
|
||||||
key: "username",
|
key: "username",
|
||||||
width: 120,
|
width: 120,
|
||||||
fixed: "left",
|
fixed: "left",
|
||||||
@@ -70,7 +69,7 @@ const columns = ref<DataTableColumn<ContestRank>[]>([
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("正确数/总提交", "streamline-ultimate-color:color-palette"),
|
title: "正确数/总提交",
|
||||||
key: "submission",
|
key: "submission",
|
||||||
width: 140,
|
width: 140,
|
||||||
align: "center",
|
align: "center",
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import SubmissionResultTag from "shared/components/SubmissionResultTag.vue"
|
|||||||
import { useUserStore } from "shared/store/user"
|
import { useUserStore } from "shared/store/user"
|
||||||
import { JUDGE_STATUS, LANGUAGE_SHOW_VALUE } from "utils/constants"
|
import { JUDGE_STATUS, LANGUAGE_SHOW_VALUE } from "utils/constants"
|
||||||
import { parseTime } from "utils/functions"
|
import { parseTime } from "utils/functions"
|
||||||
import { renderTableTitle } from "utils/renders"
|
|
||||||
import type { SubmissionListItem } from "utils/types"
|
import type { SubmissionListItem } from "utils/types"
|
||||||
import SubmissionDetail from "oj/submission/detail.vue"
|
import SubmissionDetail from "oj/submission/detail.vue"
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
@@ -31,13 +30,13 @@ function showCodePanel(id: string, problem: string) {
|
|||||||
|
|
||||||
const columns: DataTableColumn<SubmissionListItem>[] = [
|
const columns: DataTableColumn<SubmissionListItem>[] = [
|
||||||
{
|
{
|
||||||
title: renderTableTitle("提交时间", "fluent-emoji:seven-oclock"),
|
title: "提交时间",
|
||||||
key: "create_time",
|
key: "create_time",
|
||||||
width: 200,
|
width: 200,
|
||||||
render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm:ss"),
|
render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm:ss"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("编号", "fluent-emoji-flat:input-numbers"),
|
title: "编号",
|
||||||
key: "id",
|
key: "id",
|
||||||
minWidth: 160,
|
minWidth: 160,
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
@@ -68,13 +67,13 @@ const columns: DataTableColumn<SubmissionListItem>[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("状态", "streamline-emojis:panda-face"),
|
title: "状态",
|
||||||
key: "status",
|
key: "status",
|
||||||
width: 140,
|
width: 140,
|
||||||
render: (row) => h(SubmissionResultTag, { result: row.result }),
|
render: (row) => h(SubmissionResultTag, { result: row.result }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("语言", "streamline-ultimate-color:earth-pin-2"),
|
title: "语言",
|
||||||
key: "language",
|
key: "language",
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (row) => LANGUAGE_SHOW_VALUE[row.language],
|
render: (row) => LANGUAGE_SHOW_VALUE[row.language],
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import Pagination from "shared/components/Pagination.vue"
|
|||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
import { usePagination } from "shared/composables/pagination"
|
import { usePagination } from "shared/composables/pagination"
|
||||||
import { useUserStore } from "shared/store/user"
|
import { useUserStore } from "shared/store/user"
|
||||||
import { renderTableTitle } from "utils/renders"
|
|
||||||
import ProblemStatus from "./components/ProblemStatus.vue"
|
import ProblemStatus from "./components/ProblemStatus.vue"
|
||||||
import AuthorSelect from "shared/components/AuthorSelect.vue"
|
import AuthorSelect from "shared/components/AuthorSelect.vue"
|
||||||
import ProblemListTitle from "./components/ProblemListTitle.vue"
|
import ProblemListTitle from "./components/ProblemListTitle.vue"
|
||||||
@@ -160,49 +159,49 @@ onMounted(() => {
|
|||||||
|
|
||||||
const baseColumns: DataTableColumn<ProblemRow>[] = [
|
const baseColumns: DataTableColumn<ProblemRow>[] = [
|
||||||
{
|
{
|
||||||
title: renderTableTitle("状态", "streamline-emojis:high-voltage"),
|
title: "状态",
|
||||||
key: "status",
|
key: "status",
|
||||||
width: 80,
|
width: 80,
|
||||||
align: "center",
|
align: "center",
|
||||||
render: (row) => h(ProblemStatus, { status: row.status }),
|
render: (row) => h(ProblemStatus, { status: row.status }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("编号", "streamline-ultimate-color:board-game-dice-1"),
|
title: "编号",
|
||||||
key: "_id",
|
key: "_id",
|
||||||
width: 100,
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("题目", "streamline-ultimate-color:fruit-watermelon"),
|
title: "题目",
|
||||||
key: "title",
|
key: "title",
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
render: (row) => h(ProblemListTitle, { problem: row }),
|
render: (row) => h(ProblemListTitle, { problem: row }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("难度", "streamline-emojis:lady-beetle"),
|
title: "难度",
|
||||||
key: "difficulty",
|
key: "difficulty",
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
row.difficulty ? h(NTag, { type: getTagColor(row.difficulty) }, () => row.difficulty) : null,
|
row.difficulty ? h(NTag, { type: getTagColor(row.difficulty) }, () => row.difficulty) : null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("标签", "streamline-ultimate-color:attachment"),
|
title: "标签",
|
||||||
key: "tags",
|
key: "tags",
|
||||||
width: 260,
|
width: 260,
|
||||||
render: (row) => h(NFlex, () => row.tags.map((t) => h(NTag, { key: t }, () => t))),
|
render: (row) => h(NFlex, () => row.tags.map((t) => h(NTag, { key: t }, () => t))),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("出题者", "streamline-emojis:man-raising-hand-2"),
|
title: "出题者",
|
||||||
key: "author",
|
key: "author",
|
||||||
width: 130,
|
width: 130,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("提交数", "streamline-ultimate-color:paper-write"),
|
title: "提交数",
|
||||||
key: "submissionCount",
|
key: "submissionCount",
|
||||||
align: "center",
|
align: "center",
|
||||||
width: 100,
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("通过率", "streamline-emojis:victory-hand-2"),
|
title: "通过率",
|
||||||
key: "acRate",
|
key: "acRate",
|
||||||
width: 100,
|
width: 100,
|
||||||
align: "center",
|
align: "center",
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import { useBreakpoints } from "shared/composables/breakpoints"
|
|||||||
import { durationFromValue, getACRate, parseTime } from "utils/functions"
|
import { durationFromValue, getACRate, parseTime } from "utils/functions"
|
||||||
import Pagination from "shared/components/Pagination.vue"
|
import Pagination from "shared/components/Pagination.vue"
|
||||||
import { ChartType, LONG_DURATION_OPTIONS } from "utils/constants"
|
import { ChartType, LONG_DURATION_OPTIONS } from "utils/constants"
|
||||||
import { renderTableTitle } from "utils/renders"
|
|
||||||
import Chart from "./components/Chart.vue"
|
import Chart from "./components/Chart.vue"
|
||||||
import RankMedal from "./components/RankMedal.vue"
|
import RankMedal from "./components/RankMedal.vue"
|
||||||
import { useUserStore } from "shared/store/user"
|
import { useUserStore } from "shared/store/user"
|
||||||
@@ -151,14 +150,14 @@ function rowClassName(row: Rank) {
|
|||||||
|
|
||||||
const columns: DataTableColumn<Rank>[] = [
|
const columns: DataTableColumn<Rank>[] = [
|
||||||
{
|
{
|
||||||
title: renderTableTitle("排名", "streamline-emojis:flexed-biceps-1"),
|
title: "排名",
|
||||||
key: "index",
|
key: "index",
|
||||||
width: 100,
|
width: 100,
|
||||||
align: "center",
|
align: "center",
|
||||||
render: (_, index) => h(RankMedal, { index, page: query.page, limit: query.limit }),
|
render: (_, index) => h(RankMedal, { index, page: query.page, limit: query.limit }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("用户", "streamline-emojis:smiling-face-with-sunglasses"),
|
title: "用户",
|
||||||
key: "username",
|
key: "username",
|
||||||
width: 240,
|
width: 240,
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
@@ -193,24 +192,24 @@ const columns: DataTableColumn<Rank>[] = [
|
|||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("个性签名", "streamline-emojis:no-one-under-eighteen"),
|
title: "个性签名",
|
||||||
key: "mood",
|
key: "mood",
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("已解决", "streamline-emojis:raised-fist-1"),
|
title: "已解决",
|
||||||
key: "acceptedNumber",
|
key: "acceptedNumber",
|
||||||
width: 120,
|
width: 120,
|
||||||
align: "center",
|
align: "center",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("提交数", "streamline-ultimate-color:space-rocket-earth"),
|
title: "提交数",
|
||||||
key: "submissionNumber",
|
key: "submissionNumber",
|
||||||
width: 120,
|
width: 120,
|
||||||
align: "center",
|
align: "center",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("正确率", "streamline-ultimate-color:gift-box-1"),
|
title: "正确率",
|
||||||
key: "rate",
|
key: "rate",
|
||||||
width: 120,
|
width: 120,
|
||||||
align: "center",
|
align: "center",
|
||||||
@@ -261,7 +260,7 @@ const subOptions = computed<Duration>(
|
|||||||
// 宽度也压到 1280 那一档能整张放下,不出横向滚动条
|
// 宽度也压到 1280 那一档能整张放下,不出横向滚动条
|
||||||
const weeklyColumns: DataTableColumn<WeeklyRankItem>[] = [
|
const weeklyColumns: DataTableColumn<WeeklyRankItem>[] = [
|
||||||
{
|
{
|
||||||
title: renderTableTitle("排名", "streamline-emojis:flexed-biceps-1"),
|
title: "排名",
|
||||||
key: "rank",
|
key: "rank",
|
||||||
width: 100,
|
width: 100,
|
||||||
align: "center",
|
align: "center",
|
||||||
@@ -269,7 +268,7 @@ const weeklyColumns: DataTableColumn<WeeklyRankItem>[] = [
|
|||||||
render: (row) => h(RankMedal, { index: row.rank - 1, page: 1, limit: 10 }),
|
render: (row) => h(RankMedal, { index: row.rank - 1, page: 1, limit: 10 }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("用户", "streamline-emojis:smiling-face-with-sunglasses"),
|
title: "用户",
|
||||||
key: "username",
|
key: "username",
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
ellipsis: { tooltip: true },
|
ellipsis: { tooltip: true },
|
||||||
@@ -285,13 +284,13 @@ const weeklyColumns: DataTableColumn<WeeklyRankItem>[] = [
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("新解决", "fluent-emoji:party-popper"),
|
title: "新解决",
|
||||||
key: "solvedCount",
|
key: "solvedCount",
|
||||||
width: 100,
|
width: 100,
|
||||||
align: "center",
|
align: "center",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("提交", "streamline-emojis:rocket"),
|
title: "提交",
|
||||||
key: "submissionCount",
|
key: "submissionCount",
|
||||||
width: 90,
|
width: 90,
|
||||||
align: "center",
|
align: "center",
|
||||||
@@ -525,31 +524,6 @@ watch(
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-flex vertical size="large">
|
<n-flex vertical size="large">
|
||||||
<n-grid :cols="isDesktop ? 2 : 1" :x-gap="20" :y-gap="20">
|
|
||||||
<n-gi :span="1">
|
|
||||||
<n-card :bordered="false">
|
|
||||||
<template #header>
|
|
||||||
<div style="height: 34px">全服 Top10</div>
|
|
||||||
</template>
|
|
||||||
<Chart v-if="rankChart.length" :type="ChartType.Rank" :rank-data="rankChart" />
|
|
||||||
<n-empty v-else style="padding: 20px 0"></n-empty>
|
|
||||||
</n-card>
|
|
||||||
</n-gi>
|
|
||||||
<n-gi :span="1">
|
|
||||||
<n-card :bordered="false">
|
|
||||||
<template #header>日活 Top10</template>
|
|
||||||
<template #header-extra>
|
|
||||||
<n-select style="width: 120px" :options="options" v-model:value="duration" />
|
|
||||||
</template>
|
|
||||||
<Chart
|
|
||||||
v-if="activityChart.length"
|
|
||||||
:type="ChartType.Activity"
|
|
||||||
:rank-data="activityChart"
|
|
||||||
/>
|
|
||||||
<n-empty v-else style="padding: 20px 0"></n-empty>
|
|
||||||
</n-card>
|
|
||||||
</n-gi>
|
|
||||||
</n-grid>
|
|
||||||
<!--
|
<!--
|
||||||
两张榜并排:左边全服总榜(分母是历史全部 AC,名次几乎不动),右边本周进步榜
|
两张榜并排:左边全服总榜(分母是历史全部 AC,名次几乎不动),右边本周进步榜
|
||||||
(分母只有这一周)。同一屏里对照着看,「追不上」和「这周还能进前十」是一眼的事。
|
(分母只有这一周)。同一屏里对照着看,「追不上」和「这周还能进前十」是一眼的事。
|
||||||
@@ -687,6 +661,33 @@ watch(
|
|||||||
</n-card>
|
</n-card>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
|
<!-- 图表放最后:它和上面的榜单是同一份数据换个画法,放第一屏的话学生
|
||||||
|
得往下滚才看得到自己在哪 -->
|
||||||
|
<n-grid :cols="isDesktop ? 2 : 1" :x-gap="20" :y-gap="20">
|
||||||
|
<n-gi :span="1">
|
||||||
|
<n-card :bordered="false">
|
||||||
|
<template #header>
|
||||||
|
<div style="height: 34px">全服 Top10</div>
|
||||||
|
</template>
|
||||||
|
<Chart v-if="rankChart.length" :type="ChartType.Rank" :rank-data="rankChart" />
|
||||||
|
<n-empty v-else style="padding: 20px 0"></n-empty>
|
||||||
|
</n-card>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi :span="1">
|
||||||
|
<n-card :bordered="false">
|
||||||
|
<template #header>日活 Top10</template>
|
||||||
|
<template #header-extra>
|
||||||
|
<n-select style="width: 120px" :options="options" v-model:value="duration" />
|
||||||
|
</template>
|
||||||
|
<Chart
|
||||||
|
v-if="activityChart.length"
|
||||||
|
:type="ChartType.Activity"
|
||||||
|
:rank-data="activityChart"
|
||||||
|
/>
|
||||||
|
<n-empty v-else style="padding: 20px 0"></n-empty>
|
||||||
|
</n-card>
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
|
|
||||||
<n-modal
|
<n-modal
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { useBreakpoints } from "shared/composables/breakpoints"
|
|||||||
import { usePagination } from "shared/composables/pagination"
|
import { usePagination } from "shared/composables/pagination"
|
||||||
import { useUserStore } from "shared/store/user"
|
import { useUserStore } from "shared/store/user"
|
||||||
import { LANGUAGE_SHOW_VALUE } from "utils/constants"
|
import { LANGUAGE_SHOW_VALUE } from "utils/constants"
|
||||||
import { renderTableTitle } from "utils/renders"
|
|
||||||
import { FlowchartSubmissionStatus } from "utils/types"
|
import { FlowchartSubmissionStatus } from "utils/types"
|
||||||
|
|
||||||
// 流程图提交的四种状态,列表里原来一列都没有 ——
|
// 流程图提交的四种状态,列表里原来一列都没有 ——
|
||||||
@@ -356,13 +355,13 @@ watch(
|
|||||||
const columns = computed(() => {
|
const columns = computed(() => {
|
||||||
const res: DataTableColumn<SubmissionListItem>[] = [
|
const res: DataTableColumn<SubmissionListItem>[] = [
|
||||||
{
|
{
|
||||||
title: renderTableTitle("提交时间", "fluent-emoji:seven-oclock"),
|
title: "提交时间",
|
||||||
key: "create_time",
|
key: "create_time",
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm:ss"),
|
render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm:ss"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("提交编号", "fluent-emoji-flat:input-numbers"),
|
title: "提交编号",
|
||||||
key: "id",
|
key: "id",
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
@@ -372,13 +371,13 @@ const columns = computed(() => {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("状态", "streamline-emojis:panda-face"),
|
title: "状态",
|
||||||
key: "status",
|
key: "status",
|
||||||
minWidth: 140,
|
minWidth: 140,
|
||||||
render: (row) => h(SubmissionResultTag, { result: row.result }),
|
render: (row) => h(SubmissionResultTag, { result: row.result }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("题目", "streamline-emojis:blossom"),
|
title: "题目",
|
||||||
key: "problem",
|
key: "problem",
|
||||||
minWidth: 360,
|
minWidth: 360,
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
@@ -413,13 +412,13 @@ const columns = computed(() => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("语言", "streamline-ultimate-color:earth-pin-2"),
|
title: "语言",
|
||||||
key: "language",
|
key: "language",
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
render: (row) => LANGUAGE_SHOW_VALUE[row.language],
|
render: (row) => LANGUAGE_SHOW_VALUE[row.language],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("用户", "streamline-emojis:smiling-face-with-sunglasses"),
|
title: "用户",
|
||||||
key: "username",
|
key: "username",
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
@@ -438,7 +437,7 @@ const columns = computed(() => {
|
|||||||
]
|
]
|
||||||
if (!route.params.contestID && userStore.isTeacherOrAbove) {
|
if (!route.params.contestID && userStore.isTeacherOrAbove) {
|
||||||
res.push({
|
res.push({
|
||||||
title: renderTableTitle("选项", "streamline-emojis:wrench"),
|
title: "选项",
|
||||||
key: "rejudge",
|
key: "rejudge",
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
h(
|
h(
|
||||||
@@ -459,12 +458,12 @@ const columns = computed(() => {
|
|||||||
const flowchartColumns = computed(() => {
|
const flowchartColumns = computed(() => {
|
||||||
const res: DataTableColumn<FlowchartSubmissionListItem>[] = [
|
const res: DataTableColumn<FlowchartSubmissionListItem>[] = [
|
||||||
{
|
{
|
||||||
title: renderTableTitle("提交时间", "fluent-emoji:seven-oclock"),
|
title: "提交时间",
|
||||||
key: "create_time",
|
key: "create_time",
|
||||||
render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm:ss"),
|
render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm:ss"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("提交编号", "fluent-emoji-flat:input-numbers"),
|
title: "提交编号",
|
||||||
key: "id",
|
key: "id",
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
h(FlowchartLink, {
|
h(FlowchartLink, {
|
||||||
@@ -473,7 +472,7 @@ const flowchartColumns = computed(() => {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("题目", "streamline-emojis:blossom"),
|
title: "题目",
|
||||||
key: "problem_title",
|
key: "problem_title",
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
h(
|
h(
|
||||||
@@ -487,7 +486,7 @@ const flowchartColumns = computed(() => {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("状态", "fluent-emoji:hourglass-not-done"),
|
title: "状态",
|
||||||
key: "status",
|
key: "status",
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
const tag = FLOWCHART_STATUS_TAG[row.status]
|
const tag = FLOWCHART_STATUS_TAG[row.status]
|
||||||
@@ -499,7 +498,7 @@ const flowchartColumns = computed(() => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("评分", "streamline-ultimate-color:analytics-bars-3d"),
|
title: "评分",
|
||||||
key: "ai_score",
|
key: "ai_score",
|
||||||
// 只有评完的才有分数。没评完也渲染 Grade 的话会显示成 0 分,
|
// 只有评完的才有分数。没评完也渲染 Grade 的话会显示成 0 分,
|
||||||
// 看着像「评了但得了 0 分」
|
// 看着像「评了但得了 0 分」
|
||||||
@@ -512,7 +511,7 @@ const flowchartColumns = computed(() => {
|
|||||||
: h(NText, { depth: 3 }, () => "—"),
|
: h(NText, { depth: 3 }, () => "—"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: renderTableTitle("用户", "streamline-emojis:smiling-face-with-sunglasses"),
|
title: "用户",
|
||||||
key: "username",
|
key: "username",
|
||||||
minWidth: 200,
|
minWidth: 200,
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
@@ -531,7 +530,7 @@ const flowchartColumns = computed(() => {
|
|||||||
]
|
]
|
||||||
if (!route.params.contestID && userStore.isTeacherOrAbove) {
|
if (!route.params.contestID && userStore.isTeacherOrAbove) {
|
||||||
res.push({
|
res.push({
|
||||||
title: renderTableTitle("选项", "streamline-emojis:wrench"),
|
title: "选项",
|
||||||
key: "retry",
|
key: "retry",
|
||||||
// 后端只接受已完成 / 已失败的重判(其余返回 409),这里同步置灰,
|
// 后端只接受已完成 / 已失败的重判(其余返回 409),这里同步置灰,
|
||||||
// 免得老师点了才发现不行
|
// 免得老师点了才发现不行
|
||||||
|
|||||||
@@ -24,7 +24,12 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-layout position="absolute">
|
<!-- 滚动容器竖排、内容区撑满剩余高度:内容少的页面(比赛列表、班级 PK)
|
||||||
|
页脚才会贴在底上,而不是悬在半屏 -->
|
||||||
|
<n-layout
|
||||||
|
position="absolute"
|
||||||
|
content-style="display: flex; flex-direction: column; min-height: 100%"
|
||||||
|
>
|
||||||
<n-layout-header bordered style="padding: 8px">
|
<n-layout-header bordered style="padding: 8px">
|
||||||
<!-- 居中限宽套在外面,别用 class 传给 Header:那样 Header 就永远只能有
|
<!-- 居中限宽套在外面,别用 class 传给 Header:那样 Header 就永远只能有
|
||||||
一个根节点,多一个根就是 "Extraneous non-props attributes" 警告
|
一个根节点,多一个根就是 "Extraneous non-props attributes" 警告
|
||||||
@@ -34,6 +39,7 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
</n-layout-header>
|
</n-layout-header>
|
||||||
<n-layout-content
|
<n-layout-content
|
||||||
|
style="flex: 1 0 auto"
|
||||||
content-style="padding: 16px; overflow-x: initial; max-width: 2000px; margin: 0 auto;"
|
content-style="padding: 16px; overflow-x: initial; max-width: 2000px; margin: 0 auto;"
|
||||||
>
|
>
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export const JUDGE_STATUS: {
|
|||||||
export const CONTEST_STATUS: {
|
export const CONTEST_STATUS: {
|
||||||
[key in ContestStatus]: {
|
[key in ContestStatus]: {
|
||||||
name: string
|
name: string
|
||||||
type: "error" | "success" | "warning"
|
type: "default" | "success" | "warning"
|
||||||
}
|
}
|
||||||
} = {
|
} = {
|
||||||
// 这里不需要传入到后端,只是为了一开始加载数据的时候,做一个初始位
|
// 这里不需要传入到后端,只是为了一开始加载数据的时候,做一个初始位
|
||||||
@@ -136,9 +136,10 @@ export const CONTEST_STATUS: {
|
|||||||
name: "进行中",
|
name: "进行中",
|
||||||
type: "success",
|
type: "success",
|
||||||
},
|
},
|
||||||
|
// 灰的不是红的:列表里九成是已结束的比赛,一片红像满屏报错
|
||||||
"-1": {
|
"-1": {
|
||||||
name: "已结束",
|
name: "已结束",
|
||||||
type: "error",
|
type: "default",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Icon } from "@iconify/vue"
|
|
||||||
import { NFlex } from "naive-ui"
|
|
||||||
|
|
||||||
export function renderTableTitle(title: string, icon: string): any {
|
|
||||||
return h(NFlex, { align: "center", size: 4 }, () => [
|
|
||||||
h(Icon, { icon: icon, width: 20, height: 20 }),
|
|
||||||
title,
|
|
||||||
])
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user