From 3ec549ee9cb71aae1afda72061c0c9d2e3c15f30 Mon Sep 17 00:00:00 2001 From: undefined Date: Fri, 25 Mar 2022 10:53:14 +0800 Subject: [PATCH] ui: update acm scoreboard display --- packages/hydrooj/locales/zh.yaml | 1 + packages/hydrooj/package.json | 2 +- packages/hydrooj/src/handler/contest.ts | 6 +- packages/hydrooj/src/interface.ts | 3 +- packages/hydrooj/src/model/contest.ts | 112 +++++++----------- packages/ui-default/package.json | 2 +- .../pages/contest_scoreboard.page.styl | 17 ++- .../pages/homework_scoreboard.page.styl | 6 +- .../templates/contest_scoreboard.html | 12 +- packages/ui-default/templates/wiki_help.html | 2 +- packages/utils/lib/utils.ts | 9 +- packages/utils/package.json | 2 +- 12 files changed, 86 insertions(+), 88 deletions(-) diff --git a/packages/hydrooj/locales/zh.yaml b/packages/hydrooj/locales/zh.yaml index 3abcbedb4..4595b1d9a 100644 --- a/packages/hydrooj/locales/zh.yaml +++ b/packages/hydrooj/locales/zh.yaml @@ -542,6 +542,7 @@ Password: 密码 Passwords don't match.: 验证密码不匹配。 Path {0} not found.: 路径 {0} 不存在。 Peak Memory: 峰值内存 +Penalty (Minutes): 罚时(分钟) perm_contest: 比赛 perm_discussion: 讨论 perm_general: 一般 diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index f740aba73..4b4f58edc 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "3.10.7", + "version": "3.11.0", "bin": "bin/hydrooj.js", "main": "src/loader", "module": "src/loader", diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 2d928e353..bf8506f32 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -183,6 +183,10 @@ export class ContestScoreboardHandler extends Handler { if (ignoreLock && !this.user.own(tdoc)) { this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); } + const pdict = await problem.getList(domainId, tdoc.pids, true, undefined, false, [ + // Problem statistics display is allowed as we can view submission info in scoreboard. + ...problem.PROJECTION_CONTEST_LIST, 'nSubmit', 'nAccept', + ]); const [, rows, udict, , nPages] = await contest.getScoreboard.call(this, domainId, tid, false, page, ignoreLock); const path = [ ['Hydro', 'homepage'], @@ -192,7 +196,7 @@ export class ContestScoreboardHandler extends Handler { ]; this.response.template = 'contest_scoreboard.html'; this.response.body = { - tdoc, rows, path, udict, nPages, page, + tdoc, rows, path, udict, nPages, page, pdict, }; } } diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 0cb2c1b7f..b2492784e 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -304,11 +304,12 @@ export interface RecordDoc { } export interface ScoreboardNode { - type: string; + type: 'string' | 'rank' | 'user' | 'record' | 'records' | 'problem' | 'solved' | 'time' | 'total_score'; value: string; raw?: any; score?: number; style?: string; + hover?: string; } export type ScoreboardRow = ScoreboardNode[] & { raw?: any }; diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index 78ecf112b..bc810cbc5 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -84,37 +84,27 @@ const acm = buildContestRule({ const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid); const udict = await user.getList(tdoc.domainId, uids); const columns: ScoreboardRow = [ - { type: 'rank', value: _('Rank') }, + { type: 'rank', value: '#' }, { type: 'user', value: _('User') }, - { type: 'solved_problems', value: _('Solved') }, + { type: 'solved', value: `${_('Solved')}\n${_('Total Time')}` }, ]; - if (isExport) { - columns.push( - { type: 'total_time', value: _('Penalty') }, - { type: 'total_time', value: _('Total Time (Seconds)') }, - ); - } for (let i = 1; i <= tdoc.pids.length; i++) { const pid = tdoc.pids[i - 1]; if (isExport) { columns.push( { - type: 'problem_flag', + type: 'string', value: '#{0} {1}'.format(i, pdict[pid].title), }, { - type: 'problem_time', - value: '#{0} {1}'.format(i, _('Time (Seconds)')), - }, - { - type: 'problem_time_str', - value: '#{0} {1}'.format(i, _('Time')), + type: 'time', + value: '#{0} {1}'.format(i, _('Penalty (Minutes)')), }, ); } else { columns.push({ - type: 'problem_detail', - value: '#{0}'.format(i), + type: 'problem', + value: String.fromCharCode(65 + i - 1), raw: pid, }); } @@ -144,37 +134,36 @@ const acm = buildContestRule({ const tsddict: Record = {}; for (const item of tsdoc.detail || []) tsddict[item.pid] = item; const row: ScoreboardRow = [ - { type: 'string', value: rank.toString() }, + { type: 'rank', value: rank.toString() }, { type: 'user', value: udict[tsdoc.uid].uname, raw: tsdoc.uid }, - { type: 'string', value: tsdoc.accept || 0 }, + { + type: 'time', + value: `${tsdoc.accept || 0}\n${misc.formatSeconds(tsdoc.time || 0.0, false)}`, + hover: misc.formatSeconds(tsdoc.time || 0.0), + }, ]; - if (isExport) { - const penalty = Math.sum(tdoc.pids.map((i) => tsddict[i]?.naccept || 0)) * 20 * 60; - row.push( - { type: 'string', value: penalty.toString() }, - { type: 'string', value: tsdoc.time || 0.0 }, - ); - } for (const pid of tdoc.pids) { const doc = tsddict[pid] || {} as Partial; const accept = doc.status === STATUS.STATUS_ACCEPTED; - const rid = accept ? doc.rid : null; - const colAccepted = `${(accept && isExport) ? `${_('Accepted')} ` : ''}${doc.naccept ? ` (-${doc.naccept})` : ''}`; - const colTime = accept ? doc.time.toString() : '-'; - const colTimeStr = accept ? misc.formatSeconds(doc.time) : '-'; + const colTime = accept ? misc.formatSeconds(doc.real, false).toString() : ''; + const colPenalty = doc.rid ? Math.ceil(doc.penalty / 60).toString() : ''; if (isExport) { row.push( - { type: 'string', value: colAccepted }, { type: 'string', value: colTime }, - { type: 'string', value: colTimeStr }, + { type: 'string', value: colPenalty }, ); } else { row.push({ type: 'record', score: accept ? 100 : 0, - value: '{0}\n{1}'.format(colAccepted, colTimeStr), - raw: rid, - style: accept && rid.generationTime === first[pid] + value: accept + ? `${colTime}\n${doc.penalty ? `+ (-${doc.naccept})` : ''}` + : doc.rid + ? `-${doc.naccept}` + : '', + hover: accept ? misc.formatSeconds(doc.time) : '', + raw: doc.rid, + style: accept && doc.rid.generationTime === first[pid] ? 'background-color: rgb(217, 240, 199);' : undefined, }); @@ -211,20 +200,20 @@ const oi = buildContestRule({ const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid); const udict = await user.getList(tdoc.domainId, uids); const columns: ScoreboardNode[] = [ - { type: 'rank', value: _('Rank') }, + { type: 'rank', value: '#' }, { type: 'user', value: _('User') }, { type: 'total_score', value: _('Total Score') }, ]; for (let i = 1; i <= tdoc.pids.length; i++) { if (isExport) { columns.push({ - type: 'problem_score', + type: 'string', value: '#{0} {1}'.format(i, pdict[tdoc.pids[i - 1]].title), }); } else { columns.push({ - type: 'problem_detail', - value: '#{0}'.format(i), + type: 'problem', + value: String.fromCharCode(65 + i - 1), raw: tdoc.pids[i - 1], }); } @@ -251,17 +240,17 @@ const oi = buildContestRule({ } } } - const rows = [columns]; + const rows: ScoreboardNode[][] = [columns]; for (const [rank, tsdoc] of rankedTsdocs) { const tsddict = {}; if (tsdoc.journal) { for (const item of tsdoc.journal) tsddict[item.pid] = item; } - const row = []; + const row: ScoreboardNode[] = []; row.push( - { type: 'string', value: rank }, + { type: 'string', value: rank.toString() }, { type: 'user', value: udict[tsdoc.uid].uname, raw: tsdoc.uid }, - { type: 'string', value: tsdoc.score || 0 }, + { type: 'total_score', value: tsdoc.score || 0 }, ); for (const pid of tdoc.pids) { const index = `${tsdoc.uid}/${tdoc.domainId}/${pid}`; @@ -356,44 +345,37 @@ const homework = buildContestRule({ async scoreboard(isExport, _, tdoc, pdict, cursor, page) { const [rankedTsdocs, nPages] = await ranked(cursor, (a, b) => a.score === b.score, page); const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid); - const udict = await user.getList.call(this, tdoc.domainId, uids); + const udict = await user.getList(tdoc.domainId, uids); const columns: ScoreboardNode[] = [ { type: 'rank', value: _('Rank') }, { type: 'user', value: _('User') }, { type: 'total_score', value: _('Score') }, ]; if (isExport) { - columns.push( - { type: 'total_original_score', value: _('Original Score') }, - { type: 'total_time', value: _('Total Time (Seconds)') }, - ); + columns.push({ type: 'string', value: _('Original Score') }); } - columns.push({ type: 'total_time_str', value: _('Total Time') }); + columns.push({ type: 'time', value: _('Total Time') }); for (let i = 1; i <= tdoc.pids.length; i++) { const pid = tdoc.pids[i - 1]; if (isExport) { columns.push( { - type: 'problem_score', + type: 'string', value: '#{0} {1}'.format(i, pdict[pid].title), }, { - type: 'problem_original_score', + type: 'string', value: '#{0} {1}'.format(i, _('Original Score')), }, { - type: 'problem_time', + type: 'time', value: '#{0} {1}'.format(i, _('Time (Seconds)')), }, - { - type: 'problem_time_str', - value: '#{0} {1}'.format(i, _('Time')), - }, ); } else { columns.push({ - type: 'problem_detail', - value: '#{0}'.format(i), + type: 'problem', + value: String.fromCharCode(65 + i - 1), raw: pid, }); } @@ -418,21 +400,19 @@ const homework = buildContestRule({ ]; if (isExport) { row.push({ type: 'string', value: tsdoc.score || 0 }); - row.push({ type: 'string', value: tsdoc.time || 0.0 }); } - row.push({ type: 'string', value: misc.formatSeconds(tsdoc.time || 0) }); + row.push({ type: 'time', value: misc.formatSeconds(tsdoc.time || 0, false), raw: tsdoc.time }); for (const pid of tdoc.pids) { const rid = tsddict[pid]?.rid; - const colScore = tsddict[pid]?.penaltyScore || '-'; - const colOriginalScore = tsddict[pid]?.score || '-'; - const colTime = tsddict[pid]?.time || '-'; - const colTimeStr = colTime !== '-' ? misc.formatSeconds(colTime) : '-'; + const colScore = tsddict[pid]?.penaltyScore || ''; + const colOriginalScore = tsddict[pid]?.score || ''; + const colTime = tsddict[pid]?.time || ''; + const colTimeStr = colTime ? misc.formatSeconds(colTime, false) : ''; if (isExport) { row.push( { type: 'string', value: colScore }, { type: 'string', value: colOriginalScore }, - { type: 'string', value: colTime }, - { type: 'string', value: colTimeStr }, + { type: 'time', value: colTime }, ); } else { row.push({ diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 0fe62c897..517b86232 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.34.23", + "version": "4.34.24", "author": "undefined ", "license": "AGPL-3.0", "main": "hydro.js", diff --git a/packages/ui-default/pages/contest_scoreboard.page.styl b/packages/ui-default/pages/contest_scoreboard.page.styl index 806c4b2f8..c3109e23b 100644 --- a/packages/ui-default/pages/contest_scoreboard.page.styl +++ b/packages/ui-default/pages/contest_scoreboard.page.styl @@ -1,23 +1,32 @@ .page--contest_scoreboard .col--rank - width: rem(60px) + width: rem(45px) + text-align: center border-right: 1px solid $table-border-color .col--user min-width: rem(150px) - .col--total_score, .col--total_time_str, .col--solved_problems + .col--solved, .col--time + width: rem(70px) + text-align: center + + .col--total_score width: rem(100px) text-align: center - .col--problem_detail + .col--problem width: rem(100px) border-left: 1px solid $table-border-color text-align: center + .scoreboard--acm + .col--problem + width: rem(70px) !important + +mobile() .col--user width: auto - .col--problem_detail + .col--problem display: none diff --git a/packages/ui-default/pages/homework_scoreboard.page.styl b/packages/ui-default/pages/homework_scoreboard.page.styl index 315c14f79..810ef366f 100644 --- a/packages/ui-default/pages/homework_scoreboard.page.styl +++ b/packages/ui-default/pages/homework_scoreboard.page.styl @@ -6,11 +6,11 @@ .col--user width: rem(140px) - .col--total_score, .col--total_time_str, .col--solved_problems + .col--total_score width: rem(100px) text-align: center - .col--problem_detail + .col--problem, .col--record min-width: rem(100px) border-left: 1px solid $table-border-color text-align: center @@ -19,5 +19,5 @@ .col--user width: auto - .col--problem_detail + .col--problem, .col--record display: none diff --git a/packages/ui-default/templates/contest_scoreboard.html b/packages/ui-default/templates/contest_scoreboard.html index 3076b43b3..4eba62f70 100644 --- a/packages/ui-default/templates/contest_scoreboard.html +++ b/packages/ui-default/templates/contest_scoreboard.html @@ -2,7 +2,7 @@ {% extends "layout/basic.html" %} {% set type = 'homework' if tdoc.docType == 60 else 'contest' %} {% block content %} -
+
@@ -30,10 +30,12 @@ {%- for column in rows[0] -%} - {% if column.type == 'problem_detail' %} - {{ column.value }} + {% if column.type == 'problem' %} + + {{ column.value }}
{{ pdict[column.raw].nAccept }}/{{ pdict[column.raw].nSubmit }} +
{% else %} - {{ column.value }} + {{ column.value|replaceBr|safe }} {% endif %} {%- endfor -%} @@ -49,7 +51,7 @@ {% set canView = canViewAll or handler.user._id == column.raw %} {{ user.render_inline(udict[column.raw], badge=false) }} {% elif column.type == 'record' and column.raw %} - + {{ column.value|replaceBr|safe }} {% elif column.type == 'records' %} diff --git a/packages/ui-default/templates/wiki_help.html b/packages/ui-default/templates/wiki_help.html index 1f8408193..bc8517a08 100644 --- a/packages/ui-default/templates/wiki_help.html +++ b/packages/ui-default/templates/wiki_help.html @@ -150,7 +150,7 @@

{{ _('Dataset Format') }}

-

详见配置文档

+

详见配置文档

diff --git a/packages/utils/lib/utils.ts b/packages/utils/lib/utils.ts index 05ee8bc25..37dee8d33 100644 --- a/packages/utils/lib/utils.ts +++ b/packages/utils/lib/utils.ts @@ -305,13 +305,14 @@ function _digit2(number: number) { return number < 10 ? `0${number}` : number.toString(); } -export function formatSeconds(_seconds: string | number = '0') { +export function formatSeconds(_seconds: string | number = '0', showSeconds = true) { const seconds = +_seconds; - return '{0}:{1}:{2}'.format( - _digit2(Math.floor(seconds / 3600)), + let res = '{0}:{1}'.format( + showSeconds ? _digit2(Math.floor(seconds / 3600)) : Math.floor(seconds / 3600), _digit2(Math.floor((seconds % 3600) / 60)), - _digit2(seconds % 60), ); + if (showSeconds) res += `:${_digit2(seconds % 60)}`; + return res; } export function changeErrorType(err: any, Err: any) { diff --git a/packages/utils/package.json b/packages/utils/package.json index df7d8d8a8..53354dd3b 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/utils", - "version": "1.1.15", + "version": "1.1.16", "description": "hydrooj utils", "main": "package.json", "repository": "https://github.com/hydro-dev/Hydro.git",