From 41ee6e4c5b642a2783585d512435c7fc2eae1380 Mon Sep 17 00:00:00 2001 From: tanglong <842690096@qq.com> Date: Wed, 10 Jun 2026 11:11:15 +0800 Subject: [PATCH] =?UTF-8?q?ai=20=E5=AF=B9=E6=8E=A5=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 190 ++++++++ package.json | 1 + public/static/manifest.json | 2 +- .../赛事一/秩序册/秩序册1场2人_模板 .xlsx | Bin 18567 -> 16474 bytes src/utils/api.js | 128 +++++ src/utils/rosterParserEnhanced.js | 242 ++++++++++ src/views/EventConfig.vue | 239 ++++++++- src/views/RopeSkippingCounter.vue | 453 ++++++++++++------ 8 files changed, 1086 insertions(+), 169 deletions(-) create mode 100644 src/utils/api.js create mode 100644 src/utils/rosterParserEnhanced.js diff --git a/package-lock.json b/package-lock.json index 7654c49..cb8a402 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "yd-training-competition-web", "version": "1.0.0", "dependencies": { + "@microsoft/signalr": "^10.0.0", "vue": "^3.5.13", "xlsx": "^0.18.5" }, @@ -479,6 +480,19 @@ "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" }, + "node_modules/@microsoft/signalr": { + "version": "10.0.0", + "resolved": "https://registry.npmmirror.com/@microsoft/signalr/-/signalr-10.0.0.tgz", + "integrity": "sha512-0BRqz/uCx3JdrOqiqgFhih/+hfTERaUfCZXFB52uMaZJrKaPRzHzMuqVsJC/V3pt7NozcNXGspjKiQEK+X7P2w==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "eventsource": "^2.0.2", + "fetch-cookie": "^2.0.3", + "node-fetch": "^2.6.7", + "ws": "^7.5.10" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.61.0", "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.0.tgz", @@ -914,6 +928,18 @@ "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.35.tgz", "integrity": "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/adler-32": { "version": "1.3.1", "resolved": "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz", @@ -1015,6 +1041,24 @@ "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", @@ -1032,6 +1076,16 @@ } } }, + "node_modules/fetch-cookie": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/fetch-cookie/-/fetch-cookie-2.2.0.tgz", + "integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==", + "license": "Unlicense", + "dependencies": { + "set-cookie-parser": "^2.4.8", + "tough-cookie": "^4.0.0" + } + }, "node_modules/frac": { "version": "1.1.2", "resolved": "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz", @@ -1079,6 +1133,26 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", @@ -1123,6 +1197,39 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmmirror.com/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, "node_modules/rollup": { "version": "4.61.0", "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.61.0.tgz", @@ -1167,6 +1274,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1202,6 +1315,46 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmmirror.com/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/vite": { "version": "6.4.3", "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.3.tgz", @@ -1296,6 +1449,22 @@ } } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/wmf": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz", @@ -1312,6 +1481,27 @@ "node": ">=0.8" } }, + "node_modules/ws": { + "version": "7.5.11", + "resolved": "https://registry.npmmirror.com/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xlsx": { "version": "0.18.5", "resolved": "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz", diff --git a/package.json b/package.json index 17dcd58..5e4c86f 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "preview": "vite preview" }, "dependencies": { + "@microsoft/signalr": "^10.0.0", "vue": "^3.5.13", "xlsx": "^0.18.5" }, diff --git a/public/static/manifest.json b/public/static/manifest.json index 14ae719..e92f475 100644 --- a/public/static/manifest.json +++ b/public/static/manifest.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-06-05T08:50:41.686Z", + "generatedAt": "2026-06-10T02:58:33.251Z", "events": [ { "id": "赛事一", diff --git a/public/static/赛事一/秩序册/秩序册1场2人_模板 .xlsx b/public/static/赛事一/秩序册/秩序册1场2人_模板 .xlsx index 3e12ec3cda202e6448eb87b07482d6c7da327cf3..a3a748540b051819d4a4378048523f0ae3651ea7 100644 GIT binary patch literal 16474 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 literal 18567 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 Uum9FQf&?J^mPDly`t|Mq0G5%;P5=M^ diff --git a/src/utils/api.js b/src/utils/api.js new file mode 100644 index 0000000..4eca9cf --- /dev/null +++ b/src/utils/api.js @@ -0,0 +1,128 @@ +const DEFAULT_API_BASE = (import.meta.env.VITE_API_BASE_URL || 'http://localhost:9996').trim() + +export function resolveApiBase(baseUrl = '') { + const value = (baseUrl || DEFAULT_API_BASE).trim() + return value.replace(/\/+$/, '') +} + +export function resolveHubUrl(baseUrl = '') { + return `${resolveApiBase(baseUrl)}/hubs/scoring` +} + +async function request(baseUrl, path, options = {}) { + const url = `${resolveApiBase(baseUrl)}${path}` + const response = await fetch(url, { + headers: { + 'Content-Type': 'application/json', + ...(options.headers || {}), + }, + ...options, + }) + + let payload = null + const text = await response.text() + if (text) { + try { + payload = JSON.parse(text) + } catch { + payload = text + } + } + + if (!response.ok) { + const message = + (payload && typeof payload === 'object' && payload.message) || + response.statusText || + '请求失败' + throw new Error(message) + } + + return payload +} + +export function getCompetitionSession(baseUrl) { + return request(baseUrl, '/api/competition/session') +} + +export function createCompetitionSession(baseUrl, payload) { + return request(baseUrl, '/api/competition/session', { + method: 'POST', + body: JSON.stringify(payload), + }) +} + +export function selectCompetitionRound(baseUrl, roundIndex) { + return request(baseUrl, `/api/competition/rounds/${roundIndex}/select`, { + method: 'POST', + }) +} + +export function nextCompetitionRound(baseUrl) { + return request(baseUrl, '/api/competition/rounds/next', { + method: 'POST', + }) +} + +export function previousCompetitionRound(baseUrl) { + return request(baseUrl, '/api/competition/rounds/previous', { + method: 'POST', + }) +} + +export function downloadCurrentRound(baseUrl) { + return request(baseUrl, '/api/competition/rounds/current/download', { + method: 'POST', + }) +} + +export function startCompetition(baseUrl) { + return request(baseUrl, '/api/competition/test/start', { + method: 'POST', + }) +} + +export function stopCompetition(baseUrl) { + return request(baseUrl, '/api/competition/test/stop', { + method: 'POST', + }) +} + +export function resetCompetitionRound(baseUrl) { + return request(baseUrl, '/api/competition/test/reset', { + method: 'POST', + }) +} + +export function setParticipantCount(baseUrl, participantId, count) { + return request(baseUrl, `/api/competition/participants/${participantId}/count`, { + method: 'POST', + body: JSON.stringify({ count }), + }) +} + +export function changeParticipantCount(baseUrl, participantId, delta) { + return request(baseUrl, `/api/competition/participants/${participantId}/delta`, { + method: 'POST', + body: JSON.stringify({ delta }), + }) +} + +export function getSerialPorts(baseUrl) { + return request(baseUrl, '/api/serial/ports') +} + +export function getSerialStatus(baseUrl) { + return request(baseUrl, '/api/serial/status') +} + +export function openSerialPort(baseUrl, portName) { + return request(baseUrl, `/api/serial/open/${encodeURIComponent(portName)}`, { + method: 'POST', + }) +} + +export function closeSerialPort(baseUrl) { + return request(baseUrl, '/api/serial/close', { + method: 'POST', + }) +} diff --git a/src/utils/rosterParserEnhanced.js b/src/utils/rosterParserEnhanced.js new file mode 100644 index 0000000..8c49e94 --- /dev/null +++ b/src/utils/rosterParserEnhanced.js @@ -0,0 +1,242 @@ +import * as XLSX from 'xlsx' +import { resolveStaticUrl } from './staticUrl.js' + +const NAME_HEADERS = ['姓名', '名字', '选手', '名称', 'name', 'Name', '选手姓名'] +const PROJECT_HEADERS = ['项目', '比赛项目', '参赛项目', 'project', 'Project'] +const ADDRESS_HEADERS = ['通信地址', '手柄地址', '设备地址', '终端地址', 'RFID', 'rfid', 'address', 'Address'] +const TOTAL_ROUNDS_RE = /场次总数|总场次|轮次总数|总轮次/ +const ROUND_SIZE_RE = /每场人数|每轮人数|单场人数|每场|人数/ +const ADDRESS_RE = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ + +function normalizeCell(value) { + return String(value ?? '').trim() +} + +function normalizeRow(row) { + return row.map(normalizeCell) +} + +function rowText(row) { + return normalizeRow(row).filter(Boolean).join(' ') +} + +function numbersInRow(row) { + return normalizeRow(row) + .flatMap((cell) => cell.match(/\d+/g) ?? []) + .map((n) => Number(n)) + .filter((n) => Number.isFinite(n) && n > 0) +} + +function findColumn(row, headers) { + return row.findIndex((cell) => headers.includes(normalizeCell(cell))) +} + +function normalizeAddress(value) { + const raw = normalizeCell(value) + return ADDRESS_RE.test(raw) ? raw : '' +} + +function findAddressInRow(row) { + return normalizeRow(row).find((cell) => ADDRESS_RE.test(cell)) || '' +} + +function isValidName(value) { + const name = normalizeCell(value) + if (!name) return false + if (NAME_HEADERS.includes(name)) return false + if (/^\d+$/.test(name)) return false + if (ADDRESS_RE.test(name)) return false + return true +} + +function isValidProject(value) { + const project = normalizeCell(value) + if (!project) return false + if (PROJECT_HEADERS.includes(project)) return false + return true +} + +function collectParticipants(rows) { + const participants = [] + let activeNameCol = -1 + let activeProjectCol = -1 + let activeAddressCol = -1 + + for (const row of rows) { + const nameHeaderCol = findColumn(row, NAME_HEADERS) + if (nameHeaderCol >= 0) { + activeNameCol = nameHeaderCol + activeProjectCol = findColumn(row, PROJECT_HEADERS) + activeAddressCol = findColumn(row, ADDRESS_HEADERS) + continue + } + + if (activeNameCol < 0) continue + + const name = normalizeCell(row[activeNameCol]) + if (!isValidName(name)) continue + + const projectName = + activeProjectCol >= 0 && isValidProject(row[activeProjectCol]) + ? normalizeCell(row[activeProjectCol]) + : '' + + const communicationAddress = + activeAddressCol >= 0 + ? normalizeAddress(row[activeAddressCol]) + : findAddressInRow(row) + + participants.push({ + name, + projectName, + communicationAddress, + }) + } + + if (participants.length) return participants + + return rows + .map((row) => ({ + name: normalizeCell(row[0]), + projectName: '', + communicationAddress: findAddressInRow(row), + })) + .filter((participant) => isValidName(participant.name)) +} + +function summarizeRoundName(participants, index) { + const names = [...new Set(participants.map((p) => normalizeCell(p.projectName)).filter(Boolean))] + return names.length ? names.join(' / ') : `第 ${index + 1} 场` +} + +function parseTxtRows(text) { + return text + .split(/\r?\n/) + .map((line) => line.replace(/#.*$/, '').trim()) + .filter(Boolean) + .map((line) => { + if (line.includes('\t')) return line.split('\t') + return line.split(/[,,]/) + }) +} + +function parseCsvRows(text) { + const lines = text.split(/\r?\n/).filter((line) => line.trim()) + if (!lines.length) return [] + + return lines.map((line) => { + if (line.includes('\t')) return line.split('\t') + return line.split(/[,,]/).map((item) => item.trim().replace(/^"|"$/g, '')) + }) +} + +function parseWorkbookRows(buffer) { + const workbook = XLSX.read(buffer, { type: 'array' }) + const sheet = workbook.Sheets[workbook.SheetNames[0]] + return XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '' }) +} + +function parseRosterRows(rows) { + const normalizedRows = rows.map(normalizeRow).filter((row) => row.some(Boolean)) + if (!normalizedRows.length) return null + + const eventName = normalizedRows[0].find(Boolean) || '' + const metaRows = new Set([0]) + let totalRounds = 1 + let roundParticipantCounts = [] + + normalizedRows.forEach((row, index) => { + const text = rowText(row) + const numbers = numbersInRow(row) + if (!numbers.length) return + + if (TOTAL_ROUNDS_RE.test(text)) { + totalRounds = numbers[0] + metaRows.add(index) + return + } + + if (ROUND_SIZE_RE.test(text)) { + roundParticipantCounts = numbers + metaRows.add(index) + } + }) + + if (!roundParticipantCounts.length && normalizedRows[1]) { + const numbers = numbersInRow(normalizedRows[1]) + if (numbers.length) { + totalRounds = numbers[0] + metaRows.add(1) + } + } + + if (!roundParticipantCounts.length && normalizedRows[2]) { + const numbers = numbersInRow(normalizedRows[2]) + if (numbers.length) { + roundParticipantCounts = numbers + metaRows.add(2) + } + } + + if (roundParticipantCounts.length > 1 && totalRounds <= 1) { + totalRounds = roundParticipantCounts.length + } + + const nameRows = normalizedRows.filter((_, index) => !metaRows.has(index)) + const participants = collectParticipants(nameRows) + + if (!roundParticipantCounts.length && participants.length) { + roundParticipantCounts = [participants.length] + } else if (roundParticipantCounts.length === 1 && totalRounds > 1) { + roundParticipantCounts = Array.from({ length: totalRounds }, () => roundParticipantCounts[0]) + } else if (roundParticipantCounts.length < totalRounds) { + const fallback = roundParticipantCounts.at(-1) || participants.length + roundParticipantCounts = [ + ...roundParticipantCounts, + ...Array.from({ length: totalRounds - roundParticipantCounts.length }, () => fallback), + ] + } else { + roundParticipantCounts = roundParticipantCounts.slice(0, totalRounds) + } + + const roundNames = [] + let start = 0 + roundParticipantCounts.forEach((count, index) => { + const roundParticipants = participants.slice(start, start + count) + roundNames.push(summarizeRoundName(roundParticipants, index)) + start += count + }) + + return { + eventName, + totalRounds: Math.max(1, totalRounds), + roundParticipantCounts, + roundNames, + participants, + names: participants.map((p) => p.name), + } +} + +async function loadRosterRows(url) { + const response = await fetch(resolveStaticUrl(url)) + if (!response.ok) throw new Error('无法读取秩序册文件') + + const ext = url.split('.').pop()?.toLowerCase().replace(/\?.*$/, '') ?? '' + + if (ext === 'txt') return parseTxtRows(await response.text()) + if (ext === 'csv') return parseCsvRows(await response.text()) + if (ext === 'xlsx' || ext === 'xls') return parseWorkbookRows(await response.arrayBuffer()) + + throw new Error('不支持的秩序册格式,请使用 txt / csv / xlsx') +} + +export async function parseRosterDetails(url) { + const details = parseRosterRows(await loadRosterRows(url)) + if (!details?.names.length) throw new Error('秩序册中未解析到参赛姓名') + return details +} + +export async function parseRoster(url, max = 20) { + const details = await parseRosterDetails(url) + return details.names.slice(0, max) +} diff --git a/src/views/EventConfig.vue b/src/views/EventConfig.vue index ec9e90d..2b5bfef 100644 --- a/src/views/EventConfig.vue +++ b/src/views/EventConfig.vue @@ -1,7 +1,15 @@ @@ -399,19 +464,25 @@ onUnmounted(() => { {{ roundLabel }} {{ currentRoundName }} 本场 {{ participantCount }} 人 + 剩余 {{ remainingSecondsLabel }} - WS {{ wsStatus }}{{ wsError ? `:${wsError}` : '' }} + 实时 {{ serverStatus }}{{ serverError ? `:${serverError}` : '' }} + + + {{ sessionMeta.isTesting ? '测试中' : '待开始' }}
+ + + +