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

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