新增权限:实现教师与学员自服务前端
This commit is contained in:
1 parent
718d380540
commit
d8a963f611
10 files changed
+1438
-67
No files matched your search
+29
@@ -74,6 +74,35 @@ public class RbacPermissionMigrationContractTest
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void migration005ConfiguresTeacherAndStudentRouteMenusAndRoleBindings() throws Exception
|
||||
{
|
||||
String sql = new String(Files.readAllBytes(findMigration("005_training_roles_permissions.sql")), StandardCharsets.UTF_8).toLowerCase();
|
||||
|
||||
// 验证 2239 教师工作台路由配置
|
||||
assertContains(sql,
|
||||
"2239",
|
||||
"教师工作台",
|
||||
"'teacher'",
|
||||
"'train/me/teacher/index'",
|
||||
"'c'",
|
||||
"'train:me:teacher'");
|
||||
|
||||
// 验证 2240 学员中心路由配置
|
||||
assertContains(sql,
|
||||
"2240",
|
||||
"学员中心",
|
||||
"'student'",
|
||||
"'train/me/student/index'",
|
||||
"'c'",
|
||||
"'train:me:student'");
|
||||
|
||||
// 验证教师角色 (role_id=4) 与学员角色 (role_id=5) 的专属菜单绑定
|
||||
assertContains(sql,
|
||||
"select 4, 2239",
|
||||
"select 5, 2240");
|
||||
}
|
||||
|
||||
private Path findMigration(String filename)
|
||||
{
|
||||
for (Path path = Paths.get("").toAbsolutePath(); path != null; path = path.getParent())
|
||||
|
||||
@@ -249,13 +249,13 @@ INSERT INTO public.sys_menu (menu_id, menu_name, parent_id, order_num, path, com
|
||||
SELECT 2238, '班牌导出', 2174, 6, '#', '', '', 1, 0, 'F', '0', '0', 'train:board:export', '#', 'admin', CURRENT_TIMESTAMP, '电子班牌导出'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE menu_id = 2238 OR perms = 'train:board:export');
|
||||
|
||||
-- 3.11 自服务专属接口权限 (虚拟节点挂载于 0)
|
||||
-- 3.11 自服务专属菜单与页面路由 (挂载于根节点 0,供教师与学员在侧边栏直达工作台)
|
||||
INSERT INTO public.sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT 2239, '教师自服务', 0, 99, '#', '', '', 1, 0, 'F', '0', '0', 'train:me:teacher', '#', 'admin', CURRENT_TIMESTAMP, '教师专属自服务接口权限'
|
||||
SELECT 2239, '教师工作台', 0, 99, 'teacher', 'train/me/teacher/index', '', 1, 0, 'C', '0', '0', 'train:me:teacher', 'education', 'admin', CURRENT_TIMESTAMP, '教师专属工作台页面及自服务权限'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE menu_id = 2239 OR perms = 'train:me:teacher');
|
||||
|
||||
INSERT INTO public.sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
SELECT 2240, '学员自服务', 0, 100, '#', '', '', 1, 0, 'F', '0', '0', 'train:me:student', '#', 'admin', CURRENT_TIMESTAMP, '学员专属自服务接口权限'
|
||||
SELECT 2240, '学员中心', 0, 100, 'student', 'train/me/student/index', '', 1, 0, 'C', '0', '0', 'train:me:student', 'user', 'admin', CURRENT_TIMESTAMP, '学员专属个人培训中心页面及自服务权限'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.sys_menu WHERE menu_id = 2240 OR perms = 'train:me:student');
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 获取学员本人档案信息 (敏感信息已脱敏,严禁传 studentId)
|
||||
export function getStudentProfile() {
|
||||
return request({
|
||||
url: '/train/me/student',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取本人参训班级及报名状态列表
|
||||
export function getMyEnrollments(query) {
|
||||
return request({
|
||||
url: '/train/me/student/enrollments',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 获取本人参训课表日程
|
||||
export function getMyTimetable(query) {
|
||||
return request({
|
||||
url: '/train/me/student/timetable',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 获取本人最新开班报到凭据与状态
|
||||
export function getMyReport() {
|
||||
return request({
|
||||
url: '/train/me/student/report',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取本人考勤打卡流水
|
||||
export function getMyAttendance(query) {
|
||||
return request({
|
||||
url: '/train/me/student/attendance',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 获取本人累计培训学时记录
|
||||
export function getMyDuration() {
|
||||
return request({
|
||||
url: '/train/me/student/duration',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取本人多维成长雷达画像
|
||||
export function getMyPortrait() {
|
||||
return request({
|
||||
url: '/train/me/student/portrait',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 获取教师本人档案信息
|
||||
export function getTeacherProfile() {
|
||||
return request({
|
||||
url: '/train/me/teacher',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取教师本人任教的培训班列表
|
||||
export function getMyPlans(query) {
|
||||
return request({
|
||||
url: '/train/me/teacher/plans',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 获取教师授课课表日程
|
||||
export function getMyTimetable(query) {
|
||||
return request({
|
||||
url: '/train/me/teacher/timetable',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 获取任教班级学员花名册 (强制携带 planId)
|
||||
export function getMyClassStudents(query) {
|
||||
return request({
|
||||
url: '/train/me/teacher/students',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查看指定课次打卡考勤
|
||||
export function getLessonAttendance(lessonId) {
|
||||
return request({
|
||||
url: '/train/me/teacher/lessons/' + lessonId + '/attendance',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
@@ -1,36 +1,105 @@
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="24" :lg="14" style="padding-left: 20px;">
|
||||
<el-card class="box-card">
|
||||
<div slot="header" class="clearfix">
|
||||
<span style="font-weight: bold; font-size: 16px;">智慧培训管理平台</span>
|
||||
</div>
|
||||
<div class="text item" style="margin-bottom: 16px; font-size: 14px; color: #606266; line-height: 1.8;">
|
||||
<p>欢迎使用智慧培训管理平台。本平台提供培训计划编排、学员主档与参训管理、智能排课排程、电子班牌联动、教学设施与实训场地调度,以及全流程多维学习数据分析等核心功能。</p>
|
||||
</div>
|
||||
<div class="text item" style="margin-bottom: 20px; text-align: center">
|
||||
<img src="@/assets/images/version.png" alt="系统架构" style="max-width: 100%; border-radius: 4px;">
|
||||
</div>
|
||||
<el-divider style="margin: 15px 0;" />
|
||||
<div class="text item" style="display: flex; justify-content: space-between; color: #909399; font-size: 13px;">
|
||||
<span>系统版本:V1.0</span>
|
||||
<span>版权所有:中国科学院沈阳计算技术研究所有限公司</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-divider />
|
||||
<template>
|
||||
<div class="index-container">
|
||||
<!-- 正在加载角色信息时的占位保护,杜绝瞬态闪烁 -->
|
||||
<div v-if="loadingRoles" v-loading="true" style="height: 300px;" />
|
||||
|
||||
<!-- 1. 管理员优先展示管理端综合驾驶舱 (最高优先级: admin / train_admin) -->
|
||||
<div v-else-if="isAdmin" class="app-container home">
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="24" :lg="14" style="padding-left: 20px;">
|
||||
<el-card class="box-card">
|
||||
<div slot="header" class="clearfix">
|
||||
<span style="font-weight: bold; font-size: 16px;">沈阳房建公寓段学员管理系统</span>
|
||||
</div>
|
||||
<div class="text item" style="margin-bottom: 16px; font-size: 14px; color: #606266; line-height: 1.8;">
|
||||
<p>欢迎使用沈阳房建公寓段学员管理系统。本平台提供培训计划编排、学员主档与参训管理、智能排课排程、电子班牌联动、教学设施与实训场地调度,以及全流程多维学习数据分析等核心功能。</p>
|
||||
</div>
|
||||
<div class="text item" style="margin-bottom: 20px; text-align: center">
|
||||
<img src="@/assets/images/version.png" alt="系统架构" style="max-width: 100%; border-radius: 4px;">
|
||||
</div>
|
||||
<el-divider style="margin: 15px 0;" />
|
||||
<div class="text item" style="display: flex; justify-content: space-between; color: #909399; font-size: 13px;">
|
||||
<span>系统版本:V1.0</span>
|
||||
<span>版权所有:中国科学院沈阳计算技术研究所有限公司</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-divider />
|
||||
</div>
|
||||
|
||||
<!-- 2. 异常检测:同时拥有教师与学员互斥角色,展示受控安全告警 -->
|
||||
<div v-else-if="hasRoleConflict" class="app-container">
|
||||
<el-result
|
||||
icon="warning"
|
||||
title="角色互斥冲突告警"
|
||||
sub-title="系统检测到您的登录账号同时被分配了【培训教师】与【培训学员】互斥角色,违反业务实名约束。请联系教务管理员核实并调整角色配置。"
|
||||
>
|
||||
<template slot="extra">
|
||||
<el-tag type="danger">train_teacher + train_student 冲突</el-tag>
|
||||
</template>
|
||||
</el-result>
|
||||
</div>
|
||||
|
||||
<!-- 3. 教师身份 (仅 train_teacher) -->
|
||||
<teacher-dashboard v-else-if="isTeacherOnly" />
|
||||
|
||||
<!-- 4. 学员身份 (仅 train_student) -->
|
||||
<student-dashboard v-else-if="isStudentOnly" />
|
||||
|
||||
<!-- 5. 兜底普通角色/无培训业务角色 -->
|
||||
<div v-else class="app-container">
|
||||
<el-result
|
||||
icon="info"
|
||||
title="欢迎登录系统"
|
||||
sub-title="当前账号未分配具体教学业务角色,请通过左侧导航菜单开展受限工作或联系管理员授权。"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TeacherDashboard from '@/views/train/me/teacher/index'
|
||||
import StudentDashboard from '@/views/train/me/student/index'
|
||||
|
||||
export default {
|
||||
name: 'Index',
|
||||
components: {
|
||||
TeacherDashboard,
|
||||
StudentDashboard
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 版本号
|
||||
version: '3.8.5'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
roles() {
|
||||
return this.$store.getters.roles || []
|
||||
},
|
||||
loadingRoles() {
|
||||
return !this.roles || this.roles.length === 0
|
||||
},
|
||||
isAdmin() {
|
||||
return this.roles.includes('admin') || this.roles.includes('train_admin')
|
||||
},
|
||||
hasTeacher() {
|
||||
return this.roles.includes('train_teacher')
|
||||
},
|
||||
hasStudent() {
|
||||
return this.roles.includes('train_student')
|
||||
},
|
||||
hasRoleConflict() {
|
||||
// 非 Admin 且同时包含教师和学员角色时,触发受控互斥拦截
|
||||
return !this.isAdmin && this.hasTeacher && this.hasStudent
|
||||
},
|
||||
isTeacherOnly() {
|
||||
return !this.isAdmin && this.hasTeacher && !this.hasStudent
|
||||
},
|
||||
isStudentOnly() {
|
||||
return !this.isAdmin && this.hasStudent && !this.hasTeacher
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goTarget(href) {
|
||||
window.open(href, '_blank')
|
||||
@@ -102,4 +171,3 @@ export default {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="login">
|
||||
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
|
||||
<h3 class="title">智慧培训管理平台</h3>
|
||||
<h3 class="title">沈阳房建公寓段学员管理系统</h3>
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
@@ -49,8 +49,8 @@
|
||||
<span v-if="!loading">登 录</span>
|
||||
<span v-else>登 录 中...</span>
|
||||
</el-button>
|
||||
<div v-if="register" style="float: right;">
|
||||
<router-link class="link-type" :to="'/register'">立即注册</router-link>
|
||||
<div class="login-tip" style="margin-top: 15px; text-align: center; color: #909399; font-size: 13px;">
|
||||
<span>账号由培训管理部门统一创建与授权维护</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -90,7 +90,7 @@ export default {
|
||||
// 验证码开关
|
||||
captchaEnabled: false,
|
||||
// 注册开关
|
||||
register: true,
|
||||
register: false,
|
||||
redirect: undefined
|
||||
}
|
||||
},
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
v-hasPermi="['system:board:add']"
|
||||
v-hasPermi="['train:board:add']"
|
||||
icon="el-icon-plus"
|
||||
plain
|
||||
size="mini"
|
||||
@@ -83,7 +83,7 @@
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
v-hasPermi="['system:board:edit']"
|
||||
v-hasPermi="['train:board:edit']"
|
||||
:disabled="single"
|
||||
icon="el-icon-edit"
|
||||
plain
|
||||
@@ -96,7 +96,7 @@
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
v-hasPermi="['system:board:remove']"
|
||||
v-hasPermi="['train:board:remove']"
|
||||
:disabled="multiple"
|
||||
icon="el-icon-delete"
|
||||
plain
|
||||
@@ -109,7 +109,7 @@
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
v-hasPermi="['system:board:export']"
|
||||
v-hasPermi="['train:board:export']"
|
||||
icon="el-icon-download"
|
||||
plain
|
||||
size="mini"
|
||||
@@ -229,7 +229,7 @@
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
v-hasPermi="['system:board:edit']"
|
||||
v-hasPermi="['train:board:edit']"
|
||||
icon="el-icon-edit"
|
||||
size="mini"
|
||||
type="text"
|
||||
@@ -238,7 +238,7 @@
|
||||
修改
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['system:board:remove']"
|
||||
v-hasPermi="['train:board:remove']"
|
||||
icon="el-icon-delete"
|
||||
size="mini"
|
||||
type="text"
|
||||
|
||||
@@ -0,0 +1,638 @@
|
||||
<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>沈阳房建公寓段学员管理系统 · 学员个人培训中心</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">{{ 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" /> 课堂出勤打卡流水(状态严格遵循:0 缺勤, 1 正常, 2 迟到, 3 早退, 4 请假)
|
||||
</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;">
|
||||
<div v-if="hasPortraitData" class="portrait-container">
|
||||
<div ref="radarChart" style="width: 100%; height: 380px;" />
|
||||
</div>
|
||||
<el-empty v-else description="暂无综合能力画像数据(待培训考核评估完成后生成)" :image-size="90" />
|
||||
</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)
|
||||
}
|
||||
},
|
||||
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()
|
||||
this.loadEnrollments()
|
||||
this.loadTimetable()
|
||||
this.loadReport()
|
||||
this.loadAttendance()
|
||||
this.loadDuration()
|
||||
this.loadPortrait()
|
||||
},
|
||||
loadProfile() {
|
||||
this.profileLoading = true
|
||||
getStudentProfile()
|
||||
.then(res => {
|
||||
if (res.code === 200) {
|
||||
this.studentProfile = res.data
|
||||
this.identityErrorMsg = ''
|
||||
} else if (res.code === 403) {
|
||||
this.identityErrorMsg = res.msg || '当前登录账号未绑定任何学员档案,请联系管理员核对分配。'
|
||||
}
|
||||
this.profileLoading = false
|
||||
})
|
||||
.catch(err => {
|
||||
this.profileLoading = false
|
||||
if (err && err.message && err.message.includes('403')) {
|
||||
this.identityErrorMsg = '当前账号尚未关联有效学员档案,请联系培训管理部门授权。'
|
||||
}
|
||||
})
|
||||
},
|
||||
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
|
||||
// 仅当后端返回真实有效实体且具有 portraitId 时才判定为有数据,杜绝伪造数据
|
||||
if (this.portraitData && this.portraitData.portraitId) {
|
||||
this.hasPortraitData = true
|
||||
this.$nextTick(() => {
|
||||
this.renderRadarChart()
|
||||
})
|
||||
} else {
|
||||
this.hasPortraitData = false
|
||||
}
|
||||
this.portraitLoading = false
|
||||
})
|
||||
.catch(() => {
|
||||
this.hasPortraitData = false
|
||||
this.portraitLoading = false
|
||||
})
|
||||
},
|
||||
renderRadarChart() {
|
||||
if (!this.$refs.radarChart) return
|
||||
if (!this.chartInstance) {
|
||||
this.chartInstance = echarts.init(this.$refs.radarChart)
|
||||
}
|
||||
const data = this.portraitData || {}
|
||||
const option = {
|
||||
title: {
|
||||
text: (data.planName ? data.planName + ' · ' : '') + '综合成长能力画像',
|
||||
left: 'center',
|
||||
textStyle: { fontSize: 15, color: '#303133' }
|
||||
},
|
||||
tooltip: {},
|
||||
radar: {
|
||||
indicator: [
|
||||
{ name: '出勤纪律(%)', max: 100 },
|
||||
{ name: '理论成绩', max: 100 },
|
||||
{ name: '实操成绩', max: 100 },
|
||||
{ name: '学时评价', max: 100 }
|
||||
],
|
||||
radius: '65%'
|
||||
},
|
||||
series: [{
|
||||
name: '能力评价',
|
||||
type: 'radar',
|
||||
data: [{
|
||||
value: [
|
||||
Number(data.attendanceRate || 0),
|
||||
Number(data.theoryScore || 0),
|
||||
Number(data.practicalScore || 0),
|
||||
Number(data.totalStudyHour || 0)
|
||||
],
|
||||
name: '综合评分'
|
||||
}]
|
||||
}]
|
||||
}
|
||||
this.chartInstance.setOption(option)
|
||||
},
|
||||
handleResize() {
|
||||
if (this.chartInstance) {
|
||||
this.chartInstance.resize()
|
||||
}
|
||||
},
|
||||
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] || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</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>
|
||||
@@ -0,0 +1,519 @@
|
||||
<template>
|
||||
<div class="app-container teacher-workbench">
|
||||
<!-- 头部欢迎栏 -->
|
||||
<el-card class="box-card header-card" shadow="never">
|
||||
<div class="welcome-bar">
|
||||
<div class="title-area">
|
||||
<h2>沈阳房建公寓段学员管理系统 · 教师工作台</h2>
|
||||
<p class="subtitle">教师专属自服务教学空间,实时掌握任教培训班级、课表日程与学员考勤动态。</p>
|
||||
</div>
|
||||
<div v-if="teacherProfile" class="profile-chip">
|
||||
<el-tag effect="dark" type="success" class="role-tag">认证讲师</el-tag>
|
||||
<span class="user-name">{{ teacherProfile.tName || '任课教师' }}</span>
|
||||
<span class="job-title">({{ teacherProfile.jobTitle || '专职讲师' }} / {{ teacherProfile.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-user" /> 教师个人档案</span>
|
||||
</div>
|
||||
<div v-loading="profileLoading">
|
||||
<div v-if="teacherProfile" class="profile-detail">
|
||||
<div class="profile-avatar">
|
||||
<el-avatar :size="72" icon="el-icon-user-solid" style="background-color: #1890ff;" />
|
||||
<div class="name-block">{{ teacherProfile.tName }}</div>
|
||||
<div class="id-block">工号:{{ teacherProfile.tId }}</div>
|
||||
</div>
|
||||
<el-divider />
|
||||
<div class="info-list">
|
||||
<div class="info-item">
|
||||
<span class="label">性别:</span>
|
||||
<span class="val">{{ teacherProfile.tGender || '未知' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">所属单位:</span>
|
||||
<span class="val">{{ teacherProfile.orgName || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">职务/职称:</span>
|
||||
<span class="val">{{ teacherProfile.jobTitle || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">联系电话:</span>
|
||||
<span class="val">{{ teacherProfile.tPhone || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">联系邮箱:</span>
|
||||
<span class="val">{{ teacherProfile.tEmail || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">档案状态:</span>
|
||||
<el-tag size="mini" :type="teacherProfile.status === 1 ? 'success' : 'danger'">
|
||||
{{ teacherProfile.status === 1 ? '正常' : '已停用' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else description="暂无关联教师档案" :image-size="80" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 右侧:培训班列表与课表排程 -->
|
||||
<el-col :xs="24" :sm="24" :md="17" :lg="18">
|
||||
<el-tabs v-model="activeTab" type="border-card" class="workbench-tabs">
|
||||
<!-- Tab 1: 我的任教培训班 -->
|
||||
<el-tab-pane label="我的培训班" name="plans">
|
||||
<div class="tab-header">
|
||||
<div class="header-desc">
|
||||
<i class="el-icon-collection" /> 当前已分配任教的培训班级列表(支持直接查看班级学员花名册)
|
||||
</div>
|
||||
<el-button icon="el-icon-refresh" size="mini" plain @click="loadMyPlans">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="plansLoading" :data="planList" 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="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" :type="formatPlanStatusType(scope.row.status)">
|
||||
{{ formatPlanStatusText(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-user"
|
||||
@click="handleOpenStudents(scope.row)"
|
||||
>
|
||||
学员名册
|
||||
</el-button>
|
||||
</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>
|
||||
<div class="filter-controls">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="yyyy-MM-dd"
|
||||
size="mini"
|
||||
style="width: 260px; margin-right: 10px;"
|
||||
@change="loadMyTimetable"
|
||||
/>
|
||||
<el-button icon="el-icon-search" size="mini" type="primary" plain @click="loadMyTimetable">查询</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" plain @click="resetTimetable">重置</el-button>
|
||||
</div>
|
||||
</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="classroomName" label="教室/实训场地" min-width="130" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.classroomName || scope.row.siteName || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="130" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-view"
|
||||
@click="handleOpenAttendance(scope.row)"
|
||||
>
|
||||
查看考勤
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template slot="empty">
|
||||
<el-empty description="当前周期内暂无授课排程" :image-size="80" />
|
||||
</template>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 抽屉 1: 任教班级学员花名册 (脱敏视图) -->
|
||||
<el-drawer
|
||||
:title="'班级花名册 · ' + (currentPlan.planName || '')"
|
||||
:visible.sync="studentDrawerVisible"
|
||||
size="55%"
|
||||
direction="rtl"
|
||||
>
|
||||
<div class="drawer-content" style="padding: 20px;">
|
||||
<div class="drawer-tip">
|
||||
<el-alert
|
||||
title="该名册仅面向任课教师授权查看,学员联系电话已依法执行隐私掩码脱敏。"
|
||||
type="info"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
</div>
|
||||
<el-table v-loading="studentsLoading" :data="studentList" border stripe style="width: 100%; margin-top: 15px;">
|
||||
<el-table-column prop="studentId" label="学号/编号" width="120" align="center" />
|
||||
<el-table-column prop="studentName" label="学员姓名" width="110" align="center" />
|
||||
<el-table-column prop="orgName" label="选派单位" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="phone" label="联系电话(脱敏)" width="140" align="center" />
|
||||
<el-table-column label="参训状态" width="110" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" :type="formatEnrollmentStatusType(scope.row.enrollmentStatus != null ? scope.row.enrollmentStatus : scope.row.status)">
|
||||
{{ formatEnrollmentStatusText(scope.row.enrollmentStatus != null ? scope.row.enrollmentStatus : scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template slot="empty">
|
||||
<el-empty description="该培训班暂无学员参训数据" :image-size="70" />
|
||||
</template>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 抽屉 2: 课次考勤打卡流水 (只读查验) -->
|
||||
<el-drawer
|
||||
:title="'课次考勤流水 · ' + (currentLesson.courseName || '')"
|
||||
:visible.sync="attendanceDrawerVisible"
|
||||
size="60%"
|
||||
direction="rtl"
|
||||
>
|
||||
<div class="drawer-content" style="padding: 20px;">
|
||||
<div class="lesson-meta-bar" style="margin-bottom: 15px;">
|
||||
<el-tag size="small" type="primary">课次ID: {{ currentLesson.lessonId }}</el-tag>
|
||||
<span style="margin-left: 15px; font-weight: bold; color: #303133;">
|
||||
{{ currentLesson.lessonDate }} {{ currentLesson.startTime }}~{{ currentLesson.endTime }}
|
||||
</span>
|
||||
<span style="margin-left: 15px; color: #909399;">
|
||||
{{ currentLesson.classroomName || currentLesson.siteName || '' }}
|
||||
</span>
|
||||
</div>
|
||||
<el-table v-loading="attendanceLoading" :data="attendanceList" border stripe style="width: 100%;">
|
||||
<el-table-column prop="studentId" label="学员编号" width="120" align="center" />
|
||||
<el-table-column prop="studentName" label="学员姓名" width="110" align="center" />
|
||||
<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="70" />
|
||||
</template>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
getTeacherProfile,
|
||||
getMyPlans,
|
||||
getMyTimetable,
|
||||
getMyClassStudents,
|
||||
getLessonAttendance
|
||||
} from '@/api/train/meTeacher'
|
||||
|
||||
export default {
|
||||
name: 'TeacherWorkbench',
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'plans',
|
||||
profileLoading: false,
|
||||
plansLoading: false,
|
||||
timetableLoading: false,
|
||||
studentsLoading: false,
|
||||
attendanceLoading: false,
|
||||
|
||||
teacherProfile: null,
|
||||
identityErrorMsg: '',
|
||||
|
||||
planList: [],
|
||||
timetableList: [],
|
||||
dateRange: [],
|
||||
|
||||
studentDrawerVisible: false,
|
||||
currentPlan: {},
|
||||
studentList: [],
|
||||
|
||||
attendanceDrawerVisible: false,
|
||||
currentLesson: {},
|
||||
attendanceList: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.initTeacherData()
|
||||
},
|
||||
methods: {
|
||||
initTeacherData() {
|
||||
this.loadProfile()
|
||||
this.loadMyPlans()
|
||||
this.loadMyTimetable()
|
||||
},
|
||||
loadProfile() {
|
||||
this.profileLoading = true
|
||||
getTeacherProfile()
|
||||
.then(res => {
|
||||
if (res.code === 200) {
|
||||
this.teacherProfile = res.data
|
||||
this.identityErrorMsg = ''
|
||||
} else if (res.code === 403) {
|
||||
this.identityErrorMsg = res.msg || '当前登录账号未绑定任何教师档案,请联系管理员分配绑定。'
|
||||
}
|
||||
this.profileLoading = false
|
||||
})
|
||||
.catch(err => {
|
||||
this.profileLoading = false
|
||||
if (err && err.message && err.message.includes('403')) {
|
||||
this.identityErrorMsg = '当前账号尚未关联有效教师档案,请联系培训管理部门授权。'
|
||||
}
|
||||
})
|
||||
},
|
||||
loadMyPlans() {
|
||||
this.plansLoading = true
|
||||
getMyPlans()
|
||||
.then(res => {
|
||||
this.planList = res.rows || res.data || []
|
||||
this.plansLoading = false
|
||||
})
|
||||
.catch(() => {
|
||||
this.planList = []
|
||||
this.plansLoading = false
|
||||
})
|
||||
},
|
||||
loadMyTimetable() {
|
||||
this.timetableLoading = true
|
||||
const query = {}
|
||||
if (this.dateRange && this.dateRange.length === 2) {
|
||||
query.startDate = this.dateRange[0]
|
||||
query.endDate = this.dateRange[1]
|
||||
}
|
||||
getMyTimetable(query)
|
||||
.then(res => {
|
||||
this.timetableList = res.data || res.rows || []
|
||||
this.timetableLoading = false
|
||||
})
|
||||
.catch(() => {
|
||||
this.timetableList = []
|
||||
this.timetableLoading = false
|
||||
})
|
||||
},
|
||||
resetTimetable() {
|
||||
this.dateRange = []
|
||||
this.loadMyTimetable()
|
||||
},
|
||||
handleOpenStudents(plan) {
|
||||
this.currentPlan = plan || {}
|
||||
this.studentDrawerVisible = true
|
||||
this.studentsLoading = true
|
||||
getMyClassStudents({ planId: plan.planId })
|
||||
.then(res => {
|
||||
this.studentList = res.rows || res.data || []
|
||||
this.studentsLoading = false
|
||||
})
|
||||
.catch(() => {
|
||||
this.studentList = []
|
||||
this.studentsLoading = false
|
||||
})
|
||||
},
|
||||
handleOpenAttendance(lesson) {
|
||||
this.currentLesson = lesson || {}
|
||||
this.attendanceDrawerVisible = true
|
||||
this.attendanceLoading = true
|
||||
getLessonAttendance(lesson.lessonId)
|
||||
.then(res => {
|
||||
this.attendanceList = res.data || res.rows || []
|
||||
this.attendanceLoading = false
|
||||
})
|
||||
.catch(() => {
|
||||
this.attendanceList = []
|
||||
this.attendanceLoading = false
|
||||
})
|
||||
},
|
||||
formatPlanStatusText(status) {
|
||||
const map = { 0: '未开班', 1: '培训中', 2: '已结训' }
|
||||
return map[status] || '未知'
|
||||
},
|
||||
formatPlanStatusType(status) {
|
||||
const map = { 0: 'info', 1: 'primary', 2: 'success' }
|
||||
return map[status] || ''
|
||||
},
|
||||
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) {
|
||||
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] || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.teacher-workbench {
|
||||
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;
|
||||
}
|
||||
.job-title {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profile-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.workbench-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -89,33 +89,41 @@ flowchart TD
|
||||
4. `StudentSelfServiceApiTest` 6 个单测用例全部通过。
|
||||
- **完成标志**: 接口单元测试通过率 100%,敏感信息脱敏合格。
|
||||
|
||||
### 任务 5: RBAC-B5 - 教师工作台前端视图开发 (Teacher Frontend) [PENDING (Wave 2)]
|
||||
- **依赖关系**: 依赖 B2 菜单路由,可与 B3 并行(采用 Mock 或接口契约联调)。
|
||||
- **任务目标**:
|
||||
1. 在 `build_ui/src/views/train/teacher/` 下开发工作台看板与我的日程日历组件。
|
||||
2. 开发任教班级花名册抽屉视图。
|
||||
- **完成标志**: 教师登录后正常展现日历课表与花名册。
|
||||
### 任务 5: RBAC-B5 - 教师工作台前端视图开发 (Teacher Frontend) [COMPLETED (Wave 2)]
|
||||
- **依赖关系**: 依赖 B2 菜单路由与 B3 后端自服务 API。
|
||||
- **当前状态**: 已完成
|
||||
- **交付内容**:
|
||||
1. `build_ui/src/api/train/meTeacher.js`:封装 `/train/me/teacher/**` 全套轻量查询 API。
|
||||
2. `build_ui/src/views/train/me/teacher/index.vue`:教师自服务工作台,包含个人主档、任教班级列表、授课日历课表、脱敏花名册抽屉与课次考勤查看抽屉,支持 403 业务未绑定友好告警与 Empty State。
|
||||
3. `build_ui/src/views/index.vue`:实现角色智能 Landing,教师身份登录自适应直达教师工作台。
|
||||
- **完成标志**: 教师工作台组件与路由完备,构建与静态校验通过。
|
||||
|
||||
### 任务 6: RBAC-B6 - 学员个人培训中心前端开发 (Student Frontend) [PENDING (Wave 2)]
|
||||
- **依赖关系**: 依赖 B2 菜单路由,可与 B4 并行开发。
|
||||
- **任务目标**:
|
||||
1. 在 `build_ui/src/views/train/student/` 下开发学员培训个人中心。
|
||||
2. 整合培训流程步骤条、今日课表卡片、个人考勤打卡流水与雷达画像组件。
|
||||
- **完成标志**: 学员登录后清晰直观呈现个人培训全流程状态。
|
||||
### 任务 6: RBAC-B6 - 学员个人培训中心前端开发 (Student Frontend) [COMPLETED (Wave 2)]
|
||||
- **依赖关系**: 依赖 B2 菜单路由与 B4 后端自服务 API。
|
||||
- **当前状态**: 已完成
|
||||
- **交付内容**:
|
||||
1. `build_ui/src/api/train/meStudent.js`:封装 `/train/me/student/**` 个人自服务 API(无任何外部 studentId 入参)。
|
||||
2. `build_ui/src/views/train/me/student/index.vue`:学员专属培训中心,集成个人档案、参训流转状态条、所在班级日程课表、开班报到回执单卡片、规范考勤流水打卡记录、累计学时统计与 ECharts 多维能力成长雷达图。
|
||||
3. `build_ui/src/views/index.vue`:学员身份登录自适应直达学员个人中心。
|
||||
- **完成标志**: 学员工作台各模块渲染与空状态处理严密,构建通过。
|
||||
|
||||
### 任务 7: RBAC-B7 - 登录界面注册入口安全改造 (Login UI Policy) [PENDING (Wave 2)]
|
||||
- **依赖关系**: 独立任务,可立即开工。
|
||||
- **任务目标**:
|
||||
1. 修改 `build_ui/src/views/login.vue`,彻底关闭 `register` 开关,移除“立即注册”链接。
|
||||
2. 增加居中提示:“*账号由培训管理部门统一创建与授权维护*”。
|
||||
- **完成标志**: 登录页面无注册链接,提示文案清晰优雅。
|
||||
### 任务 7: RBAC-B7 - 登录界面注册入口安全改造 (Login UI Policy) [COMPLETED (Wave 2)]
|
||||
- **依赖关系**: 独立前端改造。
|
||||
- **当前状态**: 已完成
|
||||
- **交付内容**:
|
||||
1. 修改 `build_ui/src/views/login.vue`,系统标题更新为暂定对外名称“沈阳房建公寓段学员管理系统”。
|
||||
2. 固化 `register: false`,彻底移除“立即注册”路由跳转链接。
|
||||
3. 增加表单底部居中受控提示:“*账号由培训管理部门统一创建与授权维护*”。
|
||||
- **完成标志**: 登录界面无注册外漏,提示文案清晰企业化。
|
||||
|
||||
### 任务 8: RBAC-B8 - 班牌权限命名空间统一对齐 (Namespace Alignment) [PARTIALLY COMPLETED (Wave 1 后端已完成)]
|
||||
- **依赖关系**: 独立任务,可立即开工。
|
||||
- **当前状态**:
|
||||
- 后端:`TrainElectronicBoardController.java` 中的权限注解已全部从 `system:board:*` 收敛统一为 `train:board:*` [COMPLETED]
|
||||
- 前端:`build_ui/src/views/train/board/index.vue` 中的 `v-hasPermi` 对齐计划在 Wave 2 前端阶段完成 [PENDING]
|
||||
- **完成标志**: 静态契约测试 `testElectronicBoardControllerPermissionsAligned` 验证通过。
|
||||
### 任务 8: RBAC-B8 - 班牌权限命名空间统一对齐 (Namespace Alignment) [COMPLETED (Wave 1 & 2)]
|
||||
- **依赖关系**: 前后端对齐。
|
||||
- **当前状态**: 已完成
|
||||
- **交付内容**:
|
||||
- 后端:`TrainElectronicBoardController.java` 中的权限注解全部收敛统一为 `train:board:*` [COMPLETED (Wave 1)]
|
||||
- 前端:`build_ui/src/views/train/board/index.vue` 中的 6 处 `v-hasPermi` 全部统一为 `['train:board:*']`,彻底消除 `system:board:*` 遗留 [COMPLETED (Wave 2)]
|
||||
- 迁移脚本:`005_training_roles_permissions.sql` 将 2239/2240 提升为二级路由菜单(`menu_type='C'`),支持若依侧边栏独立挂载与静态契约验证。
|
||||
- **完成标志**: 前后端权限命名空间与菜单路由 100% 对齐。
|
||||
|
||||
### 任务 9: RBAC-B9 - 数据范围与 IDOR 安全渗透自动化测试 (IDOR Gate) [PARTIALLY COMPLETED (Wave 1 后端 IDOR 已通过)]
|
||||
- **依赖关系**: 依赖 B3、B4、B5、B6 联调完成。
|
||||
@@ -137,15 +145,20 @@ flowchart TD
|
||||
|
||||
## 3. 并行施工调度建议与 Wave 进展
|
||||
|
||||
- **Wave 1 (已完成)**:
|
||||
- **系统对外展示名称规范**:
|
||||
- 暂定对外展示名称: **沈阳房建公寓段学员管理系统**
|
||||
- 内部工程名、Git 仓库、Java 包名、数据库名仍严格保持 `RailwaySystem`,禁止全局非必要替换。
|
||||
- **Wave 1 (已完成并 Checkpoint)**:
|
||||
- 后端基础设施与迁移脚本准备: `RBAC-B1`, `RBAC-B2` (完成)
|
||||
- 核心身份服务与自服务后端: `RBAC-B3`, `RBAC-B4` (完成)
|
||||
- 班牌后端权限收敛: `RBAC-B8 (后端)` (完成)
|
||||
- 安全与契约单元测试: `RBAC-B9 (后端)` (完成,194/194 PASS)
|
||||
- **Wave 2 (待推进)**:
|
||||
- 教师工作台前端视图开发: `RBAC-B5`
|
||||
- 学员个人培训中心前端开发: `RBAC-B6`
|
||||
- 登录入口安全策略改造: `RBAC-B7`
|
||||
- 班牌前端权限指令对齐: `RBAC-B8 (前端)`
|
||||
- **Wave 3 (最终验收)**:
|
||||
- **Wave 2 (已完成待评审)**:
|
||||
- 教师工作台前端视图开发: `RBAC-B5` (完成)
|
||||
- 学员个人培训中心前端开发: `RBAC-B6` (完成)
|
||||
- 登录入口安全策略改造: `RBAC-B7` (完成)
|
||||
- 班牌前端权限指令对齐与菜单提升: `RBAC-B8 (前端 & 005.sql)` (完成)
|
||||
- 生产构建与代码规范校验: `npm run build:prod` & ESLint 100% 通过
|
||||
- 后端回归单测验证: 194 / 194 PASS
|
||||
- **Wave 3 (后续推进)**:
|
||||
- 三角色端到端联调与 E2E-R2 回归测试: `RBAC-B10`
|
||||
Reference in new issue
Block a user