deepseek
前台 — 路由 (router/index.ts) - my-timetable 路由的角色从 ['Student'] 扩展为 ['Student', 'Teacher'] - 新增 teacher-timetable/:teacherId 路由,用于查看指定教师的课表 前台 — 课表视图 (TimetableView.vue) - 新增 isTeacherView 和 isTeacher 计算属性,区分教师个人课表和查看他人课表 - 教师个人课表:标题显示"我的授课课表",描述改为"展示本人本学期所有授课安排" - 查看他人课表:标题显示"教师课表" - 两种模式均隐藏资源筛选面板,只保留学期选择 - 加载逻辑适配教师模式 API 前台 — 教师档案页 (PersonnelView.vue) - 教师行操作列新增 "课表" 按钮(带日历图标),仅在职教师可点击 - 点击后跳转到该教师的授课课表页面 前台 — 侧边导航 (AdminLayout.vue) - 教师角色可见"我的授课课表"菜单项,与学生的"我的课表"并列
This commit is contained in:
@@ -91,6 +91,45 @@ public sealed class TimetablesController(
|
||||
CancellationToken cancellationToken) =>
|
||||
BuildTimetableAsync(classId, academicTermId, null, cancellationToken);
|
||||
|
||||
[HttpGet("teachers/{teacherId:guid}")]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult> GetTeacherTimetable(
|
||||
Guid teacherId,
|
||||
Guid? academicTermId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await timetableDataService.BuildAsync(
|
||||
TimetableResourceType.Teacher,
|
||||
teacherId,
|
||||
academicTermId,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
cancellationToken);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("teachers/{teacherId:guid}/export.xlsx")]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult> ExportTeacherTimetable(
|
||||
Guid teacherId,
|
||||
Guid? academicTermId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await timetableDataService.BuildAsync(
|
||||
TimetableResourceType.Teacher,
|
||||
teacherId,
|
||||
academicTermId,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
cancellationToken);
|
||||
if (result is null) return NotFound();
|
||||
return ExcelFile(result);
|
||||
}
|
||||
|
||||
[HttpGet("classes/{classId:guid}/export.xlsx")]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult> ExportClassTimetable(
|
||||
@@ -112,13 +151,14 @@ public sealed class TimetablesController(
|
||||
}
|
||||
|
||||
[HttpGet("mine")]
|
||||
[Authorize(Roles = SystemRoles.Student)]
|
||||
[Authorize(Roles = SystemRoles.Student + "," + SystemRoles.Teacher)]
|
||||
public async Task<ActionResult> GetMyTimetable(
|
||||
Guid? academicTermId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!Guid.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out var userId))
|
||||
return Unauthorized();
|
||||
|
||||
var student = await db.Students.AsNoTracking()
|
||||
.Where(x => x.UserId == userId)
|
||||
.Select(x => new
|
||||
@@ -129,30 +169,51 @@ public sealed class TimetablesController(
|
||||
x.Name
|
||||
})
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
if (student is null)
|
||||
return Conflict(new ProblemDetails
|
||||
{
|
||||
Title = "学生档案未关联",
|
||||
Detail = "当前登录账号没有关联学生档案,请联系教务管理员。",
|
||||
Status = StatusCodes.Status409Conflict
|
||||
});
|
||||
if (student is not null)
|
||||
{
|
||||
return await BuildTimetableAsync(
|
||||
student.AdministrativeClassId,
|
||||
academicTermId,
|
||||
student.Id,
|
||||
cancellationToken,
|
||||
new TimetableStudentDto(student.StudentNumber, student.Name));
|
||||
}
|
||||
|
||||
return await BuildTimetableAsync(
|
||||
student.AdministrativeClassId,
|
||||
academicTermId,
|
||||
student.Id,
|
||||
cancellationToken,
|
||||
new TimetableStudentDto(student.StudentNumber, student.Name));
|
||||
var teacher = await db.Teachers.AsNoTracking()
|
||||
.Where(x => x.UserId == userId && x.Status == TeacherStatus.Active)
|
||||
.Select(x => new { x.Id })
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
if (teacher is not null)
|
||||
{
|
||||
var result = await timetableDataService.BuildAsync(
|
||||
TimetableResourceType.Teacher,
|
||||
teacher.Id,
|
||||
academicTermId,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
cancellationToken);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
return Conflict(new ProblemDetails
|
||||
{
|
||||
Title = "档案未关联",
|
||||
Detail = "当前登录账号没有关联教师或学生档案,请联系教务管理员。",
|
||||
Status = StatusCodes.Status409Conflict
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("mine/export.xlsx")]
|
||||
[Authorize(Roles = SystemRoles.Student)]
|
||||
[Authorize(Roles = SystemRoles.Student + "," + SystemRoles.Teacher)]
|
||||
public async Task<ActionResult> ExportMyTimetable(
|
||||
Guid? academicTermId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!Guid.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out var userId))
|
||||
return Unauthorized();
|
||||
|
||||
var student = await db.Students.AsNoTracking()
|
||||
.Where(x => x.UserId == userId)
|
||||
.Select(x => new
|
||||
@@ -163,18 +224,41 @@ public sealed class TimetablesController(
|
||||
x.Name
|
||||
})
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
if (student is null) return NotFound();
|
||||
var result = await timetableDataService.BuildAsync(
|
||||
TimetableResourceType.Class,
|
||||
student.AdministrativeClassId,
|
||||
academicTermId,
|
||||
null,
|
||||
false,
|
||||
student.Id,
|
||||
new TimetableStudentDto(student.StudentNumber, student.Name),
|
||||
cancellationToken);
|
||||
if (result is null) return NotFound();
|
||||
return ExcelFile(result);
|
||||
if (student is not null)
|
||||
{
|
||||
var result = await timetableDataService.BuildAsync(
|
||||
TimetableResourceType.Class,
|
||||
student.AdministrativeClassId,
|
||||
academicTermId,
|
||||
null,
|
||||
false,
|
||||
student.Id,
|
||||
new TimetableStudentDto(student.StudentNumber, student.Name),
|
||||
cancellationToken);
|
||||
if (result is null) return NotFound();
|
||||
return ExcelFile(result);
|
||||
}
|
||||
|
||||
var teacher = await db.Teachers.AsNoTracking()
|
||||
.Where(x => x.UserId == userId && x.Status == TeacherStatus.Active)
|
||||
.Select(x => new { x.Id })
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
if (teacher is not null)
|
||||
{
|
||||
var result = await timetableDataService.BuildAsync(
|
||||
TimetableResourceType.Teacher,
|
||||
teacher.Id,
|
||||
academicTermId,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
cancellationToken);
|
||||
if (result is null) return NotFound();
|
||||
return ExcelFile(result);
|
||||
}
|
||||
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
private async Task<ActionResult> BuildTimetableAsync(
|
||||
|
||||
@@ -104,7 +104,7 @@ const navigationGroups = computed<NavigationGroup[]>(() => [
|
||||
hasAnyRole(['SuperAdmin', 'AcademicAdmin']),
|
||||
{ path: '/schedules', label: '排课与课表' },
|
||||
),
|
||||
...whenVisible(isStudent.value, { path: '/my-timetable', label: '我的课表' }),
|
||||
...whenVisible(isStudent.value || isTeacher.value, { path: '/my-timetable', label: isTeacher.value ? '我的授课课表' : '我的课表' }),
|
||||
...whenVisible(isStudent.value, { path: '/free-classrooms', label: '空闲教室' }),
|
||||
...whenVisible(!isStudent.value, {
|
||||
path: '/class-timetable',
|
||||
|
||||
@@ -141,7 +141,13 @@ const router = createRouter({
|
||||
path: 'my-timetable',
|
||||
name: 'my-timetable',
|
||||
component: () => import('../views/TimetableView.vue'),
|
||||
meta: { roles: ['Student'], mine: true },
|
||||
meta: { roles: ['Student', 'Teacher'], mine: true },
|
||||
},
|
||||
{
|
||||
path: 'teacher-timetable/:teacherId',
|
||||
name: 'teacher-timetable',
|
||||
component: () => import('../views/TimetableView.vue'),
|
||||
meta: { teacherView: true },
|
||||
},
|
||||
{
|
||||
path: 'free-classrooms',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { Document, Download, Key, Plus, Refresh, Search, Upload } from '@element-plus/icons-vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Calendar, Document, Download, Key, Plus, Refresh, Search, Upload } from '@element-plus/icons-vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import http, { apiErrorMessage } from '../api/http'
|
||||
import { downloadApiFile, importExcel } from '../api/excel'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
@@ -16,6 +16,7 @@ interface PageResult {
|
||||
|
||||
const auth = useAuthStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const active = computed<Registry>(() =>
|
||||
route.meta.registry === 'students' ? 'students' : 'teachers',
|
||||
)
|
||||
@@ -341,6 +342,10 @@ async function activateTeacherAccount() {
|
||||
}
|
||||
}
|
||||
|
||||
function viewTimetable(teacher: any) {
|
||||
router.push({ name: 'teacher-timetable', params: { teacherId: teacher.id } })
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadReferences(), load()])
|
||||
})
|
||||
@@ -468,7 +473,7 @@ watch(active, async () => {
|
||||
<el-table-column
|
||||
v-if="canManage"
|
||||
label="操作"
|
||||
:width="active === 'teachers' ? 225 : 145"
|
||||
:width="active === 'teachers' ? 280 : 145"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
@@ -482,6 +487,16 @@ watch(active, async () => {
|
||||
>
|
||||
激活账号
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="active === 'teachers'"
|
||||
link
|
||||
type="warning"
|
||||
:icon="Calendar"
|
||||
:disabled="row.status !== 'Active'"
|
||||
@click="viewTimetable(row)"
|
||||
>
|
||||
课表
|
||||
</el-button>
|
||||
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="remove(row)">删除</el-button>
|
||||
</template>
|
||||
|
||||
@@ -10,9 +10,13 @@ const route = useRoute()
|
||||
const auth = useAuthStore()
|
||||
const isMine = computed(() => route.meta.mine === true)
|
||||
const isPublic = computed(() => route.meta.public === true)
|
||||
const isTeacherView = computed(() => route.meta.teacherView === true)
|
||||
const isTeacher = computed(() => auth.user?.roles.includes('Teacher') && !auth.user?.roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(role)))
|
||||
const teacherIdParam = computed(() => route.params.teacherId as string | undefined)
|
||||
const isManager = computed(() =>
|
||||
!isMine.value &&
|
||||
!isPublic.value &&
|
||||
!isTeacherView.value &&
|
||||
(auth.user?.roles.some((role) =>
|
||||
['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Leader'].includes(role)) ?? false),
|
||||
)
|
||||
@@ -378,7 +382,11 @@ function onResourceTypeChanged() {
|
||||
}
|
||||
|
||||
async function loadTimetable() {
|
||||
if (!termId.value || (!isMine.value && !selectedResourceId.value)) {
|
||||
if (!termId.value) {
|
||||
timetable.value = null
|
||||
return
|
||||
}
|
||||
if (!isMine.value && !isTeacherView.value && !selectedResourceId.value) {
|
||||
timetable.value = null
|
||||
return
|
||||
}
|
||||
@@ -388,6 +396,10 @@ async function loadTimetable() {
|
||||
timetable.value = (await http.get('/timetables/mine', {
|
||||
params: { academicTermId: termId.value },
|
||||
})).data
|
||||
} else if (isTeacherView.value && teacherIdParam.value) {
|
||||
timetable.value = (await http.get(`/timetables/teachers/${teacherIdParam.value}`, {
|
||||
params: { academicTermId: termId.value },
|
||||
})).data
|
||||
} else if (isManager.value) {
|
||||
timetable.value = (await http.get('/timetables/management/query', {
|
||||
params: {
|
||||
@@ -418,6 +430,11 @@ async function exportExcel() {
|
||||
`/timetables/mine/export.xlsx?academicTermId=${termId.value}`,
|
||||
'我的课表.xlsx',
|
||||
)
|
||||
} else if (isTeacherView.value && teacherIdParam.value) {
|
||||
await downloadApiFile(
|
||||
`/timetables/teachers/${teacherIdParam.value}/export.xlsx?academicTermId=${termId.value}`,
|
||||
`${timetable.value?.subject?.name ?? '教师'}课表.xlsx`,
|
||||
)
|
||||
} else if (isManager.value) {
|
||||
const query = new URLSearchParams({
|
||||
resourceType: resourceType.value,
|
||||
@@ -478,9 +495,19 @@ async function exportPdf() {
|
||||
}
|
||||
|
||||
watch([classId, teacherId, classroomId, planId, resourceType], loadTimetable)
|
||||
watch(teacherIdParam, (newId) => { if (newId) loadTimetable() })
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
if (isTeacherView.value) {
|
||||
const { data } = await http.get('/timetables/options')
|
||||
terms.value = data.terms
|
||||
termId.value = data.terms.find((item: any) => item.isCurrent)?.id
|
||||
?? data.terms[0]?.id
|
||||
?? ''
|
||||
await loadTimetable()
|
||||
return
|
||||
}
|
||||
await loadPublicOptions()
|
||||
if (isManager.value) await loadManagementOptions()
|
||||
await loadTimetable()
|
||||
@@ -499,9 +526,11 @@ onMounted(async () => {
|
||||
|
||||
<section class="timetable-heading">
|
||||
<div>
|
||||
<span class="section-kicker">{{ isMine ? 'MY TIMETABLE' : isManager ? 'TIMETABLE CENTER' : 'CLASS TIMETABLE' }}</span>
|
||||
<h2>{{ isMine ? '我的课表' : isManager ? '课表查询中心' : '班级课表查询' }}</h2>
|
||||
<p v-if="isMine">行政班课程与本人已选课程统一展示,仅采用教务处已发布课表。</p>
|
||||
<span class="section-kicker">{{ isMine && isTeacher ? 'MY TEACHING SCHEDULE' : isMine ? 'MY TIMETABLE' : isTeacherView ? 'TEACHER TIMETABLE' : isManager ? 'TIMETABLE CENTER' : 'CLASS TIMETABLE' }}</span>
|
||||
<h2>{{ isMine && isTeacher ? '我的授课课表' : isMine ? '我的课表' : isTeacherView ? '教师课表' : isManager ? '课表查询中心' : '班级课表查询' }}</h2>
|
||||
<p v-if="isMine && isTeacher">展示本人本学期所有授课安排,仅采用教务处已发布课表。</p>
|
||||
<p v-else-if="isMine">行政班课程与本人已选课程统一展示,仅采用教务处已发布课表。</p>
|
||||
<p v-else-if="isTeacherView">查看指定教师本学期的授课安排。</p>
|
||||
<p v-else-if="isManager">查询班级、教师与场地课表,可切换草稿或正式发布版本。</p>
|
||||
<p v-else>按年级、学院、专业和班级分类查询正式发布的课程安排。</p>
|
||||
</div>
|
||||
@@ -531,7 +560,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="!isMine" class="resource-filter-panel">
|
||||
<section v-if="!isMine && !isTeacherView" class="resource-filter-panel">
|
||||
<div v-if="isManager" class="resource-type-tabs">
|
||||
<el-radio-group v-model="resourceType" @change="onResourceTypeChanged">
|
||||
<el-radio-button value="Class">班级课表</el-radio-button>
|
||||
|
||||
Reference in New Issue
Block a user