成绩+点名

This commit is contained in:
2026-07-25 14:08:50 +08:00 Unverified
parent 7800951aee
commit 36049af692
16 changed files with 1809 additions and 98 deletions
+126 -41
View File
@@ -2,6 +2,7 @@
import { computed, onMounted, reactive, ref } from 'vue'
import {
Check,
Delete,
DocumentChecked,
EditPen,
Plus,
@@ -28,11 +29,15 @@ const termId = ref<string | undefined>()
const status = ref<string | undefined>()
const createForm = reactive({
regularWeight: 30,
midtermWeight: 0,
finalWeight: 70,
items: [] as { name: string; weight: number }[],
})
const newItemName = ref('')
const newItemWeight = ref(0)
const returnComment = ref('')
const presetItems = ['实验', '实习', '课程设计', '作业', '课堂表现', '期中']
const statusLabels: Record<string, string> = {
Draft: '录入中',
Submitted: '待审核',
@@ -46,6 +51,10 @@ const examStatusLabels: Record<string, string> = {
Deferred: '缓考',
Exempt: '免修',
}
const itemsWeight = computed(() =>
createForm.items.reduce((sum, item) => sum + item.weight, 0))
const weightSum = computed(() =>
createForm.regularWeight + createForm.finalWeight + itemsWeight.value)
const completedCount = computed(() =>
detail.value?.records.filter((record: any) =>
record.totalScore != null || record.examStatus !== 'Normal').length ?? 0)
@@ -69,8 +78,7 @@ const transcriptGpa = computed(() => {
const credits = numeric.reduce((sum: number, record: any) => sum + Number(record.credits), 0)
if (!credits) return '—'
const points = numeric.reduce(
(sum: number, record: any) => sum + Number(record.gradePoint) * Number(record.credits),
0,
(sum: number, record: any) => sum + Number(record.gradePoint) * Number(record.credits), 0,
)
return (points / credits).toFixed(2)
})
@@ -79,6 +87,29 @@ const earnedCredits = computed(() =>
.filter((record: any) => Number(record.totalScore) >= 60)
.reduce((sum: number, record: any) => sum + Number(record.credits), 0))
function addItem() {
const name = newItemName.value.trim() || '自定义分项'
if (createForm.items.some(item => item.name === name)) {
ElMessage.warning('分项名称不能重复。')
return
}
createForm.items.push({ name, weight: newItemWeight.value || 0 })
newItemName.value = ''
newItemWeight.value = 0
}
function addPresetItem(name: string) {
if (createForm.items.some(item => item.name === name)) {
ElMessage.warning('该分项已存在。')
return
}
createForm.items.push({ name, weight: 0 })
}
function removeItem(index: number) {
createForm.items.splice(index, 1)
}
async function load() {
loading.value = true
try {
@@ -127,23 +158,25 @@ async function selectTask(task: any) {
}
function openCreate() {
Object.assign(createForm, {
regularWeight: 30,
midtermWeight: 0,
finalWeight: 70,
})
createForm.regularWeight = 30
createForm.finalWeight = 70
createForm.items = []
newItemName.value = ''
newItemWeight.value = 0
createDialog.value = true
}
async function createSheet() {
if (createForm.regularWeight + createForm.midtermWeight + createForm.finalWeight !== 100) {
ElMessage.warning('三个成绩分项的比例必须合计 100%。')
if (weightSum.value !== 100) {
ElMessage.warning(`平时、期末与所有分项的比例必须合计 100%(当前 ${weightSum.value}%)。`)
return
}
try {
await http.post('/grades/sheets', {
teachingTaskId: selectedTask.value.id,
...createForm,
regularWeight: createForm.regularWeight,
finalWeight: createForm.finalWeight,
items: createForm.items.map(item => ({ name: item.name, weight: item.weight })),
})
createDialog.value = false
ElMessage.success('成绩登记册已建立')
@@ -159,8 +192,11 @@ async function saveRecords() {
records: detail.value.records.map((record: any) => ({
id: record.id,
regularScore: record.regularScore,
midtermScore: record.midtermScore,
finalScore: record.finalScore,
itemScores: record.itemScores?.map((itemScore: any) => ({
gradeItemId: itemScore.gradeItemId,
score: itemScore.score,
})),
examStatus: record.examStatus,
notes: record.notes,
})),
@@ -241,6 +277,10 @@ function scoreClass(score: number | null) {
return ''
}
function itemScore(record: any, gradeItemId: string) {
return record.itemScores?.find((itemScore: any) => itemScore.gradeItemId === gradeItemId)
}
onMounted(async () => {
try {
terms.value = (await http.get('/base-data/terms')).data
@@ -259,7 +299,7 @@ onMounted(async () => {
<span class="section-kicker">ACADEMIC RECORD</span>
<h2>{{ isStudent ? '学业成绩单' : '成绩管理' }}</h2>
<p v-if="isStudent">查看学校已正式发布的课程成绩学分与绩点</p>
<p v-else>从教师登记学院复核到校级发布保留每张成绩单的明确状态</p>
<p v-else>从教师登记学院复核到校级发布支持自定义平时实验实习等分项</p>
</div>
<el-button :icon="Refresh" @click="load">刷新</el-button>
</section>
@@ -366,7 +406,7 @@ onMounted(async () => {
<div><span>及格率</span><b>{{ passRate }}</b></div>
<p>
平时 {{ detail.regularWeight }}%
<template v-if="detail.midtermWeight"> · 期中 {{ detail.midtermWeight }}%</template>
<template v-if="detail.items?.length">{{ detail.items.map((item: any) => ` · ${item.name} ${item.weight}%`).join('') }}</template>
· 期末 {{ detail.finalWeight }}%
</p>
</section>
@@ -389,58 +429,58 @@ onMounted(async () => {
</div>
</template>
</el-table-column>
<el-table-column :label="`平时 ${detail.regularWeight}%`" width="125">
<el-table-column :label="`平时 ${detail.regularWeight}%`" width="110">
<template #default="{ row }">
<el-input-number
v-if="detail.canEdit && row.examStatus === 'Normal'"
v-model="row.regularScore"
:min="0"
:max="100"
:controls="false"
:min="0" :max="100" :controls="false" size="small"
/>
<span v-else>{{ row.regularScore ?? '' }}</span>
</template>
</el-table-column>
<el-table-column v-if="detail.midtermWeight" :label="`期中 ${detail.midtermWeight}%`" width="125">
<el-table-column
v-for="item in detail.items"
:key="item.id"
:label="`${item.name} ${item.weight}%`"
width="110"
>
<template #default="{ row }">
<el-input-number
v-if="detail.canEdit && row.examStatus === 'Normal'"
v-model="row.midtermScore"
:min="0"
:max="100"
:controls="false"
:model-value="itemScore(row, item.id)?.score"
@update:model-value="(val: number | undefined) => { const found = itemScore(row, item.id); if (found) found.score = val }"
:min="0" :max="100" :controls="false" size="small"
/>
<span v-else>{{ row.midtermScore ?? '' }}</span>
<span v-else>{{ itemScore(row, item.id)?.score ?? '—' }}</span>
</template>
</el-table-column>
<el-table-column :label="`期末 ${detail.finalWeight}%`" width="125">
<el-table-column :label="`期末 ${detail.finalWeight}%`" width="110">
<template #default="{ row }">
<el-input-number
v-if="detail.canEdit && row.examStatus === 'Normal'"
v-model="row.finalScore"
:min="0"
:max="100"
:controls="false"
:min="0" :max="100" :controls="false" size="small"
/>
<span v-else>{{ row.finalScore ?? '—' }}</span>
</template>
</el-table-column>
<el-table-column label="总评" width="90">
<el-table-column label="总评" width="80">
<template #default="{ row }">
<b class="total-score" :class="scoreClass(row.totalScore)">{{ row.totalScore ?? '—' }}</b>
</template>
</el-table-column>
<el-table-column label="考试状态" width="125">
<el-table-column label="考试状态" width="115">
<template #default="{ row }">
<el-select v-if="detail.canEdit" v-model="row.examStatus">
<el-select v-if="detail.canEdit" v-model="row.examStatus" size="small">
<el-option v-for="(label, value) in examStatusLabels" :key="value" :label="label" :value="value" />
</el-select>
<span v-else>{{ examStatusLabels[row.examStatus] }}</span>
</template>
</el-table-column>
<el-table-column label="备注" min-width="150">
<el-table-column label="备注" min-width="140">
<template #default="{ row }">
<el-input v-if="detail.canEdit" v-model="row.notes" maxlength="300" />
<el-input v-if="detail.canEdit" v-model="row.notes" maxlength="300" size="small" />
<span v-else>{{ row.notes || '—' }}</span>
</template>
</el-table-column>
@@ -463,24 +503,54 @@ onMounted(async () => {
</section>
</template>
<el-dialog v-model="createDialog" title="建立成绩登记册" width="560px">
<el-alert title="成绩构成比例合计必须为 100%" type="info" :closable="false" />
<el-dialog v-model="createDialog" title="建立成绩登记册" width="620px">
<el-alert title="平时期末与所有分项的比例必须合计 100%" type="info" :closable="false" />
<el-form label-position="top" class="grade-weight-form">
<div class="form-grid three">
<el-form-item label="平时成绩">
<el-form-item label="平时成绩 %">
<el-input-number v-model="createForm.regularWeight" :min="0" :max="100" />
</el-form-item>
<el-form-item label="期中成绩">
<el-input-number v-model="createForm.midtermWeight" :min="0" :max="100" />
</el-form-item>
<el-form-item label="期末成绩">
<el-form-item label="期末成绩 %">
<el-input-number v-model="createForm.finalWeight" :min="0" :max="100" />
</el-form-item>
<el-form-item label="当前合计">
<div class="weight-sum" :class="{ invalid: weightSum !== 100 }">
<b>{{ weightSum }}%</b>
<span v-if="weightSum !== 100">需为 100%</span>
<span v-else class="valid">✓</span>
</div>
</el-form-item>
</div>
<el-divider>成绩分项(实验、实习、课程设计等)</el-divider>
<div class="preset-items">
<span>快速添加:</span>
<el-button
v-for="name in presetItems" :key="name"
size="small" @click="addPresetItem(name)"
>{{ name }}</el-button>
</div>
<div v-if="createForm.items.length" class="item-list">
<div v-for="(item, index) in createForm.items" :key="index" class="item-row">
<span>{{ item.name }}</span>
<el-input-number v-model="item.weight" :min="0" :max="100" size="small" />
<span>%</span>
<el-button :icon="Delete" link type="danger" @click="removeItem(index)" />
</div>
</div>
<div class="add-item-row">
<el-input v-model="newItemName" placeholder="自定义名称" size="small" style="width:160px" />
<el-input-number v-model="newItemWeight" :min="0" :max="100" size="small" />
<span>%</span>
<el-button size="small" :icon="Plus" @click="addItem">添加</el-button>
</div>
</el-form>
<template #footer>
<el-button @click="createDialog = false">取消</el-button>
<el-button type="primary" @click="createSheet">建立登记册</el-button>
<el-button type="primary" :disabled="weightSum !== 100" @click="createSheet">建立登记册</el-button>
</template>
</el-dialog>
@@ -497,3 +567,18 @@ onMounted(async () => {
</el-dialog>
</div>
</template>
<style scoped>
.grade-weight-form { margin-top: 16px; }
.weight-sum { display: flex; align-items: baseline; gap: 8px; padding: 12px; border-radius: 6px; background: #f5f7fa; }
.weight-sum b { font: 700 22px/1 Consolas, monospace; color: var(--indigo); }
.weight-sum.invalid b { color: #b34e48; }
.weight-sum span { font-size: 11px; color: var(--muted); }
.weight-sum .valid { color: #2d8975; font-weight: 700; }
.preset-items { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 12px; }
.preset-items > span { font-size: 11px; color: var(--muted); }
.item-list { display: grid; gap: 6px; margin-bottom: 12px; }
.item-row { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: #eef5f0; border-radius: 4px; }
.item-row > span:first-child { flex: 1; font-size: 13px; font-weight: 650; }
.add-item-row { display: flex; align-items: center; gap: 8px; }
</style>