@@ -123,6 +123,16 @@
<!-- === === === = 选手管理 === === === = -- >
<!-- === === === = 选手管理 === === === = -- >
< template v-if = "activeMenu === 'accounts'" >
< template v-if = "activeMenu === 'accounts'" >
< h2 class = "section-heading" > 选手管理 < / h2 >
< h2 class = "section-heading" > 选手管理 < / h2 >
<!-- 二级视图切换 ( R84 续六 ) : 选手列表 ( 原有 ) / 小组视图 ( 新增 ) -- >
< div class = "view-switch" >
< el-radio-group v-model = "accountsView" size="small" @change="onAccountsViewChange" >
< el -radio -button value = "list" > 选手列表 < / el-radio-button >
< el-radio-button value = "teams" > 小组视图 < / el-radio-button >
< / el-radio-group >
< / div >
<!-- === == 视图一 : 选手列表 ( 原有 , 逻辑与筛选未改动 ) === == -- >
< template v-if = "accountsView === 'list'" >
< div class = "toolbar" >
< div class = "toolbar" >
< el-input v-model = "accountSearch" placeholder="搜索姓名/身份证号/手机号" clearable class="search-input" @change="onAccountFilterChange" / >
< el-input v-model = "accountSearch" placeholder="搜索姓名/身份证号/手机号" clearable class="search-input" @change="onAccountFilterChange" / >
< el-select v-model = "accountGroupFilter" placeholder="组别筛选" clearable @change="onAccountFilterChange" class="filter-select" >
< el-select v-model = "accountGroupFilter" placeholder="组别筛选" clearable @change="onAccountFilterChange" class="filter-select" >
@@ -195,6 +205,67 @@
@ size -change = " onAccountSizeChange "
@ size -change = " onAccountSizeChange "
/ >
/ >
< / div >
< / div >
< / template >
<!-- === == 视图二 : 小组视图 ( R84 续六新增 ) === == -- >
<!-- 需求 : 查看已激活 / 未激活小组 , 且必须携带小组所属学校名称 。
口径 : 小组键 = 组别 + 队伍号 ; 激活 = 全组成员均已激活 ;
学校 = aggregation . organization _cn ( 同一小组多校时并列显示并标 「 多校 」 ) 。 -- >
< template v-else >
< div class = "toolbar" >
< el-select v-model = "teamGroupFilter" placeholder="组别筛选" clearable @change="onTeamFilterChange" class="filter-select" >
< el -option label = "中职组" value = "zhongzhi" / >
< el-option label = "高职组" value = "gaozhi" / >
< el-option label = "本科组" value = "benke" / >
< el-option label = "教师组" value = "jiaoshi" / >
< / el-select >
< el-select v-model = "teamActivatedFilter" placeholder="激活状态" clearable @change="onTeamFilterChange" class="filter-select" >
< el -option label = "已激活(全组已激活)" value = "true" / >
< el-option label = "未激活(含部分激活)" value = "false" / >
< / el-select >
< el-checkbox v-model = "teamIncludeTest" class="import-test-flag" @change="onTeamFilterChange" >
含测试数据
< / el -checkbox >
< el-button size = "small" type = "info" plain :loading = "teamsLoading" @click ="fetchTeams" > 刷新 < / el -button >
< span class = "team-summary" v-if = "teamSummary" >
共 {{ teamSummary.total }} 个小组 · 已激活 {{ teamSummary.activated }} · 部分激活 {{ teamSummary.partial }} · 未激活 {{ teamSummary.inactive }}
| 成员 {{ teamSummary.memberActivated }} / {{ teamSummary.memberTotal }} 已激活
< / span >
< / div >
< el -table :data = "teams" stripe v-loading = "teamsLoading" class="admin-table" style="width: 100%" >
< el -table -column label = "组别" min -width = " 90 " align = "center" >
< template # default = "{ row }" > { { row . groupName } } < / template >
< / el-table-column >
< el-table-column label = "小组编号" min -width = " 210 " >
< template # default = "{ row }" > { { row . teamNumber || row . teamId || ( '队伍 #' + row . teamUid ) } } < / template >
< / el-table-column >
< el-table-column label = "所属学校" min -width = " 220 " >
< template # default = "{ row }" >
< span v-if = "row.schoolName" > {{ row.schoolName }} < / span >
< span v-else class = "team-school-missing" > — 未填写 < / span >
< el-tag v-if = "row.schoolConflict" type="warning" size="small" style="margin-left: 6px" > 多校 < / el -tag >
< / template >
< / el-table-column >
< el-table-column label = "成员数" min -width = " 90 " align = "center" >
< template # default = "{ row }" > { { row . memberCount } } < / template >
< / el-table-column >
< el-table-column label = "已激活成员" min -width = " 110 " align = "center" >
< template # default = "{ row }" > { { row . activatedCount } } / { { row . memberCount } } < / template >
< / el-table-column >
< el-table-column label = "状态" min -width = " 110 " align = "center" >
< template # default = "{ row }" >
< el-tag v-if = "row.status === 'activated'" type="success" size="small" > 已激活 < / el -tag >
< el-tag v-else-if = "row.status === 'partial'" type="warning" size="small" > 部分激活 < / el -tag >
< el-tag v-else type = "info" size = "small" > 未激活 < / el-tag >
< / template >
< / el-table-column >
< / el-table >
< el-empty
v-if = "!teamsLoading && teams.length === 0"
description = "没有符合条件的小组(可能被组别 / 激活状态 / 测试数据筛选排除)"
:image-size = "80"
/ >
< / template >
< / template >
< / template >
<!-- === === === = 试卷管理 === === === = -- >
<!-- === === === = 试卷管理 === === === = -- >
@@ -581,11 +652,23 @@
< 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" / >
< el-input v-model = "submissionSearch" placeholder="搜索选手姓名" clearable size="small" class="search-input" @input="onSubmissionFilterChange" / >
<!-- R87 : 提交状态筛选 。 判据与表格内 「 未提交 」 文案同源 ( row . files . length === 0 ) ;
选项文案与 「 成绩管理 · 成绩总览 」 的提交状态下拉逐字对齐 。 -- >
< el-select v-model = "submissionStatusFilter" placeholder="提交状态" clearable size="small" class="filter-select" @change="onSubmissionFilterChange" >
< el -option label = "已提交" value = "true" / >
< el-option label = "未提交" value = "false" / >
< / el-select >
<!-- R80 : 组别只剩本科 / 教师 ( 两组都需下载作品 ) ⇒ 原 「 仅 benke / jiaoshi 显示 」 判据已恒真 , 移除死判据 -- >
<!-- R80 : 组别只剩本科 / 教师 ( 两组都需下载作品 ) ⇒ 原 「 仅 benke / jiaoshi 显示 」 判据已恒真 , 移除死判据 -- >
<!-- R87 : 本按钮下载 「 该组全部作品 」 ( 后端全组 ZIP ) , ⚠ ️ 不受上方筛选影响 ⇒ 以 title 明示口径 -- >
< el-button
< el-button
type = "success"
type = "success"
v-guard = "() => batchDownloadWorks()"
v-guard = "() => batchDownloadWorks()"
title = "下载该组全部作品(不受上方筛选影响)"
> 批量下载作品 < / el-button >
> 批量下载作品 < / el-button >
< span class = "work-summary" v-if = "submissions.length" >
共 {{ submissionStatusCount.total }} 人 · 已提交 {{ submissionStatusCount.submitted }} · 未提交 {{ submissionStatusCount.unsubmitted }}
< template v-if = "submissionStatusFilter !== ''" > | 当前筛选出 {{ filteredSubmissions.length }} 人 < / template >
< / span >
< / div >
< / div >
< 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" / >
< el -table -column prop = "name" label = "选手姓名" width = "120" / >
@@ -1632,7 +1715,12 @@ function onMenuSelect(index) {
sessionStorage . setItem ( 'admin_activeMenu' , index )
sessionStorage . setItem ( 'admin_activeMenu' , index )
// 切换时加载对应数据
// 切换时加载对应数据
if ( index === 'dashboard' ) fetchStats ( )
if ( index === 'dashboard' ) fetchStats ( )
if ( index === 'accounts' ) fetchAccounts ( )
// 选手管理:默认加载选手列表;若用户当前停在「小组视图」页签则同时刷新小组数据,
// 避免切回该菜单时表格空白( R84 续六)
if ( index === 'accounts' ) {
fetchAccounts ( )
if ( accountsView . value === 'teams' ) fetchTeams ( )
}
if ( index === 'questions' && questionGroup . value ) {
if ( index === 'questions' && questionGroup . value ) {
// R29: 试卷管理只刷题库( 含大题库) ; 「考生答题记录 / 大题作答查证」已迁到成绩管理
// R29: 试卷管理只刷题库( 含大题库) ; 「考生答题记录 / 大题作答查证」已迁到成绩管理
refreshQuestionData ( true )
refreshQuestionData ( true )
@@ -1771,6 +1859,41 @@ const accountGroupFilter = ref('')
const accountActivatedFilter = ref ( '' )
const accountActivatedFilter = ref ( '' )
const accountDateRange = ref ( null )
const accountDateRange = ref ( null )
// ---- 小组视图( R84 续六)----
// 与「选手列表」共用二级页签 accountsView; 筛选条件独立, 互不干扰。
const accountsView = ref ( 'list' ) // 'list' | 'teams'
const teams = ref ( [ ] )
const teamsLoading = ref ( false )
const teamGroupFilter = ref ( '' )
const teamActivatedFilter = ref ( '' )
const teamIncludeTest = ref ( false )
const teamSummary = ref ( null )
async function fetchTeams ( ) {
teamsLoading . value = true
try {
const params = { }
if ( teamGroupFilter . value ) params . group = teamGroupFilter . value
if ( teamActivatedFilter . value !== '' ) params . activated = teamActivatedFilter . value
// 显式传 '0'/'1':后端只有 'true'/'1' 等真值字符串才认「含测试数据」,
// 缺省或其它值一律按「不含测试数据」处理(生产口径)。
params . includeTest = teamIncludeTest . value ? '1' : '0'
const data = await adminApi . getTeams ( params )
teams . value = ( data && data . list ) || [ ]
teamSummary . value = ( data && data . summary ) || null
} catch ( e ) {
console . warn ( '获取小组列表失败' , e )
} finally {
teamsLoading . value = false
}
}
function onTeamFilterChange ( ) { fetchTeams ( ) }
function onAccountsViewChange ( view ) {
if ( view === 'teams' ) fetchTeams ( )
}
async function fetchAccounts ( ) {
async function fetchAccounts ( ) {
accountsLoading . value = true
accountsLoading . value = true
try {
try {
@@ -2740,6 +2863,16 @@ const submissionGroup = ref('benke')
const submissionsPage = ref ( 1 )
const submissionsPage = ref ( 1 )
const submissionsPageSize = ref ( 10 )
const submissionsPageSize = ref ( 10 )
const submissionSearch = ref ( '' )
const submissionSearch = ref ( '' )
const submissionStatusFilter = ref ( '' )
// 作品管理「已提交」判据的单一真相源 —— 与表格内「未提交」文案( row.files.length === 0) 同源。
const isWorkSubmitted = row => ( row . files || [ ] ) . length > 0
const submissionStatusCount = computed ( ( ) => {
const total = submissions . value . length
const submitted = submissions . value . filter ( isWorkSubmitted ) . length
return { total , submitted , unsubmitted : total - submitted }
} )
const filteredSubmissions = computed ( ( ) => {
const filteredSubmissions = computed ( ( ) => {
let list = submissions . value
let list = submissions . value
@@ -2747,6 +2880,10 @@ const filteredSubmissions = computed(() => {
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 => ( item . name || '' ) . toLowerCase ( ) . includes ( s ) )
}
}
if ( submissionStatusFilter . value !== '' ) {
const wantSubmitted = submissionStatusFilter . value === 'true'
list = list . filter ( item => isWorkSubmitted ( item ) === wantSubmitted )
}
return list
return list
} )
} )
@@ -3670,6 +3807,17 @@ onMounted(() => {
align - items : center ;
align - items : center ;
}
}
/* R84 续六:选手管理二级视图切换(选手列表 / 小组视图) */
. view - switch { margin - bottom : 16 px ; }
. team - summary {
margin - left : auto ;
font - size : 12 px ;
color : # 606266 ;
line - height : 1.6 ;
}
/* 小组视图:未填写学校时的占位样式(区别于有值文本) */
. team - school - missing { color : # c0c4cc ; }
/* R30: `.exam-score-form` 相关样式已随「考生答题记录 · 评分」弹窗一并删除 */
/* R30: `.exam-score-form` 相关样式已随「考生答题记录 · 评分」弹窗一并删除 */
. search - input { width : 220 px ; }
. search - input { width : 220 px ; }
@@ -3686,6 +3834,7 @@ onMounted(() => {
. score - input { width : 110 px ; }
. score - input { width : 110 px ; }
. preview - frame { width : 100 % ; height : 70 vh ; border : 0 ; background : # fff ; }
. preview - frame { width : 100 % ; height : 70 vh ; border : 0 ; background : # fff ; }
. preview - file - name { color : # 303133 ; font - weight : 600 ; margin - bottom : 8 px ; }
. preview - file - name { color : # 303133 ; font - weight : 600 ; margin - bottom : 8 px ; }
. work - summary { color : # 606266 ; font - size : 13 px ; margin - left : 8 px ; }
/* ==================== 统计卡片 ==================== */
/* ==================== 统计卡片 ==================== */
. stats - row { margin - bottom : 20 px ; }
. stats - row { margin - bottom : 20 px ; }