Compare commits
3 Commits
4fd04f106a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a6707cac3 | |||
| 138592155f | |||
| 1f4cfc280e |
@@ -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 })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -322,6 +322,42 @@ export function publicityExportUrl(group) {
|
|||||||
return `/api/admin/export-publicity/${group}`
|
return `/api/admin/export-publicity/${group}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 成绩排名表导出前的统计预检(R220,⛔ 不下载文件,只回统计)
|
||||||
|
*
|
||||||
|
* 排名表是「一行 = 一个参赛小组」的速览表,前端在下载前需要知道三件事:
|
||||||
|
* ① 有没有人未归属小组(会各自单独占一行);② 本组是否根本没有可导出的数据;
|
||||||
|
* ③ 整行底色是否生成失败(后端会自动降级为无色版,文件名带「-无底色」)。
|
||||||
|
* ③ 必须提示,否则管理员会以为「导出错了」而不是「降级了」。
|
||||||
|
*
|
||||||
|
* silent: true —— 属「预检」而非用户动作,失败由调用方按业务语境提示。
|
||||||
|
* @param {string} group - 组别标识(仅 zhongzhi / gaozhi 有效)
|
||||||
|
* @returns {Promise<{fileName:string,rowCount:number,colored:boolean,degradeReason:string,
|
||||||
|
* stats:Object,topN:number,totalContestants:number,noTeamCount:number}>}
|
||||||
|
*/
|
||||||
|
export function getScoreRankMeta(group) {
|
||||||
|
return request.get(`/admin/export-score-rank/${group}`, {
|
||||||
|
params: { meta: 1 },
|
||||||
|
silent: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载成绩排名表(R220)
|
||||||
|
*
|
||||||
|
* 与另外两个导出产物的分工(⛔ 不可互相替代):
|
||||||
|
* · export-excel —— 内部核查表,全字段明细(含身份证号 / 手机号);
|
||||||
|
* · export-publicity —— 对外公示表,主办方模板、获奖等级列留空;
|
||||||
|
* · export-score-rank—— 排名速览表,一小组一行 + 区间底色 + 区间统计,无任何敏感字段。
|
||||||
|
*
|
||||||
|
* 下载动作由 `utils/download.js#safeDownload` 发起(三重校验),故只导出 URL。
|
||||||
|
* @param {string} group
|
||||||
|
* @returns {string} 下载 URL
|
||||||
|
*/
|
||||||
|
export function scoreRankExportUrl(group) {
|
||||||
|
return `/api/admin/export-score-rank/${group}`
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 读取赛项信息(赛项名称 / 赛项编号)
|
* 读取赛项信息(赛项名称 / 赛项编号)
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -39,8 +39,12 @@ export function uploadWork(group, formData, onProgress) {
|
|||||||
// 注意:不要手动设置 Content-Type: multipart/form-data(不带 boundary 会导致
|
// 注意:不要手动设置 Content-Type: multipart/form-data(不带 boundary 会导致
|
||||||
// 后端 ctx.getFileStream() 报 Missing boundary 而 500)。交由 axios/浏览器自动
|
// 后端 ctx.getFileStream() 报 Missing boundary 而 500)。交由 axios/浏览器自动
|
||||||
// 补带 boundary 的 multipart 头。字段顺序:先文本字段(file_type/work_name)再 file。
|
// 补带 boundary 的 multipart 头。字段顺序:先文本字段(file_type/work_name)再 file。
|
||||||
|
// 🔴 R225:timeout 300s -> 600s。上传上限提至 150MB 后,服务器入口带宽约
|
||||||
|
// 1.25MB/s 且为全体考生共享,单传约 120s、数人并发时可达 400s+;
|
||||||
|
// 原 300s 会让「限额已放开」的文件改以超时失败。
|
||||||
|
// nginx 侧 /api/ 的 proxy_send_timeout/proxy_read_timeout 已是 600s,两者对齐。
|
||||||
return request.post(`/upload/${group}`, formData, {
|
return request.post(`/upload/${group}`, formData, {
|
||||||
timeout: 300000,
|
timeout: 600000,
|
||||||
onUploadProgress: (e) => {
|
onUploadProgress: (e) => {
|
||||||
if (onProgress && e.total) onProgress(Math.round((e.loaded / e.total) * 100))
|
if (onProgress && e.total) onProgress(Math.round((e.loaded / e.total) * 100))
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,8 +13,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const ALLOWED = ['.doc', '.docx', '.pdf', '.ppt', '.pptx', '.zip']
|
export const ALLOWED = ['.doc', '.docx', '.pdf', '.ppt', '.pptx', '.zip']
|
||||||
export const ZIP_MAX = 200 * 1024 * 1024
|
// 上传大小上限:统一 150MB
|
||||||
export const OTHER_MAX = 100 * 1024 * 1024
|
// 🔴 2026-10-23 R225:原 ZIP_MAX=200MB / OTHER_MAX=100MB,与后端
|
||||||
|
// config.multipart.fileSize(当时 '100mb')长期不一致 ⇒ 100~200MB 的 zip
|
||||||
|
// 前端放行、后端抛 MultipartFileTooLargeError,教师组因此报障。
|
||||||
|
// 现前后端统一为 150MB(后端已同步改为 '150mb'),保持「前端放行 == 后端接收」。
|
||||||
|
export const MAX_MB = 150
|
||||||
|
export const ZIP_MAX = MAX_MB * 1024 * 1024
|
||||||
|
export const OTHER_MAX = MAX_MB * 1024 * 1024
|
||||||
|
|
||||||
export function fileExt(name) {
|
export function fileExt(name) {
|
||||||
return (String(name).split('.').pop() || '').toLowerCase()
|
return (String(name).split('.').pop() || '').toLowerCase()
|
||||||
@@ -42,7 +48,7 @@ export function validateSingleFile(name, size) {
|
|||||||
const e = '.' + fileExt(name)
|
const e = '.' + fileExt(name)
|
||||||
if (!ALLOWED.includes(e)) return `不支持的格式:${name}`
|
if (!ALLOWED.includes(e)) return `不支持的格式:${name}`
|
||||||
const max = e === '.zip' ? ZIP_MAX : OTHER_MAX
|
const max = e === '.zip' ? ZIP_MAX : OTHER_MAX
|
||||||
if (size > max) return `${name} 超过大小限制(${e === '.zip' ? '200MB' : '100MB'})`
|
if (size > max) return `${name} 超过大小限制(${MAX_MB}MB)`
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
@@ -994,6 +1015,23 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
<!-- R29:由「导出CSV」改为直接导出真正的 Excel(.xlsx) -->
|
<!-- R29:由「导出CSV」改为直接导出真正的 Excel(.xlsx) -->
|
||||||
<el-button type="primary" plain v-guard="() => exportExcel()" :disabled="!scoreExportGroup">导出Excel</el-button>
|
<el-button type="primary" plain v-guard="() => exportExcel()" :disabled="!scoreExportGroup">导出Excel</el-button>
|
||||||
|
<!-- R220:成绩排名表(中职/高职专属)。
|
||||||
|
与「导出Excel」的区别:这个是**排名速览**——一行 = 一个参赛小组,
|
||||||
|
按小组客观题分降序**全量**导出(⛔ 不截断前 80,赞助小组也要在表里),
|
||||||
|
整行按客观题分区间上底色(≥60 绿 / 50–59.99 黄 / <50 红),
|
||||||
|
前 80 行用一个**红色外框整体圈住**(首行画上边、末行画下边,行间不画横线),
|
||||||
|
表尾给三区间小组数统计(全量 + 全量排名前 80)。
|
||||||
|
🔴 只出 6 列:名次/小组编号/学校/参赛选手/人数/客观题分(R220-C:不要大题分与总分)。
|
||||||
|
⛔ 不含身份证号 / 手机号,可直接给评审看。 -->
|
||||||
|
<el-button
|
||||||
|
v-if="isExamScoreGroup"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
:loading="scoreRankExporting"
|
||||||
|
v-guard="() => exportScoreRank()"
|
||||||
|
:disabled="!scoreExportGroup"
|
||||||
|
title="按小组客观题分降序导出全量排名表(整行按客观题分区间上底色,前80名用红框圈出,表尾含各区间小组数统计)"
|
||||||
|
>导出排名表</el-button>
|
||||||
<!-- R176:成绩公示表(半成品)。
|
<!-- R176:成绩公示表(半成品)。
|
||||||
与「导出Excel」的区别:导出Excel 是**内部工作用**的全字段明细;
|
与「导出Excel」的区别:导出Excel 是**内部工作用**的全字段明细;
|
||||||
公示表是**对外正式文书**,按主办方模板只留 7 列、按分数降序、
|
公示表是**对外正式文书**,按主办方模板只留 7 列、按分数降序、
|
||||||
@@ -1081,6 +1119,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 +1253,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 +4069,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 +4118,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 +4133,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 +4251,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 +4270,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 +4353,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 +4392,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 +4414,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 +4465,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 +4692,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 +4749,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 +4769,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,
|
||||||
@@ -4750,6 +4918,78 @@ async function exportPublicity() {
|
|||||||
} finally { publicityExporting.value = false }
|
} finally { publicityExporting.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 6.2 成绩排名表(R220) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排名表与另外两个导出产物的分工(⛔ 勿合并成同一个按钮):
|
||||||
|
* · 导出Excel —— **内部核查用**,全字段明细(含身份证号 / 手机号),不排序不裁剪;
|
||||||
|
* · 下载公示表 —— **对外公示用**,主办方模板 7 列、「获奖等级」列留空;
|
||||||
|
* · 导出排名表 —— **排名速览用**,一行 = 一个参赛小组、按小组客观题分降序**全量**导出、
|
||||||
|
* 整行按分数区间上底色、表尾含区间统计,无任何敏感字段。
|
||||||
|
* 用户明确要求「所有小组都导出」:赞助小组也在表里,⛔ 不截断前 80。
|
||||||
|
*/
|
||||||
|
const scoreRankExporting = ref(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出成绩排名表:先预检(?meta=1)再下载。
|
||||||
|
*
|
||||||
|
* 三件事必须在下载前摆到管理员面前,否则下载完才发现要重走一遍:
|
||||||
|
* ① 有人未归属小组(会各自单独占一行,行数会比预期多);
|
||||||
|
* ② 本组根本没有可导出的人(导出一张只有表头的空表);
|
||||||
|
* ③ 整行底色生成失败已降级为无色版(后端会自动降级,但**必须知情**,
|
||||||
|
* 否则管理员会以为「导出错了」)。
|
||||||
|
*/
|
||||||
|
async function exportScoreRank() {
|
||||||
|
const group = scoreExportGroup.value
|
||||||
|
if (!group) return
|
||||||
|
const label = groupMap[group] || group
|
||||||
|
|
||||||
|
let meta = null
|
||||||
|
try {
|
||||||
|
meta = await adminApi.getScoreRankMeta(group)
|
||||||
|
} catch (e) {
|
||||||
|
// 预检只是辅助信息,失败不阻断下载;也不再弹提示,避免与下载错误叠加
|
||||||
|
console.warn('成绩排名表预检失败', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
const warns = []
|
||||||
|
if (meta) {
|
||||||
|
if (meta.noTeamCount > 0) warns.push(`有 ${meta.noTeamCount} 名选手未归属任何小组,将各自单独占一行`)
|
||||||
|
if (!meta.rowCount) warns.push('本组没有任何可导出的小组,导出的将是一张只有表头的空表')
|
||||||
|
if (meta.colored === false) {
|
||||||
|
warns.push(`整行底色生成失败(${meta.degradeReason || '未知原因'}),将导出无底色版本(文件名带「-无底色」)`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (warns.length) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
h('div', null, [
|
||||||
|
h('p', { style: 'margin:0 0 8px' }, '导出前发现以下情况,请确认:'),
|
||||||
|
h('ul', { style: 'margin:0;padding-left:20px' }, warns.map(w => h('li', null, w))),
|
||||||
|
]),
|
||||||
|
`导出「${label}」成绩排名表`,
|
||||||
|
{
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '仍然导出',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
distinguishCancelAndClose: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scoreRankExporting.value = true
|
||||||
|
try {
|
||||||
|
await safeDownload(adminApi.scoreRankExportUrl(group), {
|
||||||
|
fallbackName: `成绩排名-${label}.xlsx`,
|
||||||
|
timeout: 120000,
|
||||||
|
})
|
||||||
|
} finally { scoreRankExporting.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 7. 新闻管理 ====================
|
// ==================== 7. 新闻管理 ====================
|
||||||
const adminNews = ref([])
|
const adminNews = ref([])
|
||||||
const adminNewsLoading = ref(false)
|
const adminNewsLoading = ref(false)
|
||||||
@@ -5548,6 +5788,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 +5804,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; }
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<el-alert type="info" :closable="false" show-icon style="margin-bottom:16px">
|
<el-alert type="info" :closable="false" show-icon style="margin-bottom:16px">
|
||||||
<template #title>本组别无客观题:先选择文件并确认无误,再点击「上传所选文件」提交</template>
|
<template #title>本组别无客观题:先选择文件并确认无误,再点击「上传所选文件」提交</template>
|
||||||
支持 docx / pdf / ppt / zip。规则:① zip 须单独上传(与任何其他文件互斥,自动覆盖);② pdf 与 docx 相互覆盖,只能保留其一;③ 若需同时提交文档与演示稿,请选「pdf + ppt」或「docx + ppt」组合。zip 上限 200MB,其余格式上限 100MB。
|
支持 docx / pdf / ppt / zip。规则:① zip 须单独上传(与任何其他文件互斥,自动覆盖);② pdf 与 docx 相互覆盖,只能保留其一;③ 若需同时提交文档与演示稿,请选「pdf + ppt」或「docx + ppt」组合。单个文件上限 150MB。
|
||||||
</el-alert>
|
</el-alert>
|
||||||
|
|
||||||
<!-- R190:小组作品状态(组内互斥)—— 组内已有人上传后,其他成员不能再传。
|
<!-- R190:小组作品状态(组内互斥)—— 组内已有人上传后,其他成员不能再传。
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
show-icon
|
show-icon
|
||||||
style="margin-top:10px"
|
style="margin-top:10px"
|
||||||
title="存在超出大小限制的文件"
|
title="存在超出大小限制的文件"
|
||||||
description="标红的文件已超过上传上限(zip 200MB / 其他 100MB),请删除后重新选择符合要求的文件再提交。"
|
description="标红的文件已超过上传上限(单个文件 150MB),请删除后重新选择符合要求的文件再提交。"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -432,6 +432,7 @@ import {
|
|||||||
validateSingleFile,
|
validateSingleFile,
|
||||||
finalComboCheck,
|
finalComboCheck,
|
||||||
reconcileFiles,
|
reconcileFiles,
|
||||||
|
MAX_MB,
|
||||||
} from '@/utils/uploadCombo'
|
} from '@/utils/uploadCombo'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -472,8 +473,9 @@ const teamPanelVisible = computed(() => !!uploadStatus.value.teamNumber || uploa
|
|||||||
// 两处同列会重复且按钮语义不同,容易误删。
|
// 两处同列会重复且按钮语义不同,容易误删。
|
||||||
const teamSharedFiles = computed(() => (teamLocked.value ? (uploadStatus.value.teamFiles || []) : []))
|
const teamSharedFiles = computed(() => (teamLocked.value ? (uploadStatus.value.teamFiles || []) : []))
|
||||||
|
|
||||||
const ZIP_MAX = 200 * 1024 * 1024
|
// 大小上限唯一来源 = uploadCombo.MAX_MB(与后端 config.multipart.fileSize 对齐)
|
||||||
const OTHER_MAX = 100 * 1024 * 1024
|
// 🔴 R225:原先此处另存一份 ZIP_MAX=200MB / OTHER_MAX=100MB 的副本,已删除
|
||||||
|
// —— 重复常量是「前端放行、后端拒收」漂移的温床,改为单点引用。
|
||||||
|
|
||||||
function fileTypeOf(name) {
|
function fileTypeOf(name) {
|
||||||
const e = '.' + fileExt(name)
|
const e = '.' + fileExt(name)
|
||||||
@@ -493,7 +495,7 @@ const uploadComboValid = computed(() => {
|
|||||||
const pendingUploadFiles = computed(() => uploadFileList.value.filter(f => !f.uploaded && !isOverLimit(f)))
|
const pendingUploadFiles = computed(() => uploadFileList.value.filter(f => !f.uploaded && !isOverLimit(f)))
|
||||||
const hasOverLimitFile = computed(() => uploadFileList.value.some(f => isOverLimit(f)))
|
const hasOverLimitFile = computed(() => uploadFileList.value.some(f => isOverLimit(f)))
|
||||||
|
|
||||||
// 单文件是否超过上传大小限制(zip 200MB / 其他 100MB,来自 uploadCombo.validateSingleFile)
|
// 单文件是否超过上传大小限制(150MB,来自 uploadCombo.validateSingleFile)
|
||||||
function isOverLimit(file) {
|
function isOverLimit(file) {
|
||||||
const f = file.raw || file
|
const f = file.raw || file
|
||||||
return validateSingleFile(f.name, f.size) !== null
|
return validateSingleFile(f.name, f.size) !== null
|
||||||
@@ -511,7 +513,7 @@ function onUploadFileChange(file, fileList) {
|
|||||||
const over = uploadFileList.value.filter(f => isOverLimit(f))
|
const over = uploadFileList.value.filter(f => isOverLimit(f))
|
||||||
if (over.length) {
|
if (over.length) {
|
||||||
const names = over.map(f => (f.raw || f).name).join('、')
|
const names = over.map(f => (f.raw || f).name).join('、')
|
||||||
ElMessage.error(`文件「${names}」已超过大小限制(zip 200MB / 其他 100MB),已标红且无法提交`)
|
ElMessage.error(`文件「${names}」已超过大小限制(单个文件 ${MAX_MB}MB),已标红且无法提交`)
|
||||||
} else if (r.dropped.length) {
|
} else if (r.dropped.length) {
|
||||||
ElMessage.info('已按规则自动覆盖冲突文件(zip 独占 / pdf 与 docx 互斥)')
|
ElMessage.info('已按规则自动覆盖冲突文件(zip 独占 / pdf 与 docx 互斥)')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user