378 lines
14 KiB
Vue
378 lines
14 KiB
Vue
<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>
|