前端考勤:补齐培训管理员课次签到入口

This commit is contained in:
wangzhibiao committed 2026-09-21 23:21:31 +08:00
1 parent 18b8a07f55
commit dfd0133df0
2 files changed
+343 -84

No files matched your search

+9
View File
@@ -26,3 +26,12 @@ export function adjustAttendance(checkinId, data) {
data: data
})
}
// 3.4 课次学员考勤名册 (纯只读,供培训管理员点名首次签到)
export function getLessonRoster(lessonId) {
return request({
url: `/train/attendance/lessons/${lessonId}/roster`,
method: 'get'
})
}
+334 -84
View File
@@ -1,87 +1,214 @@
<template>
<div class="app-container">
<el-form v-show="showSearch" ref="queryForm" :model="queryParams" size="small" :inline="true">
<el-form-item label="培训计划" prop="planName">
<el-input v-model="queryParams.planName" placeholder="请输入计划名称" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="学员" prop="studentName">
<el-input v-model="queryParams.studentName" placeholder="请输入学员姓名" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="课程" prop="courseName">
<el-input v-model="queryParams.courseName" placeholder="请输入课程名称" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="考勤状态" prop="attendanceStatus">
<el-select v-model="queryParams.attendanceStatus" placeholder="全部" clearable>
<el-option v-for="item in attendanceStatuses" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
v-hasPermi="['train:attendance:export']"
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
>导出</el-button>
</el-col>
<right-toolbar :show-search.sync="showSearch" @queryTable="getList" />
</el-row>
<el-alert title="考勤由课次签到、签退生成;具备管理权限时,可对异常或争议记录执行全留痕人工修正。" type="info" :closable="false" show-icon class="mb8" />
<el-table v-loading="loading" :data="rows">
<el-table-column label="培训计划" prop="planName" min-width="150" show-overflow-tooltip />
<el-table-column label="课程" prop="courseName" min-width="140" show-overflow-tooltip />
<el-table-column label="学员" prop="studentName" min-width="100" />
<el-table-column label="班牌" min-width="130" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.boardName || scope.row.boardSn || "-" }}</template>
</el-table-column>
<el-table-column label="签到方式" align="center" width="100">
<template slot-scope="scope">{{ labelOf(checkinWays, scope.row.checkinWay) }}</template>
</el-table-column>
<el-table-column label="考勤状态" align="center" width="100">
<template slot-scope="scope">{{ labelOf(attendanceStatuses, scope.row.attendanceStatus) }}</template>
</el-table-column>
<el-table-column label="签到时间" prop="signInTime" width="170" />
<el-table-column label="签退时间" prop="signOutTime" width="170" />
<el-table-column label="人工修正" align="center" width="90">
<template slot-scope="scope">{{ scope.row.manualFlag === 1 ? "是" : "否" }}</template>
</el-table-column>
<el-table-column label="备注/调整说明" prop="remark" min-width="150" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.remark || "-" }}</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="140">
<template slot-scope="scope">
<el-button
v-if="scope.row.signInTime && !scope.row.signOutTime"
v-hasPermi="['train:attendance:checkin']"
size="mini"
type="text"
icon="el-icon-circle-check"
@click="handleSignOut(scope.row)"
>签退</el-button>
<el-button
v-hasPermi="['train:attendance:edit']"
size="mini"
type="text"
icon="el-icon-edit"
@click="handleAdjust(scope.row)"
>调整</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<el-tabs v-model="activeTab" type="border-card">
<!-- Tab 1: 课次考勤与点名签到 (UC-17 培训管理员首次签到闭环) -->
<el-tab-pane label="课次考勤点名 / 签到" name="roster">
<el-form :model="rosterQuery" size="small" :inline="true">
<el-form-item label="培训计划">
<el-select
v-model="rosterQuery.planId"
placeholder="请选择培训计划"
filterable
style="width: 280px;"
@change="handlePlanChange"
>
<el-option
v-for="item in planOptions"
:key="item.planId"
:label="item.planName"
:value="item.planId"
/>
</el-select>
</el-form-item>
<el-form-item label="课次">
<el-select
v-model="rosterQuery.lessonId"
placeholder="请选择课次"
filterable
:disabled="!rosterQuery.planId"
:loading="lessonSelectLoading"
style="width: 380px;"
@change="handleLessonChange"
>
<el-option
v-for="item in lessonOptions"
:key="item.lessonId"
:label="item.label"
:value="item.lessonId"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button
type="primary"
icon="el-icon-refresh"
size="mini"
:disabled="!rosterQuery.lessonId"
@click="loadRoster"
>刷新名册</el-button>
</el-form-item>
</el-form>
<el-alert
title="课次应到学员考勤名册:展示该课次全部应到参训人员;未签到学员可点击【签到】完成首次签到录入;已签到学员支持签退与留痕调整。"
type="info"
:closable="false"
show-icon
class="mb8"
/>
<el-table v-loading="rosterLoading" :data="rosterList" empty-text="请选择培训计划与课次以加载应到学员名册">
<el-table-column label="学员姓名" prop="studentName" min-width="110" />
<el-table-column label="学员ID/工号" prop="studentId" min-width="120" show-overflow-tooltip />
<el-table-column label="性别" prop="gender" align="center" width="70" />
<el-table-column label="所属机构/部门" prop="deptName" min-width="140" show-overflow-tooltip />
<el-table-column label="联系电话" prop="phoneMasked" min-width="120" />
<el-table-column label="考勤状态" align="center" width="110">
<template slot-scope="scope">
<el-tag v-if="scope.row.attendanceStatus === null || scope.row.attendanceStatus === undefined" type="info" size="small">未签到</el-tag>
<el-tag v-else-if="scope.row.attendanceStatus === 0" type="danger" size="small">缺勤</el-tag>
<el-tag v-else-if="scope.row.attendanceStatus === 1" type="success" size="small">正常</el-tag>
<el-tag v-else-if="scope.row.attendanceStatus === 2" type="warning" size="small">迟到</el-tag>
<el-tag v-else-if="scope.row.attendanceStatus === 3" type="warning" size="small">早退</el-tag>
<el-tag v-else-if="scope.row.attendanceStatus === 4" type="primary" size="small">请假</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="签到时间" prop="signInTime" width="160">
<template slot-scope="scope">{{ scope.row.signInTime || "-" }}</template>
</el-table-column>
<el-table-column label="签退时间" prop="signOutTime" width="160">
<template slot-scope="scope">{{ scope.row.signOutTime || "-" }}</template>
</el-table-column>
<el-table-column label="签到方式" align="center" width="100">
<template slot-scope="scope">{{ labelOf(checkinWays, scope.row.checkinWay) }}</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="180">
<template slot-scope="scope">
<el-button
v-if="scope.row.canSignIn"
v-hasPermi="['train:attendance:checkin']"
size="mini"
type="text"
icon="el-icon-circle-check"
@click="handleRosterSignIn(scope.row)"
>签到</el-button>
<el-button
v-if="scope.row.canSignOut"
v-hasPermi="['train:attendance:checkin']"
size="mini"
type="text"
icon="el-icon-time"
@click="handleRosterSignOut(scope.row)"
>签退</el-button>
<el-button
v-if="scope.row.canAdjust"
v-hasPermi="['train:attendance:edit']"
size="mini"
type="text"
icon="el-icon-edit"
@click="handleAdjust(scope.row)"
>调整</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<!-- Tab 2: 考勤流水记录 (原有流水与查询视图) -->
<el-tab-pane label="考勤流水记录" name="history">
<el-form v-show="showSearch" ref="queryForm" :model="queryParams" size="small" :inline="true">
<el-form-item label="培训计划" prop="planName">
<el-input v-model="queryParams.planName" placeholder="请输入计划名称" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="学员" prop="studentName">
<el-input v-model="queryParams.studentName" placeholder="请输入学员姓名" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="课程" prop="courseName">
<el-input v-model="queryParams.courseName" placeholder="请输入课程名称" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="考勤状态" prop="attendanceStatus">
<el-select v-model="queryParams.attendanceStatus" placeholder="全部" clearable>
<el-option v-for="item in attendanceStatuses" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
v-hasPermi="['train:attendance:export']"
type="warning"
plain
icon="el-icon-download"
size="mini"
@click="handleExport"
>导出</el-button>
</el-col>
<right-toolbar :show-search.sync="showSearch" @queryTable="getList" />
</el-row>
<el-table v-loading="loading" :data="rows">
<el-table-column label="培训计划" prop="planName" min-width="150" show-overflow-tooltip />
<el-table-column label="课程" prop="courseName" min-width="140" show-overflow-tooltip />
<el-table-column label="学员" prop="studentName" min-width="100" />
<el-table-column label="班牌" min-width="130" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.boardName || scope.row.boardSn || "-" }}</template>
</el-table-column>
<el-table-column label="签到方式" align="center" width="100">
<template slot-scope="scope">{{ labelOf(checkinWays, scope.row.checkinWay) }}</template>
</el-table-column>
<el-table-column label="考勤状态" align="center" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.attendanceStatus === 0" type="danger" size="small">缺勤</el-tag>
<el-tag v-else-if="scope.row.attendanceStatus === 1" type="success" size="small">正常</el-tag>
<el-tag v-else-if="scope.row.attendanceStatus === 2" type="warning" size="small">迟到</el-tag>
<el-tag v-else-if="scope.row.attendanceStatus === 3" type="warning" size="small">早退</el-tag>
<el-tag v-else-if="scope.row.attendanceStatus === 4" type="primary" size="small">请假</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="签到时间" prop="signInTime" width="170" />
<el-table-column label="签退时间" prop="signOutTime" width="170" />
<el-table-column label="人工修正" align="center" width="90">
<template slot-scope="scope">{{ scope.row.manualFlag === 1 ? "是" : "否" }}</template>
</el-table-column>
<el-table-column label="备注/调整说明" prop="remark" min-width="150" show-overflow-tooltip>
<template slot-scope="scope">{{ scope.row.remark || "-" }}</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="140">
<template slot-scope="scope">
<el-button
v-if="scope.row.signInTime && !scope.row.signOutTime"
v-hasPermi="['train:attendance:checkin']"
size="mini"
type="text"
icon="el-icon-circle-check"
@click="handleSignOut(scope.row)"
>签退</el-button>
<el-button
v-hasPermi="['train:attendance:edit']"
size="mini"
type="text"
icon="el-icon-edit"
@click="handleAdjust(scope.row)"
>调整</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</el-tab-pane>
</el-tabs>
<!-- 人工调整考勤对话框 (UC-19 / P0) -->
<el-dialog title="人工调整考勤 (Attendance Adjustment)" :visible.sync="adjustOpen" width="520px" append-to-body>
@@ -124,12 +251,15 @@
<script>
import { listCheckin } from '@/api/train/checkin'
import { adjustAttendance, signOutAttendance } from '@/api/train/attendance'
import { adjustAttendance, signOutAttendance, signInAttendance, getLessonRoster } from '@/api/train/attendance'
import { listPlan } from '@/api/train/plan'
import { getPlanTimetable } from '@/api/train/timetable'
export default {
name: 'Checkin',
data() {
return {
activeTab: 'roster',
loading: true,
showSearch: true,
total: 0,
@@ -155,10 +285,23 @@ export default {
attendanceStatus: [
{ required: true, message: '请选择调整后的考勤状态', trigger: 'change' }
]
},
// 课次考勤名册 (Roster) 相关数据
planOptions: [],
lessonOptions: [],
lessonSelectLoading: false,
rosterLoading: false,
rosterList: [],
rosterQuery: {
planId: null,
lessonId: null
}
}
},
created() { this.getList() },
created() {
this.getList()
this.loadPlans()
},
methods: {
getList() {
this.loading = true
@@ -167,6 +310,104 @@ export default {
this.total = response.total
}).finally(() => { this.loading = false })
},
loadPlans() {
listPlan({ pageNum: 1, pageSize: 100 }).then(response => {
this.planOptions = response.rows || []
if (this.planOptions.length > 0 && !this.rosterQuery.planId) {
this.rosterQuery.planId = this.planOptions[0].planId
this.handlePlanChange(this.rosterQuery.planId)
}
}).catch(() => {})
},
handlePlanChange(planId) {
this.rosterQuery.lessonId = null
this.rosterList = []
this.lessonOptions = []
if (!planId) return
this.lessonSelectLoading = true
getPlanTimetable(planId).then(res => {
const list = []
if (res.data && res.data.days) {
res.data.days.forEach(day => {
if (day.lessons && day.lessons.length) {
day.lessons.forEach(l => {
const timeStr = l.startTime ? l.startTime.substring(5, 16) : ''
const endStr = l.endTime ? l.endTime.substring(11, 16) : ''
list.push({
lessonId: l.lessonId,
courseName: l.courseName,
teacherName: l.teacherName,
startTime: l.startTime,
endTime: l.endTime,
label: `${l.courseName || '课程'} (${timeStr} ~ ${endStr}) 讲师:${l.teacherName || '-'}`
})
})
}
})
}
this.lessonOptions = list
if (list.length > 0) {
this.rosterQuery.lessonId = list[0].lessonId
this.loadRoster()
}
}).finally(() => {
this.lessonSelectLoading = false
})
},
handleLessonChange(lessonId) {
if (lessonId) {
this.loadRoster()
} else {
this.rosterList = []
}
},
loadRoster() {
if (!this.rosterQuery.lessonId) return
this.rosterLoading = true
getLessonRoster(this.rosterQuery.lessonId).then(res => {
this.rosterList = res.data || []
}).catch(err => {
const msg = (err && (err.msg || err.message)) || '获取课次考勤名册失败'
this.$modal.msgError(msg)
}).finally(() => {
this.rosterLoading = false
})
},
handleRosterSignIn(row) {
this.$modal.confirm(`确认对学员【${row.studentName}】执行课次首次签到?`).then(() => {
return signInAttendance({
enrollmentId: row.enrollmentId,
lessonId: this.rosterQuery.lessonId,
checkinWay: 4 // 教师/管理员代签
})
}).then(() => {
this.$modal.msgSuccess(`学员【${row.studentName}】签到成功`)
this.loadRoster()
this.getList()
}).catch(err => {
if (err !== 'cancel') {
const msg = (err && (err.msg || err.message)) || '签到操作失败'
this.$modal.msgError(msg)
}
})
},
handleRosterSignOut(row) {
this.$modal.confirm(`确认对学员【${row.studentName}】执行课次签退?`).then(() => {
return signOutAttendance({
enrollmentId: row.enrollmentId,
lessonId: row.lessonId || this.rosterQuery.lessonId
})
}).then(() => {
this.$modal.msgSuccess(`学员【${row.studentName}】签退成功`)
this.loadRoster()
this.getList()
}).catch(err => {
if (err !== 'cancel') {
const msg = (err && (err.msg || err.message)) || '签退操作失败'
this.$modal.msgError(msg)
}
})
},
labelOf(options, value) {
const match = options.find(item => item.value === value)
return match ? match.label : '-'
@@ -210,6 +451,12 @@ export default {
this.$modal.msgSuccess('考勤调整成功并已记录审计留痕')
this.adjustOpen = false
this.getList()
if (this.rosterQuery.lessonId) {
this.loadRoster()
}
}).catch(err => {
const msg = (err && (err.msg || err.message)) || '考勤调整失败'
this.$modal.msgError(msg)
}).finally(() => {
this.adjustSubmitting = false
})
@@ -224,6 +471,9 @@ export default {
}).then(() => {
this.$modal.msgSuccess('签退成功')
this.getList()
if (this.rosterQuery.lessonId) {
this.loadRoster()
}
}).catch(() => {})
}
}