增强筛选
This commit is contained in:
@@ -40,6 +40,13 @@ const editingRoundId = ref('')
|
||||
const editingOfferingId = ref('')
|
||||
const roster = ref<any | null>(null)
|
||||
const rosterLoading = ref(false)
|
||||
const rosterKeyword = ref('')
|
||||
const rosterGrade = ref<number | undefined>()
|
||||
const rosterMajorId = ref<string | undefined>()
|
||||
const rosterClassId = ref<string | undefined>()
|
||||
const rosterStudentPage = ref(1)
|
||||
const rosterWaitlistPage = ref(1)
|
||||
const rosterPageSize = 20
|
||||
const eligibleStudents = ref<any[]>([])
|
||||
const eligibleTotal = ref(0)
|
||||
const eligiblePage = ref(1)
|
||||
@@ -82,6 +89,8 @@ const selectedOfferings = computed(() =>
|
||||
const previewOffering = computed(() =>
|
||||
offerings.value.find((item) => item.id === previewOfferingId.value) ?? null,
|
||||
)
|
||||
const rosterClasses = computed(() => (roster.value?.filterOptions?.classes ?? [])
|
||||
.filter((item: any) => !rosterMajorId.value || item.majorId === rosterMajorId.value))
|
||||
const selectableTasks = computed(() => {
|
||||
const usedTaskIds = new Set(
|
||||
offerings.value
|
||||
@@ -540,6 +549,12 @@ async function deleteOffering(offering: any) {
|
||||
}
|
||||
|
||||
async function showRoster(offering: any) {
|
||||
rosterKeyword.value = ''
|
||||
rosterGrade.value = undefined
|
||||
rosterMajorId.value = undefined
|
||||
rosterClassId.value = undefined
|
||||
rosterStudentPage.value = 1
|
||||
rosterWaitlistPage.value = 1
|
||||
rosterDrawer.value = true
|
||||
await loadRoster(offering.id)
|
||||
}
|
||||
@@ -548,7 +563,18 @@ async function loadRoster(offeringId: string) {
|
||||
rosterLoading.value = true
|
||||
try {
|
||||
roster.value = (
|
||||
await http.get(`/course-selections/offerings/${offeringId}/roster`)
|
||||
await http.get(`/course-selections/offerings/${offeringId}/roster`, {
|
||||
params: {
|
||||
keyword: rosterKeyword.value.trim() || undefined,
|
||||
grade: rosterGrade.value,
|
||||
majorId: rosterMajorId.value,
|
||||
administrativeClassId: rosterClassId.value,
|
||||
studentPage: rosterStudentPage.value,
|
||||
studentPageSize: rosterPageSize,
|
||||
waitlistPage: rosterWaitlistPage.value,
|
||||
waitlistPageSize: rosterPageSize,
|
||||
},
|
||||
})
|
||||
).data
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
@@ -557,6 +583,12 @@ async function loadRoster(offeringId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function filterRoster() {
|
||||
rosterStudentPage.value = 1
|
||||
rosterWaitlistPage.value = 1
|
||||
if (roster.value) void loadRoster(roster.value.id)
|
||||
}
|
||||
|
||||
async function openProxyEnrollment() {
|
||||
studentKeyword.value = ''
|
||||
selectedStudentIds.value = []
|
||||
@@ -1346,6 +1378,25 @@ onMounted(async () => {
|
||||
:closable="false"
|
||||
title="强制选课将忽略容量、时间冲突、学分上限和重复课程等限制,直接加入名单。"
|
||||
/>
|
||||
<div class="roster-filter">
|
||||
<el-input
|
||||
v-model="rosterKeyword"
|
||||
clearable
|
||||
placeholder="按学号、姓名或行政班筛选"
|
||||
@keyup.enter="filterRoster"
|
||||
@clear="filterRoster"
|
||||
/>
|
||||
<el-select v-model="rosterGrade" clearable placeholder="全部年级" @change="filterRoster">
|
||||
<el-option v-for="grade in roster.filterOptions?.grades" :key="grade" :label="`${grade} 级`" :value="grade" />
|
||||
</el-select>
|
||||
<el-select v-model="rosterMajorId" clearable placeholder="全部专业" @change="() => { rosterClassId = undefined; filterRoster() }">
|
||||
<el-option v-for="major in roster.filterOptions?.majors" :key="major.majorId" :label="major.majorName" :value="major.majorId" />
|
||||
</el-select>
|
||||
<el-select v-model="rosterClassId" clearable placeholder="全部行政班" @change="filterRoster">
|
||||
<el-option v-for="item in rosterClasses" :key="item.classId" :label="item.className" :value="item.classId" />
|
||||
</el-select>
|
||||
<el-button :icon="Search" @click="filterRoster">查询</el-button>
|
||||
</div>
|
||||
<el-table v-loading="rosterLoading" :data="roster.students">
|
||||
<el-table-column prop="studentNumber" label="学号" width="130" />
|
||||
<el-table-column prop="name" label="姓名" width="90" />
|
||||
@@ -1363,6 +1414,12 @@ onMounted(async () => {
|
||||
</el-table-column>
|
||||
<template #empty><el-empty description="暂无学生选课" /></template>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-if="roster.studentTotal > rosterPageSize"
|
||||
small background layout="total, prev, pager, next"
|
||||
:current-page="rosterStudentPage" :page-size="rosterPageSize" :total="roster.studentTotal"
|
||||
@current-change="(page: number) => { rosterStudentPage = page; loadRoster(roster.id) }"
|
||||
/>
|
||||
<section class="waitlist-panel">
|
||||
<div class="waitlist-panel-head">
|
||||
<div>
|
||||
@@ -1394,6 +1451,12 @@ onMounted(async () => {
|
||||
</el-table-column>
|
||||
<template #empty><el-empty description="暂无候补学生" /></template>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-if="roster.waitlistTotal > rosterPageSize"
|
||||
small background layout="total, prev, pager, next"
|
||||
:current-page="rosterWaitlistPage" :page-size="rosterPageSize" :total="roster.waitlistTotal"
|
||||
@current-change="(page: number) => { rosterWaitlistPage = page; loadRoster(roster.id) }"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
</el-drawer>
|
||||
@@ -1528,6 +1591,11 @@ onMounted(async () => {
|
||||
.offering-ticket.waitlisted { border-color: #e6a23c; box-shadow: 0 10px 28px rgb(230 162 60 / 10%); }
|
||||
.seat-meter > small { display: block; margin-top: 5px; color: #b7791f; }
|
||||
.waitlist-panel { margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--line); }
|
||||
.roster-filter { display: flex; flex-wrap: wrap; gap: 8px; margin: 14px 0; }
|
||||
.roster-filter .el-input { max-width: 260px; }
|
||||
.roster-filter .el-select { width: 140px; }
|
||||
.roster-summary ~ :deep(.el-pagination),
|
||||
.waitlist-panel :deep(.el-pagination) { justify-content: flex-end; margin-top: 10px; }
|
||||
.waitlist-panel-head {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
@@ -1541,6 +1609,9 @@ onMounted(async () => {
|
||||
.waitlist-panel-head small { color: var(--muted); text-align: right; }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.roster-filter { align-items: stretch; flex-direction: column; }
|
||||
.roster-filter .el-input,
|
||||
.roster-filter .el-select { width: 100%; max-width: none; }
|
||||
.waitlist-panel-head { align-items: start; flex-direction: column; }
|
||||
.waitlist-panel-head small { text-align: left; }
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Download, Plus, Promotion, Upload } from '@element-plus/icons-vue'
|
||||
import { Download, Plus, Promotion, Refresh, Search, Upload } from '@element-plus/icons-vue'
|
||||
import http, { apiErrorMessage } from '../api/http'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
@@ -10,6 +10,13 @@ const auth = useAuthStore()
|
||||
const isStudent = computed(() => auth.user?.roles.includes('Student'))
|
||||
const loading = ref(false)
|
||||
const batches = ref<any[]>([])
|
||||
const batchKeyword = ref('')
|
||||
const batchStatus = ref<number | undefined>()
|
||||
const batchMetricKind = ref<number | undefined>()
|
||||
const batchDateRange = ref<string[]>([])
|
||||
const batchPage = ref(1)
|
||||
const batchPageSize = 20
|
||||
const batchTotal = ref(0)
|
||||
const selected = ref<any>(null)
|
||||
const results = ref<ResultRow[]>([])
|
||||
const mine = reactive({ best: [] as any[], history: [] as any[] })
|
||||
@@ -22,11 +29,26 @@ const isScoreMetric = computed(() => selectedMetricKind.value === 3)
|
||||
const isLevelMetric = computed(() => selectedMetricKind.value === 2)
|
||||
const isPassMetric = computed(() => selectedMetricKind.value === 1)
|
||||
|
||||
async function load() {
|
||||
async function load(resetPage = false) {
|
||||
loading.value = true
|
||||
try {
|
||||
if (isStudent.value) Object.assign(mine, (await http.get('/other-exams/mine')).data)
|
||||
else batches.value = (await http.get('/other-exams/batches')).data
|
||||
else {
|
||||
if (resetPage) batchPage.value = 1
|
||||
const response = (await http.get('/other-exams/batches', {
|
||||
params: {
|
||||
keyword: batchKeyword.value.trim() || undefined,
|
||||
status: batchStatus.value,
|
||||
metricKind: batchMetricKind.value,
|
||||
examDateFrom: batchDateRange.value[0] || undefined,
|
||||
examDateTo: batchDateRange.value[1] || undefined,
|
||||
page: batchPage.value,
|
||||
pageSize: batchPageSize,
|
||||
},
|
||||
})).data
|
||||
batches.value = response.items
|
||||
batchTotal.value = response.total
|
||||
}
|
||||
} catch (error) { ElMessage.error(apiErrorMessage(error)) } finally { loading.value = false }
|
||||
}
|
||||
async function openBatch(row: any) {
|
||||
@@ -99,7 +121,7 @@ onMounted(load)
|
||||
<section class="result-board history-board"><div class="board-title"><div><span class="kicker">历史记录</span><h2>历史成绩</h2></div><span class="board-note">每次发布的结果都会保留</span></div><el-table :data="mine.history" v-loading="loading"><el-table-column prop="examName" label="考试" min-width="180"/><el-table-column prop="examCode" label="考试编码" width="130"/><el-table-column prop="examDate" label="考试日期" width="120"/><el-table-column prop="attemptNumber" label="第几次参加" width="110"/><el-table-column label="结果" min-width="140"><template #default="{ row }">{{ displayResult(row) }}</template></el-table-column><el-table-column prop="publishedAt" label="发布时间" width="170"/></el-table></section>
|
||||
</template>
|
||||
<template v-else>
|
||||
<section class="batch-panel"><div class="panel-heading"><div><span class="kicker">考试场次</span><h2>考试场次</h2></div><span>点击场次进入成绩录入</span></div><el-table :data="batches" v-loading="loading" @row-click="openBatch"><el-table-column prop="examCode" label="考试编码" width="140"/><el-table-column prop="name" label="考试名称" min-width="190"/><el-table-column prop="examDate" label="考试日期" width="120"/><el-table-column label="评价方式" width="100"><template #default="{ row }">{{ metricName(row.metricKind) }}</template></el-table-column><el-table-column prop="resultCount" label="已录入" width="90"/><el-table-column label="状态" width="120"><template #default="{ row }"><el-tag :type="row.status === 2 ? 'success' : 'info'">{{ row.status === 2 ? `已发布 ${row.publicationCount} 次` : '草稿' }}</el-tag></template></el-table-column></el-table></section>
|
||||
<section class="batch-panel"><div class="panel-heading"><div><span class="kicker">考试场次</span><h2>考试场次</h2></div><span>点击场次进入成绩录入</span></div><div class="batch-filters"><el-input v-model="batchKeyword" clearable placeholder="考试编码、名称或组织方" @keyup.enter="load(true)" @clear="load(true)"/><el-select v-model="batchStatus" clearable placeholder="全部状态" @change="load(true)"><el-option label="草稿" :value="1"/><el-option label="已发布" :value="2"/></el-select><el-select v-model="batchMetricKind" clearable placeholder="全部方式" @change="load(true)"><el-option label="合格制" :value="1"/><el-option label="等级制" :value="2"/><el-option label="分数制" :value="3"/></el-select><el-date-picker v-model="batchDateRange" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" @change="load(true)"/><el-button :icon="Search" @click="load(true)">查询</el-button><el-button :icon="Refresh" @click="() => load()">刷新</el-button></div><el-table :data="batches" v-loading="loading" @row-click="openBatch"><el-table-column prop="examCode" label="考试编码" width="140"/><el-table-column prop="name" label="考试名称" min-width="190"/><el-table-column prop="examDate" label="考试日期" width="120"/><el-table-column label="评价方式" width="100"><template #default="{ row }">{{ metricName(row.metricKind) }}</template></el-table-column><el-table-column prop="resultCount" label="已录入" width="90"/><el-table-column label="状态" width="120"><template #default="{ row }"><el-tag :type="row.status === 2 ? 'success' : 'info'">{{ row.status === 2 ? `已发布 ${row.publicationCount} 次` : '草稿' }}</el-tag></template></el-table-column></el-table><el-pagination v-if="batchTotal > batchPageSize" small background layout="total, prev, pager, next" :current-page="batchPage" :page-size="batchPageSize" :total="batchTotal" @current-change="(page: number) => { batchPage = page; load() }"/></section>
|
||||
<section v-if="selected" class="editor-panel"><div class="editor-heading"><div><span class="kicker">{{ selected.examCode }}</span><h2>{{ selected.name }}</h2><p>{{ selected.organizer || '未填写组织方' }} · {{ selected.examDate }} · {{ metricName(selected.metricKind) }}{{ selected.metricKind === 3 ? ` · 满分 ${selected.maxScore}` : '' }}</p></div><div class="editor-actions"><el-button :icon="Download" @click="downloadTemplate">下载导入模板</el-button><label class="upload-button"><Upload />批量导入<input type="file" accept=".xlsx,.xls" @change="importExcel" /></label><el-button :icon="Plus" @click="addResult">新增一行</el-button><el-button type="primary" :loading="saving" @click="saveResults">保存记录</el-button><el-button type="success" :icon="Promotion" @click="publish">发布</el-button></div></div><div class="editor-hint">学号输入完成后离开输入框,系统自动检索姓名、学院和班级;参加次数不需要填写,由系统按考试编码和考试日期自动计算。</div><el-table :data="results" class="score-table"><el-table-column label="学号" min-width="150" fixed><template #default="{ row }"><el-input v-model="row.studentNumber" placeholder="输入学号" @blur="lookupStudent(row)" /></template></el-table-column><el-table-column label="姓名" width="110"><template #default="{ row }"><span :class="{ 'unresolved': row.studentNumber && !row.studentName }">{{ row.studentName || '待检索' }}</span></template></el-table-column><el-table-column label="学院" min-width="150"><template #default="{ row }">{{ row.collegeName || '—' }}</template></el-table-column><el-table-column label="班级" min-width="150"><template #default="{ row }">{{ row.className || '—' }}</template></el-table-column><el-table-column v-if="selected.metricKind === 3" label="成绩" width="150"><template #default="{ row }"><el-input-number v-model="row.score" :min="0" :max="selected.maxScore" :precision="2" controls-position="right" placeholder="请输入分数" /></template></el-table-column><el-table-column v-if="selected.metricKind === 2" label="等级" width="150"><template #default="{ row }"><el-select v-model="row.level" placeholder="选择等级"><el-option v-for="level in (selected.levelOptions || '').split(',').filter(Boolean)" :key="level" :label="level" :value="level" /></el-select></template></el-table-column><el-table-column v-if="selected.metricKind === 1" label="考试结果" width="150"><template #default="{ row }"><el-select v-model="row.isPassed" placeholder="选择结果"><el-option label="合格" :value="true"/><el-option label="不合格" :value="false"/></el-select></template></el-table-column><el-table-column label="参加次数" width="100"><template #default="{ row }"><span class="auto-attempt">{{ row.attemptNumber || '自动' }}</span></template></el-table-column><el-table-column label="备注" min-width="180"><template #default="{ row }"><el-input v-model="row.notes" placeholder="可选" /></template></el-table-column></el-table><el-empty v-if="!results.length" description="还没有成绩记录,点击“新增一行”或使用批量导入" /></section>
|
||||
</template>
|
||||
<el-dialog v-model="dialog" title="新建其他考试场次" width="600px"><el-form label-width="100px"><el-form-item label="考试编码" required><el-input v-model="form.examCode" placeholder="如 CET4、IELTS、计算机二级;同一考试始终使用相同编码" /></el-form-item><el-form-item label="考试名称" required><el-input v-model="form.name" placeholder="如 大学英语四级" /></el-form-item><el-form-item label="组织方"><el-input v-model="form.organizer" /></el-form-item><el-form-item label="考试日期"><el-date-picker v-model="form.examDate" type="date" value-format="YYYY-MM-DD" /></el-form-item><el-form-item label="评价方式"><el-select v-model="form.metricKind"><el-option label="分数制" :value="3"/><el-option label="等级制" :value="2"/><el-option label="合格/不合格" :value="1"/></el-select></el-form-item><el-form-item v-if="form.metricKind === 3" label="满分"><el-input-number v-model="form.maxScore" :min="1" /></el-form-item><el-form-item v-if="form.metricKind === 2" label="等级顺序"><el-input v-model="form.levelOptions" placeholder="按最优到最差填写,如 A+,A,B,C,D" /></el-form-item></el-form><template #footer><el-button @click="dialog = false">取消</el-button><el-button type="primary" @click="createBatch">建立场次</el-button></template></el-dialog>
|
||||
@@ -113,6 +135,7 @@ onMounted(load)
|
||||
.kicker { color:var(--teal); font-size:11px; letter-spacing:.17em; font-weight:800; }.exam-hero h1,.board-title h2,.panel-heading h2,.editor-heading h2 { color:var(--ink); margin:7px 0; letter-spacing:-.025em; }.exam-hero h1 { font-size:32px; }.exam-hero p,.editor-heading p { color:var(--muted); margin:0; line-height:1.7; }.result-board,.batch-panel,.editor-panel { background:#fff; border:1px solid var(--line); border-radius:14px; box-shadow:0 12px 30px rgba(32,61,73,.06); margin-bottom:18px; overflow:hidden; }.board-title,.panel-heading { display:flex; justify-content:space-between; align-items:center; padding:20px 22px 14px; }.board-title h2,.panel-heading h2,.editor-heading h2 { font-size:20px; }.board-note,.panel-heading>span { color:var(--muted); font-size:13px; }.history-board { opacity:.96; }.best-value { color:var(--navy); font-variant-numeric:tabular-nums; }.editor-heading { display:flex; justify-content:space-between; gap:20px; align-items:center; padding:20px 22px 14px; }.editor-actions { display:flex; flex-wrap:wrap; gap:8px; justify-content:flex-end; }.upload-button { display:inline-flex; align-items:center; gap:5px; border:1px solid #dcdfe6; border-radius:4px; padding:8px 14px; color:#606266; cursor:pointer; font-size:14px; }.upload-button:hover { color:var(--navy); border-color:var(--navy); }.upload-button input { display:none; }.editor-hint { margin:0 22px 14px; padding:11px 14px; border-left:3px solid #d4a72c; background:#fff9e8; color:#786223; font-size:13px; }.auto-attempt { display:inline-flex; align-items:center; padding:4px 8px; border-radius:20px; background:#edf6f5; color:var(--teal); font-size:12px; }.unresolved { color:#c27b18; }.score-table :deep(.el-input-number) { width:125px; }.score-table :deep(.el-table__cell) { padding:12px 0; }
|
||||
@media (max-width:760px) { .exam-hero,.editor-heading,.board-title,.panel-heading { display:block; }.exam-hero .el-button { margin-top:16px; }.editor-actions { justify-content:flex-start; margin-top:16px; }.board-note,.panel-heading>span { display:block; margin-top:5px; }.editor-hint { margin-left:14px; margin-right:14px; } }
|
||||
.editor-heading > div:first-child { min-width: 0; }
|
||||
.batch-filters { display:flex; flex-wrap:wrap; gap:8px; padding:0 22px 16px; }.batch-filters .el-input { max-width:280px; }.batch-filters .el-select { width:120px; }.batch-panel :deep(.el-pagination) { justify-content:flex-end; padding:14px 22px; }
|
||||
.editor-actions { flex: 0 0 auto; }
|
||||
.upload-button { flex: 0 0 auto; min-width: 112px; white-space: nowrap; justify-content: center; line-height: 1.4; }
|
||||
.publish-confirm { color:var(--ink); line-height:1.7; }.publish-confirm p { margin:8px 0 0; color:var(--muted); font-size:13px; }
|
||||
|
||||
Reference in New Issue
Block a user