diff --git a/src/api/admin/types.ts b/src/api/admin/types.ts index 28fb12b..94a2d67 100644 --- a/src/api/admin/types.ts +++ b/src/api/admin/types.ts @@ -86,6 +86,22 @@ export interface FormSchemaPayload { is_published?: boolean } +export interface TrackScoringSheetItem { + key: string + sort: number + category: string + title: string + criteria: string + max_score: number +} + +export interface TrackScoringSheet { + version: number + title: string + full_score: number + items: TrackScoringSheetItem[] +} + export interface CompetitionTrackRow { id: number competition_id: number @@ -94,6 +110,8 @@ export interface CompetitionTrackRow { description: string | null sort: number is_enabled: boolean + scoring_sheet_json?: TrackScoringSheet | null + scoring_sheet_configured?: boolean created_at?: string updated_at?: string } @@ -104,6 +122,7 @@ export interface CompetitionTrackPayload { description: string | null sort: number is_enabled: boolean + scoring_sheet_json?: TrackScoringSheet | null } export interface AdminApplicationListParams { diff --git a/src/views/admin/competition/CompetitionFormView.vue b/src/views/admin/competition/CompetitionFormView.vue index ce2987f..e3f8c02 100644 --- a/src/views/admin/competition/CompetitionFormView.vue +++ b/src/views/admin/competition/CompetitionFormView.vue @@ -42,6 +42,7 @@ import type { PublicSourceChannelRow, SignupChannelPayload, SignupChannelRow, + TrackScoringSheetItem, } from '../../../api/admin/types' import { brandingFormFromApi, @@ -133,8 +134,25 @@ const trackForm = ref({ description: '', sort: 0, is_enabled: true, + scoring_sheet_title: '', + scoring_items: [] as TrackScoringSheetItem[], }) +function emptyScoringItem(sort = 1): TrackScoringSheetItem { + return { + key: '', + sort, + category: '通用指标', + title: '', + criteria: '', + max_score: 0, + } +} + +const trackSheetMaxSum = computed(() => + trackForm.value.scoring_items.reduce((s, r) => s + (Number(r.max_score) || 0), 0), +) + const channelDialogVisible = ref(false) const channelEditingId = ref(null) const channelSaving = ref(false) @@ -576,6 +594,18 @@ function openTrackModal(row?: CompetitionTrackRow) { return } trackEditingId.value = row?.id ?? null + const sheet = row?.scoring_sheet_json + const items = + sheet && Array.isArray(sheet.items) && sheet.items.length + ? sheet.items.map((it, idx) => ({ + key: String(it.key ?? ''), + sort: Number(it.sort) || idx + 1, + category: String(it.category ?? ''), + title: String(it.title ?? ''), + criteria: String(it.criteria ?? ''), + max_score: Number(it.max_score) || 0, + })) + : [emptyScoringItem(1)] trackForm.value = row ? { track_code: row.track_code, @@ -583,6 +613,8 @@ function openTrackModal(row?: CompetitionTrackRow) { description: row.description ?? '', sort: row.sort, is_enabled: row.is_enabled, + scoring_sheet_title: String(sheet?.title ?? ''), + scoring_items: items, } : { track_code: '', @@ -590,10 +622,30 @@ function openTrackModal(row?: CompetitionTrackRow) { description: '', sort: (tracks.value[tracks.value.length - 1]?.sort ?? 0) + 10, is_enabled: true, + scoring_sheet_title: '', + scoring_items: [emptyScoringItem(1)], } trackDialogVisible.value = true } +function addTrackScoringItem() { + const nextSort = (trackForm.value.scoring_items[trackForm.value.scoring_items.length - 1]?.sort ?? 0) + 1 + trackForm.value.scoring_items = [...trackForm.value.scoring_items, emptyScoringItem(nextSort)] +} + +function removeTrackScoringItem(i: number) { + if (trackForm.value.scoring_items.length <= 1) { + trackForm.value.scoring_items = [emptyScoringItem(1)] + return + } + trackForm.value.scoring_items = trackForm.value.scoring_items.filter((_, idx) => idx !== i) +} + +function clearTrackScoringSheet() { + trackForm.value.scoring_sheet_title = '' + trackForm.value.scoring_items = [] +} + async function refreshTracks() { const cid = competitionId.value if (!cid) { @@ -622,6 +674,36 @@ async function saveTrack() { ElMessage.warning('请填写赛道编码与标题') return } + + const items = trackForm.value.scoring_items + .map((r, idx) => ({ + key: r.key.trim(), + sort: Number(r.sort) || idx + 1, + category: r.category.trim(), + title: r.title.trim(), + criteria: r.criteria.trim(), + max_score: Number(r.max_score) || 0, + })) + .filter((r) => r.key || r.title || r.category) + + if (items.length === 0) { + payload.scoring_sheet_json = null + } else { + const sum = items.reduce((s, r) => s + r.max_score, 0) + if (Math.abs(sum - 100) > 0.0001) { + ElMessage.warning(`打分表各项分值之和须为 100,当前为 ${sum}`) + return + } + payload.scoring_sheet_json = { + version: 1, + title: + trackForm.value.scoring_sheet_title.trim() || + `评分表(${payload.title})`, + full_score: 100, + items, + } + } + try { if (trackEditingId.value) { await updateTrack(cid, trackEditingId.value, payload) @@ -692,6 +774,31 @@ function formatScore(n: number | string | null | undefined): string { return v.toFixed(2).replace(/\.?0+$/, '') } +function formatReviewScoreItems(payload: Record | undefined): string { + if (!payload) return '' + const scores = + payload.scores && typeof payload.scores === 'object' && !Array.isArray(payload.scores) + ? (payload.scores as Record) + : null + const snap = payload.sheet_snapshot + const items = + snap && typeof snap === 'object' && !Array.isArray(snap) && Array.isArray((snap as { items?: unknown }).items) + ? ((snap as { items: Array<{ key?: string; title?: string }> }).items) + : [] + if (scores && items.length) { + return items + .map((it) => { + const key = String(it.key ?? '') + const title = String(it.title ?? key) + const v = scores[key] + const n = typeof v === 'number' ? v : Number(v) + return `${title} ${Number.isFinite(n) ? n : '—'}` + }) + .join(';') + } + return '' +} + function formatFileSize(size: number | null | undefined): string { if (!size || size <= 0) return '—' if (size < 1024) return `${size}B` @@ -1360,7 +1467,9 @@ onMounted(() => {
-

维护本场主题赛道。新建赛事请先保存基础信息。

+

+ 维护本场主题赛道与打分表(满分 100)。未配置打分表的赛道禁止评审。新建赛事请先保存基础信息。 +

新增赛道
@@ -1371,6 +1480,13 @@ onMounted(() => { {{ row.description || '—' }} + + + @@ -1920,12 +2036,17 @@ onMounted(() => { 均分 {{ formatScore(applicationDetail.team_avg) }}
- + - + + + + @@ -2101,9 +2222,10 @@ onMounted(() => { @@ -2127,6 +2249,61 @@ onMounted(() => { + + 打分表(满分 100,未配置则禁止评审) + + + +
+

+ 当前分值合计: + + {{ trackSheetMaxSum }} + + / 100 +

+
+ 清空打分表 + 新增指标 +
+
+ + + + + + + + + + + + + + + + + + + + + + +