优化前端

This commit is contained in:
2026-08-11 15:21:36 +08:00 Unverified
parent 34f62a6bed
commit 85c7c7a683
8 changed files with 157 additions and 71 deletions
+4 -52
View File
@@ -1,33 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue'
import http from '../api/http'
interface SystemVersionResponse {
version: string
}
const backendVersion = ref('获取中')
const currentYear = new Date().getFullYear() const currentYear = new Date().getFullYear()
const frontendVersion = __APP_VERSION__
onMounted(async () => {
try {
const { data } = await http.get<SystemVersionResponse>('/system/version')
backendVersion.value = data.version ? `v${data.version}` : '未知'
} catch {
backendVersion.value = '未知'
}
})
</script> </script>
<template> <template>
<footer class="site-footer" aria-label="网站版权与版本信息"> <footer class="site-footer" aria-label="网站版权信息">
<div class="site-footer-inner"> <div class="site-footer-inner">
<p>© {{ currentYear }} 明序教务 · 版权所有</p> <p>© {{ currentYear }} 明序教务 · 版权所有</p>
<div class="version-list" aria-label="系统版本"> <span class="footer-note">教务管理与学业服务平台</span>
<span>前端版本 <b>v{{ frontendVersion }}</b></span>
<span>后端版本 <b>{{ backendVersion }}</b></span>
</div>
</div> </div>
</footer> </footer>
</template> </template>
@@ -61,27 +40,7 @@ onMounted(async () => {
letter-spacing: .04em; letter-spacing: .04em;
} }
.version-list { .footer-note { color: #aeb9d8; font-size: 11px; }
display: flex;
align-items: center;
gap: 8px;
}
.version-list span {
padding: 5px 9px;
border: 1px solid rgba(255, 255, 255, .14);
border-radius: 4px;
color: #aeb9d8;
background: rgba(255, 255, 255, .055);
font-size: 10px;
white-space: nowrap;
}
.version-list b {
margin-left: 5px;
color: #66d5c5;
font: 700 10px/1.2 Consolas, monospace;
}
@media (max-width: 600px) { @media (max-width: 600px) {
.site-footer-inner { .site-footer-inner {
@@ -93,13 +52,6 @@ onMounted(async () => {
gap: 7px; gap: 7px;
} }
.version-list { .footer-note { font-size: 10px; }
width: 100%;
}
.version-list span {
flex: 1;
text-align: center;
}
} }
</style> </style>
+2 -2
View File
@@ -53,7 +53,7 @@ onBeforeUnmount(() => {
<div class="smart-launch-mark" aria-hidden="true"> <div class="smart-launch-mark" aria-hidden="true">
<i v-for="index in 9" :key="index" /> <i v-for="index in 9" :key="index" />
</div> </div>
<span class="smart-launch-brand">MINGXU ACADEMIC</span> <span class="smart-launch-brand">明序教务</span>
<p>{{ greeting.label }}</p> <p>{{ greeting.label }}</p>
<h1>{{ greeting.title }}</h1> <h1>{{ greeting.title }}</h1>
<small>{{ greeting.subtitle }}</small> <small>{{ greeting.subtitle }}</small>
@@ -117,7 +117,7 @@ onBeforeUnmount(() => {
color: #8fe4dc; color: #8fe4dc;
font-size: 0.68rem; font-size: 0.68rem;
font-weight: 700; font-weight: 700;
letter-spacing: 0.28em; letter-spacing: 0.16em;
} }
.smart-launch-content p { .smart-launch-content p {
+2 -2
View File
@@ -329,7 +329,7 @@ onMounted(() => {
</div> </div>
<div> <div>
<strong>明序教务</strong> <strong>明序教务</strong>
<small>ACADEMIC OFFICE</small> <small>教务管理与学业服务</small>
</div> </div>
</div> </div>
@@ -394,7 +394,7 @@ onMounted(() => {
</div> </div>
<div> <div>
<strong>明序教务</strong> <strong>明序教务</strong>
<small>ACADEMIC OFFICE</small> <small>教务管理与学业服务</small>
</div> </div>
</div> </div>
<button type="button" aria-label="关闭导航菜单" @click="mobileMenu = false">×</button> <button type="button" aria-label="关闭导航菜单" @click="mobileMenu = false">×</button>
+135 -1
View File
@@ -42,7 +42,7 @@ button { cursor: pointer; }
} }
.top-brand { min-width: 224px; padding-right: 28px; border-right: 1px solid rgba(255,255,255,.14); } .top-brand { min-width: 224px; padding-right: 28px; border-right: 1px solid rgba(255,255,255,.14); }
.brand strong { display: block; font-family: "STZhongsong", "Songti SC", serif; font-size: 21px; letter-spacing: .12em; color: white; } .brand strong { display: block; font-family: "STZhongsong", "Songti SC", serif; font-size: 21px; letter-spacing: .12em; color: white; }
.brand small { display: block; margin-top: 3px; font: 9px/1.2 Consolas, monospace; letter-spacing: .16em; color: #9faad1; } .brand small { display: block; margin-top: 3px; font: 10px/1.2 "Microsoft YaHei", sans-serif; letter-spacing: .08em; color: #9faad1; }
.brand-mark { flex: 0 0 auto; width: 31px; height: 31px; display: grid; grid-template-columns: repeat(3,1fr); gap: 3px; } .brand-mark { flex: 0 0 auto; width: 31px; height: 31px; display: grid; grid-template-columns: repeat(3,1fr); gap: 3px; }
.brand-mark span { border: 1px solid #9eabd8; } .brand-mark span { border: 1px solid #9eabd8; }
.brand-mark span:nth-child(2), .brand-mark span:nth-child(5), .brand-mark span:nth-child(8) { background: #3ec1ae; border-color: #3ec1ae; } .brand-mark span:nth-child(2), .brand-mark span:nth-child(5), .brand-mark span:nth-child(8) { background: #3ec1ae; border-color: #3ec1ae; }
@@ -1496,3 +1496,137 @@ button { cursor: pointer; }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto !important; transition: none !important; } *, *::before, *::after { scroll-behavior: auto !important; transition: none !important; }
} }
/* Shared workspace surface: keeps independently-built modules visually aligned. */
:root {
--surface: #ffffff;
--surface-muted: #f8fafc;
--surface-accent: #f0f7f6;
--shadow-card: 0 8px 24px rgba(26, 47, 80, .055);
--el-border-radius-base: 8px;
--el-border-radius-small: 6px;
--el-fill-color-blank: #ffffff;
--el-bg-color-page: #f4f6fa;
--el-border-color: #dfe5ee;
--el-text-color-primary: #182033;
--el-text-color-regular: #4f5b6c;
}
body {
background:
linear-gradient(180deg, rgba(35, 56, 118, .035), transparent 20rem),
#f4f6fa;
}
::selection { color: #fff; background: var(--indigo); }
.content { max-width: 1480px; }
.page-stack { gap: 20px; }
.page-intro,
.page-heading {
position: relative;
min-height: 96px;
padding: 20px 0 16px 18px;
border-bottom: 1px solid #dfe5ee;
}
.page-intro::before,
.page-heading::before {
position: absolute;
top: 23px;
bottom: 20px;
left: 0;
width: 3px;
content: '';
border-radius: 3px;
background: linear-gradient(180deg, var(--teal), #55b9aa);
}
.page-intro h2,
.page-heading h2 {
letter-spacing: .02em;
}
.page-intro p,
.page-heading > div > p {
max-width: 720px;
line-height: 1.7;
}
.data-card,
.page-stack > .el-card,
.page-stack .el-card,
.application-card,
.result-board,
.batch-panel,
.editor-panel {
border-color: #dfe5ee;
border-radius: 10px;
box-shadow: var(--shadow-card);
}
.data-card:hover,
.page-stack > .el-card:hover,
.application-card:hover,
.result-board:hover,
.batch-panel:hover,
.editor-panel:hover {
border-color: #cbd8e7;
}
.table-toolbar,
.filter-bar {
min-height: 76px;
padding: 16px 20px;
border-color: #e1e7ef;
background: var(--surface-muted);
}
.table-toolbar > span,
.filter-bar > span,
.pagination-bar > span { color: #768296; }
.el-button { font-weight: 600; letter-spacing: .01em; }
.el-button--primary { box-shadow: 0 4px 10px rgba(35, 56, 118, .14); }
.el-button--primary:hover { box-shadow: 0 6px 14px rgba(35, 56, 118, .2); }
.el-input__wrapper,
.el-textarea__inner,
.el-select__wrapper,
.el-date-editor.el-input__wrapper,
.el-date-editor.el-range-editor {
box-shadow: 0 0 0 1px #d7dfe9 inset;
background: #fff;
}
.el-input__wrapper:hover,
.el-select__wrapper:hover,
.el-date-editor.el-input__wrapper:hover,
.el-date-editor.el-range-editor:hover { box-shadow: 0 0 0 1px #afbfce inset; }
.el-card__header { padding: 18px 20px; border-bottom-color: #e5eaf1; }
.el-card__body { padding: 20px; }
.el-table { --el-table-border-color: #e4e9f0; --el-table-header-bg-color: #f7f9fc; }
.el-table th.el-table__cell { height: 46px; color: #536074; background: #f7f9fc; }
.el-table td.el-table__cell { padding: 12px 0; }
.el-table--enable-row-hover .el-table__body tr:hover > td.el-table__cell { background: #f3f8f8; }
.el-pagination { padding: 14px 0 2px; justify-content: flex-end; }
.el-dialog { overflow: hidden; border-radius: 12px; box-shadow: 0 20px 60px rgba(18, 37, 63, .25); }
.el-dialog__header { margin-right: 0; padding: 20px 22px 16px; border-bottom: 1px solid #e6ebf1; }
.el-dialog__body { padding: 22px; }
.el-dialog__footer { padding: 14px 22px 20px; border-top: 1px solid #edf0f4; }
.el-tag { font-weight: 600; }
@media (max-width: 760px) {
.page-intro,
.page-heading { min-height: 0; padding: 12px 0 14px 14px; }
.page-intro::before,
.page-heading::before { top: 14px; bottom: 16px; }
.page-intro h2,
.page-heading h2 { font-size: 23px; }
.table-toolbar,
.filter-bar { min-height: 0; padding: 12px 14px; }
.el-card__body { padding: 16px; }
.el-dialog__header { padding: 18px 18px 14px; }
.el-dialog__body { padding: 18px; }
.el-dialog__footer { padding: 12px 18px 18px; }
}
+1 -1
View File
@@ -438,7 +438,7 @@ onMounted(async () => {
<section class="reservation-page"> <section class="reservation-page">
<header class="page-heading"> <header class="page-heading">
<div> <div>
<p class="eyebrow">TEACHING SPACE</p> <p class="eyebrow">教学场地</p>
<h2>教室借用与预约</h2> <h2>教室借用与预约</h2>
<p>选择正式课表之外的空闲时段提交申请由申请人所在学院审核</p> <p>选择正式课表之外的空闲时段提交申请由申请人所在学院审核</p>
</div> </div>
+8 -8
View File
@@ -446,7 +446,7 @@ onMounted(async () => {
<span>{{ data.audience.scopeName }}</span> <span>{{ data.audience.scopeName }}</span>
<i>数据范围</i> <i>数据范围</i>
</div> </div>
<p class="overview-eyebrow">ACADEMIC OPERATIONS</p> <p class="overview-eyebrow">教务工作概览</p>
<h1>{{ data.greeting.title }}</h1> <h1>{{ data.greeting.title }}</h1>
<p class="overview-description">{{ data.greeting.subtitle }}</p> <p class="overview-description">{{ data.greeting.subtitle }}</p>
</div> </div>
@@ -525,7 +525,7 @@ onMounted(async () => {
</section> </section>
<section class="operation-alerts"> <section class="operation-alerts">
<header><div><span>OPERATION WATCH</span><h2>运行预警</h2></div><small>{{ operationAlerts.length ? `${operationAlerts.length}运行断点需要关注` : '当前未发现运行断点' }}</small></header> <header><div><span>运行提醒</span><h2>需要关注的事项</h2></div><small>{{ operationAlerts.length ? `${operationAlerts.length}事项需要关注` : '当前运行平稳' }}</small></header>
<div v-if="operationAlerts.length" class="operation-alert-list"> <div v-if="operationAlerts.length" class="operation-alert-list">
<button v-for="alert in operationAlerts" :key="alert.title" :class="alert.level" @click="router.push(alert.route)"><b>{{ alert.title }}</b><span>{{ alert.detail }}</span><i>立即处理 </i></button> <button v-for="alert in operationAlerts" :key="alert.title" :class="alert.level" @click="router.push(alert.route)"><b>{{ alert.title }}</b><span>{{ alert.detail }}</span><i>立即处理 </i></button>
</div> </div>
@@ -536,7 +536,7 @@ onMounted(async () => {
<article v-if="isCounselorDashboard" class="dashboard-panel counselor-radar"> <article v-if="isCounselorDashboard" class="dashboard-panel counselor-radar">
<header class="panel-heading"> <header class="panel-heading">
<div> <div>
<span class="panel-index">RADAR / COUNSELOR</span> <span class="panel-index">学业预警</span>
<h2>需重点关注的学生</h2> <h2>需重点关注的学生</h2>
<p>{{ activeCounselorWarnings.length ? `当前有 ${activeCounselorWarnings.length} 条生效预警,按最新记录展示。` : '当前没有生效中的学业预警。' }}</p> <p>{{ activeCounselorWarnings.length ? `当前有 ${activeCounselorWarnings.length} 条生效预警,按最新记录展示。` : '当前没有生效中的学业预警。' }}</p>
</div> </div>
@@ -559,7 +559,7 @@ onMounted(async () => {
<article class="dashboard-panel todo-panel"> <article class="dashboard-panel todo-panel">
<header class="panel-heading"> <header class="panel-heading">
<div> <div>
<span class="panel-index">01 / ACTION</span> <span class="panel-index">待办事项</span>
<h2>需要处理</h2> <h2>需要处理</h2>
<p>只显示当前角色和数据范围内可处理的事项</p> <p>只显示当前角色和数据范围内可处理的事项</p>
</div> </div>
@@ -595,7 +595,7 @@ onMounted(async () => {
<article class="dashboard-panel quick-panel"> <article class="dashboard-panel quick-panel">
<header class="panel-heading"> <header class="panel-heading">
<div> <div>
<span class="panel-index">02 / SHORTCUTS</span> <span class="panel-index">常用入口</span>
<h2>常用工作</h2> <h2>常用工作</h2>
<p>按当前角色整理的高频业务入口</p> <p>按当前角色整理的高频业务入口</p>
</div> </div>
@@ -622,9 +622,9 @@ onMounted(async () => {
<section class="dashboard-panel readiness-panel"> <section class="dashboard-panel readiness-panel">
<header class="panel-heading"> <header class="panel-heading">
<div> <div>
<span class="panel-index">03 / TERM READINESS</span> <span class="panel-index">学期进度</span>
<h2>本学期运行状态</h2> <h2>本学期教学运行</h2>
<p>从任务发布到课表落地成绩归档快速发现业务断点</p> <p>关注教学任务课表与成绩流程的完成情况</p>
</div> </div>
<span class="refresh-time">数据更新于 {{ new Date(data.generatedAt).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) }}</span> <span class="refresh-time">数据更新于 {{ new Date(data.generatedAt).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) }}</span>
</header> </header>
+1 -1
View File
@@ -69,7 +69,7 @@ onMounted(async () => {
</div> </div>
<div> <div>
<strong>明序教务</strong> <strong>明序教务</strong>
<small>MINGXU ACADEMIC SYSTEM</small> <small>教务管理与学业服务</small>
</div> </div>
</div> </div>
<div class="story-copy"> <div class="story-copy">
+4 -4
View File
@@ -93,13 +93,13 @@ onMounted(load)
<template> <template>
<div class="other-exams-page"> <div class="other-exams-page">
<section class="exam-hero"><div><span class="kicker">ASSESSMENT ARCHIVE</span><h1>其他考试成绩</h1><p>{{ isStudent ? '你的证书、等级考试与校外考试,按考试归档,最优结果一目了然。' : '用考试编码归并同一考试,按场次维护成绩,系统自动记录每位学生的参加次数。' }}</p></div><el-button v-if="!isStudent" type="primary" :icon="Plus" @click="dialog = true">新建考试场次</el-button></section> <section class="exam-hero"><div><span class="kicker">考试成绩</span><h1>其他考试成绩</h1><p>{{ isStudent ? '你的证书、等级考试与校外考试,按考试归档,最优结果一目了然。' : '用考试编码归并同一考试,按场次维护成绩,系统自动记录每位学生的参加次数。' }}</p></div><el-button v-if="!isStudent" type="primary" :icon="Plus" @click="dialog = true">新建考试场次</el-button></section>
<template v-if="isStudent"> <template v-if="isStudent">
<section class="result-board"><div class="board-title"><div><span class="kicker">BEST OUTCOME</span><h2>我的最优结果</h2></div><span class="board-note">同一考试编码下自动比较</span></div><el-table :data="mine.best" 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 label="最优结果" min-width="150"><template #default="{ row }"><strong class="best-value">{{ displayResult(row) }}</strong></template></el-table-column><el-table-column prop="attemptNumber" label="参加次数" width="100"/></el-table><el-empty v-if="!loading && !mine.best.length" description="暂无已发布的其他考试成绩"/></section> <section class="result-board"><div class="board-title"><div><span class="kicker">成绩汇总</span><h2>我的最优结果</h2></div><span class="board-note">同一考试编码下自动比较</span></div><el-table :data="mine.best" 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 label="最优结果" min-width="150"><template #default="{ row }"><strong class="best-value">{{ displayResult(row) }}</strong></template></el-table-column><el-table-column prop="attemptNumber" label="参加次数" width="100"/></el-table><el-empty v-if="!loading && !mine.best.length" description="暂无已发布的其他考试成绩"/></section>
<section class="result-board history-board"><div class="board-title"><div><span class="kicker">FULL HISTORY</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> <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>
<template v-else> <template v-else>
<section class="batch-panel"><div class="panel-heading"><div><span class="kicker">EXAM SESSIONS</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><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 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> <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> </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> <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>