前端考勤:补齐培训管理员课次签到入口
This commit is contained in:
1 parent
18b8a07f55
commit
dfd0133df0
2 files changed
+343
-84
No files matched your search
@@ -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'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user