Files
Academic-Affairs-System/web/src/views/TeachingPreferencesView.vue
T
2026-08-11 11:52:43 +08:00

378 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { Plus, Refresh } from '@element-plus/icons-vue'
import http, { apiErrorMessage } from '../api/http'
import { useAuthStore } from '../stores/auth'
import { academicTermLabel, academicTermOptionClass, defaultAcademicTermId } from '../utils/academicTerms'
const auth = useAuthStore()
const isReviewer = computed(() =>
auth.user?.roles.some((role) =>
['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(role)) ?? false)
const isTeacher = computed(() =>
(auth.user?.roles.includes('Teacher') ?? false) && !isReviewer.value)
const loading = ref(false)
const rows = ref<any[]>([])
const terms = ref<any[]>([])
const courses = ref<any[]>([])
const teachers = ref<any[]>([])
const submitDialog = ref(false)
const reviewDialog = ref(false)
const assignmentDialog = ref(false)
const page = ref(1)
const total = ref(0)
const pageSize = 20
const form = reactive<Record<string, any>>({})
const reviewForm = reactive<Record<string, any>>({})
const assignmentForm = reactive<Record<string, any>>({})
const filters = reactive({
academicTermId: undefined as string | undefined,
status: isTeacher.value ? undefined : 'Pending' as string | undefined,
})
const statusLabels: Record<string, string> = {
Pending: '待审核',
Approved: '已通过',
Rejected: '已驳回',
Withdrawn: '已撤回',
}
const statusTypes: Record<string, 'success' | 'warning' | 'danger' | 'info'> = {
Pending: 'warning',
Approved: 'success',
Rejected: 'danger',
Withdrawn: 'info',
}
async function load(resetPage = false) {
if (resetPage) page.value = 1
loading.value = true
try {
const endpoint = isTeacher.value
? '/teacher-course-applications/mine'
: '/teacher-course-applications/reviews'
const { data } = await http.get(endpoint, {
params: { ...filters, page: page.value, pageSize },
})
rows.value = data.items
total.value = data.total
if (rows.value.length === 0 && page.value > 1) {
page.value--
await load()
}
} catch (error) {
ElMessage.error(apiErrorMessage(error))
} finally {
loading.value = false
}
}
function openSubmit() {
Object.assign(form, {
academicTermId: filters.academicTermId ??
defaultAcademicTermId(terms.value),
courseId: undefined,
statement: '',
})
submitDialog.value = true
}
async function submit() {
if (!form.academicTermId || !form.courseId) {
ElMessage.warning('请选择学期和课程。')
return
}
try {
await http.post('/teacher-course-applications/mine', form)
submitDialog.value = false
ElMessage.success('课程申报已提交,等待学院审核')
await load()
} catch (error) {
ElMessage.error(apiErrorMessage(error))
}
}
async function withdraw(row: any) {
try {
await ElMessageBox.confirm(`撤回“${row.courseName}”的授课申报?`, '撤回申报', {
type: 'warning',
})
await http.delete(`/teacher-course-applications/mine/${row.id}`)
await load()
} catch (error: any) {
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
}
}
function openReview(row: any) {
Object.assign(reviewForm, {
id: row.id,
title: `${row.teacherName} · ${row.courseCode} ${row.courseName}`,
statement: row.statement,
status: 'Approved',
reviewComment: '',
})
reviewDialog.value = true
}
async function review() {
try {
await http.post(`/teacher-course-applications/${reviewForm.id}/review`, {
status: reviewForm.status,
reviewComment: reviewForm.reviewComment,
})
reviewDialog.value = false
ElMessage.success(reviewForm.status === 'Approved' ? '已审核通过' : '已驳回申报')
await load()
} catch (error) {
ElMessage.error(apiErrorMessage(error))
}
}
function openAssignment() {
Object.assign(assignmentForm, {
academicTermId: filters.academicTermId ??
defaultAcademicTermId(terms.value),
teacherId: undefined,
courseId: undefined,
comment: '',
})
assignmentDialog.value = true
}
async function assignTeacher() {
if (!assignmentForm.academicTermId ||
!assignmentForm.teacherId ||
!assignmentForm.courseId) {
ElMessage.warning('请选择学期、教师和课程。')
return
}
try {
await http.post('/teacher-course-applications/assign', assignmentForm)
assignmentDialog.value = false
ElMessage.success('授课资格已直接分配')
await load()
} catch (error) {
ElMessage.error(apiErrorMessage(error))
}
}
onMounted(async () => {
const requests: Promise<any>[] = [http.get('/base-data/terms')]
if (isTeacher.value) {
requests.push(http.get('/teacher-course-applications/course-options'))
} else {
requests.push(http.get('/teacher-course-applications/assignment-options'))
}
const [termRes, optionRes] = await Promise.all(requests)
terms.value = termRes.data
if (isTeacher.value) {
courses.value = optionRes?.data ?? []
} else {
courses.value = optionRes?.data?.courses ?? []
teachers.value = optionRes?.data?.teachers ?? []
}
filters.academicTermId = defaultAcademicTermId(terms.value)
await load()
})
</script>
<template>
<div class="page-stack">
<section class="page-intro">
<div>
<span class="section-kicker">TEACHING ELIGIBILITY</span>
<h2>{{ isTeacher ? '授课科目申报' : '授课资格审核' }}</h2>
<p v-if="isTeacher">在排课前申报本学期愿意承担的课程,学院审核通过后才能分配公共课教学任务。</p>
<p v-else>审核教师申报,或由学院直接指定教师;通过后进入相应学期和课程的可分配教师池。</p>
</div>
<div class="page-actions">
<el-button v-if="isTeacher" type="primary" :icon="Plus" @click="openSubmit">
申报课程
</el-button>
<el-button v-else type="primary" :icon="Plus" @click="openAssignment">
直接分配教师
</el-button>
</div>
</section>
<section class="eligibility-flow">
<div class="active"><span>教师</span><b>选择学期与科目</b></div>
<i>→</i>
<div><span>学院</span><b>审核或直接分配</b></div>
<i>→</i>
<div><span>教务</span><b>合班并分配教师</b></div>
<i>→</i>
<div><span>排课</span><b>生成课表</b></div>
</section>
<section class="data-card">
<div class="filter-bar">
<el-select v-model="filters.academicTermId" clearable placeholder="全部学期" @change="load(true)">
<el-option v-for="item in terms" :key="item.id" :label="academicTermLabel(item)" :value="item.id" :class="academicTermOptionClass(item)" />
</el-select>
<el-select v-model="filters.status" clearable placeholder="全部状态" @change="load(true)">
<el-option v-for="(label, value) in statusLabels" :key="value" :label="label" :value="value" />
</el-select>
<el-button :icon="Refresh" @click="() => load()">刷新</el-button>
<span>共 {{ total }} 条</span>
</div>
<el-table v-loading="loading" :data="rows" class="data-table">
<el-table-column v-if="!isTeacher" label="申报教师" min-width="170">
<template #default="{ row }">
<div class="course-name"><b>{{ row.teacherName }}</b><span>{{ row.teacherNumber }} · {{ row.teacherCollegeName }}</span></div>
</template>
</el-table-column>
<el-table-column label="申报课程" min-width="220">
<template #default="{ row }">
<div class="course-name"><b>{{ row.courseName }}</b><span>{{ row.courseCode }} · {{ row.courseCollegeName || row.collegeName }}</span></div>
</template>
</el-table-column>
<el-table-column prop="termName" label="学期" min-width="180" />
<el-table-column label="申报说明" min-width="180">
<template #default="{ row }">{{ row.statement || '—' }}</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }"><el-tag :type="statusTypes[row.status]">{{ statusLabels[row.status] }}</el-tag></template>
</el-table-column>
<el-table-column label="审核意见" min-width="160">
<template #default="{ row }">{{ row.reviewComment || '—' }}</template>
</el-table-column>
<el-table-column label="操作" width="110" fixed="right">
<template #default="{ row }">
<el-button v-if="isTeacher && row.status === 'Pending'" link type="danger" @click="withdraw(row)">撤回</el-button>
<el-button v-if="!isTeacher && row.status === 'Pending'" link type="primary" @click="openReview(row)">审核</el-button>
<span v-if="row.status !== 'Pending'" class="muted-action">已处理</span>
</template>
</el-table-column>
<template #empty><el-empty :description="isTeacher ? '尚未申报授课科目' : '没有待处理的授课申报'" /></template>
</el-table>
<el-pagination
v-if="total > pageSize"
v-model:current-page="page"
:page-size="pageSize"
:total="total"
layout="total, prev, pager, next"
class="table-pagination"
@current-change="() => load()"
/>
</section>
<el-dialog v-model="submitDialog" title="申报授课科目" width="600px">
<el-form label-position="top">
<div class="form-grid">
<el-form-item label="学期" required>
<el-select v-model="form.academicTermId">
<el-option v-for="item in terms" :key="item.id" :label="academicTermLabel(item)" :value="item.id" :class="academicTermOptionClass(item)" />
</el-select>
</el-form-item>
<el-form-item label="拟授课程" required>
<el-select v-model="form.courseId" filterable>
<el-option v-for="item in courses" :key="item.id" :label="`${item.code} · ${item.name}`" :value="item.id" />
</el-select>
</el-form-item>
</div>
<el-form-item label="申报说明">
<el-input v-model="form.statement" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="可填写相关教学经历、研究方向或其他说明" />
</el-form-item>
</el-form>
<template #footer><el-button @click="submitDialog = false">取消</el-button><el-button type="primary" @click="submit">提交学院审核</el-button></template>
</el-dialog>
<el-dialog v-model="reviewDialog" title="审核授课资格" width="580px">
<div class="constraint-title">{{ reviewForm.title }}</div>
<el-descriptions :column="1" border>
<el-descriptions-item label="教师说明">{{ reviewForm.statement || '未填写' }}</el-descriptions-item>
</el-descriptions>
<el-form label-position="top" style="margin-top: 16px">
<el-form-item label="审核结论">
<el-radio-group v-model="reviewForm.status">
<el-radio-button value="Approved">通过</el-radio-button>
<el-radio-button value="Rejected">驳回</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="审核意见">
<el-input v-model="reviewForm.reviewComment" type="textarea" :rows="3" maxlength="500" show-word-limit />
</el-form-item>
</el-form>
<template #footer><el-button @click="reviewDialog = false">取消</el-button><el-button type="primary" @click="review">确认审核</el-button></template>
</el-dialog>
<el-dialog v-model="assignmentDialog" title="直接分配授课教师" width="640px">
<el-alert
title="无需教师先申报,保存后立即取得所选学期与课程的授课资格。"
type="info"
:closable="false"
show-icon
/>
<el-form label-position="top" style="margin-top: 18px">
<el-form-item label="学期" required>
<el-select v-model="assignmentForm.academicTermId" style="width: 100%">
<el-option v-for="item in terms" :key="item.id" :label="academicTermLabel(item)" :value="item.id" :class="academicTermOptionClass(item)" />
</el-select>
</el-form-item>
<div class="form-grid">
<el-form-item label="教师" required>
<el-select
v-model="assignmentForm.teacherId"
filterable
style="width: 100%"
placeholder="按工号或姓名选择"
>
<el-option
v-for="item in teachers"
:key="item.id"
:label="`${item.teacherNumber} · ${item.name}`"
:value="item.id"
>
<span>{{ item.teacherNumber }} · {{ item.name }}</span>
<small class="option-hint">{{ item.collegeName }}{{ item.title ? ` · ${item.title}` : '' }}</small>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="课程" required>
<el-select
v-model="assignmentForm.courseId"
filterable
style="width: 100%"
placeholder="按编码或名称选择"
>
<el-option
v-for="item in courses"
:key="item.id"
:label="`${item.code} · ${item.name}`"
:value="item.id"
>
<span>{{ item.code }} · {{ item.name }}</span>
<small class="option-hint">{{ item.collegeName }} · {{ item.credits }} 学分</small>
</el-option>
</el-select>
</el-form-item>
</div>
<el-form-item label="分配说明">
<el-input
v-model="assignmentForm.comment"
type="textarea"
:rows="3"
maxlength="500"
show-word-limit
placeholder="可填写教研室安排、专业方向等依据"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="assignmentDialog = false">取消</el-button>
<el-button type="primary" @click="assignTeacher">确认分配</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.option-hint {
float: right;
margin-left: 20px;
color: #8a93a5;
font-size: 11px;
}
</style>