Files
railwaysystem/build_ui/src/views/train/me/student/index.vue
T

659 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="app-container student-center">
<!-- 头部欢迎栏 -->
<el-card class="box-card header-card" shadow="never">
<div class="welcome-bar">
<div class="title-area">
<h2>SYFJGYD学员管理系统 · 学员个人培训中心</h2>
<p class="subtitle">学员专属自服务空间,全流程掌握班级参训状态、排课日程、开班报到凭据与个人考勤成长画像。</p>
</div>
<div v-if="studentProfile" class="profile-chip">
<el-tag effect="dark" type="primary" class="role-tag">参训学员</el-tag>
<span class="user-name">{{ studentProfile.sName || '参训职工' }}</span>
<span class="org-name">({{ studentProfile.orgName || '房建公寓段' }})</span>
</div>
</div>
<!-- 403 业务未绑定友好告警 -->
<el-alert
v-if="identityErrorMsg"
:title="identityErrorMsg"
type="warning"
show-icon
:closable="false"
style="margin-top: 15px;"
/>
</el-card>
<!-- 主体区域 -->
<el-row :gutter="20" style="margin-top: 20px;">
<!-- 左侧:学员个人主档卡片 -->
<el-col :xs="24" :sm="24" :md="7" :lg="6">
<el-card class="box-card profile-card" shadow="hover">
<div slot="header" class="clearfix">
<span><i class="el-icon-postcard" /> 学员基本档案</span>
</div>
<div v-loading="profileLoading">
<div v-if="studentProfile" class="profile-detail">
<div class="profile-avatar">
<el-avatar :size="72" icon="el-icon-user" style="background-color: #409eff;" />
<div class="name-block">{{ studentProfile.sName }}</div>
<div class="id-block">学号:{{ studentProfile.sId }}</div>
</div>
<el-divider />
<div class="info-list">
<div class="info-item">
<span class="label">性别:</span>
<span class="val">{{ formatGender(studentProfile.sGender) }}</span>
</div>
<div class="info-item">
<span class="label">出生日期:</span>
<span class="val">{{ studentProfile.sBirth || '-' }}</span>
</div>
<div class="info-item">
<span class="label">所属单位:</span>
<span class="val">{{ studentProfile.orgName || '-' }}</span>
</div>
<div class="info-item">
<span class="label">联系电话:</span>
<span class="val">{{ studentProfile.sPhone || '-' }}</span>
</div>
<div class="info-item">
<span class="label">电子邮箱:</span>
<span class="val">{{ studentProfile.sEmail || '-' }}</span>
</div>
<div class="info-item">
<span class="label">身份证件:</span>
<span class="val">{{ studentProfile.idCardMasked || (studentProfile.idCardLast4 ? '****' + studentProfile.idCardLast4 : '-') }}</span>
</div>
<div class="info-item">
<span class="label">学员状态:</span>
<el-tag size="mini" :type="studentProfile.status === 1 ? 'success' : 'danger'">
{{ studentProfile.status === 1 ? '正常' : '已停用' }}
</el-tag>
</div>
</div>
</div>
<el-empty v-else description="暂无关联学员档案" :image-size="80" />
</div>
</el-card>
<!-- 累计学习时长卡片 -->
<el-card class="box-card duration-card" shadow="hover" style="margin-top: 20px;">
<div slot="header" class="clearfix">
<span><i class="el-icon-time" /> 累计培训学时</span>
</div>
<div v-loading="durationLoading">
<div v-if="durationList && durationList.length > 0" class="duration-summary">
<div class="total-badge">
<div class="num">{{ totalHours }}</div>
<div class="unit">总完成学时 (小时)</div>
</div>
<el-divider style="margin: 12px 0;" />
<div v-for="(item, idx) in durationList" :key="idx" class="duration-row">
<span class="course-name">{{ item.courseName || ('课程 ' + (idx + 1)) }}</span>
<span class="course-hour">{{ item.totalDurationHours || item.duration || 0 }} 学时</span>
</div>
</div>
<el-empty v-else description="暂无累计学时记录" :image-size="70" />
</div>
</el-card>
</el-col>
<!-- 右侧:培训各功能区 Tab -->
<el-col :xs="24" :sm="24" :md="17" :lg="18">
<el-tabs v-model="activeTab" type="border-card" class="student-tabs">
<!-- Tab 1: 我的培训班级 -->
<el-tab-pane label="我的培训" name="enrollments">
<div class="tab-header">
<div class="header-desc">
<i class="el-icon-guide" /> 我参与的培训班级与参训状态
</div>
<el-button icon="el-icon-refresh" size="mini" plain @click="loadEnrollments">刷新</el-button>
</div>
<el-table v-loading="enrollmentsLoading" :data="enrollmentList" border stripe style="width: 100%; margin-top: 15px;">
<el-table-column prop="planId" label="培训班编号" width="130" align="center" />
<el-table-column prop="planName" label="培训班名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="period" label="期次" width="100" align="center" />
<el-table-column prop="startDate" label="开始日期" width="120" align="center" />
<el-table-column prop="endDate" label="结训日期" width="120" align="center" />
<el-table-column prop="status" label="参训状态" width="120" align="center">
<template slot-scope="scope">
<el-tag size="mini" :type="formatEnrollmentStatusType(scope.row.status)">
{{ formatEnrollmentStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<template slot="empty">
<el-empty description="当前暂未参与任何培训班级" :image-size="80" />
</template>
</el-table>
</el-tab-pane>
<!-- Tab 2: 我的课表日程 -->
<el-tab-pane label="我的课表" name="timetable">
<div class="tab-header">
<div class="header-desc">
<i class="el-icon-date" /> 所在班级排课日程明细
</div>
<el-button icon="el-icon-refresh" size="mini" plain @click="loadTimetable">刷新</el-button>
</div>
<el-table v-loading="timetableLoading" :data="timetableList" border stripe style="width: 100%; margin-top: 15px;">
<el-table-column prop="lessonDate" label="上课日期" width="120" align="center" />
<el-table-column label="上课时间" width="140" align="center">
<template slot-scope="scope">
{{ scope.row.startTime || '-' }} ~ {{ scope.row.endTime || '-' }}
</template>
</el-table-column>
<el-table-column prop="planName" label="培训班级" min-width="150" show-overflow-tooltip />
<el-table-column prop="courseName" label="课程名称" min-width="140" show-overflow-tooltip />
<el-table-column prop="teacherName" label="授课教师" width="110" align="center" />
<el-table-column prop="classroomName" label="教室/实训场地" min-width="130" show-overflow-tooltip>
<template slot-scope="scope">
{{ scope.row.classroomName || scope.row.siteName || '-' }}
</template>
</el-table-column>
<template slot="empty">
<el-empty description="当前周期内暂无排课日程" :image-size="80" />
</template>
</el-table>
</el-tab-pane>
<!-- Tab 3: 我的开班报到凭据 -->
<el-tab-pane label="我的报到" name="report">
<div class="tab-header">
<div class="header-desc">
<i class="el-icon-document-checked" /> 开班报到确认凭据与回执信息
</div>
<el-button icon="el-icon-refresh" size="mini" plain @click="loadReport">刷新</el-button>
</div>
<div v-loading="reportLoading" style="margin-top: 20px;">
<div v-if="reportDetail" class="report-receipt-box">
<el-result icon="success" title="开班报到已确认" sub-title="您已顺利完成培训班级开班报到手续。">
<template slot="extra">
<div class="receipt-details">
<el-descriptions border :column="2">
<el-descriptions-item label="报到流水ID">{{ reportDetail.reportId || '-' }}</el-descriptions-item>
<el-descriptions-item label="培训班编号">{{ reportDetail.planId || '-' }}</el-descriptions-item>
<el-descriptions-item label="报到状态">
<el-tag size="small" type="success">已确认报到</el-tag>
</el-descriptions-item>
<el-descriptions-item label="报到确认时间">{{ reportDetail.reportTime || reportDetail.createTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="报到方式">{{ reportDetail.reportMethod || '电子班牌/终端报到' }}</el-descriptions-item>
<el-descriptions-item label="确认人员">{{ reportDetail.createBy || '教务管理员' }}</el-descriptions-item>
</el-descriptions>
</div>
</template>
</el-result>
</div>
<el-empty v-else description="暂无开班报到确认凭据" :image-size="90" />
</div>
</el-tab-pane>
<!-- Tab 4: 我的考勤打卡流水 -->
<el-tab-pane label="我的考勤" name="attendance">
<div class="tab-header">
<div class="header-desc">
<i class="el-icon-check" /> 课堂考勤打卡记录
</div>
<el-button icon="el-icon-refresh" size="mini" plain @click="loadAttendance">刷新</el-button>
</div>
<el-table v-loading="attendanceLoading" :data="attendanceList" border stripe style="width: 100%; margin-top: 15px;">
<el-table-column prop="lessonId" label="课次编号" width="110" align="center" />
<el-table-column prop="planName" label="培训班级" min-width="150" show-overflow-tooltip />
<el-table-column prop="courseName" label="课程名称" min-width="140" show-overflow-tooltip />
<el-table-column prop="checkinStatus" label="签到状态" width="100" align="center">
<template slot-scope="scope">
<el-tag size="mini" :type="formatAttendanceStatusType(scope.row.checkinStatus)">
{{ formatAttendanceStatusText(scope.row.checkinStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="checkinTime" label="签到打卡时间" min-width="150" align="center" />
<el-table-column prop="checkoutStatus" label="签退状态" width="100" align="center">
<template slot-scope="scope">
<el-tag size="mini" :type="formatAttendanceStatusType(scope.row.checkoutStatus)">
{{ formatAttendanceStatusText(scope.row.checkoutStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="checkoutTime" label="签退打卡时间" min-width="150" align="center" />
<template slot="empty">
<el-empty description="暂无考勤打卡流水记录" :image-size="80" />
</template>
</el-table>
</el-tab-pane>
<!-- Tab 5: 学员客观概况 -->
<el-tab-pane label="学业与纪律概况" name="portrait">
<div class="tab-header">
<div class="header-desc">
<i class="el-icon-data-analysis" /> 个人培训表现与纪律概况
</div>
<el-button icon="el-icon-refresh" size="mini" plain @click="loadPortrait">刷新</el-button>
</div>
<div v-loading="portraitLoading" style="margin-top: 20px;">
<!-- 考勤与参训客观事实 -->
<el-row :gutter="16">
<el-col :xs="24" :sm="12">
<el-card shadow="never" class="box-card">
<div slot="header" class="clearfix">
<span><i class="el-icon-time" style="color: #67C23A;" /> 考勤打卡记录</span>
</div>
<div style="padding: 10px 0;">
<p><b>正常出勤:</b>{{ attendanceNormalCount }} 次</p>
<p><b>异常考勤:</b>{{ attendanceAbnormalCount }} 次(迟到/早退/请假/缺勤)</p>
<p><b>应出勤总课次:</b>{{ timetableList.length }} 节</p>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12">
<el-card shadow="never" class="box-card">
<div slot="header" class="clearfix">
<span><i class="el-icon-collection" style="color: #409EFF;" /> 班级参训与开班报到</span>
</div>
<div style="padding: 10px 0;">
<p><b>当前参与班级:</b>{{ enrollmentList.length }} 个</p>
<p><b>开班报到确认:</b>
<el-tag size="mini" :type="reportDetail ? 'success' : 'info'">
{{ reportDetail ? '已完成报到' : '待确认报到' }}
</el-tag>
</p>
<p><b>累计已计学时:</b>{{ totalHours }} 小时</p>
</div>
</el-card>
</el-col>
</el-row>
<!-- 成绩与自学状态客观提示 -->
<el-row :gutter="16" style="margin-top: 16px;">
<el-col :xs="24" :sm="12">
<el-card shadow="never" class="box-card">
<div slot="header" class="clearfix">
<span><i class="el-icon-medal" style="color: #E6A23C;" /> 考核成绩状态</span>
</div>
<el-result icon="info" title="成绩暂未发布" sub-title="当前班级成绩正在录入中。">
<template slot="extra">
<el-tag size="small" type="info">录入中</el-tag>
</template>
</el-result>
</el-card>
</el-col>
<el-col :xs="24" :sm="12">
<el-card shadow="never" class="box-card">
<div slot="header" class="clearfix">
<span><i class="el-icon-reading" style="color: #67C23A;" /> 自主实训与学习平台</span>
</div>
<el-result icon="info" title="暂无自主学习记录" sub-title="暂无外部自主学习记录。">
<template slot="extra">
<el-tag size="small" type="info">暂无记录</el-tag>
</template>
</el-result>
</el-card>
</el-col>
</el-row>
</div>
</el-tab-pane>
</el-tabs>
</el-col>
</el-row>
</div>
</template>
<script>
import * as echarts from 'echarts'
import {
getStudentProfile,
getMyEnrollments,
getMyTimetable,
getMyReport,
getMyAttendance,
getMyDuration,
getMyPortrait
} from '@/api/train/meStudent'
export default {
name: 'StudentCenter',
data() {
return {
activeTab: 'enrollments',
profileLoading: false,
enrollmentsLoading: false,
timetableLoading: false,
reportLoading: false,
attendanceLoading: false,
durationLoading: false,
portraitLoading: false,
studentProfile: null,
identityErrorMsg: '',
enrollmentList: [],
timetableList: [],
reportDetail: null,
attendanceList: [],
durationList: [],
portraitData: null,
hasPortraitData: false,
chartInstance: null
}
},
computed: {
totalHours() {
if (!this.durationList || this.durationList.length === 0) return 0
return this.durationList.reduce((acc, cur) => acc + Number(cur.totalDurationHours || cur.duration || 0), 0)
},
attendanceNormalCount() {
if (!this.attendanceList || this.attendanceList.length === 0) return 0
return this.attendanceList.filter(i => i.checkinStatus === 1).length
},
attendanceAbnormalCount() {
if (!this.attendanceList || this.attendanceList.length === 0) return 0
return this.attendanceList.filter(i => i.checkinStatus != null && i.checkinStatus !== 1).length
}
},
created() {
this.initStudentData()
},
mounted() {
window.addEventListener('resize', this.handleResize)
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize)
if (this.chartInstance) {
this.chartInstance.dispose()
this.chartInstance = null
}
},
methods: {
initStudentData() {
this.loadProfile()
},
loadProfile() {
this.profileLoading = true
getStudentProfile()
.then(res => {
if (res.code === 200) {
this.studentProfile = res.data
this.identityErrorMsg = ''
// 学员身份校验通过后,再并发加载学业与选课各Tab数据
this.loadEnrollments()
this.loadTimetable()
this.loadReport()
this.loadAttendance()
this.loadDuration()
this.loadPortrait()
} else if (res.code === 403) {
this.identityErrorMsg = res.msg || '当前登录账号未绑定任何学员档案,请联系管理员核对分配。'
}
this.profileLoading = false
})
.catch(err => {
this.profileLoading = false
this.identityErrorMsg = (err && err.message && !err.message.includes('error')) ? err.message : '当前登录账号未绑定任何学员业务档案,禁止访问学员个人培训中心。请联系管理员分配绑定。'
})
},
loadEnrollments() {
this.enrollmentsLoading = true
getMyEnrollments()
.then(res => {
this.enrollmentList = res.rows || res.data || []
this.enrollmentsLoading = false
})
.catch(() => {
this.enrollmentList = []
this.enrollmentsLoading = false
})
},
loadTimetable() {
this.timetableLoading = true
getMyTimetable()
.then(res => {
this.timetableList = res.data || res.rows || []
this.timetableLoading = false
})
.catch(() => {
this.timetableList = []
this.timetableLoading = false
})
},
loadReport() {
this.reportLoading = true
getMyReport()
.then(res => {
this.reportDetail = res.data
this.reportLoading = false
})
.catch(() => {
this.reportDetail = null
this.reportLoading = false
})
},
loadAttendance() {
this.attendanceLoading = true
getMyAttendance()
.then(res => {
this.attendanceList = res.rows || res.data || []
this.attendanceLoading = false
})
.catch(() => {
this.attendanceList = []
this.attendanceLoading = false
})
},
loadDuration() {
this.durationLoading = true
getMyDuration()
.then(res => {
this.durationList = res.data || res.rows || []
this.durationLoading = false
})
.catch(() => {
this.durationList = []
this.durationLoading = false
})
},
loadPortrait() {
this.portraitLoading = true
getMyPortrait()
.then(res => {
this.portraitData = res.data
this.portraitLoading = false
})
.catch(() => {
this.portraitData = null
this.portraitLoading = false
})
},
handleResize() {
// 预留扩展
},
formatEnrollmentStatusText(status) {
const map = { 0: '待报到', 1: '已报到', 2: '请假', 3: '退训', 4: '已完成' }
return map[status] || '未知'
},
formatEnrollmentStatusType(status) {
const map = { 0: 'warning', 1: 'success', 2: 'info', 3: 'danger', 4: '' }
return map[status] || ''
},
formatAttendanceStatusText(status) {
// 状态严格使用:0 缺勤, 1 正常, 2 迟到, 3 早退, 4 请假。严禁将 1 显示为“已签到”!
const map = { 0: '缺勤', 1: '正常', 2: '迟到', 3: '早退', 4: '请假' }
return map[status] || '-'
},
formatAttendanceStatusType(status) {
const map = { 0: 'danger', 1: 'success', 2: 'warning', 3: 'warning', 4: 'info' }
return map[status] || ''
},
formatGender(gender) {
if (gender === '0' || gender === 0 || gender === '男') return '男'
if (gender === '1' || gender === 1 || gender === '女') return '女'
return gender || '未知'
}
}
}
</script>
<style scoped lang="scss">
.student-center {
padding: 20px;
background-color: #f0f2f5;
min-height: calc(100vh - 84px);
.header-card {
border-radius: 4px;
background: #fff;
.welcome-bar {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
.title-area {
h2 {
margin: 0 0 8px 0;
font-size: 20px;
color: #1f2f3d;
}
.subtitle {
margin: 0;
font-size: 13px;
color: #909399;
}
}
.profile-chip {
display: flex;
align-items: center;
margin-top: 10px;
.role-tag {
margin-right: 8px;
}
.user-name {
font-weight: bold;
font-size: 15px;
color: #303133;
}
.org-name {
font-size: 13px;
color: #606266;
}
}
}
}
.profile-card,
.duration-card {
border-radius: 4px;
background: #fff;
.profile-avatar {
text-align: center;
padding: 10px 0;
.name-block {
margin-top: 10px;
font-size: 16px;
font-weight: bold;
color: #303133;
}
.id-block {
margin-top: 4px;
font-size: 12px;
color: #909399;
}
}
.info-list {
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px dashed #ebeef5;
font-size: 13px;
.label {
color: #909399;
}
.val {
color: #303133;
font-weight: 500;
}
}
}
.duration-summary {
.total-badge {
text-align: center;
padding: 10px 0;
.num {
font-size: 32px;
font-weight: bold;
color: #1890ff;
}
.unit {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
}
.duration-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
font-size: 13px;
.course-name {
color: #606266;
}
.course-hour {
color: #303133;
font-weight: bold;
}
}
}
}
.student-tabs {
border-radius: 4px;
background: #fff;
min-height: 520px;
.tab-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
.header-desc {
font-size: 13px;
color: #606266;
}
}
.report-receipt-box {
background: #fafafa;
border-radius: 6px;
padding: 15px;
.receipt-details {
max-width: 680px;
margin: 0 auto;
text-align: left;
}
}
}
}
</style>