1405 lines
48 KiB
Vue
1405 lines
48 KiB
Vue
<template>
|
||
<div class="timetable-panel">
|
||
<!-- 头部操作与统计条 -->
|
||
<div style="margin-bottom: 15px; display: flex; justify-content: space-between; align-items: center;">
|
||
<div>
|
||
<span style="font-weight: bold; font-size: 14px; margin-right: 15px;">本期完整课程表</span>
|
||
<el-tag size="small" type="info">总课次:{{ allLessons.length }} 节</el-tag>
|
||
<span v-if="timetableData.startDate && timetableData.endDate" style="margin-left: 12px; font-size: 12px; color: #909399;">
|
||
排课周期:{{ timetableData.startDate }} ~ {{ timetableData.endDate }}
|
||
</span>
|
||
</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>
|
||
<el-button
|
||
v-hasPermi="['train:lesson:add']"
|
||
type="primary"
|
||
icon="el-icon-plus"
|
||
size="mini"
|
||
:disabled="isPlanLocked"
|
||
@click="handleOpenAddLesson"
|
||
>新增课次排课</el-button>
|
||
<el-button
|
||
icon="el-icon-refresh"
|
||
size="mini"
|
||
@click="loadTimetable"
|
||
>刷新课表</el-button>
|
||
</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">
|
||
<el-empty description="暂未编排任何课次,请点击右上角【新增课次排课】" />
|
||
</div>
|
||
|
||
<div v-else class="day-cards">
|
||
<el-card
|
||
v-for="day in timetableData.days"
|
||
:key="day.date"
|
||
class="day-card"
|
||
shadow="hover"
|
||
>
|
||
<div slot="header" class="day-card-header">
|
||
<span style="font-weight: bold; font-size: 14px; color: #303133;">
|
||
<i class="el-icon-date" style="margin-right: 6px; color: #409EFF;" />
|
||
{{ day.date }}
|
||
<el-tag size="mini" type="info" style="margin-left: 8px;">{{ formatDayOfWeek(day.dayOfWeek) }}</el-tag>
|
||
</span>
|
||
<span style="font-size: 12px; color: #909399;">
|
||
共 {{ (day.lessons && day.lessons.length) || 0 }} 节课
|
||
</span>
|
||
</div>
|
||
|
||
<div v-if="!day.lessons || day.lessons.length === 0" style="color: #c0c4cc; font-size: 13px; padding: 10px 0;">
|
||
本日无课程安排
|
||
</div>
|
||
|
||
<div v-else class="lesson-items">
|
||
<div
|
||
v-for="lesson in day.lessons"
|
||
:key="lesson.lessonId"
|
||
class="lesson-item-box"
|
||
>
|
||
<div class="lesson-time">
|
||
<i class="el-icon-time" style="margin-right: 4px;" />
|
||
{{ formatTime(lesson.startTime) }} - {{ formatTime(lesson.endTime) }}
|
||
</div>
|
||
<div class="lesson-main">
|
||
<div class="lesson-title">
|
||
{{ lesson.courseName }}
|
||
</div>
|
||
<div class="lesson-meta">
|
||
<span class="meta-item">
|
||
<i class="el-icon-user" /> 教师:{{ lesson.teacherName || '-' }}
|
||
</span>
|
||
<span class="meta-item">
|
||
<i class="el-icon-location-outline" />
|
||
场地:
|
||
<el-tag size="mini" :type="lesson.venueType === 1 ? 'primary' : 'warning'">
|
||
{{ lesson.venueType === 1 ? '教室' : '实训场地' }}
|
||
</el-tag>
|
||
{{ lesson.venueName || lesson.classRoomName || lesson.siteName || '-' }}
|
||
</span>
|
||
<span v-if="lesson.boardSn" class="meta-item">
|
||
<i class="el-icon-monitor" /> 班牌:{{ lesson.boardSn }}
|
||
</span>
|
||
<span v-if="lesson.remark" class="meta-item" style="color: #909399;">
|
||
<i class="el-icon-info" /> {{ lesson.remark }}
|
||
</span>
|
||
</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"
|
||
type="text"
|
||
icon="el-icon-edit"
|
||
:disabled="isPlanLocked"
|
||
@click="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="handleRemoveLesson(lesson)"
|
||
>删除</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 视图二:平面明细表格视图 -->
|
||
<div v-else v-loading="loading">
|
||
<el-table :data="allLessons" border style="width: 100%">
|
||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||
<el-table-column label="课程名称" prop="courseName" min-width="150" show-overflow-tooltip />
|
||
<el-table-column label="授课教师" prop="teacherName" width="100" align="center" />
|
||
<el-table-column label="开始时间" prop="startTime" width="165" align="center" />
|
||
<el-table-column label="结束时间" prop="endTime" width="165" align="center" />
|
||
<el-table-column label="场地类型" width="100" align="center">
|
||
<template slot-scope="scope">
|
||
<el-tag size="mini" :type="scope.row.venueType === 1 ? 'primary' : 'warning'">
|
||
{{ scope.row.venueType === 1 ? '教室' : '实训场地' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="上课地点" min-width="140" show-overflow-tooltip>
|
||
<template slot-scope="scope">
|
||
<span>{{ scope.row.venueName || scope.row.classRoomName || scope.row.siteName || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="电子班牌" prop="boardSn" width="120" align="center" show-overflow-tooltip>
|
||
<template slot-scope="scope">
|
||
<span>{{ scope.row.boardSn || '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="课次说明" prop="remark" min-width="120" show-overflow-tooltip />
|
||
<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"
|
||
type="text"
|
||
icon="el-icon-edit"
|
||
:disabled="isPlanLocked"
|
||
@click="handleEditLesson(scope.row)"
|
||
>修改</el-button>
|
||
<el-button
|
||
v-hasPermi="['train:lesson:remove']"
|
||
size="mini"
|
||
type="text"
|
||
icon="el-icon-delete"
|
||
style="color: #F56C6C;"
|
||
:disabled="isPlanLocked"
|
||
@click="handleRemoveLesson(scope.row)"
|
||
>删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
|
||
<!-- 新增 / 编辑课次对话框 (UC-07, UC-08) -->
|
||
<el-dialog
|
||
:title="dialog.title"
|
||
:visible.sync="dialog.open"
|
||
width="680px"
|
||
append-to-body
|
||
@close="closeDialog"
|
||
>
|
||
<el-form ref="lessonForm" :model="lessonForm" :rules="rules" label-width="110px" size="small">
|
||
<el-form-item label="本期课程" prop="planCourseId">
|
||
<el-select
|
||
v-model="lessonForm.planCourseId"
|
||
placeholder="请选择本期配置的课程"
|
||
filterable
|
||
style="width: 100%"
|
||
>
|
||
<el-option
|
||
v-for="c in planCourseOptions"
|
||
:key="c.planCourseId"
|
||
:label="`${c.cName || '未命名'} (${c.cCode || '无编码'}) - ${c.cDuration != null ? c.cDuration : 0}学时`"
|
||
:value="c.planCourseId"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="授课教师" prop="tId">
|
||
<el-select
|
||
v-model="lessonForm.tId"
|
||
placeholder="请选择授课教师"
|
||
filterable
|
||
style="width: 100%"
|
||
@change="triggerAutoConflictCheck"
|
||
>
|
||
<el-option
|
||
v-for="t in teacherOptions"
|
||
:key="t.tId"
|
||
:label="teacherOptionLabel(t)"
|
||
:value="t.tId"
|
||
:disabled="!!t.disabled"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="课次开始" prop="startTime">
|
||
<el-date-picker
|
||
v-model="lessonForm.startTime"
|
||
type="datetime"
|
||
value-format="yyyy-MM-dd HH:mm:ss"
|
||
placeholder="选择课次开始时间"
|
||
style="width: 100%"
|
||
@change="triggerAutoConflictCheck"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="课次结束" prop="endTime">
|
||
<el-date-picker
|
||
v-model="lessonForm.endTime"
|
||
type="datetime"
|
||
value-format="yyyy-MM-dd HH:mm:ss"
|
||
placeholder="选择课次结束时间"
|
||
style="width: 100%"
|
||
@change="triggerAutoConflictCheck"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-form-item label="场地类型" prop="venueType">
|
||
<el-radio-group v-model="lessonForm.venueType" @change="handleVenueTypeChange">
|
||
<el-radio :label="1">普通教学教室</el-radio>
|
||
<el-radio :label="2">专业实训场地</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
|
||
<!-- 教室与场地二选一 -->
|
||
<el-form-item
|
||
v-if="lessonForm.venueType === 1"
|
||
label="教学教室"
|
||
prop="classRoomId"
|
||
>
|
||
<el-select
|
||
v-model="lessonForm.classRoomId"
|
||
placeholder="请选择普通教室"
|
||
filterable
|
||
style="width: 100%"
|
||
@change="triggerAutoConflictCheck"
|
||
>
|
||
<el-option
|
||
v-for="item in classroomOptions"
|
||
:key="item.classRoomId"
|
||
:label="`${item.roomName} (${item.roomLocation || ''}) [容量: ${item.capacity}人]`"
|
||
:value="item.classRoomId"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item
|
||
v-if="lessonForm.venueType === 2"
|
||
label="实训场地"
|
||
prop="siteId"
|
||
>
|
||
<el-select
|
||
v-model="lessonForm.siteId"
|
||
placeholder="请选择专业实训场地"
|
||
filterable
|
||
style="width: 100%"
|
||
@change="triggerAutoConflictCheck"
|
||
>
|
||
<el-option
|
||
v-for="item in siteOptions"
|
||
:key="item.siteId"
|
||
:label="`${item.siteName} (${item.siteLocation || ''}) [容量: ${item.capacity}人]`"
|
||
:value="item.siteId"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="课次备注" prop="remark">
|
||
<el-input
|
||
v-model="lessonForm.remark"
|
||
type="textarea"
|
||
:rows="2"
|
||
maxlength="255"
|
||
show-word-limit
|
||
placeholder="请输入课次相关注意事项或说明(选填)"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<!-- 冲突预检结果展示区域 (UC-10) -->
|
||
<div style="margin-left: 110px; margin-bottom: 15px;">
|
||
<div style="display: flex; align-items: center; margin-bottom: 8px;">
|
||
<el-button
|
||
size="mini"
|
||
type="warning"
|
||
plain
|
||
icon="el-icon-connection"
|
||
:loading="conflictChecking"
|
||
@click="handleManualCheckConflict"
|
||
>排课冲突实时预检</el-button>
|
||
<span style="font-size: 12px; color: #909399; margin-left: 10px;">选择教师、场地与时间后可实时检测冲突</span>
|
||
</div>
|
||
|
||
<div v-if="conflictResult">
|
||
<el-alert
|
||
v-if="conflictResult.hasConflict"
|
||
:title="`排课冲突:${conflictResult.conflictMessage || conflictTypeLabel(conflictResult.conflictType)}`"
|
||
type="error"
|
||
:closable="false"
|
||
show-icon
|
||
>
|
||
<div v-if="conflictResult.conflictingLessons && conflictResult.conflictingLessons.length > 0" style="margin-top: 5px; font-size: 12px;">
|
||
冲突课次明细:
|
||
<div v-for="(item, idx) in conflictResult.conflictingLessons" :key="idx">
|
||
- {{ item.courseName || '课次' }} ({{ item.startTime }} ~ {{ item.endTime }})
|
||
</div>
|
||
</div>
|
||
</el-alert>
|
||
<el-alert
|
||
v-else
|
||
title="排课预检通过:教师、场地与班级时间段无重叠冲突"
|
||
type="success"
|
||
:closable="false"
|
||
show-icon
|
||
/>
|
||
</div>
|
||
</div>
|
||
</el-form>
|
||
|
||
<div slot="footer" class="dialog-footer">
|
||
<el-button
|
||
type="primary"
|
||
:loading="dialog.submitting"
|
||
@click="submitLessonForm"
|
||
>保存课次</el-button>
|
||
<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>
|
||
|
||
<script>
|
||
import {
|
||
getPlanTimetable,
|
||
addLesson,
|
||
updateLesson,
|
||
removeLesson,
|
||
checkLessonConflict,
|
||
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',
|
||
props: {
|
||
planId: {
|
||
type: [String, Number],
|
||
required: true
|
||
},
|
||
planStatus: {
|
||
type: Number,
|
||
default: 0
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
loading: false,
|
||
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: '',
|
||
startDate: '',
|
||
endDate: '',
|
||
days: []
|
||
},
|
||
// 基础下拉选项缓存
|
||
planCourseOptions: [],
|
||
teacherOptions: [],
|
||
classroomOptions: [],
|
||
siteOptions: [],
|
||
// 新增/编辑弹窗
|
||
dialog: {
|
||
open: false,
|
||
title: '新增课次排课',
|
||
isEdit: false,
|
||
submitting: false
|
||
},
|
||
lessonForm: {
|
||
lessonId: null,
|
||
planCourseId: null,
|
||
tId: null,
|
||
startTime: null,
|
||
endTime: null,
|
||
venueType: 1,
|
||
classRoomId: null,
|
||
siteId: null,
|
||
remark: ''
|
||
},
|
||
rules: {
|
||
planCourseId: [{ required: true, message: '请选择本期课程', trigger: 'change' }],
|
||
tId: [{ required: true, message: '请选择授课教师', trigger: 'change' }],
|
||
startTime: [{ required: true, message: '课次开始时间不能为空', trigger: 'change' }],
|
||
endTime: [{ required: true, message: '课次结束时间不能为空', trigger: 'change' }],
|
||
venueType: [{ required: true, message: '请选择场地类型', trigger: 'change' }],
|
||
classRoomId: [{ required: true, message: '请选择普通教室', trigger: 'change' }],
|
||
siteId: [{ required: true, message: '请选择实训场地', trigger: 'change' }]
|
||
},
|
||
// 冲突检测状态与结果
|
||
conflictChecking: false,
|
||
conflictResult: null,
|
||
// 课次考勤名册对话框
|
||
attendanceDialog: {
|
||
open: false,
|
||
loading: false,
|
||
lesson: {},
|
||
rosterList: []
|
||
}
|
||
}
|
||
},
|
||
computed: {
|
||
isPlanLocked() {
|
||
return this.planStatus === 2 || this.planStatus === 3
|
||
},
|
||
allLessons() {
|
||
if (!this.timetableData.days) return []
|
||
const list = []
|
||
this.timetableData.days.forEach(day => {
|
||
if (day.lessons && day.lessons.length) {
|
||
day.lessons.forEach(l => list.push(l))
|
||
}
|
||
})
|
||
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: {
|
||
planId: {
|
||
immediate: true,
|
||
handler(val) {
|
||
if (val) {
|
||
this.loadTimetable()
|
||
this.loadOptions()
|
||
}
|
||
}
|
||
}
|
||
},
|
||
methods: {
|
||
formatDayOfWeek(num) {
|
||
const map = { 1: '星期一', 2: '星期二', 3: '星期三', 4: '星期四', 5: '星期五', 6: '星期六', 7: '星期日' }
|
||
return map[num] || ''
|
||
},
|
||
formatTime(dateTimeStr) {
|
||
if (!dateTimeStr) return ''
|
||
if (dateTimeStr.includes(' ')) {
|
||
return dateTimeStr.split(' ')[1].substring(0, 5)
|
||
}
|
||
return dateTimeStr
|
||
},
|
||
conflictTypeLabel(type) {
|
||
const map = {
|
||
'NONE': '无冲突',
|
||
'TEACHER': '教师时间冲突',
|
||
'CLASSROOM': '教室时间占用冲突',
|
||
'SITE': '实训场地占用冲突',
|
||
'PLAN_OVERLAP': '本班课次时段重叠'
|
||
}
|
||
return map[type] || '存在排课冲突'
|
||
},
|
||
// ---- 加载课表 ----
|
||
loadTimetable() {
|
||
if (!this.planId) return
|
||
this.loading = true
|
||
getPlanTimetable(this.planId)
|
||
.then(res => {
|
||
this.timetableData = res.data || { days: [] }
|
||
})
|
||
.finally(() => {
|
||
this.loading = false
|
||
})
|
||
},
|
||
// ---- 预加载基础选项 ----
|
||
loadOptions() {
|
||
if (!this.planId) return
|
||
listPlanCourses(this.planId).then(res => {
|
||
this.planCourseOptions = res.data || []
|
||
})
|
||
// 排课候选统一走专用只读接口全量加载,不分页,避免候选被静默截断
|
||
getSchedulingOptions().then(res => {
|
||
const data = res.data || {}
|
||
this.teacherOptions = data.teachers || []
|
||
this.classroomOptions = data.classrooms || []
|
||
this.siteOptions = data.sites || []
|
||
})
|
||
},
|
||
handleVenueTypeChange(val) {
|
||
if (val === 1) {
|
||
this.lessonForm.siteId = null
|
||
} else {
|
||
this.lessonForm.classRoomId = null
|
||
}
|
||
this.triggerAutoConflictCheck()
|
||
},
|
||
// ---- 打开新增排课 ----
|
||
handleOpenAddLesson() {
|
||
this.dialog.isEdit = false
|
||
this.dialog.title = '新增课次排课'
|
||
this.resetLessonForm()
|
||
this.dialog.open = true
|
||
this.conflictResult = null
|
||
},
|
||
// 教师下拉展示文案:已停用的原授课教师仅保留姓名可见,明确标注停用状态
|
||
teacherOptionLabel(t) {
|
||
const name = t.name || t.tName || t.tId
|
||
if (t.disabled) {
|
||
return `${name}(已停用)`
|
||
}
|
||
return `${name} - ${t.workDept || t.orgName || '基地'}`
|
||
},
|
||
// 编辑历史课次时,原授课教师若已停用则不在候选列表中,
|
||
// 临时补一条禁用选项保证原教师名可见且不可被重新选中,绝不自动换人
|
||
ensureCurrentTeacherVisible(row) {
|
||
if (!row || !row.tId) return
|
||
if (this.teacherOptions.some(t => t.tId === row.tId)) return
|
||
this.teacherOptions = this.teacherOptions.concat([{
|
||
tId: row.tId,
|
||
tName: row.teacherName || row.tId,
|
||
disabled: true
|
||
}])
|
||
},
|
||
// ---- 打开修改排课 (不发明 lesson detail API,直接从已有VO回显) ----
|
||
handleEditLesson(row) {
|
||
this.ensureCurrentTeacherVisible(row)
|
||
this.dialog.isEdit = true
|
||
this.dialog.title = '编辑课次信息'
|
||
this.lessonForm = {
|
||
lessonId: row.lessonId,
|
||
planCourseId: row.planCourseId,
|
||
tId: row.tId,
|
||
startTime: row.startTime,
|
||
endTime: row.endTime,
|
||
venueType: row.venueType || 1,
|
||
classRoomId: row.classRoomId || null,
|
||
siteId: row.siteId || null,
|
||
remark: row.remark || ''
|
||
}
|
||
this.conflictResult = null
|
||
this.dialog.open = true
|
||
},
|
||
resetLessonForm() {
|
||
this.lessonForm = {
|
||
lessonId: null,
|
||
planCourseId: null,
|
||
tId: null,
|
||
startTime: null,
|
||
endTime: null,
|
||
venueType: 1,
|
||
classRoomId: null,
|
||
siteId: null,
|
||
remark: ''
|
||
}
|
||
if (this.$refs.lessonForm) {
|
||
this.$refs.lessonForm.clearValidate()
|
||
}
|
||
},
|
||
closeDialog() {
|
||
this.conflictResult = null
|
||
},
|
||
// ---- 冲突预检逻辑 (UC-10) ----
|
||
triggerAutoConflictCheck() {
|
||
// 当教师、场地与时间均选择后自动静默预检
|
||
const f = this.lessonForm
|
||
const hasVenue = (f.venueType === 1 && f.classRoomId) || (f.venueType === 2 && f.siteId)
|
||
if (f.tId && f.startTime && f.endTime && hasVenue) {
|
||
this.performConflictCheck(false)
|
||
}
|
||
},
|
||
handleManualCheckConflict() {
|
||
this.performConflictCheck(true)
|
||
},
|
||
performConflictCheck(showNoticeOnPass) {
|
||
const f = this.lessonForm
|
||
if (!f.startTime || !f.endTime) {
|
||
if (showNoticeOnPass) this.$modal.msgWarning('请先选择课次开始与结束时间')
|
||
return
|
||
}
|
||
if (f.startTime >= f.endTime) {
|
||
if (showNoticeOnPass) this.$modal.msgError('课次结束时间必须晚于开始时间')
|
||
return
|
||
}
|
||
const payload = {
|
||
planId: this.planId,
|
||
lessonId: f.lessonId,
|
||
planCourseId: f.planCourseId,
|
||
tId: f.tId,
|
||
startTime: f.startTime,
|
||
endTime: f.endTime,
|
||
venueType: f.venueType,
|
||
classRoomId: f.venueType === 1 ? f.classRoomId : null,
|
||
siteId: f.venueType === 2 ? f.siteId : null
|
||
}
|
||
this.conflictChecking = true
|
||
checkLessonConflict(payload)
|
||
.then(res => {
|
||
this.conflictResult = res.data || { hasConflict: false }
|
||
if (showNoticeOnPass && !this.conflictResult.hasConflict) {
|
||
this.$modal.msgSuccess('预检通过,未检测到任何冲突')
|
||
}
|
||
})
|
||
.finally(() => {
|
||
this.conflictChecking = false
|
||
})
|
||
},
|
||
// ---- 提交保存课次 (排课冲突告警确认) ----
|
||
submitLessonForm() {
|
||
this.$refs.lessonForm.validate(valid => {
|
||
if (!valid) return
|
||
if (this.lessonForm.startTime >= this.lessonForm.endTime) {
|
||
this.$modal.msgError('课次结束时间必须晚于开始时间')
|
||
return
|
||
}
|
||
|
||
// 构建保存 payload (严格对齐 LessonCreateDTO / LessonUpdateDTO,绝不含 force/override 字段)
|
||
const payload = {
|
||
planCourseId: this.lessonForm.planCourseId,
|
||
tId: this.lessonForm.tId,
|
||
startTime: this.lessonForm.startTime,
|
||
endTime: this.lessonForm.endTime,
|
||
venueType: this.lessonForm.venueType,
|
||
classRoomId: this.lessonForm.venueType === 1 ? this.lessonForm.classRoomId : null,
|
||
siteId: this.lessonForm.venueType === 2 ? this.lessonForm.siteId : null,
|
||
remark: this.lessonForm.remark
|
||
}
|
||
|
||
// 构建预检 DTO (编辑模式传 lessonId 排除自身,新增模式传 null)
|
||
const conflictCheckDTO = {
|
||
planId: this.planId,
|
||
lessonId: this.dialog.isEdit ? this.lessonForm.lessonId : null,
|
||
planCourseId: this.lessonForm.planCourseId,
|
||
tId: this.lessonForm.tId,
|
||
startTime: this.lessonForm.startTime,
|
||
endTime: this.lessonForm.endTime,
|
||
venueType: this.lessonForm.venueType,
|
||
classRoomId: this.lessonForm.venueType === 1 ? this.lessonForm.classRoomId : null,
|
||
siteId: this.lessonForm.venueType === 2 ? this.lessonForm.siteId : null
|
||
}
|
||
|
||
this.dialog.submitting = true
|
||
// 每次点击保存均重新发起冲突预检,避免依赖过期结果
|
||
checkLessonConflict(conflictCheckDTO)
|
||
.then(res => {
|
||
const checkData = res.data || { hasConflict: false }
|
||
this.conflictResult = checkData
|
||
|
||
if (checkData.hasConflict) {
|
||
const warningMsg = checkData.conflictMessage || this.conflictTypeLabel(checkData.conflictType)
|
||
this.$confirm(`检测到排课冲突:\n${warningMsg}\n\n是否仍然保存该课次?`, '排课冲突告警确认', {
|
||
confirmButtonText: '确认保存',
|
||
cancelButtonText: '返回调整',
|
||
type: 'warning'
|
||
}).then(() => {
|
||
this.executeSaveLesson(payload)
|
||
}).catch(() => {
|
||
this.dialog.submitting = false
|
||
})
|
||
} else {
|
||
this.executeSaveLesson(payload)
|
||
}
|
||
})
|
||
.catch(() => {
|
||
this.$modal.msgError('排课冲突检查失败,请稍后重试')
|
||
this.dialog.submitting = false
|
||
})
|
||
})
|
||
},
|
||
executeSaveLesson(payload) {
|
||
this.dialog.submitting = true
|
||
const request = this.dialog.isEdit
|
||
? updateLesson(this.lessonForm.lessonId, payload)
|
||
: addLesson(this.planId, payload)
|
||
|
||
request
|
||
.then(() => {
|
||
this.$modal.msgSuccess(this.dialog.isEdit ? '修改课次成功' : '新增课次成功')
|
||
this.dialog.open = false
|
||
this.loadTimetable()
|
||
})
|
||
.finally(() => {
|
||
this.dialog.submitting = false
|
||
})
|
||
},
|
||
// ---- 删除课次 ----
|
||
handleRemoveLesson(row) {
|
||
this.$modal.confirm(`确定要删除课次【${row.courseName} (${row.startTime})】吗?`).then(() => {
|
||
return removeLesson(row.lessonId)
|
||
}).then(() => {
|
||
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,
|
||
enrollmentId: row.enrollmentId,
|
||
checkinWay: 4
|
||
}).then(() => {
|
||
this.$modal.msgSuccess(`学员【${row.studentName}】人工签到成功`)
|
||
this.refreshAttendanceRoster()
|
||
})
|
||
},
|
||
handleRosterSignOut(row) {
|
||
signOutAttendance({
|
||
lessonId: this.attendanceDialog.lesson.lessonId,
|
||
enrollmentId: row.enrollmentId
|
||
}).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(() => {})
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.timetable-panel {
|
||
padding: 5px 0;
|
||
}
|
||
.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;
|
||
gap: 16px;
|
||
}
|
||
.day-card {
|
||
border-radius: 6px;
|
||
}
|
||
.day-card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
.lesson-items {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
.lesson-item-box {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 10px 14px;
|
||
background-color: #f8f9fa;
|
||
border-left: 4px solid #409eff;
|
||
border-radius: 4px;
|
||
}
|
||
.lesson-time {
|
||
width: 140px;
|
||
font-weight: bold;
|
||
color: #303133;
|
||
font-size: 13px;
|
||
flex-shrink: 0;
|
||
}
|
||
.lesson-main {
|
||
flex-grow: 1;
|
||
padding: 0 15px;
|
||
}
|
||
.lesson-title {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #1f2f3d;
|
||
margin-bottom: 4px;
|
||
}
|
||
.lesson-meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 15px;
|
||
font-size: 12px;
|
||
color: #606266;
|
||
}
|
||
.meta-item {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
.lesson-actions {
|
||
flex-shrink: 0;
|
||
}
|
||
</style>
|