评教系统
This commit is contained in:
Vendored
+1
@@ -40,6 +40,7 @@ declare module 'vue' {
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElSegmented: typeof import('element-plus/es')['ElSegmented']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSlider: typeof import('element-plus/es')['ElSlider']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
|
||||
@@ -132,6 +132,10 @@ const navigationGroups = computed<NavigationGroup[]>(() => [
|
||||
...whenVisible(isStudent.value, { path: '/free-classrooms', label: '空闲教室' }),
|
||||
...whenVisible(isTeacher.value || isTeachingAdmin.value || hasAnyRole(['Counselor']), { path: '/teacher-attendance', label: '教学点名' }),
|
||||
...whenVisible(isStudent.value, { path: '/my-attendance', label: '我的考勤' }),
|
||||
{
|
||||
path: '/evaluations',
|
||||
label: isStudent.value ? '学生评教' : isTeacher.value ? '评教结果' : '教学评价',
|
||||
},
|
||||
...whenVisible(isTeacher.value, { path: '/teacher-roster', label: '选课名单' }),
|
||||
...whenVisible(!isStudent.value, {
|
||||
path: '/class-timetable',
|
||||
|
||||
@@ -260,6 +260,14 @@ const router = createRouter({
|
||||
component: () => import('../views/UsersView.vue'),
|
||||
meta: { roles: ['SuperAdmin'] },
|
||||
},
|
||||
{
|
||||
path: 'evaluations',
|
||||
name: 'evaluations',
|
||||
component: () => import('../views/EvaluationView.vue'),
|
||||
meta: {
|
||||
roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Teacher', 'Student', 'Leader'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'statistics',
|
||||
name: 'statistics',
|
||||
|
||||
@@ -0,0 +1,431 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { Delete, Edit, Lock, Plus, Unlock } from '@element-plus/icons-vue'
|
||||
import http, { apiErrorMessage } from '../api/http'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
const auth = useAuthStore()
|
||||
const roles = computed(() => auth.user?.roles ?? [])
|
||||
const isStudent = computed(() => roles.value.includes('Student'))
|
||||
const isTeacher = computed(() => roles.value.includes('Teacher') && !roles.value.some((r: string) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(r)))
|
||||
const isAdmin = computed(() => roles.value.some((r: string) => ['SuperAdmin', 'AcademicAdmin'].includes(r)))
|
||||
const isStaff = computed(() => roles.value.some((r: string) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Leader'].includes(r)))
|
||||
|
||||
// ── Admin: Setups ──
|
||||
const setups = ref<any[]>([])
|
||||
const terms = ref<any[]>([])
|
||||
const setupLoading = ref(false)
|
||||
const setupDialog = ref(false)
|
||||
const editingSetup = ref<any>(null)
|
||||
const setupForm = reactive({
|
||||
academicTermId: undefined as string | undefined,
|
||||
name: '',
|
||||
startsAt: '',
|
||||
endsAt: '',
|
||||
dimensions: [] as { name: string; maxScore: number }[],
|
||||
})
|
||||
|
||||
const dimSum = computed(() => setupForm.dimensions.reduce((s, d) => s + d.maxScore, 0))
|
||||
|
||||
function addDim() { setupForm.dimensions.push({ name: '', maxScore: 10 }) }
|
||||
function removeDim(i: number) { setupForm.dimensions.splice(i, 1) }
|
||||
|
||||
function resetSetupForm() {
|
||||
setupForm.academicTermId = undefined
|
||||
setupForm.name = ''
|
||||
setupForm.startsAt = ''
|
||||
setupForm.endsAt = ''
|
||||
setupForm.dimensions = [
|
||||
{ name: '教学态度', maxScore: 20 },
|
||||
{ name: '教学内容', maxScore: 30 },
|
||||
{ name: '教学方法', maxScore: 30 },
|
||||
{ name: '教学效果', maxScore: 20 },
|
||||
]
|
||||
}
|
||||
|
||||
async function loadSetups() {
|
||||
setupLoading.value = true
|
||||
try { setups.value = (await http.get('/evaluations/setups')).data }
|
||||
catch (e) { ElMessage.error(apiErrorMessage(e)) }
|
||||
finally { setupLoading.value = false }
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editingSetup.value = null; resetSetupForm(); setupDialog.value = true
|
||||
}
|
||||
function openEdit(setup: any) {
|
||||
editingSetup.value = setup
|
||||
setupForm.academicTermId = setup.academicTermId
|
||||
setupForm.name = setup.name
|
||||
setupForm.startsAt = setup.startsAt?.split('T')[0] ?? ''
|
||||
setupForm.endsAt = setup.endsAt?.split('T')[0] ?? ''
|
||||
setupForm.dimensions = (setup.dimensions ?? []).map((d: any) => ({ name: d.name, maxScore: d.maxScore }))
|
||||
setupDialog.value = true
|
||||
}
|
||||
|
||||
async function saveSetup() {
|
||||
if (!setupForm.name.trim()) { ElMessage.warning('请输入方案名称'); return }
|
||||
if (!setupForm.academicTermId) { ElMessage.warning('请选择学期'); return }
|
||||
if (dimSum.value !== 100) { ElMessage.warning('维度满分必须合计 100'); return }
|
||||
try {
|
||||
const payload = {
|
||||
academicTermId: setupForm.academicTermId,
|
||||
name: setupForm.name,
|
||||
startsAt: setupForm.startsAt || null,
|
||||
endsAt: setupForm.endsAt || null,
|
||||
dimensions: setupForm.dimensions,
|
||||
}
|
||||
if (editingSetup.value) {
|
||||
await http.put(`/evaluations/setups/${editingSetup.value.id}`, payload)
|
||||
ElMessage.success('方案已更新')
|
||||
} else {
|
||||
await http.post('/evaluations/setups', payload)
|
||||
ElMessage.success('方案已创建')
|
||||
}
|
||||
setupDialog.value = false
|
||||
await loadSetups()
|
||||
} catch (e) { ElMessage.error(apiErrorMessage(e)) }
|
||||
}
|
||||
|
||||
async function openSetup(id: string) {
|
||||
try { await http.post(`/evaluations/setups/${id}/open`); ElMessage.success('已开启评教'); await loadSetups() }
|
||||
catch (e) { ElMessage.error(apiErrorMessage(e)) }
|
||||
}
|
||||
async function closeSetup(id: string) {
|
||||
try { await http.post(`/evaluations/setups/${id}/close`); ElMessage.success('已关闭评教'); await loadSetups() }
|
||||
catch (e) { ElMessage.error(apiErrorMessage(e)) }
|
||||
}
|
||||
async function deleteSetup(id: string) {
|
||||
try {
|
||||
await ElMessageBox.confirm('删除后不可恢复,确定删除?', '删除评教方案', { type: 'warning', confirmButtonText: '删除' })
|
||||
await http.delete(`/evaluations/setups/${id}`)
|
||||
ElMessage.success('已删除')
|
||||
await loadSetups()
|
||||
} catch (e: any) { if (e !== 'cancel' && e !== 'close') ElMessage.error(apiErrorMessage(e)) }
|
||||
}
|
||||
|
||||
// ── Admin: Results ──
|
||||
const resultSetupId = ref<string>()
|
||||
const results = ref<any>(null)
|
||||
const resultLoading = ref(false)
|
||||
|
||||
async function loadResults() {
|
||||
if (!resultSetupId.value) return
|
||||
resultLoading.value = true
|
||||
try { results.value = (await http.get(`/evaluations/setups/${resultSetupId.value}/results`)).data }
|
||||
catch (e) { ElMessage.error(apiErrorMessage(e)) }
|
||||
finally { resultLoading.value = false }
|
||||
}
|
||||
|
||||
// ── Student: Evaluation ──
|
||||
const studentTasks = ref<any[]>([])
|
||||
const studentLoading = ref(false)
|
||||
const evalDialog = ref(false)
|
||||
const evalFormData = ref<any>(null)
|
||||
const evalScores = ref<Record<string, number>>({})
|
||||
const evalSubmitting = ref(false)
|
||||
|
||||
async function loadStudentTasks() {
|
||||
studentLoading.value = true
|
||||
try { studentTasks.value = ((await http.get('/evaluations/my-tasks')).data).tasks ?? [] }
|
||||
catch (e) { ElMessage.error(apiErrorMessage(e)) }
|
||||
finally { studentLoading.value = false }
|
||||
}
|
||||
|
||||
async function openEval(task: any) {
|
||||
try {
|
||||
const { data } = await http.get(`/evaluations/tasks/${task.teachingTaskId}/form`)
|
||||
evalFormData.value = data
|
||||
evalScores.value = {}
|
||||
for (const d of data.dimensions) evalScores.value[d.id] = Math.floor(d.maxScore * 0.8)
|
||||
evalDialog.value = true
|
||||
} catch (e) { ElMessage.error(apiErrorMessage(e)) }
|
||||
}
|
||||
|
||||
const evalTotal = computed(() => {
|
||||
if (!evalFormData.value) return 0
|
||||
return evalFormData.value.dimensions.reduce((s: number, d: any) => s + (evalScores.value[d.id] ?? 0), 0)
|
||||
})
|
||||
|
||||
async function submitEval() {
|
||||
evalSubmitting.value = true
|
||||
try {
|
||||
await http.post(`/evaluations/tasks/${evalFormData.value.task.id}/submit`, {
|
||||
setupId: evalFormData.value.setupId,
|
||||
scores: evalFormData.value.dimensions.map((d: any) => ({
|
||||
dimensionId: d.id,
|
||||
score: evalScores.value[d.id] ?? 0,
|
||||
})),
|
||||
})
|
||||
ElMessage.success('评教提交成功,感谢您的反馈!')
|
||||
evalDialog.value = false
|
||||
await loadStudentTasks()
|
||||
} catch (e) { ElMessage.error(apiErrorMessage(e)) }
|
||||
finally { evalSubmitting.value = false }
|
||||
}
|
||||
|
||||
// ── Teacher: Results ──
|
||||
const teacherResults = ref<any[]>([])
|
||||
const teacherLoading = ref(false)
|
||||
|
||||
async function loadTeacherResults() {
|
||||
teacherLoading.value = true
|
||||
try { teacherResults.value = ((await http.get('/evaluations/my-results')).data).tasks ?? [] }
|
||||
catch (e) { ElMessage.error(apiErrorMessage(e)) }
|
||||
finally { teacherLoading.value = false }
|
||||
}
|
||||
|
||||
// ── Init ──
|
||||
onMounted(async () => {
|
||||
if (isAdmin.value || isStaff.value) {
|
||||
const [tRes] = await Promise.all([http.get('/base-data/terms')])
|
||||
terms.value = tRes.data
|
||||
await loadSetups()
|
||||
}
|
||||
if (isStudent.value) await loadStudentTasks()
|
||||
if (isTeacher.value) await loadTeacherResults()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-stack">
|
||||
<section class="page-intro">
|
||||
<div>
|
||||
<span class="section-kicker">EVALUATION</span>
|
||||
<h2>{{ isStudent ? '学生评教' : isTeacher ? '评教结果' : '教学评价' }}</h2>
|
||||
<p v-if="isStudent">请对本学期修读的每门课程进行客观评价,每门课满分 100 分。</p>
|
||||
<p v-else-if="isTeacher">查看您所授课程的学生评教平均分。</p>
|
||||
<p v-else>管理评教方案,查看评教结果统计。</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ Admin / Staff: Manage Setups ═══ -->
|
||||
<template v-if="isAdmin || isStaff">
|
||||
<el-tabs v-model="resultSetupId" @tab-change="loadResults">
|
||||
<el-tab-pane label="评教方案" name="">
|
||||
<section class="data-card" v-loading="setupLoading">
|
||||
<div style="display:flex;justify-content:flex-end;margin-bottom:12px">
|
||||
<el-button v-if="isAdmin" type="primary" :icon="Plus" @click="openCreate">新建方案</el-button>
|
||||
</div>
|
||||
<el-table :data="setups" size="small" empty-text="暂无评教方案">
|
||||
<el-table-column prop="name" label="方案名称" min-width="200" />
|
||||
<el-table-column prop="termName" label="学期" width="140" />
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'Draft' ? 'info' : row.status === 'Open' ? 'success' : 'warning'" size="small">
|
||||
{{ row.status === 'Draft' ? '草稿' : row.status === 'Open' ? '进行中' : '已关闭' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="维度" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<span v-for="d in row.dimensions" :key="d.id">
|
||||
<el-tag size="small" style="margin:1px">{{ d.name }}({{ d.maxScore }}分)</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="recordCount" label="评教数" width="80" />
|
||||
<el-table-column label="操作" width="200" fixed="right" v-if="isAdmin">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status === 'Draft'" size="small" :icon="Edit" @click="openEdit(row)" text>编辑</el-button>
|
||||
<el-button v-if="row.status === 'Draft'" size="small" type="success" :icon="Unlock" @click="openSetup(row.id)" text>开启</el-button>
|
||||
<el-button v-if="row.status === 'Open'" size="small" type="danger" :icon="Lock" @click="closeSetup(row.id)" text>关闭</el-button>
|
||||
<el-button v-if="row.status !== 'Open' && row.recordCount === 0" size="small" type="danger" :icon="Delete" @click="deleteSetup(row.id)" text>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane v-for="s in setups.filter((x: any) => x.status !== 'Draft')" :key="s.id" :label="s.name" :name="s.id">
|
||||
<section class="data-card" v-loading="resultLoading">
|
||||
<div class="metrics-strip" v-if="results">
|
||||
<span class="metric-chip"><b>{{ results.totalRecords }}</b> 条评教</span>
|
||||
<span class="metric-chip"><b>{{ results.distinctTasks }}</b> 个教学班</span>
|
||||
<span class="metric-chip"><b>{{ results.distinctStudents }}</b> 名学生</span>
|
||||
</div>
|
||||
<el-table :data="results?.byTask ?? []" size="small" empty-text="暂无评教数据">
|
||||
<el-table-column prop="taskNumber" label="教学班号" width="120" />
|
||||
<el-table-column prop="courseName" label="课程" min-width="150" />
|
||||
<el-table-column prop="teacherNames" label="教师" width="100" />
|
||||
<el-table-column prop="collegeName" label="开课学院" width="120" />
|
||||
<el-table-column label="总分均分" width="100" sortable="custom">
|
||||
<template #default="{ row }">
|
||||
<b :style="{ color: row.averageTotal >= 80 ? '#67C23A' : row.averageTotal >= 60 ? '#E6A23C' : '#F56C6C' }">
|
||||
{{ row.averageTotal }}
|
||||
</b>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-for="dim in results?.setup?.dimensions ?? []" :key="dim.id" :label="dim.name + '(' + dim.maxScore + ')'" width="110">
|
||||
<template #default="{ row }">
|
||||
{{ (row.dimensions?.find((x: any) => x.dimensionName === dim.name))?.averageScore ?? '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="studentCount" label="评教人数" width="80" />
|
||||
</el-table>
|
||||
</section>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<!-- Setup Dialog -->
|
||||
<el-dialog v-model="setupDialog" :title="editingSetup ? '编辑评教方案' : '新建评教方案'" width="640px">
|
||||
<el-form label-position="top">
|
||||
<div class="form-grid">
|
||||
<el-form-item label="学期">
|
||||
<el-select v-model="setupForm.academicTermId" placeholder="选择学期" :disabled="!!editingSetup">
|
||||
<el-option v-for="t in terms" :key="t.id" :label="t.name" :value="t.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="方案名称">
|
||||
<el-input v-model="setupForm.name" placeholder="如:2026年秋季学期学生评教" maxlength="120" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开始时间">
|
||||
<el-date-picker v-model="setupForm.startsAt" type="datetime" placeholder="可选" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="结束时间">
|
||||
<el-date-picker v-model="setupForm.endsAt" type="datetime" placeholder="可选" style="width:100%" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-divider>评分维度 <el-tag :type="dimSum === 100 ? 'success' : 'danger'" size="small">{{ dimSum === 100 ? '已平衡 ✓' : '合计 ' + dimSum + '(需为 100)' }}</el-tag></el-divider>
|
||||
<div v-for="(dim, idx) in setupForm.dimensions" :key="idx" style="display:flex;gap:8px;margin-bottom:8px;align-items:center">
|
||||
<el-input v-model="dim.name" placeholder="维度名称" style="flex:2" maxlength="60" />
|
||||
<el-input-number v-model="dim.maxScore" :min="1" :max="100" placeholder="满分" style="flex:1" />
|
||||
<el-button :icon="Delete" circle size="small" type="danger" @click="removeDim(idx)" :disabled="setupForm.dimensions.length <= 1" />
|
||||
</div>
|
||||
<el-button :icon="Plus" size="small" @click="addDim" style="margin-top:4px">添加维度</el-button>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="setupDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveSetup" :disabled="dimSum !== 100">保存方案</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<!-- ═══ Student: Evaluate ═══ -->
|
||||
<template v-if="isStudent">
|
||||
<section class="data-card" v-loading="studentLoading">
|
||||
<el-empty v-if="studentTasks.length === 0" description="暂无可评教的课程" />
|
||||
<div v-else class="eval-task-list">
|
||||
<article v-for="task in studentTasks" :key="task.teachingTaskId" class="eval-task-card">
|
||||
<div class="eval-task-info">
|
||||
<b>{{ task.courseName }}</b>
|
||||
<span>{{ task.courseCode }} · {{ task.teacherNames?.join('、') }}</span>
|
||||
<small>{{ task.taskNumber }}</small>
|
||||
</div>
|
||||
<div class="eval-task-action">
|
||||
<el-tag v-if="task.isEvaluated" type="success" size="small">已评教</el-tag>
|
||||
<el-button v-else type="primary" :icon="Edit" @click="openEval(task)" size="small">去评教</el-button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Eval Dialog -->
|
||||
<el-dialog v-model="evalDialog" title="学生评教" width="580px" :close-on-click-modal="false">
|
||||
<template v-if="evalFormData">
|
||||
<div class="eval-header">
|
||||
<b>{{ evalFormData.task.courseName }}</b>
|
||||
<span>{{ evalFormData.task.teacherNames?.join('、') }}</span>
|
||||
<small>{{ evalFormData.task.taskNumber }}</small>
|
||||
</div>
|
||||
<el-divider />
|
||||
<div v-for="dim in evalFormData.dimensions" :key="dim.id" style="margin-bottom:20px">
|
||||
<div style="display:flex;justify-content:space-between;margin-bottom:6px">
|
||||
<b>{{ dim.name }}</b>
|
||||
<el-tag size="small">{{ evalScores[dim.id] ?? 0 }} / {{ dim.maxScore }} 分</el-tag>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<span style="font-size:12px;color:#909399">0</span>
|
||||
<el-slider v-model="evalScores[dim.id]" :max="dim.maxScore" :step="1" show-stops style="flex:1" />
|
||||
<span style="font-size:12px;color:#909399">{{ dim.maxScore }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider />
|
||||
<div style="text-align:center">
|
||||
<span style="font-size:24px;font-weight:700;color:#409EFF">{{ evalTotal }}</span>
|
||||
<span style="color:#909399"> / 100 分</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<el-button @click="evalDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="evalSubmitting" @click="submitEval">提交评教</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<!-- ═══ Teacher: Results ═══ -->
|
||||
<template v-if="isTeacher">
|
||||
<section class="data-card" v-loading="teacherLoading">
|
||||
<el-empty v-if="teacherResults.length === 0" description="暂无评教结果" />
|
||||
<div v-else>
|
||||
<article v-for="item in teacherResults" :key="item.task.id" class="teacher-result-card">
|
||||
<div class="result-header">
|
||||
<div>
|
||||
<b>{{ item.task.courseName }}</b>
|
||||
<span>{{ item.task.courseCode }} · {{ item.task.taskNumber }}</span>
|
||||
<small>{{ item.setupName }}</small>
|
||||
</div>
|
||||
<div class="result-total">
|
||||
<span :style="{ color: item.averageTotal >= 80 ? '#67C23A' : item.averageTotal >= 60 ? '#E6A23C' : '#F56C6C' }">
|
||||
{{ item.averageTotal }}
|
||||
</span>
|
||||
<small>/ 100 · {{ item.studentCount }} 人参评</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-dims">
|
||||
<div v-for="dim in item.dimensions" :key="dim.dimensionId" class="result-dim-item">
|
||||
<span>{{ dim.dimensionName }}</span>
|
||||
<el-progress :percentage="Math.round(dim.averageScore / dim.maxScore * 100)" :color="dim.averageScore / dim.maxScore >= 0.8 ? '#67C23A' : '#E6A23C'" style="flex:1;margin:0 12px" />
|
||||
<b>{{ dim.averageScore }}<small>/{{ dim.maxScore }}</small></b>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-stack { max-width: 1200px; margin: 0 auto }
|
||||
|
||||
.metrics-strip { display: flex; gap: 16px; margin-bottom: 16px; flex-wrap: wrap }
|
||||
.metric-chip {
|
||||
background: var(--el-color-primary-light-9, #ecf5ff); color: var(--el-color-primary, #409EFF);
|
||||
padding: 4px 14px; border-radius: 16px; font-size: 13px;
|
||||
}
|
||||
|
||||
.eval-task-list { display: flex; flex-direction: column; gap: 10px }
|
||||
.eval-task-card {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 12px 16px; border: 1px solid #e4e7ed; border-radius: 8px;
|
||||
}
|
||||
.eval-task-info { display: flex; flex-direction: column; gap: 2px }
|
||||
.eval-task-info b { font-size: 15px }
|
||||
.eval-task-info span { font-size: 13px; color: #606266 }
|
||||
.eval-task-info small { font-size: 12px; color: #909399 }
|
||||
|
||||
.eval-header { text-align: center }
|
||||
.eval-header b { display: block; font-size: 16px }
|
||||
.eval-header span { color: #606266; font-size: 14px }
|
||||
.eval-header small { display: block; color: #909399; font-size: 12px }
|
||||
|
||||
.teacher-result-card {
|
||||
border: 1px solid #e4e7ed; border-radius: 8px; padding: 16px; margin-bottom: 12px;
|
||||
}
|
||||
.result-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px }
|
||||
.result-header b { display: block; font-size: 16px }
|
||||
.result-header span { font-size: 13px; color: #606266 }
|
||||
.result-header small { display: block; font-size: 12px; color: #909399 }
|
||||
.result-total { text-align: right }
|
||||
.result-total span { font-size: 28px; font-weight: 700 }
|
||||
.result-total small { display: block; font-size: 12px; color: #909399 }
|
||||
|
||||
.result-dims { display: flex; flex-direction: column; gap: 8px }
|
||||
.result-dim-item { display: flex; align-items: center }
|
||||
.result-dim-item > span { width: 80px; font-size: 13px; flex-shrink: 0 }
|
||||
.result-dim-item b { font-size: 14px; min-width: 60px; text-align: right }
|
||||
.result-dim-item b small { font-size: 11px; color: #909399; font-weight: 400 }
|
||||
|
||||
.form-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px }
|
||||
</style>
|
||||
Reference in New Issue
Block a user