feat(admin): 选手管理新增「小组视图」—— 激活状态与所属学校清单

- api/admin.js:新增 getTeams({ group, activated, includeTest })

- Admin.vue:列表/小组视图切换、组别 + 激活状态 + 含测试三筛选、汇总条、小组表格(所属学校 / 成员数 / 已激活 / 状态)

线上验证:R86 已部署(dist 157 文件 / 70 个 .gz),线上入口 index-DQf5Bj8S.js
This commit is contained in:
2026-10-09 09:33:07 +08:00
parent 680faacd1d
commit e7179bfe54
2 changed files with 132 additions and 1 deletions

View File

@@ -19,6 +19,15 @@ export function getAccounts(params) {
return request.get('/admin/accounts', { params })
}
/**
* 获取小组(团队)激活状态清单(R84 续六)
* @param {object} params - { group, activated, includeTest }
* @returns {Promise} { list, summary, filteredTotal, filters }
*/
export function getTeams(params) {
return request.get('/admin/teams', { params })
}
/**
* 导入 Excel 批量创建账号
* @param {FormData} formData - 包含 Excel 文件的 FormData

View File

@@ -123,6 +123,16 @@
<!-- ========== 选手管理 ========== -->
<template v-if="activeMenu === 'accounts'">
<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">
<el-input v-model="accountSearch" placeholder="搜索姓名/身份证号/手机号" clearable class="search-input" @change="onAccountFilterChange" />
<el-select v-model="accountGroupFilter" placeholder="组别筛选" clearable @change="onAccountFilterChange" class="filter-select">
@@ -197,6 +207,67 @@
</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 v-if="activeMenu === 'questions'">
<h2 class="section-heading">试卷管理</h2>
@@ -1632,7 +1703,12 @@ function onMenuSelect(index) {
sessionStorage.setItem('admin_activeMenu', index)
// 切换时加载对应数据
if (index === 'dashboard') fetchStats()
if (index === 'accounts') fetchAccounts()
// 选手管理:默认加载选手列表;若用户当前停在「小组视图」页签则同时刷新小组数据,
// 避免切回该菜单时表格空白(R84 续六)
if (index === 'accounts') {
fetchAccounts()
if (accountsView.value === 'teams') fetchTeams()
}
if (index === 'questions' && questionGroup.value) {
// R29:试卷管理只刷题库(含大题库);「考生答题记录 / 大题作答查证」已迁到成绩管理
refreshQuestionData(true)
@@ -1771,6 +1847,41 @@ const accountGroupFilter = ref('')
const accountActivatedFilter = ref('')
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() {
accountsLoading.value = true
try {
@@ -3670,6 +3781,17 @@ onMounted(() => {
align-items: center;
}
/* R84 续六:选手管理二级视图切换(选手列表 / 小组视图) */
.view-switch { margin-bottom: 16px; }
.team-summary {
margin-left: auto;
font-size: 12px;
color: #606266;
line-height: 1.6;
}
/* 小组视图:未填写学校时的占位样式(区别于有值文本) */
.team-school-missing { color: #c0c4cc; }
/* R30:`.exam-score-form` 相关样式已随「考生答题记录 · 评分」弹窗一并删除 */
.search-input { width: 220px; }