R194: 作品管理改「小组维度」列表 + 成绩/提交列表 includeIdle 透传
- src/views/Admin.vue
· 作品管理一行 = 一个小组(小组编号/作品目录 + 提交状态 + 该组文件与评分 + 人数 + 提交人),
替代原「按人平铺」(同组 6 人 6 行、5 行未提交)
· 搜索口径改「小组编号」(姓名已不在列表展示);计数单位 人→组
· PDF 预览常驻说明 + 「在新标签打开」出口(iframe 白屏时不误导「系统没加载」)
- src/api/admin.js:getSubmissions/getScores 增加 params,供 includeIdle 口径透传
This commit is contained in:
@@ -53,8 +53,8 @@ export function importExcel(formData) {
|
|||||||
* @param {string} group - 组别标识
|
* @param {string} group - 组别标识
|
||||||
* @returns {Promise} 作品列表
|
* @returns {Promise} 作品列表
|
||||||
*/
|
*/
|
||||||
export function getSubmissions(group) {
|
export function getSubmissions(group, params) {
|
||||||
return request.get(`/admin/submissions/${group}`)
|
return request.get(`/admin/submissions/${group}`, { params })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -256,8 +256,8 @@ export function deleteMaterial(group, fileName) {
|
|||||||
* @param {string} group - 组别标识
|
* @param {string} group - 组别标识
|
||||||
* @returns {Promise} 成绩列表
|
* @returns {Promise} 成绩列表
|
||||||
*/
|
*/
|
||||||
export function getScores(group) {
|
export function getScores(group, params) {
|
||||||
return request.get(`/admin/scores/${group}`)
|
return request.get(`/admin/scores/${group}`, { params })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -716,7 +716,9 @@
|
|||||||
<el-option label="本科组" value="benke" />
|
<el-option label="本科组" value="benke" />
|
||||||
<el-option label="教师组" value="jiaoshi" />
|
<el-option label="教师组" value="jiaoshi" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-input v-model="submissionSearch" placeholder="搜索选手姓名" clearable size="small" class="search-input" @input="onSubmissionFilterChange" />
|
<!-- R194:列表已按小组聚合(一行 = 一个小组),搜索口径同步改为小组编号/目录名,
|
||||||
|
⛔ 不再按选手姓名过滤——姓名已不在列表展示,命中也无法解释。 -->
|
||||||
|
<el-input v-model="submissionSearch" placeholder="搜索小组编号" clearable size="small" class="search-input" @input="onSubmissionFilterChange" />
|
||||||
<!-- R87:提交状态筛选。判据与表格内「未提交」文案同源(row.files.length === 0);
|
<!-- R87:提交状态筛选。判据与表格内「未提交」文案同源(row.files.length === 0);
|
||||||
选项文案与「成绩管理 · 成绩总览」的提交状态下拉逐字对齐。 -->
|
选项文案与「成绩管理 · 成绩总览」的提交状态下拉逐字对齐。 -->
|
||||||
<!-- R178:value-on-clear="" 让「清除」回到与 ref 初值一致的空字符串
|
<!-- R178:value-on-clear="" 让「清除」回到与 ref 初值一致的空字符串
|
||||||
@@ -734,19 +736,24 @@
|
|||||||
title="下载该组全部作品(不受上方筛选影响)"
|
title="下载该组全部作品(不受上方筛选影响)"
|
||||||
>批量下载作品</el-button>
|
>批量下载作品</el-button>
|
||||||
<span class="work-summary" v-if="submissions.length">
|
<span class="work-summary" v-if="submissions.length">
|
||||||
共 {{ submissionStatusCount.total }} 人 · 已提交 {{ submissionStatusCount.submitted }} · 未提交 {{ submissionStatusCount.unsubmitted }}
|
共 {{ submissionStatusCount.total }} 组 · 已提交 {{ submissionStatusCount.submitted }} · 未提交 {{ submissionStatusCount.unsubmitted }}
|
||||||
<template v-if="submissionStatusFilter === 'true' || submissionStatusFilter === 'false'">|当前筛选出 {{ filteredSubmissions.length }} 人</template>
|
<template v-if="submissionStatusFilter === 'true' || submissionStatusFilter === 'false'">|当前筛选出 {{ filteredSubmissions.length }} 组</template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 🔴 R194:作品管理改「小组维度」(用户实测:比赛以小组为准,原表格按人平铺,
|
||||||
|
同组 6 人 6 行、5 行是「未提交」,管理人员看不出小组整体是否已交)。
|
||||||
|
一行 = 一个小组:小组编号/作品目录 + 提交状态 + 该组文件与评分。 -->
|
||||||
<el-table :data="pagedSubmissions" stripe v-loading="submissionsLoading" class="admin-table">
|
<el-table :data="pagedSubmissions" stripe v-loading="submissionsLoading" class="admin-table">
|
||||||
<el-table-column prop="name" label="选手姓名" width="120" />
|
<!-- R190/R194:小组编号 / 作品目录。
|
||||||
<!-- R190:小组编号 / 作品目录。
|
|
||||||
展示值 = 后端落盘用的目录名(teamFolder),与「批量下载」压缩包内的子目录
|
展示值 = 后端落盘用的目录名(teamFolder),与「批量下载」压缩包内的子目录
|
||||||
以及「单组压缩包」文件名**完全同源**,管理人员据此对得上号。 -->
|
以及「单组压缩包」文件名**完全同源**,管理人员据此对得上号。 -->
|
||||||
<el-table-column label="小组编号 / 作品目录" width="240">
|
<el-table-column label="小组编号 / 作品目录" width="260">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="team-cell">
|
<div class="team-cell">
|
||||||
<span class="team-no" :title="row.teamFolder">{{ row.teamFolder }}</span>
|
<span class="team-no" :title="row.teamFolder">{{ row.teamFolder }}</span>
|
||||||
|
<span v-if="row.memberCount > 1" class="team-meta">{{ row.memberCount }} 人参赛</span>
|
||||||
|
<!-- R194:提交人(= 组锁口径下的归属人,最早提交者);未提交则不显示 -->
|
||||||
|
<span v-if="row.submitterName" class="team-meta">提交人:{{ row.submitterName }}</span>
|
||||||
<el-button
|
<el-button
|
||||||
text
|
text
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -758,7 +765,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="上传文件 / 评分" min-width="420">
|
<el-table-column label="提交状态" width="110" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag size="small" :type="isWorkSubmitted(row) ? 'success' : 'info'">
|
||||||
|
{{ isWorkSubmitted(row) ? '已提交' : '未提交' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="上传文件 / 评分" min-width="460">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div v-for="f in row.files" :key="f.id" class="work-file-row">
|
<div v-for="f in row.files" :key="f.id" class="work-file-row">
|
||||||
<span class="work-file-name" :title="f.fileName">{{ f.fileName }}</span>
|
<span class="work-file-name" :title="f.fileName">{{ f.fileName }}</span>
|
||||||
@@ -794,6 +808,13 @@
|
|||||||
<el-dialog v-model="previewVisible" title="文件预览" width="80%" top="5vh" @closed="closePreview">
|
<el-dialog v-model="previewVisible" title="文件预览" width="80%" top="5vh" @closed="closePreview">
|
||||||
<div v-if="previewFile">
|
<div v-if="previewFile">
|
||||||
<p class="preview-file-name">{{ previewFile.fileName }}({{ previewFile.kind === 'ppt' ? 'PPT' : previewFile.kind === 'answer' ? 'Word答案' : '文档' }})</p>
|
<p class="preview-file-name">{{ previewFile.fileName }}({{ previewFile.kind === 'ppt' ? 'PPT' : previewFile.kind === 'answer' ? 'Word答案' : '文档' }})</p>
|
||||||
|
<!-- R194:空白兜底。iframe 渲染 PDF 依赖浏览器内置阅读器(Chrome/Edge 内置,
|
||||||
|
被策略禁用/精简版系统移除时 iframe 会显示纯白且**不报错**),故常驻一条说明 +
|
||||||
|
「在新标签打开」出口,避免管理人员误判「系统没加载」。 -->
|
||||||
|
<div v-if="previewIsPdf && previewBlobUrl" class="preview-bar">
|
||||||
|
<el-button text type="primary" size="small" v-guard="() => openPreviewInNewTab()">在新标签打开</el-button>
|
||||||
|
<span class="preview-hint">若下方空白,通常是浏览器未启用内置 PDF 阅读器,可点左侧按钮或下载后查看</span>
|
||||||
|
</div>
|
||||||
<iframe v-if="previewIsPdf && previewBlobUrl" :src="previewBlobUrl" class="preview-frame"></iframe>
|
<iframe v-if="previewIsPdf && previewBlobUrl" :src="previewBlobUrl" class="preview-frame"></iframe>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<el-alert type="info" :closable="false" show-icon style="margin-bottom:12px">
|
<el-alert type="info" :closable="false" show-icon style="margin-bottom:12px">
|
||||||
@@ -1081,6 +1102,17 @@
|
|||||||
class="date-range"
|
class="date-range"
|
||||||
@change="onScoreFilterChange"
|
@change="onScoreFilterChange"
|
||||||
/>
|
/>
|
||||||
|
<!-- R195:默认只显示参赛过的人(有作答/有大题文件/已交卷)。中职/高职按小组参赛,
|
||||||
|
一组报名多人只一人上场,其余组员全 0 分占行 ⇒ 勾选后才列出全部报名者。 -->
|
||||||
|
<el-checkbox
|
||||||
|
v-if="isExamScoreGroup"
|
||||||
|
v-model="scoreIncludeIdle"
|
||||||
|
size="small"
|
||||||
|
class="idle-toggle"
|
||||||
|
@change="onScoreIncludeIdleChange"
|
||||||
|
>
|
||||||
|
显示未参赛
|
||||||
|
</el-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 成绩明细表 -->
|
<!-- 成绩明细表 -->
|
||||||
@@ -1204,6 +1236,15 @@
|
|||||||
class="date-range"
|
class="date-range"
|
||||||
@change="onExamSubsFilterChange"
|
@change="onExamSubsFilterChange"
|
||||||
/>
|
/>
|
||||||
|
<!-- R195:默认只列出参赛过的人;勾选后显示全部报名者(含从未进考场的组员)。 -->
|
||||||
|
<el-checkbox
|
||||||
|
v-model="examSubsIncludeIdle"
|
||||||
|
size="small"
|
||||||
|
class="idle-toggle"
|
||||||
|
@change="onExamSubsIncludeIdleChange"
|
||||||
|
>
|
||||||
|
显示未参赛
|
||||||
|
</el-checkbox>
|
||||||
<el-button text type="primary" v-guard="() => fetchExamSubmissions()" :loading="examSubmissionsLoading">刷新</el-button>
|
<el-button text type="primary" v-guard="() => fetchExamSubmissions()" :loading="examSubmissionsLoading">刷新</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="pagedExamSubmissions" stripe v-loading="examSubmissionsLoading" class="admin-table">
|
<el-table :data="pagedExamSubmissions" stripe v-loading="examSubmissionsLoading" class="admin-table">
|
||||||
@@ -4011,6 +4052,8 @@ async function downloadBigQuestionTpl() {
|
|||||||
|
|
||||||
// 考生答题记录
|
// 考生答题记录
|
||||||
const examSubmissions = ref([])
|
const examSubmissions = ref([])
|
||||||
|
// R195:同「成绩总览」——默认隐藏从未参赛的报名者(includeIdle=1 才看全量)
|
||||||
|
const examSubsIncludeIdle = ref(false)
|
||||||
const examSubmissionsLoading = ref(false)
|
const examSubmissionsLoading = ref(false)
|
||||||
const examSubmissionsPage = ref(1)
|
const examSubmissionsPage = ref(1)
|
||||||
const examSubmissionsPageSize = ref(10)
|
const examSubmissionsPageSize = ref(10)
|
||||||
@@ -4058,7 +4101,10 @@ async function fetchExamSubmissions() {
|
|||||||
// R29:本板块已迁入「成绩管理」⇒ 组别跟随成绩页
|
// R29:本板块已迁入「成绩管理」⇒ 组别跟随成绩页
|
||||||
examSubmissionsLoading.value = true
|
examSubmissionsLoading.value = true
|
||||||
try {
|
try {
|
||||||
const data = await adminApi.getSubmissions(scoreExportGroup.value)
|
const data = await adminApi.getSubmissions(
|
||||||
|
scoreExportGroup.value,
|
||||||
|
examSubsIncludeIdle.value ? { includeIdle: 1 } : undefined
|
||||||
|
)
|
||||||
examSubmissions.value = Array.isArray(data) ? data : (data?.list || [])
|
examSubmissions.value = Array.isArray(data) ? data : (data?.list || [])
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('获取答题记录失败', e)
|
console.warn('获取答题记录失败', e)
|
||||||
@@ -4070,6 +4116,12 @@ function onExamSubsFilterChange() {
|
|||||||
examSubmissionsPage.value = 1
|
examSubmissionsPage.value = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// R195:服务端过滤(includeIdle),切换需重新请求
|
||||||
|
async function onExamSubsIncludeIdleChange() {
|
||||||
|
examSubmissionsPage.value = 1
|
||||||
|
await fetchExamSubmissions()
|
||||||
|
}
|
||||||
|
|
||||||
// R30:原「考生答题评分弹窗」的 state 与 openExamScoreDialog / saveExamScore 已整体移除。
|
// R30:原「考生答题评分弹窗」的 state 与 openExamScoreDialog / saveExamScore 已整体移除。
|
||||||
// 大题评分改由「考生大题作答查证」按小问给分(adminApi.setBigAnswerScores
|
// 大题评分改由「考生大题作答查证」按小问给分(adminApi.setBigAnswerScores
|
||||||
// → service.bigQuestion.setSubScores → service.admin.updateScores),
|
// → service.bigQuestion.setSubScores → service.admin.updateScores),
|
||||||
@@ -4182,8 +4234,10 @@ const submissionStatusCount = computed(() => {
|
|||||||
const filteredSubmissions = computed(() => {
|
const filteredSubmissions = computed(() => {
|
||||||
let list = submissions.value
|
let list = submissions.value
|
||||||
if (submissionSearch.value) {
|
if (submissionSearch.value) {
|
||||||
|
// R194:行已按小组聚合 ⇒ 搜索口径改为「小组编号 / 作品目录」(姓名不再展示)
|
||||||
const s = submissionSearch.value.trim().toLowerCase()
|
const s = submissionSearch.value.trim().toLowerCase()
|
||||||
list = list.filter(item => (item.name || '').toLowerCase().includes(s))
|
list = list.filter(item => String(item.teamFolder || '').toLowerCase().includes(s)
|
||||||
|
|| String(item.teamNumber || '').toLowerCase().includes(s))
|
||||||
}
|
}
|
||||||
// 🔴 R178 同「成绩总览」:⛔ 不用 `!== ''` 判断是否启用筛选(清空值可能是 undefined),
|
// 🔴 R178 同「成绩总览」:⛔ 不用 `!== ''` 判断是否启用筛选(清空值可能是 undefined),
|
||||||
// 只用白名单;否则点「清除」后会静默变成「只看未提交」。
|
// 只用白名单;否则点「清除」后会静默变成「只看未提交」。
|
||||||
@@ -4199,13 +4253,77 @@ const pagedSubmissions = computed(() => {
|
|||||||
return filteredSubmissions.value.slice(start, start + submissionsPageSize.value)
|
return filteredSubmissions.value.slice(start, start + submissionsPageSize.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把后端 getWorks()「按人返回」的结果折叠成「按小组一行」(R194)。
|
||||||
|
*
|
||||||
|
* 【业务来源】用户实测:比赛以小组为准,但管理端把同组 6 个人平铺成 6 行,
|
||||||
|
* 其中 5 行是「未提交」,管理人员无法一眼判断「这个小组到底交没交」。
|
||||||
|
* 管理侧只关心:小组编号 → 是否已提交 → 文件与评分。
|
||||||
|
*
|
||||||
|
* 【分组键】与 R190 组锁 / 落盘口径**保持一致**,不另立一套:
|
||||||
|
* teamUid(有队伍号)→ aggregationUid(无队伍号的单人)→ contestantId(兜底,
|
||||||
|
* 保证任何一行都不会被吞掉——漏行比多行更难被发现)。
|
||||||
|
* ⚠️ teamUid 仅**组内**唯一且可重复,但本函数只在单一 group 的列表内聚合
|
||||||
|
* (submissionGroup 单选),故不存在跨组串号。
|
||||||
|
*
|
||||||
|
* @param {Array<Object>} people - getWorks 的选手级数据
|
||||||
|
* @returns {Array<Object>} 小组级行
|
||||||
|
*/
|
||||||
|
function groupSubmissionsByTeam(people) {
|
||||||
|
const buckets = new Map()
|
||||||
|
people.forEach(p => {
|
||||||
|
const key = (p.teamUid !== null && p.teamUid !== undefined)
|
||||||
|
? `t:${p.teamUid}`
|
||||||
|
: (p.aggregationUid !== null && p.aggregationUid !== undefined)
|
||||||
|
? `a:${p.aggregationUid}`
|
||||||
|
: `c:${p.contestantId}`
|
||||||
|
let g = buckets.get(key)
|
||||||
|
if (!g) {
|
||||||
|
g = {
|
||||||
|
teamKey: key,
|
||||||
|
teamUid: p.teamUid,
|
||||||
|
teamNumber: p.teamNumber,
|
||||||
|
teamFolder: p.teamFolder,
|
||||||
|
aggregationUid: p.aggregationUid,
|
||||||
|
memberCount: 0,
|
||||||
|
files: [],
|
||||||
|
_zipLoading: false,
|
||||||
|
_order: p.contestantId,
|
||||||
|
}
|
||||||
|
buckets.set(key, g)
|
||||||
|
}
|
||||||
|
g.memberCount += 1
|
||||||
|
// contestantId 必须跟着文件走:批量保存评分要按「人」回写 wordScore / pptScore
|
||||||
|
// _uploaderName 同理:聚合后要靠它回答「这组是谁提交的」
|
||||||
|
;(p.files || []).forEach(f => g.files.push({ ...f, contestantId: p.contestantId, _uploaderName: p.name }))
|
||||||
|
})
|
||||||
|
|
||||||
|
const list = Array.from(buckets.values())
|
||||||
|
list.forEach(g => {
|
||||||
|
// 跨成员合并后重排一次:后端按 upload_time DESC,合并会打乱顺序
|
||||||
|
g.files.sort((a, b) => String(b.uploadedAt || '').localeCompare(String(a.uploadedAt || '')))
|
||||||
|
// 🔴 提交人 = 归属人,口径与 R190 组锁一致(**最早提交者**)。
|
||||||
|
// 组锁下正常只有一人能提交,这里取最早者是为了兼容历史越权上传留下的多份文件。
|
||||||
|
const owners = g.files.filter(f => f._uploaderName)
|
||||||
|
if (owners.length) {
|
||||||
|
const ts = f => (f.uploadedAt ? String(f.uploadedAt) : '9999-12-31 23:59:59')
|
||||||
|
owners.sort((a, b) => ts(a).localeCompare(ts(b)))
|
||||||
|
g.submitterName = owners[0]._uploaderName
|
||||||
|
} else {
|
||||||
|
g.submitterName = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
list.sort((a, b) => a._order - b._order)
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchSubmissions() {
|
async function fetchSubmissions() {
|
||||||
if (!submissionGroup.value) return
|
if (!submissionGroup.value) return
|
||||||
submissionsLoading.value = true
|
submissionsLoading.value = true
|
||||||
try {
|
try {
|
||||||
const data = await adminApi.getWorks(submissionGroup.value)
|
const data = await adminApi.getWorks(submissionGroup.value)
|
||||||
const raw = Array.isArray(data) ? data : (data?.list || [])
|
const raw = Array.isArray(data) ? data : (data?.list || [])
|
||||||
submissions.value = raw.map(s => ({
|
const people = raw.map(s => ({
|
||||||
contestantId: s.contestantId,
|
contestantId: s.contestantId,
|
||||||
name: s.name,
|
name: s.name,
|
||||||
idNumber: s.idNumber,
|
idNumber: s.idNumber,
|
||||||
@@ -4218,6 +4336,7 @@ async function fetchSubmissions() {
|
|||||||
aggregationUid: s.aggregationUid === undefined ? null : s.aggregationUid,
|
aggregationUid: s.aggregationUid === undefined ? null : s.aggregationUid,
|
||||||
files: (s.files || []).map(f => ({ ...f, _score: f.score != null ? Number(f.score) : null })),
|
files: (s.files || []).map(f => ({ ...f, _score: f.score != null ? Number(f.score) : null })),
|
||||||
}))
|
}))
|
||||||
|
submissions.value = groupSubmissionsByTeam(people)
|
||||||
} catch (e) { console.warn('获取作品列表失败', e) } finally { submissionsLoading.value = false }
|
} catch (e) { console.warn('获取作品列表失败', e) } finally { submissionsLoading.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4256,7 +4375,14 @@ async function previewWork(row, f) {
|
|||||||
if (previewIsPdf.value) {
|
if (previewIsPdf.value) {
|
||||||
const r = await fetchBlobGuarded(fileDownloadUrl(submissionGroup.value, f))
|
const r = await fetchBlobGuarded(fileDownloadUrl(submissionGroup.value, f))
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
previewBlobUrl.value = URL.createObjectURL(r.blob)
|
// 🔴 R194(用户实测:预览 PDF 时浏览器反复下载「无扩展名文件」):
|
||||||
|
// 下载端点为了让「点下载」必然落盘,统一声明 Content-Type: application/octet-stream
|
||||||
|
// (见后端 admin.js#downloadSubmissionFile)。axios 的 blob 会**继承该响应头**,
|
||||||
|
// 于是 blob: URL 的 MIME 也是 octet-stream ⇒ iframe 无法渲染、Chrome 直接把它当
|
||||||
|
// 附件下载,弹窗内就是一片空白。
|
||||||
|
// 修法:预览通道把同一份字节重新包成 application/pdf(只改本地 Blob 的 type,
|
||||||
|
// 不碰后端契约,下载按钮行为不变)。字节完整性已由 fetchBlobGuarded 校验过。
|
||||||
|
previewBlobUrl.value = URL.createObjectURL(new Blob([r.blob], { type: 'application/pdf' }))
|
||||||
} else if (r.ignored) {
|
} else if (r.ignored) {
|
||||||
// 同一文件正在预览加载,被去重忽略——静默跳过,不报错
|
// 同一文件正在预览加载,被去重忽略——静默跳过,不报错
|
||||||
} else {
|
} else {
|
||||||
@@ -4271,6 +4397,16 @@ function closePreview() {
|
|||||||
previewRow.value = null
|
previewRow.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* R194:把当前预览的 blob 交给新标签页打开(iframe 空白时的确定出口)。
|
||||||
|
* ⚠️ 不要在这里 revoke:新标签可能仍在加载,revoke 会让它拿到空文档。
|
||||||
|
*/
|
||||||
|
function openPreviewInNewTab() {
|
||||||
|
if (!previewBlobUrl.value) return
|
||||||
|
const w = window.open(previewBlobUrl.value, '_blank')
|
||||||
|
if (!w) ElMessage.warning('浏览器拦截了新标签,请允许本站弹窗后重试')
|
||||||
|
}
|
||||||
|
|
||||||
// 批量下载作品(ZIP)
|
// 批量下载作品(ZIP)
|
||||||
async function batchDownloadWorks() {
|
async function batchDownloadWorks() {
|
||||||
if (!submissionGroup.value) return
|
if (!submissionGroup.value) return
|
||||||
@@ -4312,7 +4448,10 @@ async function batchSaveWorkScores() {
|
|||||||
submissions.value.forEach(s => {
|
submissions.value.forEach(s => {
|
||||||
;(s.files || []).forEach(f => {
|
;(s.files || []).forEach(f => {
|
||||||
if (f._score == null || f._score === '') return
|
if (f._score == null || f._score === '') return
|
||||||
const c = (perContestant[s.contestantId] = perContestant[s.contestantId] || {})
|
// R194:行已按小组聚合 ⇒ 回写主体不是「行」而是「文件所属选手」,
|
||||||
|
// contestantId 由 groupSubmissionsByTeam 逐文件注入(⛔ 不要读 s.contestantId)
|
||||||
|
if (f.contestantId == null) return
|
||||||
|
const c = (perContestant[f.contestantId] = perContestant[f.contestantId] || {})
|
||||||
if (f.scoreField === 'ppt') c.pptScore = Number(f._score)
|
if (f.scoreField === 'ppt') c.pptScore = Number(f._score)
|
||||||
else c.wordScore = Number(f._score)
|
else c.wordScore = Number(f._score)
|
||||||
})
|
})
|
||||||
@@ -4536,6 +4675,9 @@ watch(scoreExportGroup, () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const scoreList = ref([])
|
const scoreList = ref([])
|
||||||
|
// R195:默认只看「参赛过的人」。中职/高职按小组参赛,一组报名多人只一人上场,
|
||||||
|
// 未上场组员在成绩表里各占一行 0 分 ⇒ 默认隐藏,需核对全量时打开本开关。
|
||||||
|
const scoreIncludeIdle = ref(false)
|
||||||
const scoreListLoading = ref(false)
|
const scoreListLoading = ref(false)
|
||||||
const scoreListPage = ref(1)
|
const scoreListPage = ref(1)
|
||||||
const scoreListPageSize = ref(10)
|
const scoreListPageSize = ref(10)
|
||||||
@@ -4590,6 +4732,12 @@ function onScoreFilterChange() {
|
|||||||
scoreListPage.value = 1
|
scoreListPage.value = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// R195:「显示未参赛」是**服务端过滤**(includeIdle 参数),切换必须重新拉数据,⛔ 不能只在本地过滤。
|
||||||
|
async function onScoreIncludeIdleChange() {
|
||||||
|
scoreListPage.value = 1
|
||||||
|
await fetchScoreList()
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchPublishStatus() {
|
async function fetchPublishStatus() {
|
||||||
if (!scoreExportGroup.value) return
|
if (!scoreExportGroup.value) return
|
||||||
try {
|
try {
|
||||||
@@ -4604,7 +4752,10 @@ async function fetchScoreList() {
|
|||||||
if (!scoreExportGroup.value) return
|
if (!scoreExportGroup.value) return
|
||||||
scoreListLoading.value = true
|
scoreListLoading.value = true
|
||||||
try {
|
try {
|
||||||
const data = await adminApi.getScores(scoreExportGroup.value)
|
const data = await adminApi.getScores(
|
||||||
|
scoreExportGroup.value,
|
||||||
|
scoreIncludeIdle.value ? { includeIdle: 1 } : undefined
|
||||||
|
)
|
||||||
scoreList.value = (Array.isArray(data) ? data : (data?.list || [])).map(item => ({
|
scoreList.value = (Array.isArray(data) ? data : (data?.list || [])).map(item => ({
|
||||||
...item,
|
...item,
|
||||||
idNumber: item.idNumber ?? item.id_number,
|
idNumber: item.idNumber ?? item.id_number,
|
||||||
@@ -5548,6 +5699,9 @@ onMounted(async () => {
|
|||||||
.score-inline { display: inline-flex; align-items: center; gap: 4px; color: #606266; font-size: 13px; }
|
.score-inline { display: inline-flex; align-items: center; gap: 4px; color: #606266; font-size: 13px; }
|
||||||
.score-input { width: 110px; }
|
.score-input { width: 110px; }
|
||||||
.preview-frame { width: 100%; height: 70vh; border: 0; background: #fff; }
|
.preview-frame { width: 100%; height: 70vh; border: 0; background: #fff; }
|
||||||
|
/* R194:预览操作条(新标签打开 + 空白兜底说明) */
|
||||||
|
.preview-bar { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
|
||||||
|
.preview-hint { color: #909399; font-size: 12px; line-height: 1.5; }
|
||||||
.preview-file-name { color: #303133; font-weight: 600; margin-bottom: 8px; }
|
.preview-file-name { color: #303133; font-weight: 600; margin-bottom: 8px; }
|
||||||
.work-summary { color: #606266; font-size: 13px; margin-left: 8px; }
|
.work-summary { color: #606266; font-size: 13px; margin-left: 8px; }
|
||||||
|
|
||||||
@@ -5561,6 +5715,8 @@ onMounted(async () => {
|
|||||||
color: #303133;
|
color: #303133;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
/* R194:小组副信息(参赛人数),只报数量、不列成员姓名 */
|
||||||
|
.team-meta { color: #909399; font-size: 12px; }
|
||||||
|
|
||||||
/* ==================== 统计卡片 ==================== */
|
/* ==================== 统计卡片 ==================== */
|
||||||
.stats-row { margin-bottom: 20px; }
|
.stats-row { margin-bottom: 20px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user