diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 5e903993e..7fa3efd72 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -253,6 +253,7 @@ export class ContestProblemListHandler extends ContestDetailBaseHandler { pdict, psdict: {}, udict, rdict: {}, tdoc: this.tdoc, tsdoc: this.tsdoc, tcdocs, }; this.response.template = 'contest_problemlist.html'; + this.response.body.showScore = Object.values(this.tdoc.score || {}).some((i) => i && i !== 100); if (!this.tsdoc) return; if (this.tsdoc.attend && !this.tsdoc.startAt && contest.isOngoing(this.tdoc)) { await contest.setStatus(domainId, tid, this.user._id, { startAt: new Date() }); @@ -651,6 +652,16 @@ export class ContestManagementHandler extends ContestManagementBaseHandler { ]); this.back(); } + + @param('pid', Types.PositiveInt) + @param('score', Types.PositiveInt) + async postSetScore(domainId: string, pid: number, score: number) { + if (!this.tdoc.pids.includes(pid)) throw new ValidationError('pid'); + this.tdoc.score ||= {}; + this.tdoc.score[pid] = score; + await contest.edit(domainId, this.tdoc.docId, { score: this.tdoc.score }); + this.back(); + } } export class ContestFileDownloadHandler extends ContestDetailBaseHandler { diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 78c6803de..83a76d55e 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -358,9 +358,9 @@ export interface JudgeRequest extends Omit { export interface ScoreboardNode { type: 'string' | 'rank' | 'user' | 'email' | 'record' | 'records' | 'problem' | 'solved' | 'time' | 'total_score'; - value: string; + value: string; // 显示分数 raw?: any; - score?: number; + score?: number; // 原始分数(100,不含赛制加成) style?: string; hover?: string; } @@ -396,6 +396,7 @@ export interface Tdoc extends Document { unlocked?: boolean; autoHide?: boolean; balloon?: Record; + score?: Record; /** * In hours diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index af232245a..642055969 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -291,7 +291,9 @@ const oi = buildContestRule({ display[j.pid] = j; } } - for (const i in display) score += display[i].score || 0; + for (const i in display) { + score += ((tdoc.score?.[i] || 100) * (display[i].score || 0)) / 100; + } return { score, detail, display }; }, showScoreboard: (tdoc, now) => now > tdoc.endAt, @@ -332,6 +334,10 @@ const oi = buildContestRule({ { type: 'rank', value: rank.toString() }, { type: 'user', value: udoc.uname, raw: tsdoc.uid }, ]; + const displayScore = (pid: number, score?: number) => { + if (typeof score !== 'number') return '-'; + return score * ((tdoc.score?.[pid] || 100) / 100); + }; if (config.isExport) { row.push({ type: 'email', value: udoc.mail }); row.push({ type: 'string', value: udoc.school || '' }); @@ -357,17 +363,20 @@ const oi = buildContestRule({ type: 'records', value: '', raw: [{ - value: tsddict[pid]?.score ?? '-', + value: displayScore(pid, tsddict[pid]?.score), raw: tsddict[pid]?.rid || null, + score: tsddict[pid]?.score, }, { - value: meta?.psdict?.[index]?.score ?? '-', + value: displayScore(pid, meta?.psdict?.[index]?.score), raw: meta?.psdict?.[index]?.rid ?? null, + score: meta?.psdict?.[index]?.score, }], } : { type: 'record', - value: `${tsddict[pid]?.score ?? '-'}${tsddict[pid]?.npending + value: `${displayScore(pid, tsddict[pid]?.score)}${tsddict[pid]?.npending ? `+${tsddict[pid]?.npending}` : ''}`, raw: tsddict[pid]?.rid || null, + score: tsddict[pid]?.score, }; if (tsddict[pid]?.status === STATUS.STATUS_ACCEPTED && tsddict[pid]?.rid.getTimestamp().getTime() === meta?.first?.[pid]) { node.style = 'background-color: rgb(217, 240, 199);'; @@ -461,7 +470,7 @@ const strictioi = buildContestRule({ j.status = Math.max(...Object.values(subtasks[j.pid]).map((i) => i.status)); if (!detail[j.pid] || detail[j.pid].score < j.score) detail[j.pid] = { ...j, subtasks: subtasks[j.pid] }; } - for (const i in detail) score += detail[i].score; + for (const i in detail) score += ((tdoc.score?.[i] || 100) * (detail[i].score || 0)) / 100; return { score, detail }; }, async scoreboardRow(config, _, tdoc, pdict, udoc, rank, tsdoc, meta) { @@ -485,9 +494,10 @@ const strictioi = buildContestRule({ for (const pid of tdoc.pids) { row.push({ type: 'record', - value: tsddict[pid]?.score || '', + value: ((tsddict[pid]?.score || 0) * ((tdoc.score?.[pid] || 100) / 100)).toString() || '', hover: Object.values(tsddict[pid]?.subtasks || {}).map((i: SubtaskResult) => `${STATUS_SHORT_TEXTS[i.status]} ${i.score}`).join(','), raw: tsddict[pid]?.rid, + score: tsddict[pid]?.score, style: tsddict[pid]?.status === STATUS.STATUS_ACCEPTED && tsddict[pid]?.rid.getTimestamp().getTime() === meta?.first?.[pid] ? 'background-color: rgb(217, 240, 199);' : undefined, @@ -523,8 +533,9 @@ const ledo = buildContestRule({ let originalScore = 0; for (const pid of tdoc.pids) { if (!detail[pid]) continue; - score += detail[pid].penaltyScore; - originalScore += detail[pid].score; + const rate = (tdoc.score?.[pid] || 100) / 100; + score += detail[pid].penaltyScore * rate; + originalScore += detail[pid].score * rate; } return { score, originalScore, detail, @@ -555,9 +566,10 @@ const ledo = buildContestRule({ for (const pid of tdoc.pids) { row.push({ type: 'record', - value: tsddict[pid]?.penaltyScore?.toString() || '', + value: ((tsddict[pid]?.penaltyScore || 0) * ((tdoc.score?.[pid] || 100) / 100)).toString(), hover: tsddict[pid]?.ntry ? `-${tsddict[pid].ntry} (${Math.round(Math.max(0.7, 0.95 ** tsddict[pid].ntry) * 100)}%)` : '', raw: tsddict[pid]?.rid, + score: tsddict[pid]?.score, style: tsddict[pid]?.status === STATUS.STATUS_ACCEPTED && tsddict[pid]?.rid.getTimestamp().getTime() === meta?.first?.[pid] ? 'background-color: rgb(217, 240, 199);' : undefined, @@ -587,17 +599,18 @@ const homework = buildContestRule({ } function penaltyScore(jdoc) { + const rate = (tdoc.score?.[jdoc.pid] || 100) / 100; const exceedSeconds = Math.floor( (jdoc.rid.getTimestamp().getTime() - tdoc.penaltySince.getTime()) / 1000, ); - if (exceedSeconds < 0) return jdoc.score; + if (exceedSeconds < 0) return rate * jdoc.score; let coefficient = 1; const keys = Object.keys(tdoc.penaltyRules).map(parseFloat).sort((a, b) => a - b); for (const i of keys) { if (i * 3600 <= exceedSeconds) coefficient = tdoc.penaltyRules[i]; else break; } - return jdoc.score * coefficient; + return rate * jdoc.score * coefficient; } const detail = []; for (const j in effective) { @@ -691,7 +704,7 @@ const homework = buildContestRule({ } else { row.push({ type: 'record', - score: tsddict[pid]?.penaltyScore || 0, + score: tsddict[pid]?.score, value: colScore === colOriginalScore ? '{0}\n{1}'.format(colScore, colTimeStr) : '{0} / {1}\n{2}'.format(colScore, colOriginalScore, colTimeStr), diff --git a/packages/ui-default/locales/zh.yaml b/packages/ui-default/locales/zh.yaml index b0e1535ef..e0d311d5c 100644 --- a/packages/ui-default/locales/zh.yaml +++ b/packages/ui-default/locales/zh.yaml @@ -821,6 +821,7 @@ Set Privilege: 设置权限 Set Role: 设置角色 Set Roles for Selected User: 设置所选用户的角色 Set Roles for selected users: 为所选用户设置角色 +Set Score for Contest: 设置赛时分数 Set time and memory limits: 设置时空限制 setting_customize: 个性化 setting_display: 显示偏好 diff --git a/packages/ui-default/pages/contest.page.styl b/packages/ui-default/pages/contest.page.styl index f85fe1380..2198257ee 100644 --- a/packages/ui-default/pages/contest.page.styl +++ b/packages/ui-default/pages/contest.page.styl @@ -120,6 +120,10 @@ $highlight-button-color = #F6DF45 &.nojs .col--submit-at width: rem(160px) + .col--score + width: rem(80px) + text-align: center; + .col--problem border-left: 1px solid $table-border-color @@ -208,3 +212,7 @@ $highlight-button-color = #F6DF45 .col--awards width: rem(130px) + +.page--contest_manage + .col--score + width: rem(80px) \ No newline at end of file diff --git a/packages/ui-default/pages/contest_manage.page.ts b/packages/ui-default/pages/contest_manage.page.ts index 82ac9c7f2..173272aa4 100644 --- a/packages/ui-default/pages/contest_manage.page.ts +++ b/packages/ui-default/pages/contest_manage.page.ts @@ -1,5 +1,9 @@ import $ from 'jquery'; +import { ActionDialog } from 'vj/components/dialog'; +import Notification from 'vj/components/notification'; import { NamedPage } from 'vj/misc/Page'; +import { i18n, request, tpl } from 'vj/utils'; + function handleReplyOrBroadcast(ev) { const title = $(ev.currentTarget).data('title'); const did = $(ev.currentTarget).data('did'); @@ -19,6 +23,50 @@ function handleReplyOrBroadcast(ev) { const page = new NamedPage('contest_manage', () => { $(document).on('click', '[name="broadcast"]', handleReplyOrBroadcast); $(document).on('click', '[name="reply"]', handleReplyOrBroadcast); + $(document).on('click', '[name="set_score"]', async (ev) => { + const pid = $(ev.currentTarget).data('pid'); + const op = await new ActionDialog({ + $body: tpl` +
+

${i18n('Set Score for Contest')}

+
+
+ +
+ `, + onDispatch(action) { + if (action === 'ok' && $('[name="score"]').val() === null) { + $('[name="score"]').focus(); + return false; + } + if (!Number.isFinite(+$('[name="score"]').val()) || +$('[name="score"]').val() <= 0) { + Notification.error('Invalid score'); + return false; + } + return true; + }, + }).open(); + if (op !== 'ok') return; + try { + const res = await request.post('', { + operation: 'set_score', + pid, + score: $('[name="score"]').val(), + }); + if (!res.error) { + Notification.success('Score Updated'); + $(ev.currentTarget).text($('[name="score"]').val() as number); + $(ev.currentTarget).data('score', $('[name="score"]').val()); + } + } catch (e) { + Notification.error(e.message); + } + }); }); export default page; diff --git a/packages/ui-default/pages/files.page.styl b/packages/ui-default/pages/files.page.styl index 49be07a05..3a3b6873e 100644 --- a/packages/ui-default/pages/files.page.styl +++ b/packages/ui-default/pages/files.page.styl @@ -3,7 +3,7 @@ width: 60px .col--size - width: 120px + width: 100px .col--operation width: 40px diff --git a/packages/ui-default/templates/components/record.html b/packages/ui-default/templates/components/record.html index ffcf3fd29..bcf615cfc 100644 --- a/packages/ui-default/templates/components/record.html +++ b/packages/ui-default/templates/components/record.html @@ -1,4 +1,4 @@ -{% macro render_status_td(rdoc, rid_key='_id', class='', allDomain=false, short=false) %} +{% macro render_status_td(rdoc, rid_key='_id', class='', allDomain=false, short=false, displayScore) %}
@@ -7,7 +7,7 @@ class="record-status--text {{ model.builtin.STATUS_CODES[rdoc.status] }}" > {% if typeof(rdoc['score']) == 'number' %} - {{ rdoc.score|default(0) }} + {{ displayScore|default(rdoc.score|default(0)) }} {% endif %} {{ model.builtin.STATUS_SHORT_TEXTS[rdoc.status] if short else model.builtin.STATUS_TEXTS[rdoc.status] }} diff --git a/packages/ui-default/templates/contest_manage.html b/packages/ui-default/templates/contest_manage.html index ad6c47dfc..829a7bad9 100644 --- a/packages/ui-default/templates/contest_manage.html +++ b/packages/ui-default/templates/contest_manage.html @@ -11,10 +11,12 @@ + + @@ -25,6 +27,9 @@ {{ problem.render_problem_title(pdict[pid], tdoc=tdoc, invalid=true, alphabetic=true) }} + {%- endfor -%} diff --git a/packages/ui-default/templates/contest_problemlist.html b/packages/ui-default/templates/contest_problemlist.html index a5c7cdb2e..b590568da 100644 --- a/packages/ui-default/templates/contest_problemlist.html +++ b/packages/ui-default/templates/contest_problemlist.html @@ -17,6 +17,9 @@ {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} + {% endif %} + {% if showScore %} + {% endif %} @@ -25,6 +28,9 @@ {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} + {% endif %} + {% if showScore %} + {% endif %} @@ -34,17 +40,21 @@ {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} {% if psdict[pid] and psdict[pid].rid %} - {% set rdoc = rdict[psdict[pid].rid] %} - {% if model.contest.canShowSelfRecord.call(handler, tdoc) %} - {{ record.render_status_td(rdoc) }} - {% else %} - - {% endif %} + {% set rdoc = rdict[psdict[pid].rid] %} + {% set displayScore = ((tdoc.score[pid]|default(100)) * (psdict[pid].score|default(0)) / 100) if showScore else (psdict[pid].penaltyScore|default(psdict[pid].score)) %} + {% if model.contest.canShowSelfRecord.call(handler, tdoc) %} + {{ record.render_status_td(rdoc, displayScore=displayScore) }} + {% else %} + + {% endif %} {% else %} {% endif %} + {% endif %} + {% if showScore %} + {% endif %}
{{ _('Problem') }}{{ _('Score') }}
+ {{ tdoc.score[pid]|default(100) }} +
{{ _('Status') }} {{ _('Last Submit At') }}{{ _('Score') }}{{ _('Problem') }}
{{ _('Submitted') }}{{ _('Submitted') }}{{ datetimeSpan(rdoc._id)|safe }}{{ _('No Submissions') }} -{{ tdoc.score[pid]|default(100) }} {{ problem.render_problem_title(pdict[pid], tdoc, alphabetic=true) }} diff --git a/packages/utils/lib/status.ts b/packages/utils/lib/status.ts index eac1031c2..bd82d413f 100644 --- a/packages/utils/lib/status.ts +++ b/packages/utils/lib/status.ts @@ -80,6 +80,7 @@ export const STATUS_CODES: Record = { }; export function getScoreColor(score: number | string): string { + if (score === null || score === undefined || !Number.isFinite(+score)) return '#000000'; return [ '#ff4f4f', '#ff694f',