feat: add unified contest pool and realtime game modes
This commit is contained in:
@@ -132,6 +132,10 @@ button { color: inherit; }
|
||||
.track-switch { display: flex; gap: 5px; margin-bottom: 22px; }
|
||||
.track-switch button { border: 1px solid var(--line); padding: 9px 18px; border-radius: 99px; background: transparent; cursor: pointer; }
|
||||
.track-switch button.active { background: var(--ink); color: white; }
|
||||
.match-mode-switch { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 22px; }
|
||||
.match-mode-switch button { border: 1px solid var(--line); border-radius: 14px; padding: 15px 17px; background: rgba(255,255,252,.75); text-align: left; cursor: pointer; }
|
||||
.match-mode-switch b, .match-mode-switch small { display: block; }.match-mode-switch small { margin-top: 5px; color: var(--muted); }
|
||||
.match-mode-switch button.active { border-color: var(--green); background: var(--ink); color: white; }.match-mode-switch button.active small { color: #b9c2bc; }
|
||||
.challenge-panel { display: grid; grid-template-columns: minmax(0, 1fr) minmax(380px, .9fr); gap: 24px; align-items: center; margin-bottom: 22px; padding: 24px 26px; border: 1px solid var(--line); border-radius: 18px; background: linear-gradient(120deg, rgba(25,101,72,.07), rgba(204,232,91,.12)); }.challenge-panel h2 { margin: 7px 0 6px; font: 27px Georgia, serif; }.challenge-panel p { margin: 0; color: var(--muted); font-size: 12px; line-height: 1.7; }.challenge-actions { display: grid; gap: 9px; }.challenge-actions > .primary-button { width: 100%; }.challenge-actions form { display: grid; grid-template-columns: 1fr auto; gap: 8px; }.challenge-actions input { min-width: 0; border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; background: white; text-transform: uppercase; letter-spacing: .18em; font-weight: 700; }.challenge-actions .dark-button { width: auto; margin: 0; white-space: nowrap; }
|
||||
.realtime-status-line { display: flex; justify-content: space-between; gap: 15px; margin: 10px 0 18px; color: var(--muted); font-size: 11px; }.realtime-status-line strong { color: var(--green); }
|
||||
.realtime-waiting { display: grid; justify-items: center; gap: 16px; padding: 35px 20px; border-radius: 18px; background: #eef1eb; text-align: center; }.realtime-waiting p { max-width: 480px; margin: 0; color: var(--muted); line-height: 1.7; }.realtime-pulse { width: 18px; height: 18px; border-radius: 50%; background: var(--green); box-shadow: 0 0 0 0 rgba(25,101,72,.35); animation: realtime-pulse 1.5s infinite; }.challenge-code { border: 1px dashed var(--green); border-radius: 14px; padding: 14px 24px; background: white; color: var(--green); font: 700 31px/1 "SFMono-Regular", Consolas, monospace; letter-spacing: .22em; cursor: pointer; }
|
||||
@@ -150,7 +154,7 @@ button { color: inherit; }
|
||||
.game-card-top { display: flex; justify-content: space-between; align-items: start; }.game-card-top > span { display: grid; place-items: center; width: 58px; height: 58px; border-radius: 16px; background: var(--ink); color: var(--lime); font: 700 20px Georgia, serif; }.game-card-top small { color: var(--muted); }
|
||||
.game-card > b { margin-top: 24px; color: var(--green); font-size: 9px; letter-spacing: .18em; }.game-card h3 { margin: 8px 0; font: 28px Georgia, serif; }.game-card p { margin: 0; color: var(--muted); line-height: 1.7; }
|
||||
.game-card-controls { display: flex; gap: 10px; margin-top: auto; }.game-card-controls select { min-width: 100px; border: 1px solid var(--line); border-radius: 10px; padding: 11px; background: white; }.game-card-controls .primary-button { margin-left: auto; }
|
||||
.twenty-four-numbers { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin: 24px 0; }.twenty-four-numbers button { aspect-ratio: 1; border: 1px solid var(--line); border-radius: 18px; background: var(--ink); color: var(--lime); font: 36px Georgia, serif; cursor: pointer; }
|
||||
.twenty-four-numbers { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin: 24px 0; }.twenty-four-numbers button, .twenty-four-numbers > span { display: grid; place-items: center; aspect-ratio: 1; border: 1px solid var(--line); border-radius: 18px; background: var(--ink); color: var(--lime); font: 36px Georgia, serif; cursor: pointer; }
|
||||
.twenty-four-form { display: grid; gap: 12px; }.game-keypad { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; }.game-keypad button { border: 1px solid var(--line); border-radius: 9px; padding: 10px; background: white; cursor: pointer; }
|
||||
.sudoku-board { width: min(100%, 540px); margin: 22px auto; display: grid; grid-template-columns: repeat(9, 1fr); border: 3px solid var(--ink); background: var(--ink); gap: 1px; }
|
||||
.sudoku-board input { width: 100%; min-width: 0; aspect-ratio: 1; border: 0; border-radius: 0; background: white; color: var(--green); text-align: center; font: 600 21px Georgia, serif; outline: 2px solid transparent; outline-offset: -2px; }
|
||||
@@ -285,6 +289,7 @@ dialog::backdrop { background: rgba(17,25,20,.55); backdrop-filter: blur(5px); }
|
||||
.section-heading { display: block; }.section-heading p { margin-top: 12px; }.spirit-grid, .content-grid, .editor-shell, .metric-grid, .tool-grid, .symbol-grid, .video-grid, .graph-shell, .game-card-grid, .calc-result-grid { grid-template-columns: 1fr; }
|
||||
.content-grid { gap: 11px; }.editor-shell { min-height: 700px; }.page-title { padding-top: 42px; }.page-title h1 { font-size: 45px; }
|
||||
.preview-pane { min-height: 320px; }.match-history-item { align-items: flex-start; flex-direction: column; }.match-history-item > div:last-child { text-align: left; }
|
||||
.match-mode-switch { grid-template-columns: 1fr; }
|
||||
.tool-card { min-height: 125px; }.workspace-heading, .map-heading { display: block; }.workspace-heading p { margin-top: 12px; }.tool-workspace { padding: 16px; }.calculator-controls { grid-template-columns: 1fr 1fr; }.calculator-display { min-height: 120px; padding: 18px; }.calculator-display output { font-size: 34px; }.calc-examples .primary-button { width: 100%; margin-left: 0; }.drawing-toolbar { align-items: stretch; }.drawing-toolbar .primary-button { width: 100%; margin-left: 0; }.canvas-stage canvas { width: 100%; min-width: 0; }
|
||||
.math-games-section { margin-top: 45px; }.game-card { min-height: 260px; padding: 21px; }.game-card-controls { align-items: stretch; }.game-card-controls .primary-button { flex: 1; }.twenty-four-numbers { gap: 7px; }.twenty-four-numbers button { border-radius: 13px; font-size: 28px; }.sudoku-board input { font-size: clamp(13px, 4.5vw, 20px); }.sudoku-actions { display: grid; grid-template-columns: 1fr 1fr; }.game-keypad { gap: 5px; }
|
||||
.challenge-panel { grid-template-columns: 1fr; padding: 20px; }.challenge-actions form { grid-template-columns: 1fr; }.challenge-actions .dark-button { width: 100%; }.challenge-code { width: 100%; padding: 14px 10px; font-size: 27px; }.realtime-status-line { display: grid; }.realtime-progress-panel { grid-template-columns: 1fr 1fr; }
|
||||
|
||||
@@ -29,7 +29,7 @@ const state = {
|
||||
user: null,
|
||||
stories: [],
|
||||
contests: [],
|
||||
track: "standard",
|
||||
matchMode: "quiz",
|
||||
videoCatalog: null,
|
||||
videos: [],
|
||||
videoAbility: "",
|
||||
@@ -446,12 +446,21 @@ async function loadContests() {
|
||||
|
||||
function renderContests() {
|
||||
const root = $("#contest-list");
|
||||
const contests = state.contests.filter((contest) => contest.track === state.track);
|
||||
const contests = ["realtime", "daily", "practice"]
|
||||
.map((kind) => {
|
||||
const candidates = state.contests.filter((contest) => contest.kind === kind);
|
||||
return (
|
||||
candidates.find((contest) => contest.track === "open") ||
|
||||
candidates.find((contest) => contest.track === "standard") ||
|
||||
candidates[0]
|
||||
);
|
||||
})
|
||||
.filter(Boolean);
|
||||
root.classList.remove("loading");
|
||||
root.replaceChildren(...contests.map((contest) => card({
|
||||
meta: contest.kind === "realtime" ? "实时竞技" : contest.kind === "daily" ? "今日挑战" : "自由练习",
|
||||
title: contest.title,
|
||||
body: contest.kind === "realtime" ? "Rating 匹配,同题序列,服务端计时和唯一结算。" : "完成整组题目,正式答案只在提交后显示。",
|
||||
title: contest.title.replace(/^(入门|标准|进阶)/, ""),
|
||||
body: contest.kind === "realtime" ? "统一玩家池,支持口算、数独与 24 点,服务端计时和 Rating 结算。" : "每次随机抽取题目,正式答案只在提交后显示。",
|
||||
foot: `${contest.duration_seconds} 秒`,
|
||||
action: contest.kind === "realtime" ? "开始匹配 →" : "开始答题 →",
|
||||
onClick: () => beginContest(contest),
|
||||
@@ -827,7 +836,7 @@ async function loadProfile() {
|
||||
profile.recent_games.forEach((game) => {
|
||||
const item = document.createElement("div");
|
||||
const name = document.createElement("b");
|
||||
name.textContent = `${game.label} · ${game.difficulty}`;
|
||||
name.textContent = game.label;
|
||||
const detail = document.createElement("span");
|
||||
detail.textContent =
|
||||
game.status === "completed"
|
||||
@@ -1255,10 +1264,13 @@ function bindUI() {
|
||||
$("#register-form").classList.toggle("hidden", button.dataset.authTab !== "register");
|
||||
$("#auth-error").textContent = "";
|
||||
}));
|
||||
$$("#track-switch button").forEach((button) => button.addEventListener("click", () => {
|
||||
state.track = button.dataset.track;
|
||||
$$("#track-switch button").forEach((item) => item.classList.toggle("active", item === button));
|
||||
renderContests();
|
||||
$$("#match-mode-switch button").forEach((button) => button.addEventListener("click", () => {
|
||||
state.matchMode = button.dataset.matchMode;
|
||||
$$("#match-mode-switch button").forEach((item) => {
|
||||
item.classList.toggle("active", item === button);
|
||||
});
|
||||
const label = button.querySelector("b").textContent;
|
||||
$("#challenge-create").textContent = `创建${label}约战`;
|
||||
}));
|
||||
$("#login-form").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -14,23 +14,6 @@
|
||||
},
|
||||
};
|
||||
|
||||
function difficultySelect() {
|
||||
const select = document.createElement("select");
|
||||
select.className = "game-difficulty";
|
||||
[
|
||||
["easy", "入门"],
|
||||
["standard", "标准"],
|
||||
["hard", "进阶"],
|
||||
].forEach(([value, label]) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = value;
|
||||
option.textContent = label;
|
||||
if (value === "standard") option.selected = true;
|
||||
select.append(option);
|
||||
});
|
||||
return select;
|
||||
}
|
||||
|
||||
function gameCard(game) {
|
||||
const meta = GAME_META[game.kind];
|
||||
const article = document.createElement("article");
|
||||
@@ -50,12 +33,11 @@
|
||||
summary.textContent = game.summary;
|
||||
const controls = document.createElement("div");
|
||||
controls.className = "game-card-controls";
|
||||
const difficulty = difficultySelect();
|
||||
const start = document.createElement("button");
|
||||
start.className = "primary-button";
|
||||
start.textContent = meta.action;
|
||||
start.addEventListener("click", () => startGame(game.kind, difficulty.value));
|
||||
controls.append(difficulty, start);
|
||||
start.addEventListener("click", () => startGame(game.kind));
|
||||
controls.append(start);
|
||||
article.append(top, kicker, title, summary, controls);
|
||||
return article;
|
||||
}
|
||||
@@ -70,12 +52,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function startGame(kind, difficulty) {
|
||||
async function startGame(kind) {
|
||||
if (!requireAuth()) return;
|
||||
try {
|
||||
const attempt = await api(`contests/games/${kind}/start/`, {
|
||||
method: "POST",
|
||||
body: { difficulty },
|
||||
body: {},
|
||||
});
|
||||
renderGame(attempt);
|
||||
$game("#experience-dialog").showModal();
|
||||
@@ -88,8 +70,7 @@
|
||||
const fragment = document.createDocumentFragment();
|
||||
const label = document.createElement("span");
|
||||
label.className = "kicker";
|
||||
label.textContent =
|
||||
`${GAME_META[attempt.kind].kicker} · ${attempt.difficulty.toUpperCase()}`;
|
||||
label.textContent = GAME_META[attempt.kind].kicker;
|
||||
const title = document.createElement("h2");
|
||||
title.textContent = GAME_META[attempt.kind].title;
|
||||
fragment.append(label, title);
|
||||
|
||||
+177
-31
@@ -7,6 +7,11 @@
|
||||
opponentProgress: 0,
|
||||
reconnectTimer: null,
|
||||
};
|
||||
const GAME_LABELS = {
|
||||
quiz: "口算竞速",
|
||||
sudoku: "数独 Timerun",
|
||||
twenty_four: "24 点竞速",
|
||||
};
|
||||
|
||||
function stopTimers() {
|
||||
if (realtime.pollTimer) window.clearInterval(realtime.pollTimer);
|
||||
@@ -90,7 +95,7 @@
|
||||
match.opponent?.status &&
|
||||
match.opponent.status !== "active"
|
||||
) {
|
||||
realtime.opponentProgress = match.attempt?.questions.length || 0;
|
||||
realtime.opponentProgress = matchProgressTotal(match);
|
||||
updateProgressUI();
|
||||
updateConnectionStatus("对手已提交,完成后将立即结算");
|
||||
}
|
||||
@@ -130,7 +135,7 @@
|
||||
const label = document.createElement("span");
|
||||
label.className = "kicker";
|
||||
label.textContent =
|
||||
`${match.match_type === "challenge" ? "联机码约战" : "随机匹配"} · REALTIME`;
|
||||
`${match.match_type === "challenge" ? "联机码约战" : "随机匹配"} · ${GAME_LABELS[match.game_kind] || "实时竞技"}`;
|
||||
const title = document.createElement("h2");
|
||||
title.textContent = match.contest;
|
||||
const status = document.createElement("div");
|
||||
@@ -182,7 +187,7 @@
|
||||
message.textContent =
|
||||
match.match_type === "challenge"
|
||||
? "把联机码发给朋友。对方加入后会自动开始,无需再次点击。"
|
||||
: "正在寻找同赛道、相近 Rating 的玩家。";
|
||||
: `正在寻找${GAME_LABELS[match.game_kind] || "同玩法"}对手。`;
|
||||
panel.append(pulse, message);
|
||||
if (match.challenge_code) {
|
||||
const code = document.createElement("button");
|
||||
@@ -217,30 +222,41 @@
|
||||
updateClock();
|
||||
}
|
||||
|
||||
function matchProgressTotal(match) {
|
||||
return (
|
||||
match.game_kind === "sudoku"
|
||||
? 81
|
||||
: match.game_kind === "twenty_four"
|
||||
? 1
|
||||
: match.attempt?.questions.length || 0
|
||||
);
|
||||
}
|
||||
|
||||
function progressPanel(root, match) {
|
||||
const total = matchProgressTotal(match);
|
||||
const panel = document.createElement("div");
|
||||
panel.className = "realtime-progress-panel";
|
||||
const self = document.createElement("div");
|
||||
self.innerHTML = `<span>你</span><b id="self-progress">0 / ${match.attempt.questions.length}</b>`;
|
||||
self.innerHTML = `<span>你</span><b id="self-progress">0 / ${total}</b>`;
|
||||
const opponent = document.createElement("div");
|
||||
opponent.innerHTML =
|
||||
`<span>${match.opponent?.nickname || "对手"}</span>` +
|
||||
`<b id="opponent-progress">${realtime.opponentProgress} / ${match.attempt.questions.length}</b>`;
|
||||
`<b id="opponent-progress">${realtime.opponentProgress} / ${total}</b>`;
|
||||
panel.append(self, opponent);
|
||||
root.append(panel);
|
||||
}
|
||||
|
||||
function updateProgressUI(selfCount) {
|
||||
const total = matchProgressTotal(realtime.match);
|
||||
if (Number.isInteger(selfCount)) {
|
||||
const self = document.querySelector("#self-progress");
|
||||
if (self && realtime.match?.attempt) {
|
||||
self.textContent = `${selfCount} / ${realtime.match.attempt.questions.length}`;
|
||||
self.textContent = `${selfCount} / ${total}`;
|
||||
}
|
||||
}
|
||||
const opponent = document.querySelector("#opponent-progress");
|
||||
if (opponent && realtime.match?.attempt) {
|
||||
opponent.textContent =
|
||||
`${realtime.opponentProgress} / ${realtime.match.attempt.questions.length}`;
|
||||
opponent.textContent = `${realtime.opponentProgress} / ${total}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,6 +268,109 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function submitRealtimeGame(attempt, payload) {
|
||||
realtime.match = await api(
|
||||
`contests/games/attempts/${attempt.attempt_id}/submit/`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Idempotency-Key": createIdempotencyKey() },
|
||||
body: payload,
|
||||
},
|
||||
);
|
||||
renderMatch();
|
||||
}
|
||||
|
||||
function renderTwentyFourGame(root, match) {
|
||||
const attempt = match.attempt;
|
||||
const numbers = document.createElement("div");
|
||||
numbers.className = "twenty-four-numbers";
|
||||
attempt.puzzle.numbers.forEach((number) => {
|
||||
const tile = document.createElement("span");
|
||||
tile.textContent = number;
|
||||
numbers.append(tile);
|
||||
});
|
||||
const form = document.createElement("form");
|
||||
form.className = "twenty-four-form";
|
||||
const input = document.createElement("input");
|
||||
input.className = "formula-input";
|
||||
input.placeholder = "四个数字各用一次,例如:6/(1-3/4)";
|
||||
input.autocomplete = "off";
|
||||
input.addEventListener("input", () => {
|
||||
sendProgress(input.value.trim() ? 1 : 0);
|
||||
updateProgressUI(input.value.trim() ? 1 : 0);
|
||||
});
|
||||
const error = document.createElement("p");
|
||||
error.className = "form-error";
|
||||
const submit = document.createElement("button");
|
||||
submit.className = "primary-button";
|
||||
submit.type = "submit";
|
||||
submit.textContent = "验证并锁定答案";
|
||||
form.append(input, error, submit);
|
||||
form.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
submit.disabled = true;
|
||||
error.textContent = "";
|
||||
try {
|
||||
await submitRealtimeGame(attempt, { expression: input.value });
|
||||
} catch (requestError) {
|
||||
error.textContent = requestError.message;
|
||||
submit.disabled = false;
|
||||
}
|
||||
});
|
||||
root.append(numbers, form);
|
||||
}
|
||||
|
||||
function renderSudokuGame(root, match) {
|
||||
const attempt = match.attempt;
|
||||
const board = document.createElement("div");
|
||||
board.className = "sudoku-board";
|
||||
attempt.puzzle.grid.forEach((rowValues, row) => {
|
||||
rowValues.forEach((givenValue, column) => {
|
||||
const input = document.createElement("input");
|
||||
input.inputMode = "numeric";
|
||||
input.pattern = "[1-9]";
|
||||
input.maxLength = 1;
|
||||
input.dataset.row = row;
|
||||
input.dataset.column = column;
|
||||
input.value = givenValue || "";
|
||||
input.readOnly = Boolean(givenValue);
|
||||
input.className = givenValue ? "given" : "";
|
||||
input.setAttribute("aria-label", `第 ${row + 1} 行第 ${column + 1} 列`);
|
||||
input.addEventListener("input", () => {
|
||||
input.value = input.value.replace(/[^1-9]/g, "").slice(0, 1);
|
||||
const count = [...board.querySelectorAll("input")].filter(
|
||||
(item) => item.value
|
||||
).length;
|
||||
sendProgress(count);
|
||||
updateProgressUI(count);
|
||||
});
|
||||
board.append(input);
|
||||
});
|
||||
});
|
||||
const error = document.createElement("p");
|
||||
error.className = "form-error";
|
||||
const submit = document.createElement("button");
|
||||
submit.className = "primary-button";
|
||||
submit.type = "button";
|
||||
submit.textContent = "检查并锁定数独";
|
||||
submit.addEventListener("click", async () => {
|
||||
submit.disabled = true;
|
||||
error.textContent = "";
|
||||
const grid = Array.from({ length: 9 }, () => Array(9).fill(0));
|
||||
board.querySelectorAll("input").forEach((input) => {
|
||||
grid[Number(input.dataset.row)][Number(input.dataset.column)] =
|
||||
Number(input.value || 0);
|
||||
});
|
||||
try {
|
||||
await submitRealtimeGame(attempt, { grid });
|
||||
} catch (requestError) {
|
||||
error.textContent = requestError.message;
|
||||
submit.disabled = false;
|
||||
}
|
||||
});
|
||||
root.append(board, error, submit);
|
||||
}
|
||||
|
||||
function renderActive(root, match) {
|
||||
progressPanel(root, match);
|
||||
const attempt = match.attempt;
|
||||
@@ -264,6 +383,16 @@
|
||||
updateClock();
|
||||
return;
|
||||
}
|
||||
if (match.game_kind === "twenty_four") {
|
||||
renderTwentyFourGame(root, match);
|
||||
updateClock();
|
||||
return;
|
||||
}
|
||||
if (match.game_kind === "sudoku") {
|
||||
renderSudokuGame(root, match);
|
||||
updateClock();
|
||||
return;
|
||||
}
|
||||
const form = document.createElement("form");
|
||||
form.className = "choice-list realtime-answer-form";
|
||||
attempt.questions.forEach((question) => {
|
||||
@@ -328,7 +457,15 @@
|
||||
: "平局";
|
||||
const score = document.createElement("p");
|
||||
score.textContent =
|
||||
`你 ${match.attempt.score} 分 · ${match.opponent.score} 分 ${match.opponent.nickname}`;
|
||||
match.game_kind === "quiz"
|
||||
? `你 ${match.attempt.score} 分 · ${match.opponent.score} 分 ${match.opponent.nickname}`
|
||||
: `${GAME_LABELS[match.game_kind]} · ${
|
||||
match.attempt.status === "completed" ? "你已完成" : "你未完成"
|
||||
} · ${
|
||||
match.opponent.status === "completed"
|
||||
? `${match.opponent.nickname} 已完成`
|
||||
: `${match.opponent.nickname} 未完成`
|
||||
}`;
|
||||
const timeLine = document.createElement("p");
|
||||
timeLine.className = "realtime-time-line";
|
||||
const selfMs = match.attempt.duration_ms || 0;
|
||||
@@ -336,7 +473,11 @@
|
||||
const selfSec = (selfMs / 1000).toFixed(1);
|
||||
const opponentSec = (opponentMs / 1000).toFixed(1);
|
||||
timeLine.textContent = `你 ${selfSec}s · ${match.opponent.nickname} ${opponentSec}s`;
|
||||
if (match.attempt.score === match.opponent.score && match.result.winner !== "draw") {
|
||||
if (
|
||||
match.result.winner !== "draw" &&
|
||||
(match.game_kind !== "quiz" ||
|
||||
match.attempt.score === match.opponent.score)
|
||||
) {
|
||||
const faster = selfMs < opponentMs ? "你" : match.opponent.nickname;
|
||||
const tiebreak = document.createElement("small");
|
||||
tiebreak.className = "realtime-tiebreak";
|
||||
@@ -356,22 +497,24 @@
|
||||
}
|
||||
loadUser();
|
||||
|
||||
const review = document.createElement("div");
|
||||
review.className = "realtime-review";
|
||||
match.attempt.questions.forEach((question) => {
|
||||
const item = document.createElement("article");
|
||||
const title = document.createElement("b");
|
||||
title.textContent = `${question.order}. ${question.prompt}`;
|
||||
const answer = document.createElement("p");
|
||||
answer.textContent =
|
||||
`你的答案:${question.submitted_answer || "未作答"} · 正确答案:${question.correct_answer}`;
|
||||
const explanation = document.createElement("small");
|
||||
explanation.textContent = question.explanation || "";
|
||||
item.className = question.is_correct ? "correct" : "incorrect";
|
||||
item.append(title, answer, explanation);
|
||||
review.append(item);
|
||||
});
|
||||
root.append(review);
|
||||
if (match.game_kind === "quiz") {
|
||||
const review = document.createElement("div");
|
||||
review.className = "realtime-review";
|
||||
match.attempt.questions.forEach((question) => {
|
||||
const item = document.createElement("article");
|
||||
const title = document.createElement("b");
|
||||
title.textContent = `${question.order}. ${question.prompt}`;
|
||||
const answer = document.createElement("p");
|
||||
answer.textContent =
|
||||
`你的答案:${question.submitted_answer || "未作答"} · 正确答案:${question.correct_answer}`;
|
||||
const explanation = document.createElement("small");
|
||||
explanation.textContent = question.explanation || "";
|
||||
item.className = question.is_correct ? "correct" : "incorrect";
|
||||
item.append(title, answer, explanation);
|
||||
review.append(item);
|
||||
});
|
||||
root.append(review);
|
||||
}
|
||||
}
|
||||
|
||||
function renderMatch() {
|
||||
@@ -391,15 +534,18 @@
|
||||
}
|
||||
|
||||
function currentRealtimeContest() {
|
||||
return state.contests.find(
|
||||
(contest) => contest.kind === "realtime" && contest.track === state.track
|
||||
const contests = state.contests.filter((contest) => contest.kind === "realtime");
|
||||
return (
|
||||
contests.find((contest) => contest.track === "open") ||
|
||||
contests.find((contest) => contest.track === "standard") ||
|
||||
contests[0]
|
||||
);
|
||||
}
|
||||
|
||||
async function startRandom(contest) {
|
||||
const match = await api(`contests/${contest.slug}/matchmaking/`, {
|
||||
method: "POST",
|
||||
body: {},
|
||||
body: { game_kind: state.matchMode },
|
||||
});
|
||||
openMatch(match);
|
||||
}
|
||||
@@ -408,13 +554,13 @@
|
||||
if (!requireAuth()) return;
|
||||
const contest = currentRealtimeContest();
|
||||
if (!contest) {
|
||||
showToast("当前赛道没有可用的实时比赛");
|
||||
showToast("当前没有可用的实时比赛");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const match = await api(`contests/${contest.slug}/challenges/`, {
|
||||
method: "POST",
|
||||
body: {},
|
||||
body: { game_kind: state.matchMode },
|
||||
});
|
||||
openMatch(match);
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user