From 8d35227dfcd935cb7c590c3b8a04172d37d35148 Mon Sep 17 00:00:00 2001 From: undefined Date: Mon, 18 Dec 2023 16:28:03 +0800 Subject: [PATCH 01/14] onsite-toolkit: add resolver --- .../onsite-toolkit/frontend/resolver.page.tsx | 284 ++++++++++++++++++ packages/onsite-toolkit/index.ts | 13 +- packages/onsite-toolkit/package.json | 6 +- packages/onsite-toolkit/public/resolver.css | 95 ++++++ .../onsite-toolkit/templates/resolver.html | 16 + packages/ui-default/backendlib/builder.ts | 29 +- 6 files changed, 436 insertions(+), 7 deletions(-) create mode 100644 packages/onsite-toolkit/frontend/resolver.page.tsx create mode 100644 packages/onsite-toolkit/public/resolver.css create mode 100644 packages/onsite-toolkit/templates/resolver.html diff --git a/packages/onsite-toolkit/frontend/resolver.page.tsx b/packages/onsite-toolkit/frontend/resolver.page.tsx new file mode 100644 index 000000000..0c57f140b --- /dev/null +++ b/packages/onsite-toolkit/frontend/resolver.page.tsx @@ -0,0 +1,284 @@ +import { animated, easings, useSprings } from '@react-spring/web'; +import useKey from 'react-use/lib/useKey'; +import { + addPage, NamedPage, React, ReactDOM, request, +} from '@hydrooj/ui-default'; + +function convertPayload(ghost: string, lock: number) { + const lines = ghost.split('\n'); + const data = { + contest_name: lines[0].split('"')[1].split('"')[0], + problem_count: +(lines[2].split(' ')[1]), + frozen_seconds: +lock || 1800, + teams: +(lines[3].split(' ')[1]), + submissions: +(lines[4].split(' ')[1]), + users: {}, + solutions: {}, + }; + for (let i = 5 + data.problem_count; i < 5 + data.problem_count + data.teams; i++) { + const team = lines[i].match(/@t (\d+),\d+,\d+,(.*)/); + data.users[team[1]] = { + name: team[2].split('-')[1], + college: team[2].split('-')[0], + is_exclude: false, + }; + } + for (let i = 5 + data.problem_count + data.teams; i < 5 + data.problem_count + data.teams + data.submissions; i++) { + // @s 3,C,1,10066,AC + const line = lines[i].split(' ')[1].split(','); + data.solutions[i] = { + user_id: line[0], + problem_index: +(line[1].charCodeAt(0) - 'A'.charCodeAt(0)) + 1, + verdict: line[4], + submitted_seconds: +(line[3]), + }; + } + const s = Object.keys(data.solutions).map((key) => data.solutions[key]); + s.sort((a, b) => a.submitted_seconds - b.submitted_seconds); + data.solutions = {}; + s.forEach((solution, index) => { + data.solutions[index] = solution; + }); + return data; +} + +async function scrollTo(offset) { + const fixedOffset = offset.toFixed(); + await new Promise((resolve) => { + const onScroll = function () { + if (window.pageYOffset.toFixed() === fixedOffset) { + window.removeEventListener('scroll', onScroll); + resolve(null); + } + }; + + window.addEventListener('scroll', onScroll); + onScroll(); + window.scrollTo({ + top: offset, + behavior: 'smooth', + }); + }); +} + +type Verdict = 'RJ' | 'AC' | 'NA'; + +addPage(new NamedPage(['resolver'], () => { + function processData({ + contest_name, solutions, users, problem_count, frozen_seconds, + }) { + $('title').text(contest_name); + $('#title').text(contest_name); + $('.footer').css('display', 'none'); + + // const resolver = new Resolver(data.solutions, data.users, data.problem_count, data.frozen_seconds); + // resolver.calcOperations(); + + const teams = Object.keys(users).map((key) => ({ + id: key, + rank: 0, + score: 0, + penalty: 0, + ranked: !users[key].is_exclude, + total: 0, + problems: Array.from({ length: problem_count }, (v, i) => i + 1).map(() => ({ + old: 0, + frozen: 0, + pass: false, + })), + })); + const allSubmissions: Array<{ + user_id: string, problem_index: string, verdict: Verdict, submitted_seconds: number + }> = Object.values(solutions).sort((a: any, b: any) => a.submitted_seconds - b.submitted_seconds) as any; + const allAc = allSubmissions.filter((i) => i.verdict === 'AC'); + for (const submission of allSubmissions) { + const team = teams.find((v) => v.id === submission.user_id); + if (!team) continue; + const isFrozen = submission.submitted_seconds > frozen_seconds; + const problem = team.problems[+submission.problem_index - 1]; + if (problem.pass) continue; + team.total++; + if (isFrozen) { + problem.frozen += 1; + } else { + if (submission.verdict === 'AC') { + problem.pass = true; + team.score += 1; + team.penalty += submission.submitted_seconds + problem.old * 20 * 60; + } + problem.old += 1; + } + } + function processRank() { + const clone = [...teams]; + clone.sort((a, b) => b.score - a.score || a.penalty - b.penalty || b.total - a.total); + let rank = 1; + for (const team of clone) { + if (team.ranked) { + team.rank = rank; + rank++; + } else { + team.rank = -1; + } + } + return clone.map((i) => teams.indexOf(i)); + } + const initial = processRank(); + + function status(problem) { + if (!problem) return 'untouched'; + if (problem.pass) return 'ac'; + if (!problem.old && !problem.frozen) return 'untouched'; + if (problem.frozen) return 'frozen'; + return 'failed'; + } + + function submissions(problem) { + const st = status(problem); + if (st === 'ac') { return `${problem.old}`; } + if (st === 'frozen') { return `${problem.old}+${problem.frozen}`; } + if (st === 'failed') { return problem.old; } + return String.fromCharCode('A'.charCodeAt(0) + problem.problem_index); + } + + function MainList(props) { + const [selectedTeam, setTeam] = React.useState(''); + const [selectedProblem, setP] = React.useState(null); + const [ready, setReady] = React.useState(true); + const order = React.useRef(initial); + + const [springs, api] = useSprings(teams.length, (index) => ({ + y: order.current.indexOf(index) * 103 - index * 103, + scale: 1, + zIndex: 0, + shadow: 1, + immediate: (key: string) => key === 'y' || key === 'zIndex', + })); + + React.useEffect(() => { + window.scrollTo(0, document.body.scrollHeight); + }, []); + + useKey('ArrowRight', () => { + console.log('click'); + if (!ready) return; + for (let i = teams.length - 1; i > 0; i--) { + const team = teams[order.current[i]]; + for (let j = 0; j < problem_count; j++) { + const problem = team.problems[j]; + if (problem.frozen && !problem.pass) { + setReady(false); + setTeam(team); + setP(j); + // scroll to selected line + console.log(i, team.id, order.current.indexOf(i)); + scrollTo(i * 103 - window.innerHeight + 261).then(() => { + setTimeout(() => { + if (allAc.find((s) => s.user_id === team.id && +s.problem_index === j + 1)) { + const sub = allSubmissions.filter((s) => s.user_id === team.id && +s.problem_index === j + 1); + let penalty = 0; + for (const s of sub) { + if (s.verdict !== 'AC') { + penalty += 20 * 60; + problem.old++; + } else { + penalty += s.submitted_seconds; + break; + } + } + team.penalty += penalty; + team.score += 1; + problem.pass = true; + problem.frozen = 0; + setP(null); + setTimeout(() => { + order.current = processRank(); + api.start((index) => ({ + y: order.current.indexOf(index) * 103 - index * 103, + scale: 1, + zIndex: 0, + shadow: 1, + config: { + easing: easings.steps(5), + }, + })); + setReady(true); + }, 1000); + } else { + problem.old += problem.frozen; + problem.frozen = 0; + setReady(true); + setP(null); + } + }, 1000); + }); + return; + } + } + } + }, {}, [ready]); + + return (<> + {springs.map(({ + zIndex, y, + }, i) => { + const team = teams[i]; + return `rgba(0, 0, 0, 0.15) 0px ${s}px ${2 * s}px 0px`), + y, + ...(selectedTeam === team.id ? { + backgroundColor: '#406b82', + } : {}), + }} + children={<> +
{team.rank === -1 ? '*' : team.rank}
+
+
{users[team.id].college}--{users[team.id].name}
+
    + {Array.from({ length: problem_count }, (v, i) => i).map((v, n) => { + const uncover = team?.id === selectedTeam && selectedProblem === n; + return
  • +
    {submissions(team.problems[n])}
    +
  • ; + })} +
+
+
{Math.floor(team.penalty / 60)}
+
{team.score}
+ } + />; + })} + ); + } + ReactDOM.createRoot(document.getElementById('rank-list')!).render(); + } + + const current = new URL(window.location.href); + + async function loadAndStart(input: string) { + let data; + try { + data = JSON.parse(input); + } catch (e) { + console.log(`load data from url. [url=${input}]`); + const res = await request.get(input, {}, { + dataType: 'text', + }); + if (res.startsWith('@')) data = convertPayload(res, +(current.searchParams.get('lock') || 0)); + else data = JSON.parse(res); + } + processData(data); + } + + const input = current.searchParams.get('input'); + if (input) loadAndStart(input); + $('#load').on('click', () => { + const src = $('#input-data').val()?.toString()?.trim(); + if (src) loadAndStart(src); + }); +})); diff --git a/packages/onsite-toolkit/index.ts b/packages/onsite-toolkit/index.ts index 0e0fc30ce..5ca478e76 100644 --- a/packages/onsite-toolkit/index.ts +++ b/packages/onsite-toolkit/index.ts @@ -1,5 +1,7 @@ import { - Context, db, ForbiddenError, UserModel, + Context, db, ForbiddenError, PRIV, ContestModel, PERM, UserModel, + ContestScoreboardHiddenError, Types, param, ProblemModel, ObjectId, Time, + ContestNotLiveError, Counter, STATUS, Handler, } from 'hydrooj'; interface IpLoginInfo { @@ -19,6 +21,13 @@ function normalizeIp(ip: string) { return ip; } + +export class ContestResolverHandler extends Handler { + async get(domainId: string, tid: ObjectId) { + this.response.template = 'resolver.html'; + } +} + export function apply(ctx: Context) { ctx.on('handler/init', async (that) => { const iplogin = await coll.findOne({ _id: normalizeIp(that.request.ip) }); @@ -33,4 +42,6 @@ export function apply(ctx: Context) { that.session.user = that.user; } }); + + ctx.Route('resolver', '/resolver', ContestResolverHandler); } diff --git a/packages/onsite-toolkit/package.json b/packages/onsite-toolkit/package.json index d1ccd0de0..18dbe74ef 100644 --- a/packages/onsite-toolkit/package.json +++ b/packages/onsite-toolkit/package.json @@ -1,4 +1,8 @@ { "name": "@hydrooj/onsite-toolkit", - "version": "0.0.1" + "version": "0.0.1", + "dependencies": { + "@react-spring/web": "^9.7.3", + "react-use": "^17.4.2" + } } diff --git a/packages/onsite-toolkit/public/resolver.css b/packages/onsite-toolkit/public/resolver.css new file mode 100644 index 000000000..e8cdf9c7a --- /dev/null +++ b/packages/onsite-toolkit/public/resolver.css @@ -0,0 +1,95 @@ +.navbar { + margin-bottom: 0; +} +.app { + color: white; +} +.rank-list { + background: repeating-linear-gradient( + 180deg, + #3e3e3e 0px, + #3e3e3e 103px, + black 103px, + black 206px + ); +} +.item { + border-radius: 3px; +} +.rank, .content, .problems li { + float: left; +} +.solved, .penalty { + float: right; +} +.rank, .solved, .penalty { + font-size: 35px; + text-align: center; + vertical-align: middle; + line-height: 83px; +} +.rank-list-item { + padding: 10px; + height: 103px; + position: relative; + background: transparent; +} +.ac { + background-color: #5eb95e; +} +.failed, .WA { + background-color: #dd514c; +} +.frozen { + background-color: #607D8B; +} +.untouched { + background-color: #1f1f1f; +} +.uncover { + animation: flashing 300ms infinite; + -webkit-animation: flashing 30ms infinite; /*Safari and Chrome*/ +} +@keyframes flashing { + from { background-color: #8a6d3b } + to { background-color: #BD995B } +} +@-webkit-keyframes flashing {/*Safari and Chrome*/ + from { background-color: #8a6d3b } + to { background-color: #BD995B } +} +.rank { + width: 58px; + height: 58px; + font-size: 35px; + text-align: center; + vertical-align: middle; + line-height: 83px; + margin-right: 15px; +} +.name { + font-size: 35px; + margin-bottom: 5px; +} +.problems { + list-style-type: none; + margin: 0; + padding: 0; + font-size: 12px; +} +.problems .item { + padding: 2px; + margin-right: 5px; + width: 80px; + text-align: center; +} +.problems .item .p-content { + padding: 1px 0; + font-size: 15px; +} +.solved { + width: 70px; +} +.penalty { + width: 100px; +} \ No newline at end of file diff --git a/packages/onsite-toolkit/templates/resolver.html b/packages/onsite-toolkit/templates/resolver.html new file mode 100644 index 000000000..eaf9ddb57 --- /dev/null +++ b/packages/onsite-toolkit/templates/resolver.html @@ -0,0 +1,16 @@ +{% extends "layout/html5.html" %} +{% block body %} +
+
+
+

+ +

+

+ +

+
+
+ + +{% endblock %} diff --git a/packages/ui-default/backendlib/builder.ts b/packages/ui-default/backendlib/builder.ts index 844d7ef57..c39b7e672 100644 --- a/packages/ui-default/backendlib/builder.ts +++ b/packages/ui-default/backendlib/builder.ts @@ -34,14 +34,33 @@ const tmp = tmpdir(); const federationPlugin: esbuild.Plugin = { name: 'federation', setup(b) { + const packages = { + react: 'React', + 'react-dom': 'ReactDOM', + 'jquery': '$', + }; b.onResolve({ filter: /^@hydrooj\/ui-default/ }, () => ({ path: 'api', namespace: 'ui-default', })); - b.onLoad({ filter: /.*/, namespace: 'ui-default' }, () => ({ - contents: 'module.exports = window.HydroExports;', - loader: 'tsx', - })); + for (const key in packages) { + b.onResolve({ filter: new RegExp(`^${key}($|\\/)`) }, () => ({ + path: packages[key], + namespace: 'ui-default', + })); + } + b.onLoad({ filter: /.*/, namespace: 'ui-default' }, (args) => { + if (args.path === 'api') { + return { + contents: 'module.exports = window.HydroExports;', + loader: 'tsx', + }; + } + return { + contents: `module.exports = window.HydroExports['${args.path}'];`, + loader: 'tsx', + }; + }); }, }; @@ -144,7 +163,7 @@ export async function apply(ctx: Context) { ctx.on('app/started', buildUI); const debouncedBuildUI = debounce(buildUI, 2000, { trailing: true }); const triggerHotUpdate = (path?: string) => { - if (path && !path.includes('/ui-default/') && !path.includes('/public/')) return; + if (path && !['/ui-default/', '/public/', '/frontend/'].some((i) => path.includes(i))) return; debouncedBuildUI(); updateLogo(); }; From ad62ff57ea048a5d2e2d03a6ce25d0e8e5c63602 Mon Sep 17 00:00:00 2001 From: undefined Date: Tue, 19 Dec 2023 20:59:13 +0800 Subject: [PATCH 02/14] fix --- packages/onsite-toolkit/index.ts | 7 ++----- packages/ui-default/backendlib/builder.ts | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/onsite-toolkit/index.ts b/packages/onsite-toolkit/index.ts index 5ca478e76..6b95c5726 100644 --- a/packages/onsite-toolkit/index.ts +++ b/packages/onsite-toolkit/index.ts @@ -1,7 +1,5 @@ import { - Context, db, ForbiddenError, PRIV, ContestModel, PERM, UserModel, - ContestScoreboardHiddenError, Types, param, ProblemModel, ObjectId, Time, - ContestNotLiveError, Counter, STATUS, Handler, + Context, db, ForbiddenError, Handler, UserModel, } from 'hydrooj'; interface IpLoginInfo { @@ -21,9 +19,8 @@ function normalizeIp(ip: string) { return ip; } - export class ContestResolverHandler extends Handler { - async get(domainId: string, tid: ObjectId) { + async get() { this.response.template = 'resolver.html'; } } diff --git a/packages/ui-default/backendlib/builder.ts b/packages/ui-default/backendlib/builder.ts index c39b7e672..479a6ab34 100644 --- a/packages/ui-default/backendlib/builder.ts +++ b/packages/ui-default/backendlib/builder.ts @@ -37,7 +37,7 @@ const federationPlugin: esbuild.Plugin = { const packages = { react: 'React', 'react-dom': 'ReactDOM', - 'jquery': '$', + jquery: '$', }; b.onResolve({ filter: /^@hydrooj\/ui-default/ }, () => ({ path: 'api', From a96af9299a8209499dc00ceb7f5a723e448a2c9b Mon Sep 17 00:00:00 2001 From: undefined Date: Thu, 4 Jul 2024 16:24:28 +0800 Subject: [PATCH 03/14] update resolver --- .../onsite-toolkit/frontend/resolver.page.tsx | 382 +++++++++--------- packages/onsite-toolkit/index.ts | 47 ++- packages/onsite-toolkit/interface.ts | 33 ++ 3 files changed, 275 insertions(+), 187 deletions(-) create mode 100644 packages/onsite-toolkit/interface.ts diff --git a/packages/onsite-toolkit/frontend/resolver.page.tsx b/packages/onsite-toolkit/frontend/resolver.page.tsx index 0c57f140b..6560881db 100644 --- a/packages/onsite-toolkit/frontend/resolver.page.tsx +++ b/packages/onsite-toolkit/frontend/resolver.page.tsx @@ -1,8 +1,10 @@ +/* eslint-disable no-await-in-loop */ import { animated, easings, useSprings } from '@react-spring/web'; import useKey from 'react-use/lib/useKey'; import { - addPage, NamedPage, React, ReactDOM, request, + addPage, NamedPage, React, ReactDOM, request, sleep, } from '@hydrooj/ui-default'; +import { ResolverInput } from '../interface'; function convertPayload(ghost: string, lock: number) { const lines = ghost.split('\n'); @@ -17,6 +19,7 @@ function convertPayload(ghost: string, lock: number) { }; for (let i = 5 + data.problem_count; i < 5 + data.problem_count + data.teams; i++) { const team = lines[i].match(/@t (\d+),\d+,\d+,(.*)/); + if (!team) continue; data.users[team[1]] = { name: team[2].split('-')[1], college: team[2].split('-')[0], @@ -60,55 +63,74 @@ async function scrollTo(offset) { }); }); } +interface DisplaySettings { + showAvatar: boolean; + showSchool: boolean; +} -type Verdict = 'RJ' | 'AC' | 'NA'; +interface Props extends DisplaySettings { + data: ResolverInput; +} -addPage(new NamedPage(['resolver'], () => { - function processData({ - contest_name, solutions, users, problem_count, frozen_seconds, - }) { - $('title').text(contest_name); - $('#title').text(contest_name); - $('.footer').css('display', 'none'); +function status(problem) { + if (!problem) return 'untouched'; + if (problem.pass) return 'ac'; + if (!problem.old && !problem.frozen) return 'untouched'; + if (problem.frozen) return 'frozen'; + return 'failed'; +} - // const resolver = new Resolver(data.solutions, data.users, data.problem_count, data.frozen_seconds); - // resolver.calcOperations(); +function submissions(problem) { + const st = status(problem); + if (st === 'ac') { return `${problem.old}`; } + if (st === 'frozen') { return `${problem.old}+${problem.frozen}`; } + if (st === 'failed') { return problem.old; } + return String.fromCharCode('A'.charCodeAt(0) + problem.problem_index); +} - const teams = Object.keys(users).map((key) => ({ - id: key, - rank: 0, - score: 0, - penalty: 0, - ranked: !users[key].is_exclude, - total: 0, - problems: Array.from({ length: problem_count }, (v, i) => i + 1).map(() => ({ - old: 0, - frozen: 0, - pass: false, - })), - })); - const allSubmissions: Array<{ - user_id: string, problem_index: string, verdict: Verdict, submitted_seconds: number - }> = Object.values(solutions).sort((a: any, b: any) => a.submitted_seconds - b.submitted_seconds) as any; - const allAc = allSubmissions.filter((i) => i.verdict === 'AC'); - for (const submission of allSubmissions) { - const team = teams.find((v) => v.id === submission.user_id); - if (!team) continue; - const isFrozen = submission.submitted_seconds > frozen_seconds; - const problem = team.problems[+submission.problem_index - 1]; - if (problem.pass) continue; - team.total++; - if (isFrozen) { - problem.frozen += 1; - } else { - if (submission.verdict === 'AC') { - problem.pass = true; - team.score += 1; - team.penalty += submission.submitted_seconds + problem.old * 20 * 60; - } - problem.old += 1; +function start(data: ResolverInput, options: DisplaySettings) { + $('title').text(data.name); + $('#title').text(data.name); + $('.footer').css('display', 'none'); + const teams = data.teams.map((v) => ({ + id: v.id, + rank: 0, + score: 0, + penalty: 0, + ranked: !v.exclude, + total: 0, + problems: data.problems.map((v) => ({ + old: 0, + frozen: 0, + pass: false, + id: v.id, + })), + })); + const allSubmissions = data.submissions.sort((a, b) => a.time - b.time); + const allAc = allSubmissions.filter((i) => i.verdict === 'AC'); + for (const submission of allSubmissions) { + const team = teams.find((v) => v.id === submission.team); + if (!team) continue; + const isFrozen = submission.time > data.frozen; + const problem = team.problems.find((i) => i.id === submission.problem); + if (!problem || problem.pass) continue; + team.total++; + if (isFrozen) problem.frozen += 1; + else { + if (submission.verdict === 'AC') { + problem.pass = true; + team.score += 1; + team.penalty += submission.time + problem.old * 20 * 60; } + problem.old += 1; } + } + + function MainList(props: Props) { + const [selectedTeam, setTeam] = React.useState(''); + const [selectedProblem, setP] = React.useState(null); + const [ready, setReady] = React.useState(true); + function processRank() { const clone = [...teams]; clone.sort((a, b) => b.score - a.score || a.penalty - b.penalty || b.total - a.total); @@ -123,162 +145,154 @@ addPage(new NamedPage(['resolver'], () => { } return clone.map((i) => teams.indexOf(i)); } - const initial = processRank(); - function status(problem) { - if (!problem) return 'untouched'; - if (problem.pass) return 'ac'; - if (!problem.old && !problem.frozen) return 'untouched'; - if (problem.frozen) return 'frozen'; - return 'failed'; - } + const order = React.useRef(processRank()); - function submissions(problem) { - const st = status(problem); - if (st === 'ac') { return `${problem.old}`; } - if (st === 'frozen') { return `${problem.old}+${problem.frozen}`; } - if (st === 'failed') { return problem.old; } - return String.fromCharCode('A'.charCodeAt(0) + problem.problem_index); - } - - function MainList(props) { - const [selectedTeam, setTeam] = React.useState(''); - const [selectedProblem, setP] = React.useState(null); - const [ready, setReady] = React.useState(true); - const order = React.useRef(initial); + const [springs, api] = useSprings(teams.length, (index) => ({ + y: order.current.indexOf(index) * 103 - index * 103, + scale: 1, + zIndex: 0, + shadow: 1, + immediate: (key: string) => key === 'y' || key === 'zIndex', + })); - const [springs, api] = useSprings(teams.length, (index) => ({ - y: order.current.indexOf(index) * 103 - index * 103, - scale: 1, - zIndex: 0, - shadow: 1, - immediate: (key: string) => key === 'y' || key === 'zIndex', - })); + React.useEffect(() => { + window.scrollTo(0, document.body.scrollHeight); + }, []); - React.useEffect(() => { - window.scrollTo(0, document.body.scrollHeight); - }, []); + useKey('ArrowRight', async () => { + console.log('click', ready); + if (!ready) return; + for (let i = teams.length - 1; i > 0; i--) { + const team = teams[order.current[i]]; + for (const pinfo of data.problems) { + const problem = team.problems.find((i) => i.id === pinfo.id); + if (!problem || !problem.frozen || problem.pass) continue; + setReady(false); + setTeam(team.id); + setP(pinfo.id); + // scroll to selected line + console.log(i, team.id, order.current.indexOf(i)); - useKey('ArrowRight', () => { - console.log('click'); - if (!ready) return; - for (let i = teams.length - 1; i > 0; i--) { - const team = teams[order.current[i]]; - for (let j = 0; j < problem_count; j++) { - const problem = team.problems[j]; - if (problem.frozen && !problem.pass) { - setReady(false); - setTeam(team); - setP(j); - // scroll to selected line - console.log(i, team.id, order.current.indexOf(i)); - scrollTo(i * 103 - window.innerHeight + 261).then(() => { - setTimeout(() => { - if (allAc.find((s) => s.user_id === team.id && +s.problem_index === j + 1)) { - const sub = allSubmissions.filter((s) => s.user_id === team.id && +s.problem_index === j + 1); - let penalty = 0; - for (const s of sub) { - if (s.verdict !== 'AC') { - penalty += 20 * 60; - problem.old++; - } else { - penalty += s.submitted_seconds; - break; - } - } - team.penalty += penalty; - team.score += 1; - problem.pass = true; - problem.frozen = 0; - setP(null); - setTimeout(() => { - order.current = processRank(); - api.start((index) => ({ - y: order.current.indexOf(index) * 103 - index * 103, - scale: 1, - zIndex: 0, - shadow: 1, - config: { - easing: easings.steps(5), - }, - })); - setReady(true); - }, 1000); - } else { - problem.old += problem.frozen; - problem.frozen = 0; - setReady(true); - setP(null); - } - }, 1000); - }); - return; + await scrollTo(i * 103 - window.innerHeight + 261); + await sleep(1000); + if (allAc.find((s) => s.team === team.id && s.problem === pinfo.id)) { + const sub = allSubmissions.filter((s) => s.team === team.id && s.problem === pinfo.id); + let penalty = 0; + for (const s of sub) { + if (s.verdict !== 'AC') { + penalty += 20 * 60; + problem.old++; + } else { + penalty += s.time; + break; + } } + team.penalty += penalty; + team.score += 1; + problem.pass = true; + problem.frozen = 0; + setP(null); + await sleep(1000); + order.current = processRank(); + api.start((index) => ({ + y: order.current.indexOf(index) * 103 - index * 103, + scale: 1, + zIndex: 0, + shadow: 1, + config: { + easing: easings.steps(5), + }, + })); + } else { + problem.old += problem.frozen; + problem.frozen = 0; + setP(null); } + setReady(true); + return; } - }, {}, [ready]); + } + }, {}, [ready]); - return (<> - {springs.map(({ - zIndex, y, - }, i) => { - const team = teams[i]; - return `rgba(0, 0, 0, 0.15) 0px ${s}px ${2 * s}px 0px`), - y, - ...(selectedTeam === team.id ? { - backgroundColor: '#406b82', - } : {}), - }} - children={<> -
{team.rank === -1 ? '*' : team.rank}
-
-
{users[team.id].college}--{users[team.id].name}
-
    - {Array.from({ length: problem_count }, (v, i) => i).map((v, n) => { - const uncover = team?.id === selectedTeam && selectedProblem === n; - return
  • -
    {submissions(team.problems[n])}
    -
  • ; - })} -
+ return (<> + {springs.map(({ + zIndex, y, + }, i) => { + const team = teams[i]; + const teamInfo = data.teams.find((i) => i.id === team.id); + if (!teamInfo) return Team info for id {team.id} not found; + return `rgba(0, 0, 0, 0.15) 0px ${s}px ${2 * s}px 0px`), + y, + ...(selectedTeam === team.id ? { + backgroundColor: '#406b82', + } : {}), + }} + children={<> +
{team.rank === -1 ? '*' : team.rank}
+ {props.showAvatar && } +
+
+ {props.showSchool ? `${teamInfo.institution}--` : ''}{teamInfo.name}
-
{Math.floor(team.penalty / 60)}
-
{team.score}
- } - />; - })} - ); - } - ReactDOM.createRoot(document.getElementById('rank-list')!).render(); +
    + {data.problems.map((v) => { + const uncover = team?.id === selectedTeam && selectedProblem === v.id; + const problemStatus = team.problems.find((i) => i.id === v.id); + return
  • +
    {submissions(problemStatus)}
    +
  • ; + })} +
+
+
{Math.floor(team.penalty / 60)}
+
{team.score}
+ } + />; + })} + ); } + ReactDOM.createRoot(document.getElementById('rank-list')!).render(); +} - const current = new URL(window.location.href); - - async function loadAndStart(input: string) { - let data; - try { - data = JSON.parse(input); - } catch (e) { - console.log(`load data from url. [url=${input}]`); - const res = await request.get(input, {}, { - dataType: 'text', - }); - if (res.startsWith('@')) data = convertPayload(res, +(current.searchParams.get('lock') || 0)); - else data = JSON.parse(res); - } - processData(data); +async function loadAndStart(input: string, lock = 0, options: DisplaySettings) { + let data; + try { + if (input.startsWith('@')) data = convertPayload(input, lock); + else data = JSON.parse(input); + } catch (e) { + console.log(`load data from url. [url=${input}]`); + const res = await request.get(input, {}, { + dataType: 'text', + }); + if (res.startsWith('@')) data = convertPayload(res, lock); + else data = JSON.parse(res); } + start(data, options); +} +addPage(new NamedPage(['resolver'], () => { + const current = new URL(window.location.href); const input = current.searchParams.get('input'); - if (input) loadAndStart(input); + if (input) { + loadAndStart(input, +(current.searchParams.get('lock') || 0), { + showAvatar: true, + showSchool: true, + }); + } $('#load').on('click', () => { const src = $('#input-data').val()?.toString()?.trim(); - if (src) loadAndStart(src); + if (src) { + loadAndStart(src, +($('[name="lock"]').val() || 0), { + showAvatar: $('#show-avatar').prop('checked') || false, + showSchool: $('#show-school').prop('checked') || false, + }); + } }); })); diff --git a/packages/onsite-toolkit/index.ts b/packages/onsite-toolkit/index.ts index 6b95c5726..2049d7f23 100644 --- a/packages/onsite-toolkit/index.ts +++ b/packages/onsite-toolkit/index.ts @@ -1,6 +1,9 @@ import { - Context, db, ForbiddenError, Handler, UserModel, + avatar, ContestModel, ContestNotFoundError, + Context, db, ForbiddenError, Handler, + ObjectId, param, PERM, STATUS, Time, Types, UserModel, } from 'hydrooj'; +import { ResolverInput } from './interface'; interface IpLoginInfo { _id: string; @@ -20,8 +23,46 @@ function normalizeIp(ip: string) { } export class ContestResolverHandler extends Handler { - async get() { - this.response.template = 'resolver.html'; + @param('tid', Types.ObjectId, true) + async get({ domainId }, tid: ObjectId) { + if (!tid) { + this.response.template = 'resolver.html'; + return; + } + const tdoc = await ContestModel.get(domainId, tid); + if (!tdoc) throw new ContestNotFoundError('Contest not found'); + if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); + const teams = await ContestModel.getMultiStatus(domainId, { docId: tid }).toArray(); + const udict = await UserModel.getList(domainId, teams.map((i) => i.uid)); + const teamIds: Record = {}; + for (let i = 1; i <= teams.length; i++) teamIds[teams[i - 1].uid] = i; + const time = (t: ObjectId) => Math.floor((t.getTimestamp().getTime() - tdoc.beginAt.getTime()) / Time.second); + const pid = (i: number) => String.fromCharCode(65 + i); + const unknownSchool = this.translate('Unknown School'); + const submissions = teams.flatMap((i) => { + if (!i.journal) return []; + return i.journal.filter((s) => tdoc.pids.includes(s.pid)).map((s) => ({ ...s, uid: i.uid })); + }); + console.log(submissions); + + this.response.body = { + name: tdoc.title, + duration: Math.floor((new Date(tdoc.endAt).getTime() - new Date(tdoc.beginAt).getTime()) / 1000), + frozen: Math.floor((new Date(tdoc.endAt).getTime() - new Date(tdoc.lockAt).getTime()) / 1000), + problems: tdoc.pids.map((i, n) => ({ name: pid(n), id: i.toString() })), + teams: teams.map((t) => ({ + id: t.uid.toString(), + name: udict[t.uid].uname, + avatar: avatar(udict[t.uid].avatar), + institution: udict[t.uid].school || unknownSchool, + })), + submissions: submissions.map((i) => ({ + team: i.uid.toString(), + problem: i.pid.toString(), + verdict: i.status === STATUS.STATUS_ACCEPTED ? 'AC' : 'RJ', + time: time(i.rid), + })), + } as ResolverInput; } } diff --git a/packages/onsite-toolkit/interface.ts b/packages/onsite-toolkit/interface.ts new file mode 100644 index 000000000..d7f02dc85 --- /dev/null +++ b/packages/onsite-toolkit/interface.ts @@ -0,0 +1,33 @@ +type Verdict = 'RJ' | 'AC' | 'NA'; +export interface ProblemInfo { + color?: string; // hex color + id: string; + name: string; // A, B, C, ... +} +export interface TeamInfo { + id: string; + name: string; + avatar?: string; + institution?: string; + exclude?: boolean; // false by default +} +export interface InstitiutionInfo { + id: string; + name: string; + avatar?: string; +} +export interface SubmissionInfo { + team: string; + problem: string; + verdict: Verdict; + time: number; // in seconds +} +export interface ResolverInput { + name: string; + duration: number; // in seconds + frozen: number; // in seconds + problems: ProblemInfo[]; + submissions: SubmissionInfo[]; + teams: TeamInfo[]; + institutions: Record; +} From d54733d896c909d72d7123d67bbffd8e630fc42f Mon Sep 17 00:00:00 2001 From: undefined Date: Tue, 9 Jul 2024 18:25:30 +0800 Subject: [PATCH 04/14] resolver: pre-calc all operations --- .../onsite-toolkit/frontend/resolver.page.tsx | 181 +++++++++++------- 1 file changed, 114 insertions(+), 67 deletions(-) diff --git a/packages/onsite-toolkit/frontend/resolver.page.tsx b/packages/onsite-toolkit/frontend/resolver.page.tsx index 6560881db..e9db9d2f7 100644 --- a/packages/onsite-toolkit/frontend/resolver.page.tsx +++ b/packages/onsite-toolkit/frontend/resolver.page.tsx @@ -6,42 +6,38 @@ import { } from '@hydrooj/ui-default'; import { ResolverInput } from '../interface'; -function convertPayload(ghost: string, lock: number) { +function convertPayload(ghost: string, lock: number): ResolverInput { const lines = ghost.split('\n'); - const data = { - contest_name: lines[0].split('"')[1].split('"')[0], - problem_count: +(lines[2].split(' ')[1]), - frozen_seconds: +lock || 1800, - teams: +(lines[3].split(' ')[1]), - submissions: +(lines[4].split(' ')[1]), - users: {}, - solutions: {}, + + const problemCount = +(lines[2].split(' ')[1]); + const teamCount = +(lines[3].split(' ')[1]); + const submissionCount = +(lines[4].split(' ')[1]); + const data: ResolverInput = { + name: lines[0].split('"')[1].split('"')[0], + frozen: +lock || 1800, + teams: [], + submissions: [], }; - for (let i = 5 + data.problem_count; i < 5 + data.problem_count + data.teams; i++) { + for (let i = 5 + problemCount; i < 5 + problemCount + teamCount; i++) { const team = lines[i].match(/@t (\d+),\d+,\d+,(.*)/); if (!team) continue; - data.users[team[1]] = { + data.teams.push({ + id: team[2].split('-')[1], name: team[2].split('-')[1], - college: team[2].split('-')[0], - is_exclude: false, - }; + institution: team[2].split('-')[0], + exclude: false, + }); } - for (let i = 5 + data.problem_count + data.teams; i < 5 + data.problem_count + data.teams + data.submissions; i++) { + for (let i = 5 + problemCount + teamCount; i < 5 + problemCount + teamCount + submissionCount; i++) { // @s 3,C,1,10066,AC const line = lines[i].split(' ')[1].split(','); - data.solutions[i] = { - user_id: line[0], - problem_index: +(line[1].charCodeAt(0) - 'A'.charCodeAt(0)) + 1, - verdict: line[4], - submitted_seconds: +(line[3]), - }; + data.submissions.push({ + team: line[0], + problem: line[1], + verdict: line[4] as 'AC' | 'RJ', + time: +(line[3]), + }); } - const s = Object.keys(data.solutions).map((key) => data.solutions[key]); - s.sort((a, b) => a.submitted_seconds - b.submitted_seconds); - data.solutions = {}; - s.forEach((solution, index) => { - data.solutions[index] = solution; - }); return data; } @@ -129,10 +125,11 @@ function start(data: ResolverInput, options: DisplaySettings) { function MainList(props: Props) { const [selectedTeam, setTeam] = React.useState(''); const [selectedProblem, setP] = React.useState(null); - const [ready, setReady] = React.useState(true); + const [executeIdx, setExecuteIdx] = React.useState(0); + const [, setRenderC] = React.useState(0); - function processRank() { - const clone = [...teams]; + function processRank(source = teams) { + const clone = [...source]; clone.sort((a, b) => b.score - a.score || a.penalty - b.penalty || b.total - a.total); let rank = 1; for (const team of clone) { @@ -143,7 +140,7 @@ function start(data: ResolverInput, options: DisplaySettings) { team.rank = -1; } } - return clone.map((i) => teams.indexOf(i)); + return clone.map((i) => source.indexOf(i)); } const order = React.useRef(processRank()); @@ -156,28 +153,74 @@ function start(data: ResolverInput, options: DisplaySettings) { immediate: (key: string) => key === 'y' || key === 'zIndex', })); - React.useEffect(() => { - window.scrollTo(0, document.body.scrollHeight); - }, []); + const operations = { + async highlightTeam(teamId: string, scrollIdx: number) { + setP(null); + setTeam(teamId); + await scrollTo(scrollIdx * 103 - window.innerHeight + 261); + }, + async highlightProblem(problemId: string) { + setP(problemId); + }, + async revealProblem(teamId: string, problemId: string) { + const team = teams.find((i) => i.id === teamId); + const problem = team?.problems.find((i) => i.id === problemId); + if (!team || !problem) return; + if (allAc.find((s) => s.team === teamId && s.problem === problemId)) { + const sub = allSubmissions.filter((s) => s.team === teamId && s.problem === problemId); + let penalty = 0; + for (const s of sub) { + if (s.verdict !== 'AC') { + penalty += 20 * 60; + problem.old++; + } else { + penalty += s.time; + break; + } + } + team.penalty += penalty; + team.score += 1; + problem.pass = true; + problem.frozen = 0; + } else { + problem.old += problem.frozen; + problem.frozen = 0; + } + setP(null); + }, + async updateRank() { + order.current = processRank(); + api.start((index) => ({ + y: order.current.indexOf(index) * 103 - index * 103, + scale: 1, + zIndex: 0, + shadow: 1, + config: { + easing: easings.steps(5), + }, + })); + }, + }; - useKey('ArrowRight', async () => { - console.log('click', ready); - if (!ready) return; - for (let i = teams.length - 1; i > 0; i--) { - const team = teams[order.current[i]]; + const calculated = React.useMemo(() => { + window.scrollTo(0, document.body.scrollHeight); + const clone = JSON.parse(JSON.stringify(teams)); + const ops: { name: string, args: any[] }[] = []; + function queueOperations(name: string, ...args: any[]) { + ops.push({ name, args }); + } + let order = processRank(clone); + for (let i = clone.length - 1; i > 0; i--) { + const team = clone[order[i]]; + queueOperations('highlightTeam', team.id, i); for (const pinfo of data.problems) { const problem = team.problems.find((i) => i.id === pinfo.id); if (!problem || !problem.frozen || problem.pass) continue; - setReady(false); - setTeam(team.id); - setP(pinfo.id); + queueOperations('highlightProblem', pinfo.id); + queueOperations('revealProblem', team.id, pinfo.id); // scroll to selected line - console.log(i, team.id, order.current.indexOf(i)); - - await scrollTo(i * 103 - window.innerHeight + 261); - await sleep(1000); - if (allAc.find((s) => s.team === team.id && s.problem === pinfo.id)) { - const sub = allSubmissions.filter((s) => s.team === team.id && s.problem === pinfo.id); + if (allAc.find((s) => s.team === team.id && s.problem === problem.id)) { + const sub = allSubmissions.filter((s) => s.team === team.id && s.problem === problem.id); let penalty = 0; for (const s of sub) { if (s.verdict !== 'AC') { @@ -192,28 +235,31 @@ function start(data: ResolverInput, options: DisplaySettings) { team.score += 1; problem.pass = true; problem.frozen = 0; - setP(null); - await sleep(1000); - order.current = processRank(); - api.start((index) => ({ - y: order.current.indexOf(index) * 103 - index * 103, - scale: 1, - zIndex: 0, - shadow: 1, - config: { - easing: easings.steps(5), - }, - })); + queueOperations('updateRank'); + const oldOrder = JSON.stringify(order); + order = processRank(clone); + if (oldOrder !== JSON.stringify(order)) { + i++; + break; + } } else { problem.old += problem.frozen; problem.frozen = 0; - setP(null); } - setReady(true); - return; } } - }, {}, [ready]); + console.log(ops); + return ops; + }, [data]); + + useKey('ArrowRight', async () => { + const op = calculated[executeIdx]; + if (!op) return; + setExecuteIdx(executeIdx + 1); + console.log(op.name, op.args); + await operations[op.name](...op.args); + setRenderC((i) => i + 1); + }, {}, [executeIdx, calculated]); return (<> {springs.map(({ @@ -221,18 +267,19 @@ function start(data: ResolverInput, options: DisplaySettings) { }, i) => { const team = teams[i]; const teamInfo = data.teams.find((i) => i.id === team.id); - if (!teamInfo) return Team info for id {team.id} not found; + if (!teamInfo) return Team info for id {team.id} not found; return `rgba(0, 0, 0, 0.15) 0px ${s}px ${2 * s}px 0px`), y, ...(selectedTeam === team.id ? { backgroundColor: '#406b82', - } : {}), + } : { + background: 'transparent', + }), }} children={<>
{team.rank === -1 ? '*' : team.rank}
From f46f111c03d302e428ab4f93ca86b826843ee652 Mon Sep 17 00:00:00 2001 From: undefined Date: Wed, 10 Jul 2024 10:55:04 +0800 Subject: [PATCH 05/14] a11y: optimize performance test output on web --- packages/a11y/performance-test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/a11y/performance-test.ts b/packages/a11y/performance-test.ts index 263bb71b7..36edd86ad 100644 --- a/packages/a11y/performance-test.ts +++ b/packages/a11y/performance-test.ts @@ -195,9 +195,13 @@ export async function startPerformanceTest(args: { enable5: boolean }, report) { const timeArr = results[key]; if (!timeArr) continue; const avgTime = Math.sum(...timeArr) / 20; - report({ message: `-------Test ${key}-------` }); - report({ message: ` Avg: ${formatL(avgTime)} D: ${formatR(Math.sum(...timeArr.map((i) => (i - avgTime) ** 2)) / 20, 5)} ` }); - report({ message: ` Max: ${formatL(Math.max(...timeArr))} Min: ${formatR(Math.min(...timeArr))} ` }); + await report({ + message: [ + `-------Test ${key}-------`, + ` Avg: ${formatL(avgTime)} D: ${formatR(Math.sum(...timeArr.map((i) => (i - avgTime) ** 2)) / 20, 5)} `, + ` Max: ${formatL(Math.max(...timeArr))} Min: ${formatR(Math.min(...timeArr))} `, + ].join('\n'), + }); } return true; } From 2c567b31868dbd61b0297347e69bec7e7e9eb116 Mon Sep 17 00:00:00 2001 From: undefined Date: Tue, 12 Nov 2024 04:26:44 +0800 Subject: [PATCH 06/14] resolver: update to ScoreboardView API --- .../onsite-toolkit/frontend/resolver.page.tsx | 9 +- packages/onsite-toolkit/index.ts | 90 +++++++++---------- .../onsite-toolkit/templates/resolver.html | 1 + 3 files changed, 51 insertions(+), 49 deletions(-) diff --git a/packages/onsite-toolkit/frontend/resolver.page.tsx b/packages/onsite-toolkit/frontend/resolver.page.tsx index e9db9d2f7..2585bf5d7 100644 --- a/packages/onsite-toolkit/frontend/resolver.page.tsx +++ b/packages/onsite-toolkit/frontend/resolver.page.tsx @@ -2,7 +2,7 @@ import { animated, easings, useSprings } from '@react-spring/web'; import useKey from 'react-use/lib/useKey'; import { - addPage, NamedPage, React, ReactDOM, request, sleep, + addPage, NamedPage, React, ReactDOM, request, } from '@hydrooj/ui-default'; import { ResolverInput } from '../interface'; @@ -325,6 +325,13 @@ async function loadAndStart(input: string, lock = 0, options: DisplaySettings) { } addPage(new NamedPage(['resolver'], () => { + if (UiContext.payload) { + start(UiContext.payload, { + showAvatar: true, + showSchool: true, + }); + return; + } const current = new URL(window.location.href); const input = current.searchParams.get('input'); if (input) { diff --git a/packages/onsite-toolkit/index.ts b/packages/onsite-toolkit/index.ts index 2049d7f23..7f34a541b 100644 --- a/packages/onsite-toolkit/index.ts +++ b/packages/onsite-toolkit/index.ts @@ -1,7 +1,6 @@ import { - avatar, ContestModel, ContestNotFoundError, - Context, db, ForbiddenError, Handler, - ObjectId, param, PERM, STATUS, Time, Types, UserModel, + avatar, ContestModel, Context, db, ForbiddenError, + ObjectId, PERM, STATUS, Time, UserModel, } from 'hydrooj'; import { ResolverInput } from './interface'; @@ -22,50 +21,6 @@ function normalizeIp(ip: string) { return ip; } -export class ContestResolverHandler extends Handler { - @param('tid', Types.ObjectId, true) - async get({ domainId }, tid: ObjectId) { - if (!tid) { - this.response.template = 'resolver.html'; - return; - } - const tdoc = await ContestModel.get(domainId, tid); - if (!tdoc) throw new ContestNotFoundError('Contest not found'); - if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); - const teams = await ContestModel.getMultiStatus(domainId, { docId: tid }).toArray(); - const udict = await UserModel.getList(domainId, teams.map((i) => i.uid)); - const teamIds: Record = {}; - for (let i = 1; i <= teams.length; i++) teamIds[teams[i - 1].uid] = i; - const time = (t: ObjectId) => Math.floor((t.getTimestamp().getTime() - tdoc.beginAt.getTime()) / Time.second); - const pid = (i: number) => String.fromCharCode(65 + i); - const unknownSchool = this.translate('Unknown School'); - const submissions = teams.flatMap((i) => { - if (!i.journal) return []; - return i.journal.filter((s) => tdoc.pids.includes(s.pid)).map((s) => ({ ...s, uid: i.uid })); - }); - console.log(submissions); - - this.response.body = { - name: tdoc.title, - duration: Math.floor((new Date(tdoc.endAt).getTime() - new Date(tdoc.beginAt).getTime()) / 1000), - frozen: Math.floor((new Date(tdoc.endAt).getTime() - new Date(tdoc.lockAt).getTime()) / 1000), - problems: tdoc.pids.map((i, n) => ({ name: pid(n), id: i.toString() })), - teams: teams.map((t) => ({ - id: t.uid.toString(), - name: udict[t.uid].uname, - avatar: avatar(udict[t.uid].avatar), - institution: udict[t.uid].school || unknownSchool, - })), - submissions: submissions.map((i) => ({ - team: i.uid.toString(), - problem: i.pid.toString(), - verdict: i.status === STATUS.STATUS_ACCEPTED ? 'AC' : 'RJ', - time: time(i.rid), - })), - } as ResolverInput; - } -} - export function apply(ctx: Context) { ctx.on('handler/init', async (that) => { const iplogin = await coll.findOne({ _id: normalizeIp(that.request.ip) }); @@ -81,5 +36,44 @@ export function apply(ctx: Context) { } }); - ctx.Route('resolver', '/resolver', ContestResolverHandler); + ctx.inject(['scoreboard'], ({ scoreboard }) => { + scoreboard.addView('resolver', 'Resolver', { tdoc: 'tdoc' }, { + async display({ tdoc }) { + if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); + const teams = await ContestModel.getMultiStatus(tdoc.domainId, { docId: tdoc.docId }).toArray(); + const udict = await UserModel.getList(tdoc.domainId, teams.map((i) => i.uid)); + const teamIds: Record = {}; + for (let i = 1; i <= teams.length; i++) teamIds[teams[i - 1].uid] = i; + const time = (t: ObjectId) => Math.floor((t.getTimestamp().getTime() - tdoc.beginAt.getTime()) / Time.second); + const pid = (i: number) => String.fromCharCode(65 + i); + const unknownSchool = this.translate('Unknown School'); + const submissions = teams.flatMap((i) => { + if (!i.journal) return []; + return i.journal.filter((s) => tdoc.pids.includes(s.pid)).map((s) => ({ ...s, uid: i.uid })); + }); + this.response.body = { + payload: { + name: tdoc.title, + duration: Math.floor((new Date(tdoc.endAt).getTime() - new Date(tdoc.beginAt).getTime()) / 1000), + frozen: Math.floor((new Date(tdoc.endAt).getTime() - new Date(tdoc.lockAt).getTime()) / 1000), + problems: tdoc.pids.map((i, n) => ({ name: pid(n), id: i.toString() })), + teams: teams.map((t) => ({ + id: t.uid.toString(), + name: udict[t.uid].uname, + avatar: avatar(udict[t.uid].avatar), + institution: udict[t.uid].school || unknownSchool, + })), + submissions: submissions.map((i) => ({ + team: i.uid.toString(), + problem: i.pid.toString(), + verdict: i.status === STATUS.STATUS_ACCEPTED ? 'AC' : 'RJ', + time: time(i.rid), + })), + } as ResolverInput, + }; + this.response.template = 'resolver.html'; + }, + supportedRules: ['acm'], + }); + }); } diff --git a/packages/onsite-toolkit/templates/resolver.html b/packages/onsite-toolkit/templates/resolver.html index eaf9ddb57..189b0a6ae 100644 --- a/packages/onsite-toolkit/templates/resolver.html +++ b/packages/onsite-toolkit/templates/resolver.html @@ -1,5 +1,6 @@ {% extends "layout/html5.html" %} {% block body %} +{{ set(UiContext, 'payload', payload) }}
From 6d16d8bc94a086ec377db35eb6edfd6754bc21a9 Mon Sep 17 00:00:00 2001 From: panda Date: Tue, 12 Nov 2024 10:17:48 +0000 Subject: [PATCH 07/14] use new style feature --- .../onsite-toolkit/frontend/resolver.page.ts | 81 +++++++++++ .../{resolver.page.tsx => resolver.tsx} | 134 ++++-------------- packages/onsite-toolkit/index.ts | 2 +- packages/onsite-toolkit/public/resolver.css | 110 +++++++------- .../onsite-toolkit/templates/resolver.html | 41 ++++-- 5 files changed, 187 insertions(+), 181 deletions(-) create mode 100644 packages/onsite-toolkit/frontend/resolver.page.ts rename packages/onsite-toolkit/frontend/{resolver.page.tsx => resolver.tsx} (65%) diff --git a/packages/onsite-toolkit/frontend/resolver.page.ts b/packages/onsite-toolkit/frontend/resolver.page.ts new file mode 100644 index 000000000..7d50134a3 --- /dev/null +++ b/packages/onsite-toolkit/frontend/resolver.page.ts @@ -0,0 +1,81 @@ +import { addPage, NamedPage, request } from '@hydrooj/ui-default'; +import { ResolverInput } from '../interface'; +import { DisplaySettings, start } from './resolver'; + +function convertPayload(ghost: string, lock: number): ResolverInput { + const lines = ghost.split('\n'); + + const problemCount = +(lines[2].split(' ')[1]); + const teamCount = +(lines[3].split(' ')[1]); + const submissionCount = +(lines[4].split(' ')[1]); + const data: ResolverInput = { + name: lines[0].split('"')[1].split('"')[0], + frozen: +lock || 1800, + teams: [], + submissions: [], + }; + for (let i = 5 + problemCount; i < 5 + problemCount + teamCount; i++) { + const team = lines[i].match(/@t (\d+),\d+,\d+,(.*)/); + if (!team) continue; + data.teams.push({ + id: team[2].split('-')[1], + name: team[2].split('-')[1], + institution: team[2].split('-')[0], + exclude: false, + }); + } + for (let i = 5 + problemCount + teamCount; i < 5 + problemCount + teamCount + submissionCount; i++) { + // @s 3,C,1,10066,AC + const line = lines[i].split(' ')[1].split(','); + data.submissions.push({ + team: line[0], + problem: line[1], + verdict: line[4] as 'AC' | 'RJ', + time: +(line[3]), + }); + } + return data; +} + +async function loadAndStart(input: string, lock = 0, options: DisplaySettings) { + let data; + try { + if (input.startsWith('@')) data = convertPayload(input, lock); + else data = JSON.parse(input); + } catch (e) { + console.log(`load data from url. [url=${input}]`); + const res = await request.get(input, {}, { + dataType: 'text', + }); + if (res.startsWith('@')) data = convertPayload(res, lock); + else data = JSON.parse(res); + } + start(data, options); +} + +addPage(new NamedPage(['resolver'], () => { + if (UiContext.payload) { + start(UiContext.payload, { + showAvatar: true, + showSchool: true, + }); + return; + } + const current = new URL(window.location.href); + const input = current.searchParams.get('input'); + if (input) { + loadAndStart(input, +(current.searchParams.get('lock') || 0), { + showAvatar: true, + showSchool: true, + }); + } + $('#load').on('click', () => { + const src = $('#input-data').val()?.toString()?.trim(); + if (src) { + loadAndStart(src, +($('[name="lock"]').val() || 0), { + showAvatar: $('#show-avatar').prop('checked') || false, + showSchool: $('#show-school').prop('checked') || false, + }); + } + }); +})); diff --git a/packages/onsite-toolkit/frontend/resolver.page.tsx b/packages/onsite-toolkit/frontend/resolver.tsx similarity index 65% rename from packages/onsite-toolkit/frontend/resolver.page.tsx rename to packages/onsite-toolkit/frontend/resolver.tsx index 2585bf5d7..15170909c 100644 --- a/packages/onsite-toolkit/frontend/resolver.page.tsx +++ b/packages/onsite-toolkit/frontend/resolver.tsx @@ -1,46 +1,9 @@ /* eslint-disable no-await-in-loop */ import { animated, easings, useSprings } from '@react-spring/web'; import useKey from 'react-use/lib/useKey'; -import { - addPage, NamedPage, React, ReactDOM, request, -} from '@hydrooj/ui-default'; +import { React, ReactDOM } from '@hydrooj/ui-default'; import { ResolverInput } from '../interface'; -function convertPayload(ghost: string, lock: number): ResolverInput { - const lines = ghost.split('\n'); - - const problemCount = +(lines[2].split(' ')[1]); - const teamCount = +(lines[3].split(' ')[1]); - const submissionCount = +(lines[4].split(' ')[1]); - const data: ResolverInput = { - name: lines[0].split('"')[1].split('"')[0], - frozen: +lock || 1800, - teams: [], - submissions: [], - }; - for (let i = 5 + problemCount; i < 5 + problemCount + teamCount; i++) { - const team = lines[i].match(/@t (\d+),\d+,\d+,(.*)/); - if (!team) continue; - data.teams.push({ - id: team[2].split('-')[1], - name: team[2].split('-')[1], - institution: team[2].split('-')[0], - exclude: false, - }); - } - for (let i = 5 + problemCount + teamCount; i < 5 + problemCount + teamCount + submissionCount; i++) { - // @s 3,C,1,10066,AC - const line = lines[i].split(' ')[1].split(','); - data.submissions.push({ - team: line[0], - problem: line[1], - verdict: line[4] as 'AC' | 'RJ', - time: +(line[3]), - }); - } - return data; -} - async function scrollTo(offset) { const fixedOffset = offset.toFixed(); await new Promise((resolve) => { @@ -59,7 +22,7 @@ async function scrollTo(offset) { }); }); } -interface DisplaySettings { +export interface DisplaySettings { showAvatar: boolean; showSchool: boolean; } @@ -77,17 +40,16 @@ function status(problem) { } function submissions(problem) { + console.log(problem); const st = status(problem); if (st === 'ac') { return `${problem.old}`; } if (st === 'frozen') { return `${problem.old}+${problem.frozen}`; } if (st === 'failed') { return problem.old; } - return String.fromCharCode('A'.charCodeAt(0) + problem.problem_index); + return String.fromCharCode('A'.charCodeAt(0) + problem.index); } -function start(data: ResolverInput, options: DisplaySettings) { - $('title').text(data.name); - $('#title').text(data.name); - $('.footer').css('display', 'none'); +export function start(data: ResolverInput, options: DisplaySettings) { + $('title').text(`${data.name} - Ranklist`); const teams = data.teams.map((v) => ({ id: v.id, rank: 0, @@ -95,11 +57,12 @@ function start(data: ResolverInput, options: DisplaySettings) { penalty: 0, ranked: !v.exclude, total: 0, - problems: data.problems.map((v) => ({ + problems: data.problems.map((problem, idx) => ({ old: 0, frozen: 0, pass: false, - id: v.id, + id: problem.id, + index: idx, })), })); const allSubmissions = data.submissions.sort((a, b) => a.time - b.time); @@ -209,12 +172,12 @@ function start(data: ResolverInput, options: DisplaySettings) { function queueOperations(name: string, ...args: any[]) { ops.push({ name, args }); } - let order = processRank(clone); + let orders = processRank(clone); for (let i = clone.length - 1; i > 0; i--) { - const team = clone[order[i]]; + const team = clone[orders[i]]; queueOperations('highlightTeam', team.id, i); for (const pinfo of data.problems) { - const problem = team.problems.find((i) => i.id === pinfo.id); + const problem = team.problems.find((idx) => idx.id === pinfo.id); if (!problem || !problem.frozen || problem.pass) continue; queueOperations('highlightProblem', pinfo.id); queueOperations('revealProblem', team.id, pinfo.id); @@ -236,9 +199,9 @@ function start(data: ResolverInput, options: DisplaySettings) { problem.pass = true; problem.frozen = 0; queueOperations('updateRank'); - const oldOrder = JSON.stringify(order); - order = processRank(clone); - if (oldOrder !== JSON.stringify(order)) { + const oldOrder = JSON.stringify(orders); + orders = processRank(clone); + if (oldOrder !== JSON.stringify(orders)) { i++; break; } @@ -266,7 +229,7 @@ function start(data: ResolverInput, options: DisplaySettings) { zIndex, y, }, i) => { const team = teams[i]; - const teamInfo = data.teams.find((i) => i.id === team.id); + const teamInfo = data.teams.find((idx) => idx.id === team.id); if (!teamInfo) return Team info for id {team.id} not found; return
{team.rank === -1 ? '*' : team.rank}
- {props.showAvatar && } + {props.showAvatar && }
-
- {props.showSchool ? `${teamInfo.institution}--` : ''}{teamInfo.name} +
+ {props.showSchool ? `${teamInfo.institution} - ` : ''}{teamInfo.name}
-
    +
    {data.problems.map((v) => { const uncover = team?.id === selectedTeam && selectedProblem === v.id; - const problemStatus = team.problems.find((i) => i.id === v.id); - return
  • -
    {submissions(problemStatus)}
    -
  • ; + const problemStatus = team.problems.find((idx) => idx.id === v.id); + return + {submissions(problemStatus)} + ; })} -
+
-
{Math.floor(team.penalty / 60)}
-
{team.score}
+
{Math.floor(team.penalty / 60)}
+
{team.score}
} />; })} @@ -307,46 +270,3 @@ function start(data: ResolverInput, options: DisplaySettings) { } ReactDOM.createRoot(document.getElementById('rank-list')!).render(); } - -async function loadAndStart(input: string, lock = 0, options: DisplaySettings) { - let data; - try { - if (input.startsWith('@')) data = convertPayload(input, lock); - else data = JSON.parse(input); - } catch (e) { - console.log(`load data from url. [url=${input}]`); - const res = await request.get(input, {}, { - dataType: 'text', - }); - if (res.startsWith('@')) data = convertPayload(res, lock); - else data = JSON.parse(res); - } - start(data, options); -} - -addPage(new NamedPage(['resolver'], () => { - if (UiContext.payload) { - start(UiContext.payload, { - showAvatar: true, - showSchool: true, - }); - return; - } - const current = new URL(window.location.href); - const input = current.searchParams.get('input'); - if (input) { - loadAndStart(input, +(current.searchParams.get('lock') || 0), { - showAvatar: true, - showSchool: true, - }); - } - $('#load').on('click', () => { - const src = $('#input-data').val()?.toString()?.trim(); - if (src) { - loadAndStart(src, +($('[name="lock"]').val() || 0), { - showAvatar: $('#show-avatar').prop('checked') || false, - showSchool: $('#show-school').prop('checked') || false, - }); - } - }); -})); diff --git a/packages/onsite-toolkit/index.ts b/packages/onsite-toolkit/index.ts index 7f34a541b..006848cbb 100644 --- a/packages/onsite-toolkit/index.ts +++ b/packages/onsite-toolkit/index.ts @@ -37,7 +37,7 @@ export function apply(ctx: Context) { }); ctx.inject(['scoreboard'], ({ scoreboard }) => { - scoreboard.addView('resolver', 'Resolver', { tdoc: 'tdoc' }, { + scoreboard.addView('resolver-tiny', 'Resolver(Tiny)', { tdoc: 'tdoc' }, { async display({ tdoc }) { if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); const teams = await ContestModel.getMultiStatus(tdoc.domainId, { docId: tdoc.docId }).toArray(); diff --git a/packages/onsite-toolkit/public/resolver.css b/packages/onsite-toolkit/public/resolver.css index e8cdf9c7a..88c4e3e45 100644 --- a/packages/onsite-toolkit/public/resolver.css +++ b/packages/onsite-toolkit/public/resolver.css @@ -1,50 +1,71 @@ -.navbar { - margin-bottom: 0; -} -.app { - color: white; +body { + color: #eee; + background-color: #121212; } .rank-list { background: repeating-linear-gradient( 180deg, #3e3e3e 0px, - #3e3e3e 103px, - black 103px, - black 206px + #3e3e3e 80px, + #121212 80px, + #121212 160px ); } -.item { - border-radius: 3px; -} -.rank, .content, .problems li { - float: left; -} -.solved, .penalty { - float: right; -} -.rank, .solved, .penalty { - font-size: 35px; - text-align: center; - vertical-align: middle; - line-height: 83px; -} .rank-list-item { padding: 10px; - height: 103px; + height: 80px; position: relative; background: transparent; + display: flex; + align-items: center; + vertical-align: middle; + +} +.rank-list-item .rank, .solved, .penalty { + padding: 10px 20px; + min-width: 60px; + font-size: 40px; + text-align: center; + vertical-align: middle; +} +.rank-list-item .avatar { + height: 60px; + width: 60px; + border-radius: 5px; + margin-right: 15px; +} +.rank-list-item .content { + flex-grow: 1; + height: 60px +} +.rank-list-item .content .name { + font-size: 30px; + margin-bottom: 6px; +} +.rank-list-item .content .problems { + display: flex; + align-items: center; + gap: 5px; +} +.rank-list-item .problems .item { + padding: 2px; + width: 95px; + text-align: center; + border-radius: 5px; + font-size: 20px; } .ac { - background-color: #5eb95e; + background-color: #009c00; } .failed, .WA { background-color: #dd514c; } .frozen { - background-color: #607D8B; + background-color: #6083e2; } .untouched { - background-color: #1f1f1f; + background-color: #282828; + color: #8a919b; } .uncover { animation: flashing 300ms infinite; @@ -57,39 +78,4 @@ @-webkit-keyframes flashing {/*Safari and Chrome*/ from { background-color: #8a6d3b } to { background-color: #BD995B } -} -.rank { - width: 58px; - height: 58px; - font-size: 35px; - text-align: center; - vertical-align: middle; - line-height: 83px; - margin-right: 15px; -} -.name { - font-size: 35px; - margin-bottom: 5px; -} -.problems { - list-style-type: none; - margin: 0; - padding: 0; - font-size: 12px; -} -.problems .item { - padding: 2px; - margin-right: 5px; - width: 80px; - text-align: center; -} -.problems .item .p-content { - padding: 1px 0; - font-size: 15px; -} -.solved { - width: 70px; -} -.penalty { - width: 100px; } \ No newline at end of file diff --git a/packages/onsite-toolkit/templates/resolver.html b/packages/onsite-toolkit/templates/resolver.html index 189b0a6ae..544277218 100644 --- a/packages/onsite-toolkit/templates/resolver.html +++ b/packages/onsite-toolkit/templates/resolver.html @@ -2,16 +2,35 @@ {% block body %} {{ set(UiContext, 'payload', payload) }}
-
-
-

- -

-

- -

+ +{% if not payload %} +
+
+
+
+
+

加载数据

+
+
+
+
+ +
+
+
+
+
+ +
+
+
-
- - +
+
+{% endif %} {% endblock %} From 88ff12e5652af1268ef3812ac8ab7f73b4090bc0 Mon Sep 17 00:00:00 2001 From: panda Date: Tue, 12 Nov 2024 14:48:38 +0000 Subject: [PATCH 08/14] fix --- packages/onsite-toolkit/frontend/resolver.tsx | 9 ++++----- packages/onsite-toolkit/public/resolver.css | 6 +++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/onsite-toolkit/frontend/resolver.tsx b/packages/onsite-toolkit/frontend/resolver.tsx index 15170909c..9f3dfcdb2 100644 --- a/packages/onsite-toolkit/frontend/resolver.tsx +++ b/packages/onsite-toolkit/frontend/resolver.tsx @@ -40,7 +40,6 @@ function status(problem) { } function submissions(problem) { - console.log(problem); const st = status(problem); if (st === 'ac') { return `${problem.old}`; } if (st === 'frozen') { return `${problem.old}+${problem.frozen}`; } @@ -109,7 +108,7 @@ export function start(data: ResolverInput, options: DisplaySettings) { const order = React.useRef(processRank()); const [springs, api] = useSprings(teams.length, (index) => ({ - y: order.current.indexOf(index) * 103 - index * 103, + y: order.current.indexOf(index) * 80 - index * 80, scale: 1, zIndex: 0, shadow: 1, @@ -120,7 +119,8 @@ export function start(data: ResolverInput, options: DisplaySettings) { async highlightTeam(teamId: string, scrollIdx: number) { setP(null); setTeam(teamId); - await scrollTo(scrollIdx * 103 - window.innerHeight + 261); + console.log('highlightTeam', scrollIdx * 80 - window.innerHeight + 161); + await scrollTo(scrollIdx * 80 - window.innerHeight + 161); }, async highlightProblem(problemId: string) { setP(problemId); @@ -154,7 +154,7 @@ export function start(data: ResolverInput, options: DisplaySettings) { async updateRank() { order.current = processRank(); api.start((index) => ({ - y: order.current.indexOf(index) * 103 - index * 103, + y: order.current.indexOf(index) * 80 - index * 80, scale: 1, zIndex: 0, shadow: 1, @@ -211,7 +211,6 @@ export function start(data: ResolverInput, options: DisplaySettings) { } } } - console.log(ops); return ops; }, [data]); diff --git a/packages/onsite-toolkit/public/resolver.css b/packages/onsite-toolkit/public/resolver.css index 88c4e3e45..4ff0a18c8 100644 --- a/packages/onsite-toolkit/public/resolver.css +++ b/packages/onsite-toolkit/public/resolver.css @@ -12,7 +12,7 @@ body { ); } .rank-list-item { - padding: 10px; + padding: 10px 0; height: 80px; position: relative; background: transparent; @@ -22,8 +22,8 @@ body { } .rank-list-item .rank, .solved, .penalty { - padding: 10px 20px; - min-width: 60px; + padding: 10px 0px; + width: 90px; font-size: 40px; text-align: center; vertical-align: middle; From 079a929f53b3613da2204b9d6365bc1f3285c5b0 Mon Sep 17 00:00:00 2001 From: panda Date: Tue, 12 Nov 2024 15:17:56 +0000 Subject: [PATCH 09/14] fix style --- packages/onsite-toolkit/frontend/resolver.tsx | 4 +--- packages/onsite-toolkit/public/resolver.css | 16 +++++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/onsite-toolkit/frontend/resolver.tsx b/packages/onsite-toolkit/frontend/resolver.tsx index 9f3dfcdb2..6063abdac 100644 --- a/packages/onsite-toolkit/frontend/resolver.tsx +++ b/packages/onsite-toolkit/frontend/resolver.tsx @@ -119,7 +119,6 @@ export function start(data: ResolverInput, options: DisplaySettings) { async highlightTeam(teamId: string, scrollIdx: number) { setP(null); setTeam(teamId); - console.log('highlightTeam', scrollIdx * 80 - window.innerHeight + 161); await scrollTo(scrollIdx * 80 - window.innerHeight + 161); }, async highlightProblem(problemId: string) { @@ -214,11 +213,10 @@ export function start(data: ResolverInput, options: DisplaySettings) { return ops; }, [data]); - useKey('ArrowRight', async () => { + useKey('n', async () => { const op = calculated[executeIdx]; if (!op) return; setExecuteIdx(executeIdx + 1); - console.log(op.name, op.args); await operations[op.name](...op.args); setRenderC((i) => i + 1); }, {}, [executeIdx, calculated]); diff --git a/packages/onsite-toolkit/public/resolver.css b/packages/onsite-toolkit/public/resolver.css index 4ff0a18c8..c43d1aa46 100644 --- a/packages/onsite-toolkit/public/resolver.css +++ b/packages/onsite-toolkit/public/resolver.css @@ -61,21 +61,23 @@ body { background-color: #dd514c; } .frozen { - background-color: #6083e2; + background-color: #3c6ef6; } .untouched { background-color: #282828; color: #8a919b; } .uncover { - animation: flashing 300ms infinite; - -webkit-animation: flashing 30ms infinite; /*Safari and Chrome*/ + animation: flashing 1000ms infinite; + -webkit-animation: flashing 1000ms infinite; /*Safari and Chrome*/ } @keyframes flashing { - from { background-color: #8a6d3b } - to { background-color: #BD995B } + from { background-color: #3c6ef6 } + 50% { background-color: #7699ea } + to { background-color: #3c6ef6 } } @-webkit-keyframes flashing {/*Safari and Chrome*/ - from { background-color: #8a6d3b } - to { background-color: #BD995B } + from { background-color: #3c6ef6 } + 50% { background-color: #7699ea } + to { background-color: #3c6ef6 } } \ No newline at end of file From 67d4f735ad1b948d24a0776a44dddd68a11c743f Mon Sep 17 00:00:00 2001 From: panda Date: Tue, 12 Nov 2024 15:35:15 +0000 Subject: [PATCH 10/14] add header --- packages/onsite-toolkit/frontend/resolver.tsx | 5 ++-- packages/onsite-toolkit/public/resolver.css | 28 +++++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/onsite-toolkit/frontend/resolver.tsx b/packages/onsite-toolkit/frontend/resolver.tsx index 6063abdac..cf2834b0d 100644 --- a/packages/onsite-toolkit/frontend/resolver.tsx +++ b/packages/onsite-toolkit/frontend/resolver.tsx @@ -48,7 +48,8 @@ function submissions(problem) { } export function start(data: ResolverInput, options: DisplaySettings) { - $('title').text(`${data.name} - Ranklist`); + $('title').text(`${data.name} - Resolver`); + $('.header .title').text(`${data.name}`); const teams = data.teams.map((v) => ({ id: v.id, rank: 0, @@ -258,8 +259,8 @@ export function start(data: ResolverInput, options: DisplaySettings) { })} -
{Math.floor(team.penalty / 60)}
{team.score}
+
{Math.floor(team.penalty / 60)}
} />; })} diff --git a/packages/onsite-toolkit/public/resolver.css b/packages/onsite-toolkit/public/resolver.css index c43d1aa46..60a933877 100644 --- a/packages/onsite-toolkit/public/resolver.css +++ b/packages/onsite-toolkit/public/resolver.css @@ -19,9 +19,33 @@ body { display: flex; align-items: center; vertical-align: middle; - } -.rank-list-item .rank, .solved, .penalty { +.header { + display: flex; + align-items: center; + padding: 10px 0; + height: 40px; +} +.header .rank, .header .solved, .header .penalty { + width: 90px; + font-size: 20px; + text-align: center; +} +.header .content { + flex-grow: 1; + text-align: left; + display: flex; +} +.header .content .title { + flex-grow: 1; + text-align: left; + display: flex; +} +.header .content .copyright { + color: #8a919b; + font-size: 15px; +} +.rank-list-item .rank, .rank-list-item .solved, .rank-list-item .penalty { padding: 10px 0px; width: 90px; font-size: 40px; From 26d7c5432211793ede646212d263e7f896984042 Mon Sep 17 00:00:00 2001 From: panda Date: Tue, 12 Nov 2024 15:36:57 +0000 Subject: [PATCH 11/14] fix --- packages/onsite-toolkit/public/resolver.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/onsite-toolkit/public/resolver.css b/packages/onsite-toolkit/public/resolver.css index 60a933877..d324d43b8 100644 --- a/packages/onsite-toolkit/public/resolver.css +++ b/packages/onsite-toolkit/public/resolver.css @@ -35,9 +35,12 @@ body { flex-grow: 1; text-align: left; display: flex; + justify-content: center; + align-items: center; } .header .content .title { flex-grow: 1; + font-size: 20px; text-align: left; display: flex; } From e8a55407b9ecb1c578972c616ca2d65cc12b880e Mon Sep 17 00:00:00 2001 From: panda Date: Tue, 12 Nov 2024 15:48:49 +0000 Subject: [PATCH 12/14] add header --- packages/onsite-toolkit/templates/resolver.html | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/onsite-toolkit/templates/resolver.html b/packages/onsite-toolkit/templates/resolver.html index 544277218..c693f5649 100644 --- a/packages/onsite-toolkit/templates/resolver.html +++ b/packages/onsite-toolkit/templates/resolver.html @@ -1,6 +1,15 @@ {% extends "layout/html5.html" %} {% block body %} {{ set(UiContext, 'payload', payload) }} +
+ #Rank + + Contest + @Hydro/TinyResolver + + Solved + Penalty +
{% if not payload %} From 0d7d3110aa7a2b06694f9821a6ff108580cd39ad Mon Sep 17 00:00:00 2001 From: panda Date: Tue, 12 Nov 2024 15:55:20 +0000 Subject: [PATCH 13/14] fix scrollTo --- packages/onsite-toolkit/frontend/resolver.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/onsite-toolkit/frontend/resolver.tsx b/packages/onsite-toolkit/frontend/resolver.tsx index cf2834b0d..e3aab67d4 100644 --- a/packages/onsite-toolkit/frontend/resolver.tsx +++ b/packages/onsite-toolkit/frontend/resolver.tsx @@ -120,7 +120,7 @@ export function start(data: ResolverInput, options: DisplaySettings) { async highlightTeam(teamId: string, scrollIdx: number) { setP(null); setTeam(teamId); - await scrollTo(scrollIdx * 80 - window.innerHeight + 161); + await scrollTo(scrollIdx * 80 - window.innerHeight + 241 + 40); }, async highlightProblem(problemId: string) { setP(problemId); From 0349a51b0f2cd04c93f25201de22866d3e76fd70 Mon Sep 17 00:00:00 2001 From: undefined Date: Wed, 13 Nov 2024 01:52:28 +0800 Subject: [PATCH 14/14] resolver: make ci happy --- .../{resolver.tsx => resolver.page..tsx} | 16 +++- .../onsite-toolkit/frontend/resolver.page.ts | 81 ------------------- .../onsite-toolkit/templates/resolver.html | 30 ------- 3 files changed, 13 insertions(+), 114 deletions(-) rename packages/onsite-toolkit/frontend/{resolver.tsx => resolver.page..tsx} (94%) delete mode 100644 packages/onsite-toolkit/frontend/resolver.page.ts diff --git a/packages/onsite-toolkit/frontend/resolver.tsx b/packages/onsite-toolkit/frontend/resolver.page..tsx similarity index 94% rename from packages/onsite-toolkit/frontend/resolver.tsx rename to packages/onsite-toolkit/frontend/resolver.page..tsx index e3aab67d4..3affec267 100644 --- a/packages/onsite-toolkit/frontend/resolver.tsx +++ b/packages/onsite-toolkit/frontend/resolver.page..tsx @@ -1,7 +1,9 @@ /* eslint-disable no-await-in-loop */ import { animated, easings, useSprings } from '@react-spring/web'; import useKey from 'react-use/lib/useKey'; -import { React, ReactDOM } from '@hydrooj/ui-default'; +import { + addPage, NamedPage, React, ReactDOM, +} from '@hydrooj/ui-default'; import { ResolverInput } from '../interface'; async function scrollTo(offset) { @@ -228,6 +230,7 @@ export function start(data: ResolverInput, options: DisplaySettings) { }, i) => { const team = teams[i]; const teamInfo = data.teams.find((idx) => idx.id === team.id); + if (!teams[i]) return Team {i} not found; if (!teamInfo) return Team info for id {team.id} not found; return
{data.problems.map((v) => { - const uncover = team?.id === selectedTeam && selectedProblem === v.id; + const uncover = team.id === selectedTeam && selectedProblem === v.id; const problemStatus = team.problems.find((idx) => idx.id === v.id); - return + return {submissions(problemStatus)} ; })} @@ -268,3 +271,10 @@ export function start(data: ResolverInput, options: DisplaySettings) { } ReactDOM.createRoot(document.getElementById('rank-list')!).render(); } + +addPage(new NamedPage(['resolver'], () => { + start(UiContext.payload, { + showAvatar: true, + showSchool: true, + }); +})); diff --git a/packages/onsite-toolkit/frontend/resolver.page.ts b/packages/onsite-toolkit/frontend/resolver.page.ts deleted file mode 100644 index 7d50134a3..000000000 --- a/packages/onsite-toolkit/frontend/resolver.page.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { addPage, NamedPage, request } from '@hydrooj/ui-default'; -import { ResolverInput } from '../interface'; -import { DisplaySettings, start } from './resolver'; - -function convertPayload(ghost: string, lock: number): ResolverInput { - const lines = ghost.split('\n'); - - const problemCount = +(lines[2].split(' ')[1]); - const teamCount = +(lines[3].split(' ')[1]); - const submissionCount = +(lines[4].split(' ')[1]); - const data: ResolverInput = { - name: lines[0].split('"')[1].split('"')[0], - frozen: +lock || 1800, - teams: [], - submissions: [], - }; - for (let i = 5 + problemCount; i < 5 + problemCount + teamCount; i++) { - const team = lines[i].match(/@t (\d+),\d+,\d+,(.*)/); - if (!team) continue; - data.teams.push({ - id: team[2].split('-')[1], - name: team[2].split('-')[1], - institution: team[2].split('-')[0], - exclude: false, - }); - } - for (let i = 5 + problemCount + teamCount; i < 5 + problemCount + teamCount + submissionCount; i++) { - // @s 3,C,1,10066,AC - const line = lines[i].split(' ')[1].split(','); - data.submissions.push({ - team: line[0], - problem: line[1], - verdict: line[4] as 'AC' | 'RJ', - time: +(line[3]), - }); - } - return data; -} - -async function loadAndStart(input: string, lock = 0, options: DisplaySettings) { - let data; - try { - if (input.startsWith('@')) data = convertPayload(input, lock); - else data = JSON.parse(input); - } catch (e) { - console.log(`load data from url. [url=${input}]`); - const res = await request.get(input, {}, { - dataType: 'text', - }); - if (res.startsWith('@')) data = convertPayload(res, lock); - else data = JSON.parse(res); - } - start(data, options); -} - -addPage(new NamedPage(['resolver'], () => { - if (UiContext.payload) { - start(UiContext.payload, { - showAvatar: true, - showSchool: true, - }); - return; - } - const current = new URL(window.location.href); - const input = current.searchParams.get('input'); - if (input) { - loadAndStart(input, +(current.searchParams.get('lock') || 0), { - showAvatar: true, - showSchool: true, - }); - } - $('#load').on('click', () => { - const src = $('#input-data').val()?.toString()?.trim(); - if (src) { - loadAndStart(src, +($('[name="lock"]').val() || 0), { - showAvatar: $('#show-avatar').prop('checked') || false, - showSchool: $('#show-school').prop('checked') || false, - }); - } - }); -})); diff --git a/packages/onsite-toolkit/templates/resolver.html b/packages/onsite-toolkit/templates/resolver.html index c693f5649..9a0c437ec 100644 --- a/packages/onsite-toolkit/templates/resolver.html +++ b/packages/onsite-toolkit/templates/resolver.html @@ -12,34 +12,4 @@
-{% if not payload %} -
-
-
-
-
-

加载数据

-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
-
-{% endif %} {% endblock %}