轮播管理
添加轮播
- 保存排序
+ 保存排序
拖拽卡片可调整排序
@@ -1563,13 +1584,18 @@ function onMenuSelect(index) {
if (index === 'dashboard') fetchStats()
if (index === 'accounts') fetchAccounts()
if (index === 'questions' && questionGroup.value) {
- // 单一刷新入口:题库 + 考生答题记录 +(大题库 / 大题作答)
+ // R29:试卷管理只刷题库(含大题库);「考生答题记录 / 大题作答查证」已迁到成绩管理
refreshQuestionData(true)
}
if (index === 'materials') fetchAdminMaterials()
if (index === 'submissions' && submissionGroup.value) fetchSubmissions()
if (index === 'times') fetchTimes()
- if (index === 'scores') { fetchPublishStatus(); fetchScoreList() }
+ if (index === 'scores') {
+ fetchPublishStatus()
+ fetchScoreList()
+ // R29:成绩管理还承载两个由试卷管理迁入的板块(仅中职/高职)
+ refreshExamCtxData(true)
+ }
if (index === 'news') fetchAdminNews()
if (index === 'carousel') fetchCarousels()
}
@@ -2056,7 +2082,8 @@ function onBigAnswerSelectionChange(rows) {
* ⚠️ 与同页「导出选中/全部 Word」(exportBigQuestions,导出的是**题库题目**)是两个功能,勿混。
*/
async function exportBigAnswers(ids) {
- const g = questionGroup.value
+ // R29:本板块已迁入「成绩管理」⇒ 组别跟随成绩页
+ const g = scoreExportGroup.value
const groupLabel = g === 'zhongzhi' ? '中职组' : '高职组'
const isAll = !Array.isArray(ids)
// 兜底:剔除非本组 / 未抽题的残留 ID(与 exportBigQuestions 同源处理)。
@@ -2086,20 +2113,21 @@ async function exportBigAnswers(ids) {
}
async function fetchBigAnswers() {
- // 同 fetchBigQuestions:固定发起时的组别,防止切组竞态把别组数据写回
- const g = questionGroup.value
+ // R29:本板块已由「试卷管理」迁到「成绩管理」⇒ 组别跟随成绩页的 scoreExportGroup(中职/高职)。
+ // 仍固定发起时的组别,防止切组竞态把别组数据写回。
+ const g = scoreExportGroup.value
bigAnswersLoading.value = true
try {
const data = await adminApi.getBigAnswers(g)
const result = data?.data || data
- if (questionGroup.value !== g) return
+ if (scoreExportGroup.value !== g) return
bigAnswers.value = result?.list || []
bigAnswersTotal.value = Number(result?.total) || bigAnswers.value.length
bigAnswersDrawn.value = Number(result?.drawnCount) || 0
if (Number(result?.subCount) > 0) bigSubCount.value = Number(result.subCount)
} catch (e) {
console.warn('获取大题作答失败', e)
- if (questionGroup.value !== g) return
+ if (scoreExportGroup.value !== g) return
bigAnswers.value = []
bigAnswersTotal.value = 0
bigAnswersDrawn.value = 0
@@ -2118,26 +2146,46 @@ async function fetchBigAnswers() {
*/
const bigDataGroup = ref('')
-/** 保证大题库 + 大题作答与当前组别一致(同组且非强制时不重复请求) */
+/** 保证大题库与当前组别一致(同组且非强制时不重复请求) */
async function ensureBigData(force = false) {
const g = questionGroup.value
if (!force && bigDataGroup.value === g && bigQuestions.value.length > 0) return
- await Promise.allSettled([fetchBigQuestions(), fetchBigAnswers()])
+ await fetchBigQuestions()
if (questionGroup.value === g) bigDataGroup.value = g
}
/**
- * 刷新「题目管理」菜单下与组别相关的全部数据(2026-09-23)。
+ * 刷新「试卷管理」菜单下的数据(R29 收敛)。
*
- * 原先各入口各刷一半(切组只刷题库、评分只刷大题作答),导致另一处仍是旧值,
- * 测试人员只能手动刷新整页。此处收敛为**唯一刷新入口**,避免再出现遗漏。
+ * ⚠️ R29 起「考生答题记录 / 大题作答查证」已迁到「成绩管理」,故此处**只刷题库**
+ * (含大题库);那两块改由 refreshExamCtxData 负责 —— 原来一个入口刷三块,
+ * 迁走后若仍在这里刷,就会用**试卷页的组别**去拉**成绩页的板块**,必然串组。
*/
async function refreshQuestionData(force = false) {
- await Promise.allSettled([
- fetchQuestions(),
- fetchExamSubmissions(),
- ensureBigData(force),
- ])
+ await Promise.allSettled([fetchQuestions(), ensureBigData(force)])
+}
+
+/** 「考生答题记录 / 大题作答查证」数据当前所属组别(用于避免重复拉取) */
+const examCtxDataGroup = ref('')
+
+/**
+ * 刷新成绩管理页内两个由试卷管理迁入的板块(R29)。
+ * 仅中职/高职有客观题与综合大题;本科/教师是 Word/PPT 作品,无此二块 ⇒ 直接跳过。
+ */
+async function refreshExamCtxData(force = false) {
+ const g = scoreExportGroup.value
+ if (g !== 'zhongzhi' && g !== 'gaozhi') return
+ if (!force && examCtxDataGroup.value === g) return
+ await Promise.allSettled([fetchExamSubmissions(), fetchBigAnswers()])
+ if (scoreExportGroup.value === g) examCtxDataGroup.value = g
+}
+
+/**
+ * 刷新「成绩管理」页全部数据(R29 收敛):成绩明细 + 两个迁入板块。
+ * 评分保存后必须走这里,否则明细表与答题记录会出现「一处已更新、一处还是旧分」。
+ */
+async function refreshScorePageData(force = false) {
+ await Promise.allSettled([fetchScoreList(), refreshExamCtxData(force)])
}
/** ==================== 大题导出 Word(2026-09-23) ==================== */
@@ -2225,16 +2273,17 @@ async function saveBigScores() {
}
bigScoreSaving.value = true
try {
- const res = await adminApi.setBigAnswerScores(questionGroup.value, row.aggregationUid, {
+ // R29:组别跟随「成绩管理」页(本弹窗已随板块迁入成绩管理)
+ const res = await adminApi.setBigAnswerScores(scoreExportGroup.value, row.aggregationUid, {
score_1: Number(bigScoreForm.score_1) || 0,
score_2: Number(bigScoreForm.score_2) || 0,
score_3: Number(bigScoreForm.score_3) || 0,
})
ElMessage.success(`评分已保存(大题合计 ${res?.total ?? bigScoreTotal.value} 分)`)
bigAnswerDialogVisible.value = false
- // 一次刷新全部相关列表:大题作答(含大题得分列)+ 考生答题记录(卷面分/大题分)。
- // 原实现只调 fetchBigAnswers(),切到「考生答题记录」仍是旧分数 ⇒ 测试人员只能刷整页。
- await refreshQuestionData(true)
+ // 一次刷新全部相关列表:成绩明细(总分 = 客观分 + 大题分)+ 大题作答 + 考生答题记录。
+ // 原实现只调 fetchBigAnswers(),测试人员看到另一处仍是旧分只能刷整页。
+ await refreshScorePageData(true)
} catch (e) {
ElMessage.error(e?.message || '评分保存失败')
} finally {
@@ -2453,9 +2502,10 @@ const pagedExamSubmissions = computed(() => {
})
async function fetchExamSubmissions() {
+ // R29:本板块已迁入「成绩管理」⇒ 组别跟随成绩页
examSubmissionsLoading.value = true
try {
- const data = await adminApi.getSubmissions(questionGroup.value)
+ const data = await adminApi.getSubmissions(scoreExportGroup.value)
examSubmissions.value = Array.isArray(data) ? data : (data?.list || [])
} catch (e) {
console.warn('获取答题记录失败', e)
@@ -2481,7 +2531,8 @@ async function saveExamScore() {
if (!examScoreDialogRow.value) return
examScoreSaving.value = true
try {
- await adminApi.setScores(questionGroup.value, {
+ // R29:组别跟随「成绩管理」页
+ await adminApi.setScores(scoreExportGroup.value, {
scores: [{
contestantId: examScoreDialogRow.value.contestantId,
// 客观分回传弹窗所见值,避免后端按答题记录重算导致与所见不一致
@@ -2491,8 +2542,8 @@ async function saveExamScore() {
})
ElMessage.success('评分已保存')
examScoreDialogVisible.value = false
- // 同上:客观分 / 大题分同时影响两张表,必须一起刷新(原仅刷考生答题记录)
- await refreshQuestionData(true)
+ // 客观分 / 大题分同时影响成绩明细表与考生答题记录,必须一起刷新
+ await refreshScorePageData(true)
} catch (e) { ElMessage.error('保存失败') } finally { examScoreSaving.value = false }
}
@@ -2503,6 +2554,12 @@ watch(questionGroup, () => {
if (activeMenu.value === 'questions') refreshQuestionData(true)
})
+// R29:成绩管理页切组 → 刷新成绩明细 + 两个由试卷管理迁入的板块
+// (这两个板块的组别跟随 scoreExportGroup,故其刷新必须挂在这里)
+watch(scoreExportGroup, () => {
+ if (activeMenu.value === 'scores') refreshExamCtxData(true)
+})
+
// ==================== 3.5 备赛资料管理 ====================
const materialsGroup = ref('benke')
const adminMaterials = ref([])
@@ -2691,7 +2748,8 @@ async function batchSaveWorkScores() {
ElMessage.success('评分保存成功')
// 保存后立即刷新作品列表 + 考生答题记录,保证两处分数同步可见(评分不同步修复)
await fetchSubmissions()
- if (submissionGroup.value === questionGroup.value) fetchExamSubmissions()
+ // R29:「考生答题记录」的组别已改跟随成绩管理页(scoreExportGroup)
+ if (submissionGroup.value === scoreExportGroup.value) fetchExamSubmissions()
} catch (e) { ElMessage.error('保存失败') }
}
@@ -2750,6 +2808,12 @@ async function saveTimes() {
// ==================== 6. 成绩管理 ====================
const scoresPublishedMap = reactive({})
const scoreExportGroup = ref('')
+/**
+ * 是否为「考试组」(中职 / 高职)——R29 由「试卷管理」迁入的两个板块
+ * (考生答题记录 / 考生大题作答查证)只对这两组有意义:
+ * 本科 / 教师组提交的是 Word / PPT 作品,没有客观题与综合大题数据。
+ */
+const isExamScoreGroup = computed(() => scoreExportGroup.value === 'zhongzhi' || scoreExportGroup.value === 'gaozhi')
const scoreList = ref([])
const scoreListLoading = ref(false)
const scoreListPage = ref(1)
@@ -2835,10 +2899,11 @@ async function togglePublish() {
} catch (e) { ElMessage.error('操作失败') }
}
-async function exportCsv() {
- // safeDownload 自带「HTTP 状态 + content-type」双重校验,避免把错误 JSON 存成 .csv
- await safeDownload(`/api/admin/export-csv/${scoreExportGroup.value}`, {
- fallbackName: `${scoreExportGroup.value}_scores.csv`,
+async function exportExcel() {
+ // R29:由 CSV 改为直接导出 .xlsx(CSV 在 Excel 中中文易乱码、身份证/手机号变科学计数法)
+ // safeDownload 自带「HTTP 状态 + content-type」双重校验,避免把错误 JSON 存成 .xlsx
+ await safeDownload(`/api/admin/export-excel/${scoreExportGroup.value}`, {
+ fallbackName: `${scoreExportGroup.value}_scores.xlsx`,
})
}
@@ -3477,6 +3542,15 @@ onMounted(() => {
align-items: center;
}
+/* R29:评分弹窗标签左对齐且禁止折行(「客观分(卷面)」曾折行成两行导致错位) */
+.exam-score-form .el-form-item__label {
+ text-align: left;
+ white-space: nowrap;
+}
+.exam-score-form .exam-score-readonly {
+ color: #606266;
+}
+
.search-input { width: 220px; }
.filter-select { width: 140px; }