新增开班主流程:实现学校周课表展示、住宿状态直观外露及课次考勤闭环联动
This commit is contained in:
1 parent
55143af4a5
commit
1149c2b141
6 files changed
+773
-10
No files matched your search
@@ -55,6 +55,18 @@ public class EnrollmentItemVO
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
/** 宿舍楼栋 (当前分配) */
|
||||
private String dormBuilding;
|
||||
|
||||
/** 宿舍房间号 (当前分配) */
|
||||
private String dormRoom;
|
||||
|
||||
/** 床位号 (当前分配) */
|
||||
private String bedNo;
|
||||
|
||||
/** 住宿状态 (0: 预分配, 1: 已入住, 2: 已退宿) */
|
||||
private Integer accommodationStatus;
|
||||
|
||||
public Long getEnrollmentId()
|
||||
{
|
||||
return enrollmentId;
|
||||
@@ -204,4 +216,44 @@ public class EnrollmentItemVO
|
||||
{
|
||||
this.remark = remark;
|
||||
}
|
||||
|
||||
public String getDormBuilding()
|
||||
{
|
||||
return dormBuilding;
|
||||
}
|
||||
|
||||
public void setDormBuilding(String dormBuilding)
|
||||
{
|
||||
this.dormBuilding = dormBuilding;
|
||||
}
|
||||
|
||||
public String getDormRoom()
|
||||
{
|
||||
return dormRoom;
|
||||
}
|
||||
|
||||
public void setDormRoom(String dormRoom)
|
||||
{
|
||||
this.dormRoom = dormRoom;
|
||||
}
|
||||
|
||||
public String getBedNo()
|
||||
{
|
||||
return bedNo;
|
||||
}
|
||||
|
||||
public void setBedNo(String bedNo)
|
||||
{
|
||||
this.bedNo = bedNo;
|
||||
}
|
||||
|
||||
public Integer getAccommodationStatus()
|
||||
{
|
||||
return accommodationStatus;
|
||||
}
|
||||
|
||||
public void setAccommodationStatus(Integer accommodationStatus)
|
||||
{
|
||||
this.accommodationStatus = accommodationStatus;
|
||||
}
|
||||
}
|
||||
@@ -78,9 +78,23 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
e.accommodation_required as accommodationRequired,
|
||||
e.join_time as joinTime,
|
||||
e.complete_time as completeTime,
|
||||
e.remark as remark
|
||||
e.remark as remark,
|
||||
d.dorm_building as dormBuilding,
|
||||
d.dorm_room as dormRoom,
|
||||
a.bed_no as bedNo,
|
||||
a.accommodation_status as accommodationStatus
|
||||
from train_enrollment e
|
||||
left join train_student s on e.student_id = s.s_id
|
||||
left join (
|
||||
select a1.enrollment_id, a1.dorm_id, a1.bed_no, a1.accommodation_status
|
||||
from train_accommodation a1
|
||||
inner join (
|
||||
select enrollment_id, max(accommodation_id) as max_id
|
||||
from train_accommodation
|
||||
group by enrollment_id
|
||||
) a2 on a1.accommodation_id = a2.max_id
|
||||
) a on e.enrollment_id = a.enrollment_id
|
||||
left join train_dormitory_info d on a.dorm_id = d.dorm_id
|
||||
<where>
|
||||
e.plan_id = #{planId}
|
||||
<if test="studentName != null and studentName != ''">
|
||||
|
||||
@@ -389,15 +389,20 @@ export default {
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
loadPlans() {
|
||||
const queryPlanId = this.$route.query.planId
|
||||
const queryLessonId = this.$route.query.lessonId
|
||||
listPlan({ pageNum: 1, pageSize: 100 }).then(response => {
|
||||
this.planOptions = response.rows || []
|
||||
if (this.planOptions.length > 0 && !this.rosterQuery.planId) {
|
||||
if (queryPlanId && this.planOptions.some(p => p.planId === queryPlanId)) {
|
||||
this.rosterQuery.planId = queryPlanId
|
||||
this.handlePlanChange(queryPlanId, queryLessonId)
|
||||
} else if (this.planOptions.length > 0 && !this.rosterQuery.planId) {
|
||||
this.rosterQuery.planId = this.planOptions[0].planId
|
||||
this.handlePlanChange(this.rosterQuery.planId)
|
||||
}
|
||||
}).catch(() => {})
|
||||
},
|
||||
handlePlanChange(planId) {
|
||||
handlePlanChange(planId, targetLessonId) {
|
||||
this.rosterQuery.lessonId = null
|
||||
this.rosterList = []
|
||||
this.lessonOptions = []
|
||||
@@ -425,7 +430,9 @@ export default {
|
||||
}
|
||||
this.lessonOptions = list
|
||||
if (list.length > 0) {
|
||||
this.rosterQuery.lessonId = list[0].lessonId
|
||||
const parsedTarget = targetLessonId ? Number(targetLessonId) : null
|
||||
const matched = parsedTarget ? list.find(item => item.lessonId === parsedTarget) : null
|
||||
this.rosterQuery.lessonId = matched ? matched.lessonId : list[0].lessonId
|
||||
this.loadRoster()
|
||||
}
|
||||
}).finally(() => {
|
||||
|
||||
@@ -82,6 +82,31 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="住宿安排" align="center" min-width="170" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<div v-if="scope.row.dormBuilding || scope.row.dormRoom" style="display: flex; align-items: center; justify-content: center; gap: 4px;">
|
||||
<span>{{ scope.row.dormBuilding || '' }} {{ scope.row.dormRoom || '' }} ({{ scope.row.bedNo || '-' }})</span>
|
||||
<el-tag
|
||||
size="mini"
|
||||
:type="scope.row.accommodationStatus === 1 ? 'success' : (scope.row.accommodationStatus === 0 ? 'info' : 'danger')"
|
||||
>
|
||||
{{ scope.row.accommodationStatus === 1 ? '在住' : (scope.row.accommodationStatus === 0 ? '预分配' : '已退宿') }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div v-else-if="scope.row.accommodationRequired === 1">
|
||||
<el-tag size="mini" type="danger">未分配</el-tag>
|
||||
<el-button
|
||||
v-hasPermi="['train:accommodation:add']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-plus"
|
||||
style="margin-left: 4px;"
|
||||
@click="handleOpenAccommodation(scope.row)"
|
||||
>去分配</el-button>
|
||||
</div>
|
||||
<span v-else style="color: #909399; font-size: 12px;">无需住宿</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="参训状态" align="center" prop="enrollmentStatus" width="95">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" :type="statusTagType(scope.row.enrollmentStatus)">
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<el-radio-group v-model="viewMode" size="mini" style="margin-right: 10px;">
|
||||
<el-radio-button label="week">学校周课表</el-radio-button>
|
||||
<el-radio-button label="timeline">按日日程</el-radio-button>
|
||||
<el-radio-button label="table">课次明细</el-radio-button>
|
||||
</el-radio-group>
|
||||
@@ -30,6 +31,119 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视图零:学校周课表视图 (UC-11 学校风格课表展示) -->
|
||||
<div v-if="viewMode === 'week'" v-loading="loading" class="school-timetable-view">
|
||||
<!-- 周切换条 -->
|
||||
<div class="week-nav-bar">
|
||||
<el-button-group size="mini">
|
||||
<el-button icon="el-icon-arrow-left" @click="handlePrevWeek">上一周</el-button>
|
||||
<el-button @click="handleCurrentOrFirstWeek">本周/首周</el-button>
|
||||
<el-button @click="handleNextWeek">下一周<i class="el-icon-arrow-right el-icon--right" /></el-button>
|
||||
</el-button-group>
|
||||
<span class="week-title-display">
|
||||
<i class="el-icon-date" style="color: #409EFF; margin-right: 5px;" />
|
||||
{{ currentWeekDisplayTitle }}
|
||||
</span>
|
||||
<span style="font-size: 12px; color: #909399;">
|
||||
本周课次:{{ currentWeekLessonCount }} 节
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 课表格子结构 -->
|
||||
<div class="school-grid-container">
|
||||
<table class="school-grid-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="time-col-header">时段 / 星期</th>
|
||||
<th
|
||||
v-for="day in currentWeekDays"
|
||||
:key="day.dateStr"
|
||||
:class="['day-col-header', { 'is-today': isToday(day.dateStr) }]"
|
||||
>
|
||||
<div class="header-day-label">{{ day.label }}</div>
|
||||
<div class="header-date-label">{{ day.displayDate }}</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="period in periodList" :key="period.key">
|
||||
<td class="period-title-cell">
|
||||
<div class="period-name">{{ period.label }}</div>
|
||||
<div class="period-time-range">{{ period.timeRange }}</div>
|
||||
</td>
|
||||
<td
|
||||
v-for="day in currentWeekDays"
|
||||
:key="`${day.dateStr}_${period.key}`"
|
||||
class="timetable-cell"
|
||||
>
|
||||
<!-- 格子中的课次卡片列表 -->
|
||||
<div
|
||||
v-for="lesson in getLessonsInCell(day.dateStr, period.key)"
|
||||
:key="lesson.lessonId"
|
||||
class="school-lesson-card"
|
||||
>
|
||||
<div class="slc-time-tag">
|
||||
<i class="el-icon-time" />
|
||||
{{ formatTime(lesson.startTime) }} - {{ formatTime(lesson.endTime) }}
|
||||
</div>
|
||||
<div class="slc-course-title" :title="lesson.courseName">
|
||||
{{ lesson.courseName }}
|
||||
</div>
|
||||
<div class="slc-meta">
|
||||
<span class="meta-row">
|
||||
<i class="el-icon-user" /> {{ lesson.teacherName || '教师待定' }}
|
||||
</span>
|
||||
<span class="meta-row">
|
||||
<i class="el-icon-location-outline" />
|
||||
{{ lesson.venueName || lesson.classRoomName || lesson.siteName || '地点待定' }}
|
||||
</span>
|
||||
<span v-if="lesson.boardSn" class="meta-row" style="color: #67C23A;">
|
||||
<i class="el-icon-monitor" /> {{ lesson.boardSn }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="slc-card-actions">
|
||||
<el-button
|
||||
v-hasPermi="['train:attendance:list']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-date"
|
||||
@click.stop="handleOpenAttendance(lesson)"
|
||||
>考勤</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['train:lesson:edit']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
:disabled="isPlanLocked"
|
||||
@click.stop="handleEditLesson(lesson)"
|
||||
>修改</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['train:lesson:remove']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
style="color: #F56C6C;"
|
||||
:disabled="isPlanLocked"
|
||||
@click.stop="handleRemoveLesson(lesson)"
|
||||
>删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空格子快速排课入口 -->
|
||||
<div
|
||||
v-if="getLessonsInCell(day.dateStr, period.key).length === 0 && !isPlanLocked"
|
||||
class="empty-cell-placeholder"
|
||||
@click="handleQuickAddLesson(day.dateStr, period.key)"
|
||||
>
|
||||
<i class="el-icon-plus" /> 排课
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视图一:按日日程视图 -->
|
||||
<div v-if="viewMode === 'timeline'" v-loading="loading" class="timeline-view">
|
||||
<div v-if="!timetableData.days || timetableData.days.length === 0" class="empty-tip">
|
||||
@@ -93,6 +207,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="lesson-actions">
|
||||
<el-button
|
||||
v-hasPermi="['train:attendance:list']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-date"
|
||||
@click="handleOpenAttendance(lesson)"
|
||||
>考勤</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['train:lesson:edit']"
|
||||
size="mini"
|
||||
@@ -143,8 +264,15 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="课次说明" prop="remark" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="操作" align="center" width="130" fixed="right">
|
||||
<el-table-column label="操作" align="center" width="180" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
v-hasPermi="['train:attendance:list']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-date"
|
||||
@click="handleOpenAttendance(scope.row)"
|
||||
>考勤</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['train:lesson:edit']"
|
||||
size="mini"
|
||||
@@ -347,6 +475,112 @@
|
||||
<el-button @click="dialog.open = false">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 课次学员考勤名册对话框 (第四优先级) -->
|
||||
<el-dialog
|
||||
:title="`课次考勤名册 - ${attendanceDialog.lesson.courseName || ''}`"
|
||||
:visible.sync="attendanceDialog.open"
|
||||
width="900px"
|
||||
append-to-body
|
||||
>
|
||||
<div v-loading="attendanceDialog.loading">
|
||||
<!-- 课次基础概要 -->
|
||||
<el-descriptions :column="3" border size="small" style="margin-bottom: 12px;">
|
||||
<el-descriptions-item label="课程名称">{{ attendanceDialog.lesson.courseName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="授课教师">{{ attendanceDialog.lesson.teacherName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="上课地点">{{ attendanceDialog.lesson.venueName || attendanceDialog.lesson.classRoomName || attendanceDialog.lesson.siteName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="课次时间" :span="2">
|
||||
{{ attendanceDialog.lesson.startTime }} ~ {{ attendanceDialog.lesson.endTime }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="电子班牌">{{ attendanceDialog.lesson.boardSn || '无' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div style="margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center;">
|
||||
<div>
|
||||
<span style="font-weight: bold; font-size: 13px;">应到学员考勤名册</span>
|
||||
<el-tag size="mini" type="info" style="margin-left: 8px;">共 {{ attendanceDialog.rosterList.length }} 人</el-tag>
|
||||
</div>
|
||||
<div>
|
||||
<el-button size="mini" icon="el-icon-refresh" @click="refreshAttendanceRoster">刷新名册</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-s-promotion"
|
||||
@click="goToFullCheckinPage"
|
||||
>前往考勤中心</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="attendanceDialog.rosterList" border size="small" max-height="360" empty-text="当前课次暂无应到参训学员(学员可能尚未报到或未入班)">
|
||||
<el-table-column type="index" label="序号" width="50" align="center" />
|
||||
<el-table-column label="学员姓名" prop="studentName" width="100" align="center" />
|
||||
<el-table-column label="学员学号" prop="studentId" width="110" align="center" show-overflow-tooltip />
|
||||
<el-table-column label="选派单位" prop="deptName" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column label="参训状态" width="85" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" :type="formatEnrollmentStatusTag(scope.row.enrollmentStatus).type">
|
||||
{{ formatEnrollmentStatusTag(scope.row.enrollmentStatus).text }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="考勤状态" width="85" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" :type="formatAttendanceStatusTag(scope.row.attendanceStatus).type">
|
||||
{{ formatAttendanceStatusTag(scope.row.attendanceStatus).text }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="签到时间" prop="signInTime" width="140" align="center">
|
||||
<template slot-scope="scope">{{ scope.row.signInTime || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="签退时间" prop="signOutTime" width="140" align="center">
|
||||
<template slot-scope="scope">{{ scope.row.signOutTime || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="170" align="center" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
v-if="canRosterSignIn(scope.row)"
|
||||
v-hasPermi="['train:attendance:checkin']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-check"
|
||||
@click="handleRosterSignIn(scope.row)"
|
||||
>签到</el-button>
|
||||
<el-button
|
||||
v-if="canRosterMarkAbsence(scope.row)"
|
||||
v-hasPermi="['train:attendance:edit']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-close"
|
||||
style="color: #E6A23C;"
|
||||
@click="handleRosterMarkAbsence(scope.row)"
|
||||
>缺勤</el-button>
|
||||
<el-button
|
||||
v-if="canRosterSignOut(scope.row)"
|
||||
v-hasPermi="['train:attendance:checkin']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-time"
|
||||
@click="handleRosterSignOut(scope.row)"
|
||||
>签退</el-button>
|
||||
<el-button
|
||||
v-if="canRosterAdjust(scope.row)"
|
||||
v-hasPermi="['train:attendance:edit']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
@click="handleRosterAdjust(scope.row)"
|
||||
>调整</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="attendanceDialog.open = false">关 闭</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -360,6 +594,13 @@ import {
|
||||
getSchedulingOptions
|
||||
} from '@/api/train/timetable'
|
||||
import { listPlanCourses } from '@/api/train/planCourse'
|
||||
import {
|
||||
getLessonRoster,
|
||||
signInAttendance,
|
||||
signOutAttendance,
|
||||
adjustAttendance,
|
||||
markAbsenceAttendance
|
||||
} from '@/api/train/attendance'
|
||||
|
||||
export default {
|
||||
name: 'TimetablePanel',
|
||||
@@ -376,7 +617,13 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
viewMode: 'timeline', // 'timeline' 或 'table'
|
||||
viewMode: 'week', // 默认优先展示学校周课表视图,亦可切换 'timeline' 或 'table'
|
||||
currentWeekAnchor: new Date(), // 当前周所锚定的日期对象
|
||||
periodList: [
|
||||
{ key: 'morning', label: '上午', timeRange: '08:00 - 12:00' },
|
||||
{ key: 'afternoon', label: '下午', timeRange: '14:00 - 18:00' },
|
||||
{ key: 'evening', label: '晚上', timeRange: '18:30 - 21:30' }
|
||||
],
|
||||
timetableData: {
|
||||
planId: null,
|
||||
planName: '',
|
||||
@@ -418,7 +665,14 @@ export default {
|
||||
},
|
||||
// 冲突检测状态与结果
|
||||
conflictChecking: false,
|
||||
conflictResult: null
|
||||
conflictResult: null,
|
||||
// 课次考勤名册对话框
|
||||
attendanceDialog: {
|
||||
open: false,
|
||||
loading: false,
|
||||
lesson: {},
|
||||
rosterList: []
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -434,6 +688,47 @@ export default {
|
||||
}
|
||||
})
|
||||
return list
|
||||
},
|
||||
// 当前锚定周的周一到周日 7 天日期清单
|
||||
currentWeekDays() {
|
||||
const anchor = this.currentWeekAnchor ? new Date(this.currentWeekAnchor) : new Date()
|
||||
// JS中 getDay() 0为周日,1为周一...6为周六
|
||||
const day = anchor.getDay()
|
||||
const diffToMonday = day === 0 ? -6 : 1 - day
|
||||
const monday = new Date(anchor)
|
||||
monday.setDate(anchor.getDate() + diffToMonday)
|
||||
|
||||
const labels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
|
||||
const days = []
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const d = new Date(monday)
|
||||
d.setDate(monday.getDate() + i)
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const date = String(d.getDate()).padStart(2, '0')
|
||||
const dateStr = `${year}-${month}-${date}`
|
||||
days.push({
|
||||
dateStr,
|
||||
displayDate: `${month}-${date}`,
|
||||
label: labels[i],
|
||||
dayOfWeek: i + 1
|
||||
})
|
||||
}
|
||||
return days
|
||||
},
|
||||
currentWeekDisplayTitle() {
|
||||
if (!this.currentWeekDays || this.currentWeekDays.length === 0) return ''
|
||||
const first = this.currentWeekDays[0].dateStr
|
||||
const last = this.currentWeekDays[6].dateStr
|
||||
return `${first} 至 ${last}`
|
||||
},
|
||||
currentWeekLessonCount() {
|
||||
if (!this.currentWeekDays || !this.allLessons) return 0
|
||||
const dates = new Set(this.currentWeekDays.map(d => d.dateStr))
|
||||
return this.allLessons.filter(l => {
|
||||
const d = (l.startTime || '').substring(0, 10)
|
||||
return dates.has(d)
|
||||
}).length
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -699,6 +994,191 @@ export default {
|
||||
this.$modal.msgSuccess('删除成功')
|
||||
this.loadTimetable()
|
||||
}).catch(() => {})
|
||||
},
|
||||
// ---- 学校周课表方法 (UC-11) ----
|
||||
handlePrevWeek() {
|
||||
const anchor = this.currentWeekAnchor ? new Date(this.currentWeekAnchor) : new Date()
|
||||
anchor.setDate(anchor.getDate() - 7)
|
||||
this.currentWeekAnchor = anchor
|
||||
},
|
||||
handleNextWeek() {
|
||||
const anchor = this.currentWeekAnchor ? new Date(this.currentWeekAnchor) : new Date()
|
||||
anchor.setDate(anchor.getDate() + 7)
|
||||
this.currentWeekAnchor = anchor
|
||||
},
|
||||
handleCurrentOrFirstWeek() {
|
||||
if (this.timetableData.startDate) {
|
||||
const parsed = new Date(this.timetableData.startDate.replace(/-/g, '/'))
|
||||
if (!isNaN(parsed.getTime())) {
|
||||
this.currentWeekAnchor = parsed
|
||||
return
|
||||
}
|
||||
}
|
||||
this.currentWeekAnchor = new Date()
|
||||
},
|
||||
isToday(dateStr) {
|
||||
if (!dateStr) return false
|
||||
const now = new Date()
|
||||
const y = now.getFullYear()
|
||||
const m = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const d = String(now.getDate()).padStart(2, '0')
|
||||
return dateStr === `${y}-${m}-${d}`
|
||||
},
|
||||
getLessonsInCell(dateStr, periodKey) {
|
||||
if (!this.allLessons || !dateStr) return []
|
||||
return this.allLessons.filter(lesson => {
|
||||
const lessonDate = (lesson.startTime || '').substring(0, 10)
|
||||
if (lessonDate !== dateStr) return false
|
||||
const timePart = (lesson.startTime || '').split(' ')[1] || ''
|
||||
const hour = parseInt(timePart.split(':')[0], 10)
|
||||
if (isNaN(hour)) return false
|
||||
if (periodKey === 'morning') return hour < 13
|
||||
if (periodKey === 'afternoon') return hour >= 13 && hour < 18
|
||||
if (periodKey === 'evening') return hour >= 18
|
||||
return false
|
||||
})
|
||||
},
|
||||
handleQuickAddLesson(dateStr, periodKey) {
|
||||
this.dialog.isEdit = false
|
||||
this.dialog.title = `新增课次排课 (${dateStr})`
|
||||
this.resetLessonForm()
|
||||
let startH = '09:00:00'
|
||||
let endH = '11:30:00'
|
||||
if (periodKey === 'afternoon') {
|
||||
startH = '14:00:00'
|
||||
endH = '16:30:00'
|
||||
} else if (periodKey === 'evening') {
|
||||
startH = '19:00:00'
|
||||
endH = '21:00:00'
|
||||
}
|
||||
this.lessonForm.startTime = `${dateStr} ${startH}`
|
||||
this.lessonForm.endTime = `${dateStr} ${endH}`
|
||||
this.dialog.open = true
|
||||
this.conflictResult = null
|
||||
},
|
||||
// 支持从“本期课程”外部直接调用排课并预选课程
|
||||
openAddLessonForCourse(planCourseId) {
|
||||
this.dialog.isEdit = false
|
||||
this.dialog.title = '新增课次排课'
|
||||
this.resetLessonForm()
|
||||
this.lessonForm.planCourseId = planCourseId
|
||||
this.dialog.open = true
|
||||
this.conflictResult = null
|
||||
},
|
||||
// ---- 课次学员考勤名册与签到/缺勤 (第四优先级) ----
|
||||
handleOpenAttendance(lesson) {
|
||||
this.attendanceDialog.lesson = lesson || {}
|
||||
this.attendanceDialog.open = true
|
||||
this.refreshAttendanceRoster()
|
||||
},
|
||||
refreshAttendanceRoster() {
|
||||
if (!this.attendanceDialog.lesson || !this.attendanceDialog.lesson.lessonId) return
|
||||
this.attendanceDialog.loading = true
|
||||
getLessonRoster(this.attendanceDialog.lesson.lessonId)
|
||||
.then(res => {
|
||||
this.attendanceDialog.rosterList = res.data || []
|
||||
})
|
||||
.finally(() => {
|
||||
this.attendanceDialog.loading = false
|
||||
})
|
||||
},
|
||||
goToFullCheckinPage() {
|
||||
this.attendanceDialog.open = false
|
||||
this.$router.push({
|
||||
path: '/train/checkin',
|
||||
query: {
|
||||
planId: this.planId,
|
||||
lessonId: this.attendanceDialog.lesson.lessonId
|
||||
}
|
||||
})
|
||||
},
|
||||
formatEnrollmentStatusTag(status) {
|
||||
const map = {
|
||||
0: { text: '待报到', type: 'info' },
|
||||
1: { text: '已报到', type: 'success' },
|
||||
2: { text: '请假', type: 'warning' },
|
||||
3: { text: '放弃', type: 'danger' },
|
||||
4: { text: '已完成', type: 'primary' }
|
||||
}
|
||||
return map[status] || { text: '未知', type: 'info' }
|
||||
},
|
||||
formatAttendanceStatusTag(status) {
|
||||
const map = {
|
||||
0: { text: '缺勤', type: 'danger' },
|
||||
1: { text: '正常', type: 'success' },
|
||||
2: { text: '迟到', type: 'warning' },
|
||||
3: { text: '早退', type: 'warning' },
|
||||
4: { text: '请假', type: 'info' }
|
||||
}
|
||||
if (status == null) {
|
||||
return { text: '未签到', type: 'info' }
|
||||
}
|
||||
return map[status] || { text: '已记录', type: 'info' }
|
||||
},
|
||||
canRosterSignIn(row) {
|
||||
return row.enrollmentStatus === 1 && (row.attendanceStatus == null || row.attendanceStatus === 0)
|
||||
},
|
||||
canRosterMarkAbsence(row) {
|
||||
return row.attendanceStatus !== 0
|
||||
},
|
||||
canRosterSignOut(row) {
|
||||
return (row.attendanceStatus === 1 || row.attendanceStatus === 2) && !row.signOutTime
|
||||
},
|
||||
canRosterAdjust(row) {
|
||||
return row.checkinId != null
|
||||
},
|
||||
handleRosterSignIn(row) {
|
||||
signInAttendance({
|
||||
lessonId: this.attendanceDialog.lesson.lessonId,
|
||||
studentId: row.studentId,
|
||||
checkinWay: 4
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess(`学员【${row.studentName}】人工签到成功`)
|
||||
this.refreshAttendanceRoster()
|
||||
})
|
||||
},
|
||||
handleRosterSignOut(row) {
|
||||
signOutAttendance({
|
||||
lessonId: this.attendanceDialog.lesson.lessonId,
|
||||
studentId: row.studentId,
|
||||
checkinWay: 4
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess(`学员【${row.studentName}】签退成功`)
|
||||
this.refreshAttendanceRoster()
|
||||
})
|
||||
},
|
||||
handleRosterMarkAbsence(row) {
|
||||
this.$prompt(`请输入学员【${row.studentName}】的缺勤原因:`, '课次缺勤登记', {
|
||||
confirmButtonText: '确定登记',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /\S+/,
|
||||
inputErrorMessage: '缺勤原因不能为空'
|
||||
}).then(({ value }) => {
|
||||
return markAbsenceAttendance({
|
||||
lessonId: this.attendanceDialog.lesson.lessonId,
|
||||
enrollmentId: row.enrollmentId,
|
||||
remark: value
|
||||
})
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess(`已登记学员【${row.studentName}】缺勤`)
|
||||
this.refreshAttendanceRoster()
|
||||
}).catch(() => {})
|
||||
},
|
||||
handleRosterAdjust(row) {
|
||||
this.$prompt(`请输入调整后的考勤状态(1:正常 2:迟到 3:早退 4:请假 0:缺勤):`, '调整考勤状态', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /^[0-4]$/,
|
||||
inputErrorMessage: '请输入0-4之间的数字状态码'
|
||||
}).then(({ value }) => {
|
||||
return adjustAttendance(row.checkinId, {
|
||||
attendanceStatus: parseInt(value, 10),
|
||||
remark: '管理员人工调整'
|
||||
})
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess('考勤调整成功')
|
||||
this.refreshAttendanceRoster()
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -711,6 +1191,159 @@ export default {
|
||||
.empty-tip {
|
||||
padding: 40px 0;
|
||||
}
|
||||
.school-timetable-view {
|
||||
margin-top: 5px;
|
||||
}
|
||||
.week-nav-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
background: #fdfdfd;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.week-title-display {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
}
|
||||
.school-grid-container {
|
||||
overflow-x: auto;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
}
|
||||
.school-grid-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
.school-grid-table th,
|
||||
.school-grid-table td {
|
||||
border: 1px solid #ebeef5;
|
||||
padding: 6px;
|
||||
vertical-align: top;
|
||||
}
|
||||
.time-col-header {
|
||||
width: 100px;
|
||||
background-color: #fafafa;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
font-weight: 600;
|
||||
}
|
||||
.day-col-header {
|
||||
text-align: center;
|
||||
background-color: #fafafa;
|
||||
color: #303133;
|
||||
padding: 8px 4px;
|
||||
}
|
||||
.day-col-header.is-today {
|
||||
background-color: #ecf5ff;
|
||||
border-top: 3px solid #409eff;
|
||||
}
|
||||
.header-day-label {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.header-date-label {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
.period-title-cell {
|
||||
background-color: #fafafa;
|
||||
text-align: center;
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
.period-name {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.period-time-range {
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
}
|
||||
.timetable-cell {
|
||||
min-height: 120px;
|
||||
height: 135px;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
}
|
||||
.school-lesson-card {
|
||||
background: linear-gradient(135deg, #f0f7ff 0%, #e6f1fc 100%);
|
||||
border: 1px solid #b3d8ff;
|
||||
border-left: 4px solid #409eff;
|
||||
border-radius: 4px;
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 6px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.school-lesson-card:hover {
|
||||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.25);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.slc-time-tag {
|
||||
font-size: 11px;
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.slc-course-title {
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
color: #1f2f3d;
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.slc-meta {
|
||||
font-size: 11px;
|
||||
color: #606266;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.meta-row {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.slc-card-actions {
|
||||
margin-top: 5px;
|
||||
padding-top: 3px;
|
||||
border-top: 1px dashed #dcdfe6;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
.empty-cell-placeholder {
|
||||
display: none;
|
||||
height: 100%;
|
||||
min-height: 80px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #c0c4cc;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
margin: 4px;
|
||||
}
|
||||
.timetable-cell:hover .empty-cell-placeholder {
|
||||
display: flex;
|
||||
background: #f9f9f9;
|
||||
color: #409eff;
|
||||
}
|
||||
.day-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -734,7 +1367,7 @@ export default {
|
||||
align-items: center;
|
||||
padding: 10px 14px;
|
||||
background-color: #f8f9fa;
|
||||
border-left: 4px solid #409EFF;
|
||||
border-left: 4px solid #409eff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.lesson-time {
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
>{{ scope.row.enrolledCount || 0 }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="290" fixed="right">
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="340" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
v-hasPermi="['train:plan:query']"
|
||||
@@ -93,6 +93,13 @@
|
||||
icon="el-icon-reading"
|
||||
@click="handleCourseConfig(scope.row)"
|
||||
>课程配置</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['train:attendance:list']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-date"
|
||||
@click="handleGoCheckin(scope.row)"
|
||||
>考勤</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['train:plan:edit']"
|
||||
size="mini"
|
||||
@@ -317,8 +324,16 @@
|
||||
<el-table-column label="学时" prop="cDuration" width="70" align="center" />
|
||||
<el-table-column label="排序号" prop="sortNo" width="70" align="center" />
|
||||
<el-table-column label="配置说明" prop="remark" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="操作" align="center" width="80" fixed="right">
|
||||
<el-table-column label="操作" align="center" width="140" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
v-hasPermi="['train:lesson:add']"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-date"
|
||||
:disabled="isLocked(planDetailDialog.plan.planStatus)"
|
||||
@click="handleGoScheduleCourse(scope.row)"
|
||||
>去排课</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['train:plan:edit']"
|
||||
size="mini"
|
||||
@@ -346,6 +361,7 @@
|
||||
<el-tab-pane label="课程表" name="timetable">
|
||||
<timetable-panel
|
||||
v-if="planDetailDialog.open && planDetailDialog.plan.planId"
|
||||
ref="timetablePanel"
|
||||
:plan-id="planDetailDialog.plan.planId"
|
||||
:plan-status="planDetailDialog.plan.planStatus"
|
||||
/>
|
||||
@@ -625,6 +641,22 @@ export default {
|
||||
this.courseDialog.planName = row.planName
|
||||
this.courseDialog.planStatus = row.planStatus
|
||||
this.loadPlanCourses(row.planId)
|
||||
},
|
||||
// 跳转考勤管理中心并预选指定培训班
|
||||
handleGoCheckin(row) {
|
||||
this.$router.push({
|
||||
path: '/train/checkin',
|
||||
query: { planId: row.planId }
|
||||
})
|
||||
},
|
||||
// 从本期课程列表中快捷前往课程表Tab并直接打开排课对话框
|
||||
handleGoScheduleCourse(courseRow) {
|
||||
this.planDetailDialog.activeTab = 'timetable'
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.timetablePanel && this.$refs.timetablePanel.openAddLessonForCourse) {
|
||||
this.$refs.timetablePanel.openAddLessonForCourse(courseRow.planCourseId)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user