From 6bf8c6e1d9efe8a9bf8ada5c3d624f78fad81049 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Sun, 27 Sep 2026 20:13:29 +0800 Subject: [PATCH] =?UTF-8?q?style(=E5=89=8D=E5=8F=B0):=20=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E9=A1=B5=E5=8E=BB=E6=8E=89=E8=A1=A8=E5=A4=B4=20emoji=E3=80=81?= =?UTF-8?q?=E5=B7=B2=E7=BB=93=E6=9D=9F=E7=9A=84=E6=AF=94=E8=B5=9B=E6=94=B9?= =?UTF-8?q?=E6=88=90=E7=81=B0=E8=89=B2=EF=BC=9B=E6=AF=94=E8=B5=9B=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E7=BD=AE=E9=A1=B6=E8=BF=9B=E8=A1=8C=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E6=AF=94=E8=B5=9B=EF=BC=8C=E6=8E=92=E5=90=8D=E9=A1=B5=E5=85=88?= =?UTF-8?q?=E7=9C=8B=E6=A6=9C=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 前台 53 处表头改成纯文字,删掉只剩这一个用途的 utils/renders.ts - 「已结束」从红色改灰色(比赛列表 / 详情 / 后台 / 首页一起) - 比赛列表第一页、无筛选时顶部列出进行中和即将开始的比赛 - 内容少的页面页脚贴底,不再悬在半屏 - 排名页两张柱状图挪到最后,第一屏是 Top100、本周进步榜和我的排名 Co-Authored-By: Claude Opus 5.5 --- apps/web/CLAUDE.md | 1 - apps/web/src/oj/announcement/list.vue | 9 +- apps/web/src/oj/contest/list.vue | 112 +++++++++++++++--- apps/web/src/oj/contest/pages/problems.vue | 11 +- apps/web/src/oj/contest/pages/rank.vue | 7 +- .../problem/components/ProblemSubmission.vue | 9 +- apps/web/src/oj/problem/list.vue | 17 ++- apps/web/src/oj/rank/list.vue | 73 ++++++------ apps/web/src/oj/submission/list.vue | 29 +++-- apps/web/src/shared/layout/default.vue | 8 +- apps/web/src/utils/constants.ts | 5 +- apps/web/src/utils/renders.ts | 9 -- 12 files changed, 183 insertions(+), 107 deletions(-) delete mode 100644 apps/web/src/utils/renders.ts diff --git a/apps/web/CLAUDE.md b/apps/web/CLAUDE.md index 4b6d29d..d7a7359 100644 --- a/apps/web/CLAUDE.md +++ b/apps/web/CLAUDE.md @@ -158,7 +158,6 @@ Naive 的日期选择器按浏览器本地时区渲染、没有 `timezone` 属 - `utils/functions.ts` — `parseTime` / `zonedParts` / `zonedYear`(东八区时间口径,见上)、 `duration`、压缩与剪贴板等杂项 - `utils/judge.ts` — Judge-related utilities -- `utils/renders.ts` — Table column render helpers for Naive UI DataTable ### Environment Variables diff --git a/apps/web/src/oj/announcement/list.vue b/apps/web/src/oj/announcement/list.vue index baf9cf1..48696ce 100644 --- a/apps/web/src/oj/announcement/list.vue +++ b/apps/web/src/oj/announcement/list.vue @@ -4,7 +4,6 @@ import { getAnnouncement, getAnnouncementList } from "oj/api" import Pagination from "shared/components/Pagination.vue" import { useBreakpoints } from "shared/composables/breakpoints" import { parseTime } from "utils/functions" -import { renderTableTitle } from "utils/renders" import type { AnnouncementListItem } from "utils/types" import TitleWithTag from "./components/TitleWithTag.vue" @@ -22,25 +21,25 @@ const query = reactive({ const columns: DataTableColumn[] = [ { key: "title", - title: renderTableTitle("公告标题", "streamline-emojis:fire"), + title: "公告标题", render: (row) => h(TitleWithTag, { title: row.title, top: row.top }), minWidth: 300, }, { key: "tag", - title: renderTableTitle("标签", "fluent-emoji-flat:keycap-hashtag"), + title: "标签", width: 100, render: (row) => h(NTag, () => row.tag || "公告"), }, { key: "createTime", - title: renderTableTitle("发布时间", "fluent-emoji-flat:eight-oclock"), + title: "发布时间", render: (row) => parseTime(row.createTime), width: 180, }, { key: "username", - title: renderTableTitle("发布人", "streamline-emojis:ghost"), + title: "发布人", render: (row) => row.createdBy.username, width: 120, }, diff --git a/apps/web/src/oj/contest/list.vue b/apps/web/src/oj/contest/list.vue index 3520047..2358752 100644 --- a/apps/web/src/oj/contest/list.vue +++ b/apps/web/src/oj/contest/list.vue @@ -9,8 +9,7 @@ import Pagination from "shared/components/Pagination.vue" import { useAuthModalStore } from "shared/store/authModal" import { usePagination } from "shared/composables/pagination" import { useUserStore } from "shared/store/user" -import { CONTEST_STATUS, ContestType } from "utils/constants" -import { renderTableTitle } from "utils/renders" +import { CONTEST_STATUS, ContestStatus, ContestType } from "utils/constants" const router = useRouter() const userStore = useUserStore() @@ -48,7 +47,7 @@ const tags: SelectOption[] = [ const columns: DataTableColumn[] = [ { - title: renderTableTitle("状态", "streamline-emojis:collision"), + title: "状态", key: "status", width: 100, render: (row) => @@ -59,25 +58,25 @@ const columns: DataTableColumn[] = [ ), }, { - title: renderTableTitle("比赛", "streamline-emojis:bouquet"), + title: "比赛", key: "title", minWidth: 360, render: (row) => h(ContestTitle, { contest: row }), }, { - title: renderTableTitle("标签", "fluent-emoji-flat:keycap-hashtag"), + title: "标签", key: "tag", width: 100, render: (row) => h(NTag, () => row.tag), }, { - title: renderTableTitle("开始时间", "fluent-emoji-flat:eleven-thirty"), + title: "开始时间", key: "start_time", width: 180, render: (row) => parseTime(row.startTime), }, { - title: renderTableTitle("比赛时长", "streamline-emojis:fishing-pole"), + title: "比赛时长", key: "duration", width: 180, render: (row) => duration(row.startTime, row.endTime), @@ -116,18 +115,46 @@ watchDebounced(() => query.keyword, listContests, { // 监听其他查询条件变化 watch(() => [query.page, query.limit, query.status, query.tag], listContests) +function openContest(row: Contest) { + if (!userStore.isAuthed && row.contestType === ContestType.private) { + authStore.openLoginModal() + } else { + router.push("/contest/" + row.id) + } +} + function rowProps(row: Contest) { return { style: "cursor: pointer", - onClick() { - if (!userStore.isAuthed && row.contestType === ContestType.private) { - authStore.openLoginModal() - } else { - router.push("/contest/" + row.id) - } - }, + onClick: () => openContest(row), } } + +// 进行中 / 即将开始的比赛单独拎到最上面:表格按开始时间倒序,一场开了一周的 +// 练习赛会被后来建的比赛压到第二页去,学生找不着正在比的那一场 +const active = ref([]) +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")} 开始` +} + + diff --git a/apps/web/src/oj/contest/pages/problems.vue b/apps/web/src/oj/contest/pages/problems.vue index 6bcc6cf..bd1d8a2 100644 --- a/apps/web/src/oj/contest/pages/problems.vue +++ b/apps/web/src/oj/contest/pages/problems.vue @@ -2,7 +2,6 @@ import type { ProblemRow } from "utils/types" import ProblemStatus from "oj/problem/components/ProblemStatus.vue" import { useContestStore } from "oj/store/contest" -import { renderTableTitle } from "utils/renders" const props = defineProps<{ contestID: string }>() @@ -10,29 +9,29 @@ const router = useRouter() const contestStore = useContestStore() const problemsColumns: DataTableColumn[] = [ { - title: renderTableTitle("状态", "streamline-ultimate-color:music-note-1"), + title: "状态", key: "status", width: 100, render: (row) => h(ProblemStatus, { status: row.status }), }, { - title: renderTableTitle("编号", "fluent-emoji-flat:input-numbers"), + title: "编号", key: "_id", width: 100, }, { - title: renderTableTitle("题目", "streamline-emojis:rice-ball"), + title: "题目", key: "title", minWidth: 200, }, { - title: renderTableTitle("提交数", "streamline-emojis:clinking-beer-mugs"), + title: "提交数", key: "submissionCount", align: "center", width: 120, }, { - title: renderTableTitle("通过率", "streamline-emojis:clapping-hands-1"), + title: "通过率", key: "acRate", align: "center", width: 120, diff --git a/apps/web/src/oj/contest/pages/rank.vue b/apps/web/src/oj/contest/pages/rank.vue index c46c29b..85495ff 100644 --- a/apps/web/src/oj/contest/pages/rank.vue +++ b/apps/web/src/oj/contest/pages/rank.vue @@ -7,7 +7,6 @@ import { useContestStore } from "oj/store/contest" import Pagination from "shared/components/Pagination.vue" import { usePagination } from "shared/composables/pagination" import { ContestStatus } from "utils/constants" -import { renderTableTitle } from "utils/renders" import type { ContestRank, ProblemRow } from "utils/types" import AcAndSubmission from "../components/AcAndSubmission.vue" import LineChart from "../components/LineChart.vue" @@ -45,7 +44,7 @@ const { query } = usePagination({}, { defaultLimit: 50 }) const columns = ref[]>([ { - title: renderTableTitle("编号", "fluent-emoji-flat:input-numbers"), + title: "编号", key: "id", width: 80, fixed: "left", @@ -53,7 +52,7 @@ const columns = ref[]>([ render: (_, index) => index + (query.page - 1) * query.limit + 1, }, { - title: renderTableTitle("用户", "streamline-emojis:smiling-face-with-sunglasses"), + title: "用户", key: "username", width: 120, fixed: "left", @@ -70,7 +69,7 @@ const columns = ref[]>([ ), }, { - title: renderTableTitle("正确数/总提交", "streamline-ultimate-color:color-palette"), + title: "正确数/总提交", key: "submission", width: 140, align: "center", diff --git a/apps/web/src/oj/problem/components/ProblemSubmission.vue b/apps/web/src/oj/problem/components/ProblemSubmission.vue index d41b0d4..515b502 100644 --- a/apps/web/src/oj/problem/components/ProblemSubmission.vue +++ b/apps/web/src/oj/problem/components/ProblemSubmission.vue @@ -7,7 +7,6 @@ import SubmissionResultTag from "shared/components/SubmissionResultTag.vue" import { useUserStore } from "shared/store/user" import { JUDGE_STATUS, LANGUAGE_SHOW_VALUE } from "utils/constants" import { parseTime } from "utils/functions" -import { renderTableTitle } from "utils/renders" import type { SubmissionListItem } from "utils/types" import SubmissionDetail from "oj/submission/detail.vue" import { useBreakpoints } from "shared/composables/breakpoints" @@ -31,13 +30,13 @@ function showCodePanel(id: string, problem: string) { const columns: DataTableColumn[] = [ { - title: renderTableTitle("提交时间", "fluent-emoji:seven-oclock"), + title: "提交时间", key: "create_time", width: 200, render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm:ss"), }, { - title: renderTableTitle("编号", "fluent-emoji-flat:input-numbers"), + title: "编号", key: "id", minWidth: 160, render: (row) => { @@ -68,13 +67,13 @@ const columns: DataTableColumn[] = [ }, }, { - title: renderTableTitle("状态", "streamline-emojis:panda-face"), + title: "状态", key: "status", width: 140, render: (row) => h(SubmissionResultTag, { result: row.result }), }, { - title: renderTableTitle("语言", "streamline-ultimate-color:earth-pin-2"), + title: "语言", key: "language", width: 100, render: (row) => LANGUAGE_SHOW_VALUE[row.language], diff --git a/apps/web/src/oj/problem/list.vue b/apps/web/src/oj/problem/list.vue index cfef5db..db7d193 100644 --- a/apps/web/src/oj/problem/list.vue +++ b/apps/web/src/oj/problem/list.vue @@ -13,7 +13,6 @@ import Pagination from "shared/components/Pagination.vue" import { useBreakpoints } from "shared/composables/breakpoints" import { usePagination } from "shared/composables/pagination" import { useUserStore } from "shared/store/user" -import { renderTableTitle } from "utils/renders" import ProblemStatus from "./components/ProblemStatus.vue" import AuthorSelect from "shared/components/AuthorSelect.vue" import ProblemListTitle from "./components/ProblemListTitle.vue" @@ -160,49 +159,49 @@ onMounted(() => { const baseColumns: DataTableColumn[] = [ { - title: renderTableTitle("状态", "streamline-emojis:high-voltage"), + title: "状态", key: "status", width: 80, align: "center", render: (row) => h(ProblemStatus, { status: row.status }), }, { - title: renderTableTitle("编号", "streamline-ultimate-color:board-game-dice-1"), + title: "编号", key: "_id", width: 100, }, { - title: renderTableTitle("题目", "streamline-ultimate-color:fruit-watermelon"), + title: "题目", key: "title", minWidth: 200, render: (row) => h(ProblemListTitle, { problem: row }), }, { - title: renderTableTitle("难度", "streamline-emojis:lady-beetle"), + title: "难度", key: "difficulty", width: 100, render: (row) => row.difficulty ? h(NTag, { type: getTagColor(row.difficulty) }, () => row.difficulty) : null, }, { - title: renderTableTitle("标签", "streamline-ultimate-color:attachment"), + title: "标签", key: "tags", width: 260, render: (row) => h(NFlex, () => row.tags.map((t) => h(NTag, { key: t }, () => t))), }, { - title: renderTableTitle("出题者", "streamline-emojis:man-raising-hand-2"), + title: "出题者", key: "author", width: 130, }, { - title: renderTableTitle("提交数", "streamline-ultimate-color:paper-write"), + title: "提交数", key: "submissionCount", align: "center", width: 100, }, { - title: renderTableTitle("通过率", "streamline-emojis:victory-hand-2"), + title: "通过率", key: "acRate", width: 100, align: "center", diff --git a/apps/web/src/oj/rank/list.vue b/apps/web/src/oj/rank/list.vue index 70a72e6..6076bc8 100644 --- a/apps/web/src/oj/rank/list.vue +++ b/apps/web/src/oj/rank/list.vue @@ -22,7 +22,6 @@ import { useBreakpoints } from "shared/composables/breakpoints" import { durationFromValue, getACRate, parseTime } from "utils/functions" import Pagination from "shared/components/Pagination.vue" import { ChartType, LONG_DURATION_OPTIONS } from "utils/constants" -import { renderTableTitle } from "utils/renders" import Chart from "./components/Chart.vue" import RankMedal from "./components/RankMedal.vue" import { useUserStore } from "shared/store/user" @@ -151,14 +150,14 @@ function rowClassName(row: Rank) { const columns: DataTableColumn[] = [ { - title: renderTableTitle("排名", "streamline-emojis:flexed-biceps-1"), + title: "排名", key: "index", width: 100, align: "center", render: (_, index) => h(RankMedal, { index, page: query.page, limit: query.limit }), }, { - title: renderTableTitle("用户", "streamline-emojis:smiling-face-with-sunglasses"), + title: "用户", key: "username", width: 240, render: (row) => @@ -193,24 +192,24 @@ const columns: DataTableColumn[] = [ ]), }, { - title: renderTableTitle("个性签名", "streamline-emojis:no-one-under-eighteen"), + title: "个性签名", key: "mood", minWidth: 200, }, { - title: renderTableTitle("已解决", "streamline-emojis:raised-fist-1"), + title: "已解决", key: "acceptedNumber", width: 120, align: "center", }, { - title: renderTableTitle("提交数", "streamline-ultimate-color:space-rocket-earth"), + title: "提交数", key: "submissionNumber", width: 120, align: "center", }, { - title: renderTableTitle("正确率", "streamline-ultimate-color:gift-box-1"), + title: "正确率", key: "rate", width: 120, align: "center", @@ -261,7 +260,7 @@ const subOptions = computed( // 宽度也压到 1280 那一档能整张放下,不出横向滚动条 const weeklyColumns: DataTableColumn[] = [ { - title: renderTableTitle("排名", "streamline-emojis:flexed-biceps-1"), + title: "排名", key: "rank", width: 100, align: "center", @@ -269,7 +268,7 @@ const weeklyColumns: DataTableColumn[] = [ render: (row) => h(RankMedal, { index: row.rank - 1, page: 1, limit: 10 }), }, { - title: renderTableTitle("用户", "streamline-emojis:smiling-face-with-sunglasses"), + title: "用户", key: "username", minWidth: 120, ellipsis: { tooltip: true }, @@ -285,13 +284,13 @@ const weeklyColumns: DataTableColumn[] = [ ), }, { - title: renderTableTitle("新解决", "fluent-emoji:party-popper"), + title: "新解决", key: "solvedCount", width: 100, align: "center", }, { - title: renderTableTitle("提交", "streamline-emojis:rocket"), + title: "提交", key: "submissionCount", width: 90, align: "center", @@ -525,31 +524,6 @@ watch(