合并发布:集成GitLab远端主数据扩展与教学资源基线

This commit is contained in:
wangzhibiao committed 2026-10-09 14:29:26 +08:00
commit b93ac3fa71
43 files changed
+2583 -65

No files matched your search

@@ -0,0 +1,93 @@
package com.ruoyi.web.controller.train;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.train.domain.TrainResource;
import com.ruoyi.train.service.ITrainResourceService;
/**
* 教学资源Controller
*
* @author ruoyi
*/
@RestController
@RequestMapping("/train/resource")
public class TrainResourceController extends BaseController
{
@Autowired
private ITrainResourceService trainResourceService;
/**
* 查询教学资源列表
*/
@PreAuthorize("@ss.hasPermi('train:resource:list')")
@GetMapping("/list")
public TableDataInfo list(TrainResource trainResource)
{
startPage();
List<TrainResource> list = trainResourceService.selectTrainResourceList(trainResource);
return getDataTable(list);
}
/**
* 获取教学资源详细信息
*/
@PreAuthorize("@ss.hasPermi('train:resource:list')")
@GetMapping(value = "/{resourceId}")
public AjaxResult getInfo(@PathVariable("resourceId") String resourceId)
{
return success(trainResourceService.selectTrainResourceByResourceId(resourceId));
}
/**
* 新增教学资源
*/
@PreAuthorize("@ss.hasPermi('train:resource:edit')")
@Log(title = "教学资源", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody TrainResource trainResource)
{
trainResource.setCreateBy(SecurityUtils.getUsername());
return toAjax(trainResourceService.insertTrainResource(trainResource));
}
/**
* 删除教学资源 (RESTful DELETE /train/resource/{resourceIds})
*/
@PreAuthorize("@ss.hasPermi('train:resource:edit')")
@Log(title = "教学资源", businessType = BusinessType.DELETE)
@DeleteMapping("/{resourceIds}")
public AjaxResult remove(@PathVariable String[] resourceIds)
{
return toAjax(trainResourceService.deleteTrainResourceByResourceIds(resourceIds));
}
/**
* 修改教学资源 (RESTful PUT /train/resource/{resourceId})
*/
@PreAuthorize("@ss.hasPermi('train:resource:edit')")
@Log(title = "教学资源", businessType = BusinessType.UPDATE)
@PutMapping("/{resourceId}")
public AjaxResult edit(@PathVariable("resourceId") String resourceId, @Validated @RequestBody TrainResource trainResource)
{
trainResource.setResourceId(resourceId);
trainResource.setUpdateBy(SecurityUtils.getUsername());
return toAjax(trainResourceService.updateTrainResource(trainResource));
}
}
@@ -1,15 +1,12 @@
package com.ruoyi.web.controller.train;
import java.util.List;
import java.util.regex.Pattern;
import javax.validation.Valid;
import com.ruoyi.common.utils.StringUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import com.github.pagehelper.PageInfo;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.constant.HttpStatus;
@@ -18,6 +15,7 @@ import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.train.domain.dto.StudentCreateDTO;
import com.ruoyi.train.domain.dto.StudentUpdateDTO;
import com.ruoyi.train.domain.query.StudentQuery;
import com.ruoyi.train.domain.vo.StudentAdminVO;
import com.ruoyi.train.domain.vo.StudentDetailVO;
@@ -30,6 +28,8 @@ import com.ruoyi.web.service.train.TrainAccountBindingAdminService;
@RequestMapping({"/train/student", "/train/students"})
public class TrainStudentController extends BaseController
{
private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
private final ITrainStudentService trainStudentService;
private final TrainAccountBindingAdminService bindingAdminService;
@@ -76,6 +76,27 @@ public class TrainStudentController extends BaseController
return success(trainStudentService.createStudent(dto));
}
/**
* 修改学员信息 (PUT /train/student/{sId})
*/
@PreAuthorize("@ss.hasPermi('train:student:edit')")
@Log(title = "学员主档", businessType = BusinessType.UPDATE,
isSaveRequestData = false, isSaveResponseData = true)
@PutMapping(value = "/{sId}")
public AjaxResult edit(@PathVariable("sId") String sId, @Valid @RequestBody StudentUpdateDTO dto)
{
dto.setsId(sId);
if (StringUtils.isNotEmpty(dto.getsName()) && dto.getsName().trim().length() > 50)
{
return AjaxResult.error("学员姓名长度不能超过50个字符");
}
if (StringUtils.isNotEmpty(dto.getsPhone()) && !isValidPhone(dto.getsPhone()))
{
return AjaxResult.error("学员手机号码格式不正确");
}
return success(trainStudentService.updateTrainStudent(dto));
}
@PreAuthorize("@ss.hasPermi('train:student:remove')")
@Log(title = "学员主档", businessType = BusinessType.DELETE)
@DeleteMapping("/{sId}")
@@ -83,4 +104,9 @@ public class TrainStudentController extends BaseController
{
return toAjax(trainStudentService.deleteTrainStudentBySId(sId));
}
private boolean isValidPhone(String phone)
{
return PHONE_PATTERN.matcher(phone).matches();
}
}
@@ -4,6 +4,7 @@ import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@@ -67,6 +68,17 @@ public class TrainTrainingSiteController extends BaseController
return toAjax(trainTrainingSiteService.insertTrainTrainingSite(trainTrainingSite));
}
/**
* 删除实训场地 (RESTful DELETE /train/training-site/{siteIds})
*/
@PreAuthorize("@ss.hasPermi('train:site:edit')")
@Log(title = "实训场地", businessType = BusinessType.DELETE)
@DeleteMapping("/{siteIds}")
public AjaxResult remove(@PathVariable String[] siteIds)
{
return toAjax(trainTrainingSiteService.deleteTrainTrainingSiteBySiteIds(siteIds));
}
/**
* 修改实训场地 (RESTful PUT /train/training-site/{siteId})
*/
@@ -63,7 +63,7 @@ public class SysLoginService
public String login(String username, String password, String code, String uuid)
{
// 验证码校验
//validateCaptcha(username, code, uuid);
validateCaptcha(username, code, uuid);
// 登录前置校验
loginPreCheck(username, password);
// 用户验证
@@ -46,6 +46,12 @@ public class TrainClassroomInfo extends BaseEntity
/** 删除标志 (0代表存在 2代表删除) */
private String delFlag;
private Integer multimedia;
private Integer projector;
private String seatArrange;
public void setClassRoomId(String classRoomId)
{
this.classRoomId = classRoomId;
@@ -106,6 +112,30 @@ public class TrainClassroomInfo extends BaseEntity
return delFlag;
}
public Integer getMultimedia() {
return multimedia;
}
public void setMultimedia(Integer multimedia) {
this.multimedia = multimedia;
}
public Integer getProjector() {
return projector;
}
public void setProjector(Integer projector) {
this.projector = projector;
}
public String getSeatArrange() {
return seatArrange;
}
public void setSeatArrange(String seatArrange) {
this.seatArrange = seatArrange;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
@@ -120,6 +150,9 @@ public class TrainClassroomInfo extends BaseEntity
.append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime())
.append("remark", getRemark())
.append("multimedia", getMultimedia())
.append("projector", getProjector())
.append("seatArrange", getSeatArrange())
.toString();
}
}
@@ -43,6 +43,10 @@ public class TrainCourse extends BaseEntity
@Excel(name = "课程内容")
private String courseContent;
/** 课程资源ID,多个资源以英文逗号分隔,对应 train_resource.resource_id */
@Excel(name = "课程资源ID")
private String resourceId;
/** 课程状态 (1:正常, 0:停用) */
@Excel(name = "课程状态", readConverterExp = "1=正常,0=停用")
private Integer status;
@@ -180,6 +184,16 @@ public class TrainCourse extends BaseEntity
return courseContent;
}
public void setResourceId(String resourceId)
{
this.resourceId = resourceId;
}
public String getResourceId()
{
return resourceId;
}
public void setStatus(Integer status)
{
this.status = status;
@@ -210,6 +224,7 @@ public class TrainCourse extends BaseEntity
.append("cDuration", getcDuration())
.append("teachingObjective", getTeachingObjective())
.append("courseContent", getCourseContent())
.append("resourceId", getResourceId())
.append("status", getStatus())
.append("delFlag", getDelFlag())
.append("createBy", getCreateBy())
@@ -0,0 +1,149 @@
package com.ruoyi.train.domain;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
/**
* 教学资源对象 train_resource
*
* <p>统一教学资源:文档、视频、3D模型、VR资源、外部教学链接、其他。
* 该表被 train_course_resource、train_study_duration 外键引用,删除一律走逻辑删除。</p>
*
* @author ruoyi
*/
public class TrainResource extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 资源唯一ID */
private String resourceId;
/** 资源名称 */
@NotBlank(message = "资源名称不能为空")
@Size(max = 150, message = "资源名称长度不能超过150个字符")
@Excel(name = "资源名称")
private String resourceName;
/** 资源类型 (1:文档, 2:视频, 3:3D模型, 4:VR资源, 5:外部教学链接, 6:其他) */
@NotNull(message = "资源类型不能为空")
@Min(value = 1, message = "资源类型取值必须为1-6")
@Max(value = 6, message = "资源类型取值必须为1-6")
@Excel(name = "资源类型", readConverterExp = "1=文档,2=视频,3=3D模型,4=VR资源,5=外部教学链接,6=其他")
private Integer resourceType;
/** 资源存储地址/在线链接 */
@NotBlank(message = "资源地址不能为空")
@Size(max = 500, message = "资源地址长度不能超过500个字符")
@Excel(name = "资源地址")
private String resourceUrl;
/** 资源描述/使用说明 */
@Size(max = 500, message = "资源描述长度不能超过500个字符")
@Excel(name = "资源描述")
private String description;
/** 状态 (1:正常, 0:停用) */
@Min(value = 0, message = "状态取值必须为0或1")
@Max(value = 1, message = "状态取值必须为0或1")
@Excel(name = "状态", readConverterExp = "0=停用,1=正常")
private Integer status;
/** 删除标志 (0代表存在 2代表删除) */
private String delFlag;
public void setResourceId(String resourceId)
{
this.resourceId = resourceId;
}
public String getResourceId()
{
return resourceId;
}
public void setResourceName(String resourceName)
{
this.resourceName = resourceName;
}
public String getResourceName()
{
return resourceName;
}
public void setResourceType(Integer resourceType)
{
this.resourceType = resourceType;
}
public Integer getResourceType()
{
return resourceType;
}
public void setResourceUrl(String resourceUrl)
{
this.resourceUrl = resourceUrl;
}
public String getResourceUrl()
{
return resourceUrl;
}
public void setDescription(String description)
{
this.description = description;
}
public String getDescription()
{
return description;
}
public void setStatus(Integer status)
{
this.status = status;
}
public Integer getStatus()
{
return status;
}
public void setDelFlag(String delFlag)
{
this.delFlag = delFlag;
}
public String getDelFlag()
{
return delFlag;
}
@Override
public String toString()
{
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
.append("resourceId", getResourceId())
.append("resourceName", getResourceName())
.append("resourceType", getResourceType())
.append("resourceUrl", getResourceUrl())
.append("description", getDescription())
.append("status", getStatus())
.append("delFlag", getDelFlag())
.append("createBy", getCreateBy())
.append("createTime", getCreateTime())
.append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime())
.append("remark", getRemark())
.toString();
}
}
@@ -42,6 +42,10 @@ public class TrainStudent extends BaseEntity
private Integer status;
private String delFlag;
private String avatar;
private Integer dutyLevel;
public String getsId()
{
return sId;
@@ -194,6 +198,22 @@ public class TrainStudent extends BaseEntity
this.delFlag = delFlag;
}
public String getAvatar() {
return avatar;
}
public void setAvatar(String avatar) {
this.avatar = avatar;
}
public Integer getDutyLevel() {
return dutyLevel;
}
public void setDutyLevel(Integer dutyLevel) {
this.dutyLevel = dutyLevel;
}
/**
* 安全日志表示:刻意不包含身份证密文、查重值或任何完整身份证信息。
*/
@@ -219,6 +239,8 @@ public class TrainStudent extends BaseEntity
.append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime())
.append("remark", getRemark())
.append("dutyLevel", getDutyLevel())
.append("avatar", getAvatar())
.toString();
}
}
@@ -60,6 +60,15 @@ public class TrainTeacher extends BaseEntity
/** 删除标志 (0:存在, 2:删除) */
private String delFlag;
private String avatar;
private String major;
private String courses;
private Date startTeaching;
public void settId(String tId)
{
this.tId = tId;
@@ -180,6 +189,38 @@ public class TrainTeacher extends BaseEntity
return delFlag;
}
public String getAvatar() {
return avatar;
}
public void setAvatar(String avatar) {
this.avatar = avatar;
}
public String getMajor() {
return major;
}
public void setMajor(String major) {
this.major = major;
}
public String getCourses() {
return courses;
}
public void setCourses(String courses) {
this.courses = courses;
}
public Date getStartTeaching() {
return startTeaching;
}
public void setStartTeaching(Date startTeaching) {
this.startTeaching = startTeaching;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
@@ -199,6 +240,10 @@ public class TrainTeacher extends BaseEntity
.append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime())
.append("remark", getRemark())
.append("avatar", getAvatar())
.append("major", getMajor())
.append("courses", getCourses())
.append("startTeaching", getStartTeaching())
.toString();
}
}
@@ -50,6 +50,10 @@ public class TrainTrainingSite extends BaseEntity
/** 删除标志 (0代表存在 2代表删除) */
private String delFlag;
private String major;
private String seatArrange;
public void setSiteId(String siteId)
{
this.siteId = siteId;
@@ -120,6 +124,22 @@ public class TrainTrainingSite extends BaseEntity
return delFlag;
}
public String getMajor() {
return major;
}
public void setMajor(String major) {
this.major = major;
}
public String getSeatArrange() {
return seatArrange;
}
public void setSeatArrange(String seatArrange) {
this.seatArrange = seatArrange;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
@@ -135,6 +155,8 @@ public class TrainTrainingSite extends BaseEntity
.append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime())
.append("remark", getRemark())
.append("major", getMajor())
.append("seatArrange", getSeatArrange())
.toString();
}
}
@@ -32,6 +32,12 @@ public class ClassroomCreateDTO implements Serializable
/** 教室状态 (0: 正在使用, 1: 可用空闲, 2: 维护中) */
private Integer roomStatus;
private Integer multimedia;
private Integer projector;
private String seatArrange;
/** 备注 */
@Size(max = 500, message = "备注长度不能超过500个字符")
private String remark;
@@ -85,4 +91,28 @@ public class ClassroomCreateDTO implements Serializable
{
this.remark = remark;
}
public Integer getMultimedia() {
return multimedia;
}
public void setMultimedia(Integer multimedia) {
this.multimedia = multimedia;
}
public Integer getProjector() {
return projector;
}
public void setProjector(Integer projector) {
this.projector = projector;
}
public String getSeatArrange() {
return seatArrange;
}
public void setSeatArrange(String seatArrange) {
this.seatArrange = seatArrange;
}
}
@@ -39,6 +39,10 @@ public class SiteCreateDTO implements Serializable
@Size(max = 500, message = "备注长度不能超过500个字符")
private String remark;
private String seatArrange;
private String major;
public String getSiteName()
{
return siteName;
@@ -98,4 +102,20 @@ public class SiteCreateDTO implements Serializable
{
this.remark = remark;
}
public String getSeatArrange() {
return seatArrange;
}
public void setSeatArrange(String seatArrange) {
this.seatArrange = seatArrange;
}
public String getMajor() {
return major;
}
public void setMajor(String major) {
this.major = major;
}
}
@@ -46,6 +46,9 @@ public class StudentCreateDTO implements Serializable
@Size(max = 500, message = "备注长度不能超过500个字符")
private String remark;
private Integer dutyLevel;
private String avatar;
public String getsName() { return sName; }
public void setsName(String sName) { this.sName = sName; }
public String getsGender() { return sGender; }
@@ -65,6 +68,22 @@ public class StudentCreateDTO implements Serializable
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
public Integer getDutyLevel() {
return dutyLevel;
}
public void setDutyLevel(Integer dutyLevel) {
this.dutyLevel = dutyLevel;
}
public String getAvatar() {
return avatar;
}
public void setAvatar(String avatar) {
this.avatar = avatar;
}
@Override
public String toString()
{
@@ -0,0 +1,36 @@
package com.ruoyi.train.domain.dto;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
/**
* 学员手工修改请求。
*
* <p>复用 {@link StudentCreateDTO} 的可编辑字段与校验规则,额外要求携带学员主键 sId。
* 身份证号沿用「只写不读」策略:未提交时保留库内既有密文,不做清空。</p>
*/
public class StudentUpdateDTO extends StudentCreateDTO
{
private static final long serialVersionUID = 1L;
@NotBlank(message = "学员ID不能为空")
@Size(max = 64, message = "学员ID长度不能超过64个字符")
private String sId;
public String getsId()
{
return sId;
}
public void setsId(String sId)
{
this.sId = sId;
}
@Override
public String toString()
{
return "StudentUpdateDTO{sId='" + sId + "', sName='" + getsName()
+ "', idCardProvided=" + (getIdCard() != null) + "}";
}
}
@@ -40,6 +40,12 @@ public class ClassroomDetailVO implements Serializable
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
private String seatArrange;
private Integer multimedia;
private Integer projector;
public String getClassRoomId()
{
return classRoomId;
@@ -119,4 +125,28 @@ public class ClassroomDetailVO implements Serializable
{
this.createTime = createTime;
}
public String getSeatArrange() {
return seatArrange;
}
public void setSeatArrange(String seatArrange) {
this.seatArrange = seatArrange;
}
public Integer getMultimedia() {
return multimedia;
}
public void setMultimedia(Integer multimedia) {
this.multimedia = multimedia;
}
public Integer getProjector() {
return projector;
}
public void setProjector(Integer projector) {
this.projector = projector;
}
}
@@ -60,6 +60,8 @@ public class CourseDetailVO implements Serializable
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
private String resourceId;
/**
* 已关联的教学资源列表
* 注:当前 ISSUE-01-03 未实现真实 Course<->Resource 查询;
@@ -91,6 +93,7 @@ public class CourseDetailVO implements Serializable
vo.setCreateTime(course.getCreateTime());
vo.setUpdateBy(course.getUpdateBy());
vo.setUpdateTime(course.getUpdateTime());
vo.setResourceId(course.getResourceId());
// 资源绑定/解绑能力属于 P1,当前返回空列表保持响应结构稳定
vo.setResources(Collections.emptyList());
return vo;
@@ -286,6 +289,14 @@ public class CourseDetailVO implements Serializable
this.updateTime = updateTime;
}
public String getResourceId() {
return resourceId;
}
public void setResourceId(String resourceId) {
this.resourceId = resourceId;
}
public List<Object> getResources()
{
return resources;
@@ -38,6 +38,10 @@ public class SiteDetailVO implements Serializable
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
private String seatArrange;
private String major;
public String getSiteId()
{
return siteId;
@@ -117,4 +121,20 @@ public class SiteDetailVO implements Serializable
{
this.createTime = createTime;
}
public String getSeatArrange() {
return seatArrange;
}
public void setSeatArrange(String seatArrange) {
this.seatArrange = seatArrange;
}
public String getMajor() {
return major;
}
public void setMajor(String major) {
this.major = major;
}
}
@@ -14,6 +14,10 @@ public class StudentDetailVO extends StudentItemVO
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
private Integer dutyLevel;
private String avatar;
public String getFaceRef() { return faceRef; }
public void setFaceRef(String faceRef) { this.faceRef = faceRef; }
public String getRemark() { return remark; }
@@ -22,4 +26,20 @@ public class StudentDetailVO extends StudentItemVO
public void setCreateBy(String createBy) { this.createBy = createBy; }
public Date getCreateTime() { return createTime; }
public void setCreateTime(Date createTime) { this.createTime = createTime; }
public Integer getDutyLevel() {
return dutyLevel;
}
public void setDutyLevel(Integer dutyLevel) {
this.dutyLevel = dutyLevel;
}
public String getAvatar() {
return avatar;
}
public void setAvatar(String avatar) {
this.avatar = avatar;
}
}
@@ -22,6 +22,10 @@ public class StudentItemVO implements Serializable
private Integer sourceType;
private Integer status;
private String avatar;
private Integer dutyLevel;
public String getsId() { return sId; }
public void setsId(String sId) { this.sId = sId; }
public String getsName() { return sName; }
@@ -44,4 +48,20 @@ public class StudentItemVO implements Serializable
public void setSourceType(Integer sourceType) { this.sourceType = sourceType; }
public Integer getStatus() { return status; }
public void setStatus(Integer status) { this.status = status; }
public String getAvatar() {
return avatar;
}
public void setAvatar(String avatar) {
this.avatar = avatar;
}
public Integer getDutyLevel() {
return dutyLevel;
}
public void setDutyLevel(Integer dutyLevel) {
this.dutyLevel = dutyLevel;
}
}
@@ -0,0 +1,60 @@
package com.ruoyi.train.mapper;
import java.util.List;
import com.ruoyi.train.domain.TrainResource;
/**
* 教学资源Mapper接口
*
* @author ruoyi
*/
public interface TrainResourceMapper
{
/**
* 查询教学资源
*
* @param resourceId 教学资源主键
* @return 教学资源
*/
public TrainResource selectTrainResourceByResourceId(String resourceId);
/**
* 查询教学资源列表
*
* @param trainResource 教学资源查询条件
* @return 教学资源集合
*/
public List<TrainResource> selectTrainResourceList(TrainResource trainResource);
/**
* 新增教学资源
*
* @param trainResource 教学资源
* @return 结果
*/
public int insertTrainResource(TrainResource trainResource);
/**
* 修改教学资源
*
* @param trainResource 教学资源
* @return 结果
*/
public int updateTrainResource(TrainResource trainResource);
/**
* 删除教学资源(逻辑删除)
*
* @param resourceId 教学资源主键
* @return 结果
*/
public int deleteTrainResourceByResourceId(String resourceId);
/**
* 批量删除教学资源(逻辑删除)
*
* @param resourceIds 需要删除的数据主键集合
* @return 结果
*/
public int deleteTrainResourceByResourceIds(String[] resourceIds);
}
@@ -18,6 +18,14 @@ public interface TrainStudentMapper
int insertTrainStudent(TrainStudent trainStudent);
/**
* 修改学员主档(仅更新非空字段,身份信息随身份证变更同步刷新)
*
* @param trainStudent 学员信息(必须携带 sId)
* @return 影响行数
*/
int updateTrainStudent(TrainStudent trainStudent);
/**
* 根据系统用户ID查询学员信息 (del_flag = '0')
*
@@ -0,0 +1,60 @@
package com.ruoyi.train.service;
import java.util.List;
import com.ruoyi.train.domain.TrainResource;
/**
* 教学资源Service接口
*
* @author ruoyi
*/
public interface ITrainResourceService
{
/**
* 查询教学资源
*
* @param resourceId 教学资源主键
* @return 教学资源
*/
public TrainResource selectTrainResourceByResourceId(String resourceId);
/**
* 查询教学资源列表
*
* @param trainResource 教学资源查询条件
* @return 教学资源集合
*/
public List<TrainResource> selectTrainResourceList(TrainResource trainResource);
/**
* 新增教学资源
*
* @param trainResource 教学资源
* @return 结果
*/
public int insertTrainResource(TrainResource trainResource);
/**
* 修改教学资源
*
* @param trainResource 教学资源
* @return 结果
*/
public int updateTrainResource(TrainResource trainResource);
/**
* 批量删除教学资源
*
* @param resourceIds 需要删除的教学资源主键集合
* @return 结果
*/
public int deleteTrainResourceByResourceIds(String[] resourceIds);
/**
* 删除教学资源信息
*
* @param resourceId 教学资源主键
* @return 结果
*/
public int deleteTrainResourceByResourceId(String resourceId);
}
@@ -2,6 +2,7 @@ package com.ruoyi.train.service;
import java.util.List;
import com.ruoyi.train.domain.dto.StudentCreateDTO;
import com.ruoyi.train.domain.dto.StudentUpdateDTO;
import com.ruoyi.train.domain.query.StudentQuery;
import com.ruoyi.train.domain.vo.StudentDetailVO;
import com.ruoyi.train.domain.vo.StudentItemVO;
@@ -15,6 +16,14 @@ public interface ITrainStudentService
String createStudent(StudentCreateDTO dto);
/**
* 修改学员主档
*
* @param dto 学员修改请求(携带 sId)
* @return 学员主档ID
*/
String updateTrainStudent(StudentUpdateDTO dto);
/**
* 安全删除学员档案(ZERO-REFERENCE SAFE DELETE)
*
@@ -57,6 +57,9 @@ public class TrainClassroomInfoServiceImpl implements ITrainClassroomInfoService
vo.setRoomStatus(classroom.getRoomStatus());
vo.setRemark(classroom.getRemark());
vo.setCreateTime(classroom.getCreateTime());
vo.setSeatArrange(classroom.getSeatArrange());
vo.setMultimedia(classroom.getMultimedia());
vo.setProjector(classroom.getProjector());
// 按 bind_classroom_id 查询绑定的电子班牌(0..N),映射为轻量摘要 VO,无绑定时返回空列表 []
TrainElectronicBoard boardQuery = new TrainElectronicBoard();
@@ -0,0 +1,159 @@
package com.ruoyi.train.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.uuid.IdUtils;
import com.ruoyi.train.domain.TrainResource;
import com.ruoyi.train.mapper.TrainResourceMapper;
import com.ruoyi.train.service.ITrainResourceService;
/**
* 教学资源Service业务层处理
*
* <p>资源表被 train_course_resource、train_study_duration 外键引用,
* 因此删除统一使用逻辑删除(del_flag = '2'),禁止物理删除。</p>
*
* @author ruoyi
*/
@Service
public class TrainResourceServiceImpl implements ITrainResourceService
{
@Autowired
private TrainResourceMapper trainResourceMapper;
/**
* 查询教学资源
*
* @param resourceId 教学资源主键
* @return 教学资源
*/
@Override
public TrainResource selectTrainResourceByResourceId(String resourceId)
{
return trainResourceMapper.selectTrainResourceByResourceId(resourceId);
}
/**
* 查询教学资源列表
*
* @param trainResource 教学资源查询条件
* @return 教学资源集合
*/
@Override
public List<TrainResource> selectTrainResourceList(TrainResource trainResource)
{
return trainResourceMapper.selectTrainResourceList(trainResource);
}
/**
* 新增教学资源
*
* @param trainResource 教学资源
* @return 结果
*/
@Override
public int insertTrainResource(TrainResource trainResource)
{
validateResourceType(trainResource.getResourceType());
validateStatus(trainResource.getStatus());
// 未指定主键时自动生成32位UUID,与 resource_id VARCHAR(32) 对齐
if (StringUtils.isEmpty(trainResource.getResourceId()))
{
trainResource.setResourceId(IdUtils.fastSimpleUUID());
}
if (trainResource.getStatus() == null)
{
trainResource.setStatus(1); // 默认正常
}
trainResource.setDelFlag("0");
trainResource.setCreateTime(DateUtils.getNowDate());
return trainResourceMapper.insertTrainResource(trainResource);
}
/**
* 修改教学资源
*
* @param trainResource 教学资源(必须携带 resourceId)
* @return 结果
*/
@Override
public int updateTrainResource(TrainResource trainResource)
{
if (StringUtils.isEmpty(trainResource.getResourceId()))
{
throw new ServiceException("资源ID不能为空");
}
if (trainResource.getResourceType() != null)
{
validateResourceType(trainResource.getResourceType());
}
if (trainResource.getStatus() != null)
{
validateStatus(trainResource.getStatus());
}
trainResource.setUpdateTime(DateUtils.getNowDate());
return trainResourceMapper.updateTrainResource(trainResource);
}
/**
* 批量删除教学资源
*
* @param resourceIds 需要删除的教学资源主键集合
* @return 结果
*/
@Override
public int deleteTrainResourceByResourceIds(String[] resourceIds)
{
if (resourceIds == null || resourceIds.length == 0)
{
throw new ServiceException("请选择要删除的资源");
}
return trainResourceMapper.deleteTrainResourceByResourceIds(resourceIds);
}
/**
* 删除教学资源信息
*
* @param resourceId 教学资源主键
* @return 结果
*/
@Override
public int deleteTrainResourceByResourceId(String resourceId)
{
if (StringUtils.isEmpty(resourceId))
{
throw new ServiceException("资源ID不能为空");
}
return trainResourceMapper.deleteTrainResourceByResourceId(resourceId);
}
/**
* 校验资源类型取值:1文档 2视频 33D模型 4VR资源 5外部教学链接 6其他
*/
private void validateResourceType(Integer resourceType)
{
if (resourceType == null)
{
throw new ServiceException("资源类型不能为空");
}
if (resourceType < 1 || resourceType > 6)
{
throw new ServiceException("资源类型取值必须为1-6");
}
}
/**
* 校验状态取值:0停用 1正常
*/
private void validateStatus(Integer status)
{
if (status != null && status != 0 && status != 1)
{
throw new ServiceException("资源状态取值必须为0或1");
}
}
}
@@ -13,6 +13,7 @@ import com.ruoyi.common.utils.uuid.IdUtils;
import com.ruoyi.train.constant.StudentErrorCode;
import com.ruoyi.train.domain.TrainStudent;
import com.ruoyi.train.domain.dto.StudentCreateDTO;
import com.ruoyi.train.domain.dto.StudentUpdateDTO;
import com.ruoyi.train.domain.query.StudentQuery;
import com.ruoyi.train.domain.vo.StudentDetailVO;
import com.ruoyi.train.domain.vo.StudentItemVO;
@@ -99,6 +100,8 @@ public class TrainStudentServiceImpl implements ITrainStudentService
student.setOrgName(trimToNull(dto.getOrgName()));
student.setsPhone(trimToNull(dto.getsPhone()));
student.setsEmail(trimToNull(dto.getsEmail()));
student.setAvatar(trimToNull(dto.getAvatar()));
student.setDutyLevel(dto.getDutyLevel());
if (normalizedIdCard != null)
{
@@ -144,6 +147,93 @@ public class TrainStudentServiceImpl implements ITrainStudentService
return student.getsId();
}
/**
* 修改学员主档信息。
*
* <p>与 createStudent 保持完全一致的安全语义:姓名必填、身份证格式校验、
* 身份证以可逆密文 + 冻结查重哈希落库、末四位仅用于脱敏展示。差异点:</p>
* <ul>
* <li>身份证未提供时保留库内既有密文,避免误清空已归档的身份信息;</li>
* <li>身份证变更时才做全局查重,哈希未变化视为本人,不做重复判定;</li>
* <li>userId / sourceType / status / delFlag 不属于编辑范围,一律不覆盖。</li>
* </ul>
*
* @param dto 学员修改请求(携带 sId)
* @return 学员主档ID
*/
@Override
@Transactional(rollbackFor = Exception.class)
public String updateTrainStudent(StudentUpdateDTO dto)
{
if (dto == null || StringUtils.isEmpty(dto.getsId()))
{
throw new ServiceException("学员ID不能为空");
}
validate(dto);
TrainStudent existing = trainStudentMapper.selectTrainStudentBySId(dto.getsId());
if (existing == null)
{
throw new ServiceException(StudentErrorCode.STUDENT_NOT_FOUND_MESSAGE,
StudentErrorCode.STUDENT_NOT_FOUND);
}
TrainStudent student = new TrainStudent();
student.setsId(existing.getsId());
student.setsName(dto.getsName().trim());
student.setsGender(trimToNull(dto.getsGender()));
student.setsBirth(dto.getsBirth());
student.setOrgName(trimToNull(dto.getOrgName()));
student.setsPhone(trimToNull(dto.getsPhone()));
student.setsEmail(trimToNull(dto.getsEmail()));
student.setAvatar(trimToNull(dto.getAvatar()));
student.setFaceRef(trimToNull(dto.getFaceRef()));
student.setDutyLevel(dto.getDutyLevel());
student.setRemark(trimToNull(dto.getRemark()));
String normalizedIdCard = normalizeIdCard(dto.getIdCard());
if (normalizedIdCard != null)
{
String hash = identityProtection.deterministicHash(normalizedIdCard);
// 哈希未变化说明仍是同一人,只有真正变更身份证时才需要全局查重
if (!hash.equals(existing.getIdCardHash())
&& trainStudentMapper.selectOneByIdCardHash(hash) != null)
{
throw duplicateIdentity();
}
student.setIdCardCiphertext(identityProtection.encrypt(normalizedIdCard));
student.setIdCardHash(hash);
student.setIdCardLast4(normalizedIdCard.substring(normalizedIdCard.length() - 4));
}
else
{
student.setIdCardCiphertext(existing.getIdCardCiphertext());
student.setIdCardHash(existing.getIdCardHash());
student.setIdCardLast4(existing.getIdCardLast4());
}
student.setUpdateTime(DateUtils.getNowDate());
try
{
student.setUpdateBy(SecurityUtils.getUsername());
}
catch (Exception ignored)
{
// 非 Web 单元测试无登录上下文;不使用任何请求数据作为审计用户兜底。
}
try
{
trainStudentMapper.updateTrainStudent(student);
}
catch (DuplicateKeyException e)
{
// 应用层预检之外,数据库唯一约束继续封堵并发重复身份证。
throw duplicateIdentity();
}
return student.getsId();
}
private StudentItemVO toItemVO(TrainStudent student)
{
StudentItemVO item = new StudentItemVO();
@@ -164,6 +254,8 @@ public class TrainStudentServiceImpl implements ITrainStudentService
target.setIdCardMasked(maskStoredIdentity(student));
target.setSourceType(student.getSourceType());
target.setStatus(student.getStatus());
target.setAvatar(student.getAvatar());
target.setDutyLevel(student.getDutyLevel());
}
private String maskStoredIdentity(TrainStudent student)
@@ -16,19 +16,24 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
<result property="seatArrange" column="seat_arrange" />
<result property="multimedia" column="multimedia" />
<result property="projector" column="projector" />
</resultMap>
<sql id="selectTrainClassroomInfoVo">
select class_room_id, room_name, room_location, capacity, room_status, del_flag, create_by, create_time, update_by, update_time, remark from train_classroom_info
select class_room_id, room_name, room_location, capacity, room_status, del_flag, create_by, create_time, update_by, update_time, remark, seat_arrange, multimedia, projector from train_classroom_info
</sql>
<select id="selectTrainClassroomInfoList" parameterType="TrainClassroomInfo" resultMap="TrainClassroomInfoResult">
<include refid="selectTrainClassroomInfoVo"/>
where del_flag = '0'
<if test="roomName != null and roomName != ''"> and room_name like concat('%', #{roomName}, '%')</if>
<if test="roomLocation != null and roomLocation != ''"> and room_location = #{roomLocation}</if>
<if test="roomLocation != null and roomLocation != ''"> and room_location like concat('%', #{roomLocation}, '%')</if>
<if test="capacity != null "> and capacity = #{capacity}</if>
<if test="roomStatus != null "> and room_status = #{roomStatus}</if>
<if test="multimedia != null "> and multimedia = #{multimedia}</if>
<if test="projector != null "> and projector = #{projector}</if>
<if test="createBy != null and createBy != ''"> and create_by = #{createBy}</if>
order by create_time desc
</select>
@@ -61,6 +66,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="updateBy != null and updateBy != ''">update_by,</if>
<if test="updateTime != null">update_time,</if>
<if test="remark != null">remark,</if>
<if test="seatArrange != null">seat_arrange,</if>
<if test="multimedia != null">multimedia,</if>
<if test="projector != null">projector,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="classRoomId != null">#{classRoomId},</if>
@@ -74,6 +82,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="updateBy != null and updateBy != ''">#{updateBy},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="remark != null">#{remark},</if>
<if test="seatArrange != null">#{seatArrange},</if>
<if test="multimedia != null">#{multimedia},</if>
<if test="projector != null">#{projector},</if>
</trim>
</insert>
@@ -88,6 +99,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="seatArrange != null">seat_arrange = #{seatArrange},</if>
<if test="multimedia != null">multimedia = #{multimedia},</if>
<if test="projector != null">projector = #{projector},</if>
</trim>
where class_room_id = #{classRoomId}
</update>
@@ -12,6 +12,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="cDuration" column="c_duration" />
<result property="teachingObjective" column="teaching_objective" />
<result property="courseContent" column="course_content" />
<result property="resourceId" column="resource_id" />
<result property="status" column="status" />
<result property="delFlag" column="del_flag" />
<result property="createBy" column="create_by" />
@@ -22,7 +23,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<sql id="selectTrainCourseVo">
select c_id, c_code, c_name, c_type, c_duration, teaching_objective, course_content, status, del_flag, create_by, create_time, update_by, update_time, remark from train_course
select c_id, c_code, c_name, c_type, c_duration, teaching_objective, course_content, resource_id, status, del_flag, create_by, create_time, update_by, update_time, remark from train_course
</sql>
<select id="selectTrainCourseList" parameterType="TrainCourse" resultMap="TrainCourseResult">
@@ -63,6 +64,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="cDuration != null">c_duration,</if>
<if test="teachingObjective != null and teachingObjective != ''">teaching_objective,</if>
<if test="courseContent != null and courseContent != ''">course_content,</if>
<if test="resourceId != null and resourceId != ''">resource_id,</if>
<if test="status != null">status,</if>
<if test="delFlag != null and delFlag != ''">del_flag,</if>
<if test="createBy != null and createBy != ''">create_by,</if>
@@ -79,6 +81,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="cDuration != null">#{cDuration},</if>
<if test="teachingObjective != null and teachingObjective != ''">#{teachingObjective},</if>
<if test="courseContent != null and courseContent != ''">#{courseContent},</if>
<if test="resourceId != null and resourceId != ''">#{resourceId},</if>
<if test="status != null">#{status},</if>
<if test="delFlag != null and delFlag != ''">#{delFlag},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if>
@@ -98,6 +101,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="cDuration != null">c_duration = #{cDuration},</if>
<if test="teachingObjective != null">teaching_objective = #{teachingObjective},</if>
<if test="courseContent != null">course_content = #{courseContent},</if>
<if test="resourceId != null">resource_id = #{resourceId},</if>
<if test="status != null">status = #{status},</if>
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.train.mapper.TrainResourceMapper">
<resultMap type="TrainResource" id="TrainResourceResult">
<result property="resourceId" column="resource_id" />
<result property="resourceName" column="resource_name" />
<result property="resourceType" column="resource_type" />
<result property="resourceUrl" column="resource_url" />
<result property="description" column="description" />
<result property="status" column="status" />
<result property="delFlag" column="del_flag" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
</resultMap>
<sql id="selectTrainResourceVo">
select resource_id, resource_name, resource_type, resource_url, description, status, del_flag, create_by, create_time, update_by, update_time, remark from train_resource
</sql>
<select id="selectTrainResourceList" parameterType="TrainResource" resultMap="TrainResourceResult">
<include refid="selectTrainResourceVo"/>
where del_flag = '0'
<if test="resourceName != null and resourceName != ''"> and resource_name like concat('%', #{resourceName}, '%')</if>
<if test="resourceType != null"> and resource_type = #{resourceType}</if>
<if test="resourceUrl != null and resourceUrl != ''"> and resource_url like concat('%', #{resourceUrl}, '%')</if>
<if test="description != null and description != ''"> and description like concat('%', #{description}, '%')</if>
<if test="status != null"> and status = #{status}</if>
<if test="createBy != null and createBy != ''"> and create_by = #{createBy}</if>
<if test="params.beginTime != null and params.beginTime != ''"> and create_time &gt;= #{params.beginTime}</if>
<if test="params.endTime != null and params.endTime != ''"> and create_time &lt;= #{params.endTime}</if>
order by create_time desc
</select>
<select id="selectTrainResourceByResourceId" parameterType="String" resultMap="TrainResourceResult">
<include refid="selectTrainResourceVo"/>
where resource_id = #{resourceId} and del_flag = '0'
</select>
<insert id="insertTrainResource" parameterType="TrainResource">
insert into train_resource
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="resourceId != null">resource_id,</if>
<if test="resourceName != null and resourceName != ''">resource_name,</if>
<if test="resourceType != null">resource_type,</if>
<if test="resourceUrl != null and resourceUrl != ''">resource_url,</if>
<if test="description != null">description,</if>
<if test="status != null">status,</if>
<if test="delFlag != null and delFlag != ''">del_flag,</if>
<if test="createBy != null and createBy != ''">create_by,</if>
<if test="createTime != null">create_time,</if>
<if test="updateBy != null and updateBy != ''">update_by,</if>
<if test="updateTime != null">update_time,</if>
<if test="remark != null">remark,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="resourceId != null">#{resourceId},</if>
<if test="resourceName != null and resourceName != ''">#{resourceName},</if>
<if test="resourceType != null">#{resourceType},</if>
<if test="resourceUrl != null and resourceUrl != ''">#{resourceUrl},</if>
<if test="description != null">#{description},</if>
<if test="status != null">#{status},</if>
<if test="delFlag != null and delFlag != ''">#{delFlag},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateBy != null and updateBy != ''">#{updateBy},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="remark != null">#{remark},</if>
</trim>
</insert>
<update id="updateTrainResource" parameterType="TrainResource">
update train_resource
<trim prefix="SET" suffixOverrides=",">
<if test="resourceName != null and resourceName != ''">resource_name = #{resourceName},</if>
<if test="resourceType != null">resource_type = #{resourceType},</if>
<if test="resourceUrl != null and resourceUrl != ''">resource_url = #{resourceUrl},</if>
<if test="description != null">description = #{description},</if>
<if test="status != null">status = #{status},</if>
<if test="delFlag != null and delFlag != ''">del_flag = #{delFlag},</if>
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="remark != null">remark = #{remark},</if>
</trim>
where resource_id = #{resourceId} and del_flag = '0'
</update>
<update id="deleteTrainResourceByResourceId" parameterType="String">
update train_resource set del_flag = '2' where resource_id = #{resourceId}
</update>
<update id="deleteTrainResourceByResourceIds" parameterType="String">
update train_resource set del_flag = '2' where resource_id in
<foreach item="resourceId" collection="array" open="(" separator="," close=")">
#{resourceId}
</foreach>
</update>
</mapper>
@@ -25,12 +25,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
<result property="delFlag" column="del_flag" />
<result property="avatar" column="avatar" />
<result property="dutyLevel" column="duty_level" />
</resultMap>
<sql id="selectTrainStudentDomain">
select s_id, s_name, s_gender, s_birth, user_id, org_name, s_phone, s_email,
id_card_ciphertext, id_card_hash, id_card_last4, face_ref, source_type,
status, create_by, create_time, update_by, update_time, remark, del_flag
status, create_by, create_time, update_by, update_time, remark, del_flag,
avatar, duty_level
from train_student
</sql>
@@ -77,14 +80,39 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
insert into train_student (
s_id, s_name, s_gender, s_birth, user_id, org_name, s_phone, s_email,
id_card_ciphertext, id_card_hash, id_card_last4, face_ref, source_type,
status, create_by, create_time, update_by, update_time, remark, del_flag
status, create_by, create_time, update_by, update_time, remark, del_flag,
avatar, duty_level
) values (
#{sId}, #{sName}, #{sGender}, #{sBirth}, #{userId}, #{orgName}, #{sPhone}, #{sEmail},
#{idCardCiphertext}, #{idCardHash}, #{idCardLast4}, #{faceRef}, #{sourceType},
#{status}, #{createBy}, #{createTime}, #{updateBy}, #{updateTime}, #{remark}, #{delFlag}
#{status}, #{createBy}, #{createTime}, #{updateBy}, #{updateTime}, #{remark}, #{delFlag},
#{avatar}, #{dutyLevel}
)
</insert>
<!-- 修改学员主档:仅覆盖传入的非空字段,userId/sourceType/status/delFlag 不在编辑范围 -->
<update id="updateTrainStudent" parameterType="TrainStudent">
update train_student
<trim prefix="SET" suffixOverrides=",">
<if test="sName != null">s_name = #{sName},</if>
<if test="sGender != null">s_gender = #{sGender},</if>
<if test="sBirth != null">s_birth = #{sBirth},</if>
<if test="orgName != null">org_name = #{orgName},</if>
<if test="sPhone != null">s_phone = #{sPhone},</if>
<if test="sEmail != null">s_email = #{sEmail},</if>
<if test="idCardCiphertext != null">id_card_ciphertext = #{idCardCiphertext},</if>
<if test="idCardHash != null">id_card_hash = #{idCardHash},</if>
<if test="idCardLast4 != null">id_card_last4 = #{idCardLast4},</if>
<if test="faceRef != null">face_ref = #{faceRef},</if>
<if test="avatar != null">avatar = #{avatar},</if>
<if test="dutyLevel != null">duty_level = #{dutyLevel},</if>
<if test="remark != null">remark = #{remark},</if>
update_time = #{updateTime},
update_by = #{updateBy}
</trim>
where s_id = #{sId} and del_flag = '0'
</update>
<!-- 根据系统用户ID查询学员 (仅限未删除 del_flag = '0') -->
<select id="selectTrainStudentByUserId" parameterType="Long" resultMap="TrainStudentResult">
<include refid="selectTrainStudentDomain" />
@@ -21,10 +21,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
<result property="avatar" column="avatar" />
<result property="major" column="major" />
<result property="courses" column="courses" />
<result property="startTeaching" column="start_teaching" />
</resultMap>
<sql id="selectTrainTeacherVo">
select t_id, t_name, t_gender, t_birth, user_id, org_name, job_title, t_phone, t_email, status, del_flag, create_by, create_time, update_by, update_time, remark from train_teacher
select t_id, t_name, t_gender, t_birth, user_id, org_name, job_title, t_phone, t_email, status, del_flag, create_by, create_time, update_by, update_time, remark, avatar, major, courses, start_teaching from train_teacher
</sql>
<select id="selectTrainTeacherList" parameterType="TrainTeacher" resultMap="TrainTeacherResult">
@@ -39,6 +43,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="tPhone != null and tPhone != ''"> and t_phone like concat('%', #{tPhone}, '%')</if>
<if test="tEmail != null and tEmail != ''"> and t_email = #{tEmail}</if>
<if test="status != null"> and status = #{status}</if>
<if test="major != null and major != ''"> and major like concat('%', #{major}, '%')</if>
<if test="courses != null and courses != ''"> and courses like concat('%', #{courses}, '%')</if>
</where>
order by create_time desc
</select>
@@ -72,6 +78,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="updateBy != null and updateBy != ''">update_by,</if>
<if test="updateTime != null">update_time,</if>
<if test="remark != null">remark,</if>
<if test="avatar != null">avatar,</if>
<if test="major != null">major,</if>
<if test="courses != null">courses,</if>
<if test="startTeaching != null">start_teaching,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="tId != null">#{tId},</if>
@@ -90,6 +100,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="updateBy != null and updateBy != ''">#{updateBy},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="remark != null">#{remark},</if>
<if test="avatar != null">#{avatar},</if>
<if test="major != null">#{major},</if>
<if test="courses != null">#{courses},</if>
<if test="startTeaching != null">#{startTeaching},</if>
</trim>
</insert>
@@ -108,6 +122,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="avatar != null">avatar = #{avatar},</if>
<if test="major != null">major = #{major},</if>
<if test="courses != null">courses = #{courses},</if>
<if test="startTeaching != null">start_teaching = #{startTeaching},</if>
</trim>
where t_id = #{tId} and del_flag = '0'
</update>
@@ -17,20 +17,23 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
<result property="major" column="major" />
<result property="seatArrange" column="seat_arrange" />
</resultMap>
<sql id="selectTrainTrainingSiteVo">
select site_id, site_name, site_location, capacity, site_device, site_status, del_flag, create_by, create_time, update_by, update_time, remark from train_training_site
select site_id, site_name, site_location, capacity, site_device, site_status, del_flag, create_by, create_time, update_by, update_time, remark, major, seat_arrange from train_training_site
</sql>
<select id="selectTrainTrainingSiteList" parameterType="TrainTrainingSite" resultMap="TrainTrainingSiteResult">
<include refid="selectTrainTrainingSiteVo"/>
where del_flag = '0'
<if test="siteName != null and siteName != ''"> and site_name like concat('%', #{siteName}, '%')</if>
<if test="siteLocation != null and siteLocation != ''"> and site_location = #{siteLocation}</if>
<if test="siteLocation != null and siteLocation != ''"> and site_location like concat('%', #{siteLocation}, '%')</if>
<if test="capacity != null "> and capacity = #{capacity}</if>
<if test="siteDevice != null and siteDevice != ''"> and site_device = #{siteDevice}</if>
<if test="siteStatus != null "> and site_status = #{siteStatus}</if>
<if test="major != null and major != ''"> and major like concat('%', #{major}, '%')</if>
<if test="createBy != null and createBy != ''"> and create_by = #{createBy}</if>
order by create_time desc
</select>
@@ -64,6 +67,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="updateBy != null and updateBy != ''">update_by,</if>
<if test="updateTime != null">update_time,</if>
<if test="remark != null">remark,</if>
<if test="major != null">major,</if>
<if test="seatArrange != null">seat_arrange,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="siteId != null">#{siteId},</if>
@@ -78,6 +83,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="updateBy != null and updateBy != ''">#{updateBy},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="remark != null">#{remark},</if>
<if test="major != null">#{major},</if>
<if test="seatArrange != null">#{seatArrange},</if>
</trim>
</insert>
@@ -93,6 +100,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="major != null">major = #{major},</if>
<if test="seatArrange != null">seat_arrange = #{seatArrange},</if>
</trim>
where site_id = #{siteId}
</update>
+1 -1
View File
@@ -96,7 +96,7 @@ export function updateUserPwd(oldPassword, newPassword) {
return request({
url: '/system/user/profile/updatePwd',
method: 'put',
params: data
data: data
})
}
+44
View File
@@ -0,0 +1,44 @@
import request from '@/utils/request'
// 查询教学资源列表
export function listResource(query) {
return request({
url: '/train/resource/list',
method: 'get',
params: query
})
}
// 查询教学资源详细
export function getResource(resourceId) {
return request({
url: '/train/resource/' + resourceId,
method: 'get'
})
}
// 新增教学资源
export function addResource(data) {
return request({
url: '/train/resource',
method: 'post',
data: data
})
}
// 修改教学资源
export function updateResource(data) {
return request({
url: '/train/resource/' + data.resourceId,
method: 'put',
data: data
})
}
// 删除教学资源
export function delResource(resourceId) {
return request({
url: '/train/resource/' + resourceId,
method: 'delete'
})
}
+8
View File
@@ -26,6 +26,14 @@ export function addSite(data) {
})
}
// 删除实训场地
export function delSite(ids) {
return request({
url: '/train/training-site/' + ids,
method: 'delete'
})
}
// 修改实训场地
export function updateSite(data) {
return request({
+9
View File
@@ -26,6 +26,15 @@ export function addStudent(data) {
})
}
// 修改教师信息
export function updateStudent(data) {
return request({
url: '/train/student/' + (data.sId || ''),
method: 'put',
data: data
})
}
// 查询可关联学员的系统用户候选列表
export function getStudentBindableUsers(keyword) {
return request({
+86 -3
View File
@@ -1,6 +1,6 @@
<template>
<div class="app-container">
<el-form v-show="showSearch" ref="queryForm" :model="queryParams" size="small" :inline="true" label-width="68px">
<el-form v-show="showSearch" ref="queryForm" :model="queryParams" size="small" :inline="true" label-width="88px">
<el-form-item label="教室名称" prop="roomName">
<el-input
v-model="queryParams.roomName"
@@ -24,6 +24,26 @@
<el-option label="维护中" :value="2" />
</el-select>
</el-form-item>
<el-form-item label="具备多媒体" prop="multimedia">
<el-select v-model="queryParams.multimedia" placeholder="具备多媒体" clearable>
<el-option
v-for="dict in yesNoOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="具备投影" prop="projector">
<el-select v-model="queryParams.projector" placeholder="具备投影" clearable>
<el-option
v-for="dict in yesNoOptions"
:key="dict.value"
:label="dict.label"
:value="dict.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>
@@ -71,7 +91,17 @@
<el-tag v-else type="info">维护中</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
<el-table-column label="具备多媒体" align="center" prop="multimedia">
<template slot-scope="scope">
<dict-tag :options="dict.type.train_yes_no" :value="scope.row.multimedia" />
</template>
</el-table-column>
<el-table-column label="具备投影" align="center" prop="projector">
<template slot-scope="scope">
<dict-tag :options="dict.type.train_yes_no" :value="scope.row.projector" />
</template>
</el-table-column>
<!-- <el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />-->
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="140">
<template slot-scope="scope">
<el-button
@@ -120,6 +150,24 @@
<el-radio :label="2">维护中</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="具备多媒体" prop="multimedia">
<el-radio-group v-model="form.multimedia">
<el-radio
v-for="dict in yesNoOptions"
:key="dict.value"
:label="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="具备投影" prop="projector">
<el-radio-group v-model="form.projector">
<el-radio
v-for="dict in yesNoOptions"
:key="dict.value"
:label="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" maxlength="500" />
</el-form-item>
@@ -137,6 +185,8 @@ import { listClassroom, getClassroom, addClassroom, updateClassroom, delClassroo
export default {
name: 'Classroom',
// 是否类字典(sys_yes_no),注入后可通过 dict.type.sys_yes_no 访问
dicts: ['sys_yes_no', 'train_yes_no'],
data() {
return {
// 遮罩层
@@ -164,7 +214,9 @@ export default {
roomName: null,
roomLocation: null,
capacity: null,
roomStatus: null
roomStatus: null,
multimedia: null,
projector: null
},
// 表单参数
form: {},
@@ -191,10 +243,35 @@ export default {
}
}
},
computed: {
/*
* 是否(sys_yes_no)单选选项。
* 数据库 multimedia / projector 为数值型(1=是,0=否),
* 字典 dictValue 为 Y/N 字符串,此处统一转换为数值,保证回显与提交类型一致。
*/
yesNoOptions() {
const list = (this.dict && this.dict.type && this.dict.type.train_yes_no) || []
return list.map(item => ({
label: item.label,
//value: this.toYesNoValue(item.value)
value: /^\d+$/.test(String(item.value)) ? Number(item.value) : item.value
}))
}
},
created() {
this.getList()
},
methods: {
/** 是否值归一化:Y->1,N->0,纯数字字符串转 Number */
toYesNoValue(value) {
if (value === null || value === undefined || value === '') {
return null
}
const text = String(value)
if (text === 'Y' || text === 'y') return 1
if (text === 'N' || text === 'n') return 0
return /^\d+$/.test(text) ? Number(text) : value
},
/** 查询教室信息列表 */
getList() {
this.loading = true
@@ -217,6 +294,8 @@ export default {
roomLocation: null,
capacity: 50,
roomStatus: 1,
multimedia: 0,
projector: 0,
remark: null
}
this.resetForm('form')
@@ -249,6 +328,10 @@ export default {
const classRoomId = row.classRoomId || this.ids
getClassroom(classRoomId).then(response => {
this.form = response.data
console.log("multimedia==="+this.form.multimedia);
// 归一化是否值,保证与单选选项(数值)类型一致,避免回显不选中
//this.$set(this.form, 'multimedia', /^\d+$/.test(String(this.form.multimedia)) ? Number(this.form.multimedia) : this.form.multimedia)
//this.$set(this.form, 'projector', /^\d+$/.test(String(this.form.projector)) ? Number(this.form.projector) : this.form.projector)
this.open = true
this.title = '修改教室信息'
})
+109 -14
View File
@@ -18,12 +18,19 @@
/>
</el-form-item>
<el-form-item label="课程类型" prop="cType">
<el-input
<el-select
v-model="queryParams.cType"
placeholder="请输入课程类型"
placeholder="请选择课程类型"
clearable
@keyup.enter.native="handleQuery"
/>
>
<el-option
v-for="dict in dict.type.train_course_type"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="课程状态" clearable>
@@ -66,10 +73,14 @@
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="课程编码" align="center" prop="cCode" min-width="120" :show-overflow-tooltip="true" />
<el-table-column label="课程名称" align="center" prop="cName" min-width="140" :show-overflow-tooltip="true" />
<el-table-column label="课程类型" align="center" prop="cType" width="120" />
<el-table-column label="课程类型" align="center" prop="cType" width="120">
<template slot-scope="scope">
<dict-tag :options="dict.type.train_course_type" :value="scope.row.cType" />
</template>
</el-table-column>
<el-table-column label="学时" align="center" prop="cDuration" width="80" />
<el-table-column label="教学目标" align="center" prop="teachingObjective" min-width="150" :show-overflow-tooltip="true" />
<el-table-column label="课程内容" align="center" prop="courseContent" min-width="160" :show-overflow-tooltip="true" />
<!-- <el-table-column label="教学目标" align="center" prop="teachingObjective" min-width="150" :show-overflow-tooltip="true" />
<el-table-column label="课程内容" align="center" prop="courseContent" min-width="160" :show-overflow-tooltip="true" />-->
<el-table-column label="状态" align="center" prop="status" width="90">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'info'">
@@ -77,8 +88,8 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" min-width="120" :show-overflow-tooltip="true" />
<el-table-column label="创建时间" align="center" prop="createTime" width="160" />
<!-- <el-table-column label="备注" align="center" prop="remark" min-width="120" :show-overflow-tooltip="true" />
<el-table-column label="创建时间" align="center" prop="createTime" width="160" />-->
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="180">
<template slot-scope="scope">
<el-button
@@ -136,7 +147,14 @@
<el-input v-model="form.cName" placeholder="请输入课程名称" maxlength="100" />
</el-form-item>
<el-form-item label="课程类型" prop="cType">
<el-input v-model="form.cType" placeholder="请输入课程类型 (如 理论课/实操课)" maxlength="30" />
<el-select v-model="form.cType" placeholder="请选择课程类型" clearable style="width: 100%">
<el-option
v-for="dict in dict.type.train_course_type"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="课程学时" prop="cDuration">
<el-input-number v-model="form.cDuration" :min="1" :max="9999" placeholder="请输入课程学时" style="width: 100%;" />
@@ -147,6 +165,23 @@
<el-form-item label="课程内容" prop="courseContent">
<el-input v-model="form.courseContent" type="textarea" :rows="4" placeholder="请输入课程内容/摘要" />
</el-form-item>
<el-form-item label="课程资源" prop="resourceId">
<el-select
v-model="resourceIdList"
multiple
filterable
collapse-tags
placeholder="请选择课程资源 (可多选)"
style="width: 100%"
>
<el-option
v-for="item in resourceOptions"
:key="item.resourceId"
:label="item.resourceName"
:value="String(item.resourceId)"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio :label="1">正常</el-radio>
@@ -167,9 +202,11 @@
<script>
import { listCourse, getCourse, addCourse, updateCourse, delCourse } from '@/api/train/course'
import { listResource, getResource } from '@/api/train/resource'
export default {
name: 'Course',
dicts: ['train_course_type'],
data() {
const validateDuration = (rule, value, callback) => {
if (value !== undefined && value !== null && value !== '') {
@@ -200,6 +237,10 @@ export default {
title: '',
// 是否显示弹出层
open: false,
// 课程资源下拉备选项
resourceOptions: [],
// 已选课程资源ID集合 (对应 form.resourceId 逗号分隔字符串)
resourceIdList: [],
// 查询参数
queryParams: {
pageNum: 1,
@@ -229,13 +270,52 @@ export default {
},
created() {
this.getList()
this.getResourceOptions()
},
methods: {
/** 查询课程资源下拉备选项 (不做状态过滤,保证已选的停用资源也能正常回显) */
getResourceOptions() {
return listResource({ pageNum: 1, pageSize: 1000 }).then(response => {
this.resourceOptions = response.rows || []
return this.resourceOptions
}).catch(() => {
this.resourceOptions = this.resourceOptions || []
return this.resourceOptions
})
},
/** 课程资源ID字符串 -> 多选数组 (统一转成字符串,避免类型不一致导致无法勾选) */
parseResourceIds(value) {
if (value === null || value === undefined || value === '') {
return []
}
return String(value).split(',').map(item => String(item).trim()).filter(item => item !== '')
},
/** 保证已选资源在下拉选项中存在;element-ui 多选值若无对应 option 会不显示,故必须补齐后再回显 */
ensureResourceOptions(ids) {
const existingIds = this.resourceOptions.map(item => String(item.resourceId).trim())
const missingIds = ids.filter(id => existingIds.indexOf(id) === -1)
if (missingIds.length === 0) {
return Promise.resolve()
}
return Promise.all(missingIds.map(id => {
return getResource(id).then(response => {
if (response.data) {
this.resourceOptions.push(response.data)
}
}).catch(() => {})
}))
},
/** 备选项就绪后再回显已选资源 */
prepareResourceOptions(ids) {
const load = this.resourceOptions.length > 0 ? Promise.resolve() : this.getResourceOptions()
return Promise.resolve(load).then(() => this.ensureResourceOptions(ids))
},
/** 查询课程信息列表 */
getList() {
this.loading = true
listCourse(this.queryParams).then(response => {
this.courseList = response.rows
//console.log(response.rows);
this.total = response.total
this.loading = false
})
@@ -255,9 +335,11 @@ export default {
cDuration: null,
teachingObjective: null,
courseContent: null,
resourceId: null,
status: 1,
remark: null
}
this.resourceIdList = []
this.resetForm('form')
},
/** 搜索按钮操作 */
@@ -278,24 +360,37 @@ export default {
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.open = true
this.title = '添加课程'
this.prepareResourceOptions([]).then(() => {
this.open = true
this.title = '添加课程'
})
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const cId = row.cId || this.ids
getCourse(cId).then(response => {
//console.log(response.data);
this.form = response.data
if (this.form.status === undefined || this.form.status === null) {
this.form.status = 1
}
this.open = true
this.title = '修改课程'
})
const selectedIds = this.parseResourceIds(this.form.resourceId)
// 必须先保证这些ID在下拉备选项中存在,否则多选不会被勾选/显示
return this.prepareResourceOptions(selectedIds).then(() => {
//this.resourceIdList = selectedIds
//console.log("selectedIds===============");
//console.log(selectedIds);
this.$set(this,"resourceIdList",selectedIds);
this.open = true
this.title = '修改课程'
})
}).catch(() => {})
},
/** 提交按钮 */
submitForm() {
// 多选资源ID以逗号分隔写入 resource_id 字段
this.form.resourceId = this.resourceIdList.length > 0 ? this.resourceIdList.join(',') : null
this.$refs['form'].validate(valid => {
if (valid) {
if (this.form.cId != null) {
+383
View File
@@ -0,0 +1,383 @@
<template>
<div class="app-container">
<el-form v-show="showSearch" ref="queryForm" :model="queryParams" size="small" :inline="true" label-width="68px">
<el-form-item label="资源名称" prop="resourceName">
<el-input
v-model="queryParams.resourceName"
placeholder="请输入资源名称"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="资源类型" prop="resourceType">
<el-select v-model="queryParams.resourceType" placeholder="资源类型" clearable>
<el-option
v-for="dict in resourceTypeOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="资源状态" clearable>
<el-option label="正常" :value="1" />
<el-option label="停用" :value="0" />
</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:resource:edit']"
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
v-hasPermi="['train:resource:edit']"
type="success"
plain
icon="el-icon-edit"
size="mini"
:disabled="single"
@click="handleUpdate"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
v-hasPermi="['train:resource:edit']"
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
>删除</el-button>
</el-col>
<right-toolbar :show-search.sync="showSearch" @queryTable="getList" />
</el-row>
<el-table v-loading="loading" :data="resourceList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="资源名称" align="center" prop="resourceName" min-width="160" :show-overflow-tooltip="true" />
<el-table-column label="资源类型" align="center" prop="resourceType">
<template slot-scope="scope">
<dict-tag :options="dict.type.train_resource_type" :value="scope.row.resourceType" />
</template>
</el-table-column>
<el-table-column label="资源文件/地址" align="center" prop="resourceUrl" min-width="200" :show-overflow-tooltip="true">
<template slot-scope="scope">
<el-link
v-if="scope.row.resourceType === 5"
:href="scope.row.resourceUrl"
type="primary"
target="_blank"
:underline="false"
>{{ scope.row.resourceUrl }}</el-link>
<el-link
v-else
:href="fileUrl(scope.row.resourceUrl)"
type="primary"
target="_blank"
:underline="false"
>{{ fileName(scope.row.resourceUrl) }}</el-link>
</template>
</el-table-column>
<el-table-column label="资源描述" align="center" prop="description" :show-overflow-tooltip="true" />
<el-table-column label="状态" align="center" prop="status">
<template slot-scope="scope">
<el-tag v-if="scope.row.status === 1" type="success">正常</el-tag>
<el-tag v-else type="danger">停用</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="160">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="180" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
v-hasPermi="['train:resource:edit']"
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
>修改</el-button>
<el-button
v-hasPermi="['train:resource:edit']"
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(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-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="资源名称" prop="resourceName">
<el-input v-model="form.resourceName" placeholder="请输入资源名称" maxlength="150" />
</el-form-item>
<el-form-item label="资源类型" prop="resourceType">
<el-select v-model="form.resourceType" placeholder="请选择资源类型" style="width: 100%">
<el-option
v-for="dict in resourceTypeOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item v-if="isExternalLink" label="外部链接" prop="resourceUrl">
<el-input v-model="form.resourceUrl" placeholder="请输入外部教学链接,以 http(s):// 开头" maxlength="500" />
</el-form-item>
<el-form-item v-else label="资源文件" prop="resourceUrl">
<file-upload
v-model="form.resourceUrl"
:limit="1"
:file-size="100"
:file-type="fileTypes"
/>
</el-form-item>
<el-form-item label="资源描述" prop="description">
<el-input v-model="form.description" type="textarea" :rows="3" maxlength="500" placeholder="请输入资源描述/使用说明" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio :label="1">正常</el-radio>
<el-radio :label="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" maxlength="500" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listResource, getResource, addResource, updateResource, delResource } from '@/api/train/resource'
export default {
name: 'TrainResource',
dicts: ['train_resource_type'],
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 教学资源表格数据
resourceList: [],
// 弹出层标题
title: '',
// 是否显示弹出层
open: false,
// 允许上传的资源文件格式
fileTypes: ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'pdf', 'txt', 'zip', 'rar', '7z', 'mp4', 'avi', 'mov', 'wmv', 'mp3', 'png', 'jpg', 'jpeg', 'gif', 'glb', 'gltf', 'obj', 'fbx'],
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
resourceName: null,
resourceType: null,
status: null
},
// 表单参数
form: {},
// 表单校验
rules: {
resourceName: [
{ required: true, message: '资源名称不能为空', trigger: 'blur' }
],
resourceType: [
{ required: true, message: '资源类型不能为空', trigger: 'change' }
],
resourceUrl: [
{ required: true, message: '资源文件或外部链接不能为空', trigger: ['blur', 'change'] }
]
}
}
},
computed: {
// 资源类型下拉项:字典值为字符串,数据表字段为整型,需转成数字
resourceTypeOptions() {
return (this.dict.type.train_resource_type || []).map(item => ({
label: item.label,
value: /^\d+$/.test(String(item.value)) ? Number(item.value) : item.value
}))
},
// 资源类型为「外部教学链接」时不走上传,改为填写地址
isExternalLink() {
return Number(this.form.resourceType) === 5
}
},
watch: {
// 切换资源类型时清空已选资源地址,避免类型与地址不匹配
'form.resourceType'(newVal, oldVal) {
if (oldVal !== undefined && newVal !== oldVal && this.open) {
this.$set(this.form, 'resourceUrl', null)
this.$nextTick(() => {
if (this.$refs['form']) {
this.$refs['form'].clearValidate('resourceUrl')
}
})
}
}
},
created() {
this.getList()
},
methods: {
/** 查询教学资源列表 */
getList() {
this.loading = true
listResource(this.queryParams).then(response => {
this.resourceList = response.rows
this.total = response.total
this.loading = false
})
},
// 取消按钮
cancel() {
this.open = false
this.reset()
},
// 表单重置
reset() {
this.form = {
resourceId: null,
resourceName: null,
resourceType: 1,
resourceUrl: null,
description: null,
status: 1,
remark: null
}
this.resetForm('form')
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm('queryForm')
this.handleQuery()
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.resourceId)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.open = true
this.title = '添加教学资源'
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const resourceId = row.resourceId || this.ids
getResource(resourceId).then(response => {
// 资源类型为整型,防止后端返回字符串导致下拉/单选回显失败
this.form = {
...response.data,
resourceType: response.data.resourceType == null ? null : Number(response.data.resourceType),
status: response.data.status == null ? 1 : Number(response.data.status)
}
this.open = true
this.title = '修改教学资源'
})
},
/** 提交按钮 */
submitForm() {
this.$refs['form'].validate(valid => {
if (valid) {
if (this.form.resourceId != null) {
updateResource(this.form).then(response => {
this.$modal.msgSuccess('修改成功')
this.open = false
this.getList()
})
} else {
addResource(this.form).then(response => {
this.$modal.msgSuccess('新增成功')
this.open = false
this.getList()
})
}
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
const ids = row.resourceId || this.ids
if (!ids || (Array.isArray(ids) && ids.length === 0)) {
this.$modal.msgWarning('请选择要删除的数据')
return
}
this.$modal.confirm('是否确认删除资源编号为"' + ids + '"的数据项?').then(function() {
return delResource(ids)
}).then(() => {
this.getList()
this.$modal.msgSuccess('删除成功')
}).catch(() => {})
},
// 资源文件访问地址
fileUrl(url) {
if (!url) {
return ''
}
if (/^(http|https):\/\//.test(url)) {
return url
}
return process.env.VUE_APP_BASE_API + url
},
// 资源文件名
fileName(url) {
if (!url) {
return ''
}
return url.substring(url.lastIndexOf('/') + 1)
}
}
}
</script>
+38 -3
View File
@@ -23,6 +23,14 @@
<el-option label="停用" :value="0" />
</el-select>
</el-form-item>
<el-form-item label="适用专业" prop="major">
<el-input
v-model="queryParams.major"
placeholder="请输入适用专业"
clearable
@keyup.enter.native="handleQuery"
/>
</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>
@@ -63,6 +71,7 @@
<el-tag type="info">{{ scope.row.capacity }} 人</el-tag>
</template>
</el-table-column>
<el-table-column label="适用专业" align="center" prop="major" :show-overflow-tooltip="true" />
<el-table-column label="配套设备描述" align="center" prop="siteDevice" :show-overflow-tooltip="true" />
<el-table-column label="场地状态" align="center" prop="siteStatus">
<template slot-scope="scope">
@@ -70,7 +79,7 @@
<el-tag v-else type="danger">停用</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
<!-- <el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />-->
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
@@ -80,6 +89,13 @@
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
>修改</el-button>
<el-button
v-hasPermi="['train:site:edit']"
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -104,6 +120,9 @@
<el-form-item label="最大容纳人数" prop="capacity">
<el-input-number v-model="form.capacity" :min="1" :max="10000" placeholder="最大容纳人数" style="width: 100%" />
</el-form-item>
<el-form-item label="适用专业" prop="major">
<el-input v-model="form.major" placeholder="请输入适用专业 (如 铁道供电技术)" maxlength="100" />
</el-form-item>
<el-form-item label="场地配套设备" prop="siteDevice">
<el-input v-model="form.siteDevice" type="textarea" placeholder="例如:VR头显12套、3D数字孪生演练台等" :rows="3" />
</el-form-item>
@@ -126,7 +145,7 @@
</template>
<script>
import { listSite, getSite, addSite, updateSite } from '@/api/train/site'
import { listSite, getSite, addSite, updateSite, delSite } from '@/api/train/site'
export default {
name: 'TrainingSite',
@@ -157,7 +176,8 @@ export default {
siteName: null,
siteLocation: null,
capacity: null,
siteStatus: null
siteStatus: null,
major: null
},
// 表单参数
form: {},
@@ -211,6 +231,7 @@ export default {
capacity: 30,
siteDevice: null,
siteStatus: 1,
major: null,
remark: null
}
this.resetForm('form')
@@ -231,6 +252,20 @@ export default {
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 删除按钮操作 */
handleDelete(row) {
const ids = row.siteId || this.ids
if (!ids || (Array.isArray(ids) && ids.length === 0)) {
this.$modal.msgWarning('请选择要删除的数据')
return
}
this.$modal.confirm('是否确认删除场地编号为"' + ids + '"的数据项?').then(function() {
return delSite(ids)
}).then(() => {
this.getList()
this.$modal.msgSuccess('删除成功')
}).catch(() => {})
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
+278 -14
View File
@@ -16,13 +16,14 @@
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="所属单位" prop="orgName">
<el-input
<el-form-item label="所属部门" prop="orgName">
<!-- <el-input
v-model="queryParams.orgName"
placeholder="请输入所属单位"
clearable
@keyup.enter.native="handleQuery"
/>
/>-->
<treeselect style="width: 210px;" v-model="queryParams.orgName" :options="deptOptions" :normalizer="normalizer" :show-count="true" placeholder="请选择归属部门" />
</el-form-item>
<el-form-item label="联系电话" prop="sPhone">
<el-input
@@ -59,6 +60,20 @@
</el-row>
<el-table v-loading="loading" :data="studentList">
<el-table-column label="头像" align="center" width="80">
<template slot-scope="scope">
<el-image
:src="avatarUrl(scope.row.avatar)"
:preview-src-list="[avatarUrl(scope.row.avatar)]"
fit="cover"
class="student-avatar"
>
<div slot="error" class="image-slot">
<i class="el-icon-picture-outline" />
</div>
</el-image>
</template>
</el-table-column>
<el-table-column label="学员姓名" align="center" prop="sName" min-width="110" />
<el-table-column label="性别" align="center" prop="sGender" width="70">
<template slot-scope="scope">
@@ -71,6 +86,7 @@
</template>
</el-table-column>
<el-table-column label="所属单位" align="center" prop="orgName" min-width="140" show-overflow-tooltip />
<el-table-column label="联系电话" align="center" prop="sPhone" width="130" />
<el-table-column label="邮箱" align="center" prop="sEmail" min-width="160" show-overflow-tooltip />
<el-table-column label="身份证号" align="center" prop="idCardMasked" min-width="190" />
@@ -98,7 +114,7 @@
<el-tag v-else type="info" size="mini">未关联</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="210">
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="280">
<template slot-scope="scope">
<el-button
v-hasPermi="['train:student:query']"
@@ -107,6 +123,13 @@
icon="el-icon-view"
@click="handleDetail(scope.row)"
>详情</el-button>
<el-button
v-hasPermi="['train:student:edit']"
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
>修改</el-button>
<el-button
v-if="!scope.row.userName && !scope.row.userId"
v-hasPermi="['train:student:bindUser']"
@@ -144,16 +167,27 @@
@pagination="getList"
/>
<el-dialog title="新增学员主档" :visible.sync="createOpen" width="560px" append-to-body @closed="resetCreate">
<el-dialog :title="createTitle" :visible.sync="createOpen" width="560px" append-to-body @closed="resetCreate">
<el-form ref="createForm" :model="createForm" :rules="rules" label-width="90px">
<el-form-item label="学员头像" prop="avatar">
<studentAvatar :user="{ avatar: createForm.avatar }" @success="handleAvatarSuccess" />
</el-form-item>
<el-form-item label="学员姓名" prop="sName">
<el-input v-model="createForm.sName" placeholder="请输入学员姓名" maxlength="50" show-word-limit />
</el-form-item>
<el-form-item label="性别" prop="sGender">
<el-radio-group v-model="createForm.sGender">
<!-- <el-radio-group v-model="createForm.sGender">
<el-radio label="男">男</el-radio>
<el-radio label="女">女</el-radio>
</el-radio-group>
</el-radio-group>-->
<el-select v-model="createForm.sGender" placeholder="请选择性别">
<el-option
v-for="dict in dict.type.sys_user_sex"
:key="dict.value"
:label="dict.label"
:value="dict.value"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="出生日期" prop="sBirth">
<el-date-picker
@@ -165,7 +199,18 @@
/>
</el-form-item>
<el-form-item label="所属单位" prop="orgName">
<el-input v-model="createForm.orgName" placeholder="请输入所属单位/部门" maxlength="100" />
<!-- <el-input v-model="createForm.orgName" placeholder="请输入所属单位/部门" maxlength="100" />-->
<treeselect v-model="createForm.orgName" :options="deptOptions" :normalizer="normalizer" :show-count="true" placeholder="请选择归属部门" />
</el-form-item>
<el-form-item label="岗位级别" prop="dutyLevel">
<el-select v-model="createForm.dutyLevel" placeholder="请选择岗位级别" clearable style="width: 100%">
<el-option
v-for="dict in dutyLevelOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="联系电话" prop="sPhone">
<el-input v-model="createForm.sPhone" placeholder="请输入联系电话" maxlength="20" />
@@ -197,6 +242,16 @@
<el-dialog title="学员主档详情" :visible.sync="detailOpen" width="560px" append-to-body>
<el-form :model="detail" label-width="110px" class="student-detail">
<el-form-item label="学员头像">
<el-image
v-if="detail.avatar"
:src="avatarUrl(detail.avatar)"
:preview-src-list="[avatarUrl(detail.avatar)]"
fit="cover"
class="student-avatar"
/>
<span v-else>-</span>
</el-form-item>
<el-form-item label="学员姓名"><span>{{ detail.sName || '-' }}</span></el-form-item>
<el-form-item label="系统编号"><span>{{ detail.sId || '-' }}</span></el-form-item>
<el-form-item label="关联系统账号">
@@ -204,6 +259,10 @@
<el-tag v-else type="info" size="mini">未关联</el-tag>
</el-form-item>
<el-form-item label="所属单位"><span>{{ detail.orgName || '-' }}</span></el-form-item>
<el-form-item label="岗位级别">
<dict-tag v-if="detail.dutyLevel !== null && detail.dutyLevel !== undefined" :options="dict.type.train_duty_level" :value="detail.dutyLevel" />
<span v-else>-</span>
</el-form-item>
<el-form-item label="联系电话"><span>{{ detail.sPhone || '-' }}</span></el-form-item>
<el-form-item label="邮箱"><span>{{ detail.sEmail || '-' }}</span></el-form-item>
<el-form-item label="身份证号"><span>{{ detail.idCardMasked || '-' }}</span></el-form-item>
@@ -275,10 +334,19 @@
</template>
<script>
import { addStudent, getStudent, listStudent, delStudent, getStudentBindableUsers, bindStudentUser, unbindStudentUser } from '@/api/train/student'
import request from '@/utils/request'
import { addStudent, getStudent, listStudent, delStudent, getStudentBindableUsers, bindStudentUser, unbindStudentUser, updateStudent } from '@/api/train/student'
import {deptTreeSelect} from "@/api/system/user";
import Treeselect from "@riophae/vue-treeselect";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
import {listDept} from "@/api/system/dept";
import studentAvatar from "./studentAvatar.vue";
import {addTeacher, updateTeacher} from "@/api/train/teacher";
export default {
name: 'Student',
components: { studentAvatar, Treeselect },
// 岗位级别字典(train_duty_level),注入后可通过 dict.type.train_duty_level 访问
dicts: ['train_duty_level', 'sys_user_sex'],
data() {
const validateIdCard = (rule, value, callback) => {
if (!value || value.trim() === '') {
@@ -298,6 +366,9 @@ export default {
total: 0,
studentList: [],
createOpen: false,
createTitle: '新增学员主档',
// 弹窗是否为修改模式(true=修改,false=新增),用于复用同一弹窗
isEdit: false,
detailOpen: false,
detail: {},
// 账号关联弹窗
@@ -316,7 +387,12 @@ export default {
sPhone: null,
status: null
},
// 部门树选项
deptOptions: undefined,
deptArr: {},
createForm: {},
// 头像上传组件用的学员对象(avatar 为入库的相对地址)
user: { avatar: '' },
rules: {
sName: [
{ required: true, message: '学员姓名不能为空', trigger: 'blur' },
@@ -331,9 +407,27 @@ export default {
}
}
},
computed: {
/*
* 岗位级别下拉选项:取自字典 train_duty_level。
* 后端 duty_level 为数值型,字典 dictValue 为字符串,
* 此处将纯数字的字典值转为 Number,保证下拉回显时类型一致。
*/
dutyLevelOptions() {
const list = (this.dict && this.dict.type && this.dict.type.train_duty_level) || []
return list.map(item => ({
label: item.label,
value: /^\d+$/.test(String(item.value)) ? Number(item.value) : item.value
}))
}
},
created() {
this.resetCreate()
this.mapDept()
this.getList()
this.getDeptTree()
},
methods: {
getList() {
@@ -341,10 +435,53 @@ export default {
listStudent(this.queryParams).then(response => {
this.studentList = response.rows
this.total = response.total
this.studentList.forEach(stud => {
let studOrgName = this.formatOrgName(stud.orgName);
//console.log(stud.orgName +"--"+ studOrgName);
this.$set(stud, "orgName", studOrgName);
});
}).finally(() => {
this.loading = false
})
},
/** 查询部门下拉树结构 */
getDeptTree() {
deptTreeSelect().then(response => {
this.deptOptions = response.data;
});
},
// 筛选节点
filterNode(value, data) {
if (!value) return true;
return data.label.indexOf(value) !== -1;
},
// 节点单击事件
handleNodeClick(data) {
this.createForm.orgName = data.id;
this.handleQuery();
},
// 数据格式化函数,把后端返回部门树形转成treeselect需要格式
normalizer(node) {
return {
id: node.id,
label: node.label,
children: node.children
}
},
mapDept() {
listDept().then(response => {
let deptList = response.data || [];
let deptArr = {};
deptList.forEach(dept => {
if (dept && dept.deptId !== undefined && dept.deptId !== null) {
deptArr[dept.deptId] = dept.deptName;
}
});
this.deptArr = deptArr;
});
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
@@ -355,42 +492,140 @@ export default {
},
handleAdd() {
this.resetCreate()
this.isEdit = false
this.createTitle = '新增学员主档'
this.createOpen = true
},
/** 修改按钮:复用新增弹窗,回填学员信息 */
handleUpdate(row) {
this.resetCreate()
this.isEdit = true
this.createTitle = '修改学员主档'
// 身份证号为脱敏字段,不回显;留空表示不修改
this.createForm = {
sId: row.sId,
avatar: row.avatar || null,
sName: row.sName || null,
sGender: row.sGender || '男',
sBirth: row.sBirth || null,
orgName: this.toDeptValue(row.orgName),
dutyLevel: row.dutyLevel !== undefined && row.dutyLevel !== null ? this.toDutyLevelValue(row.dutyLevel) : null,
sPhone: row.sPhone || null,
sEmail: row.sEmail || null,
idCard: null,
faceRef: row.faceRef || null,
remark: row.remark || null
}
// 同步头像组件预览
this.user = { avatar: this.createForm.avatar || '' }
this.createOpen = true
},
/** 岗位级别值归一化:数字字符串转 Number,保证与下拉选项 value 类型一致 */
toDutyLevelValue(value) {
if (value === null || value === undefined || value === '') {
return null
}
return /^\d+$/.test(String(value)) ? Number(value) : value
},
/** 部门值归一化:数字字符串转 Number,保证与部门树选项 id 类型一致 */
toDeptValue(value) {
if (value === null || value === undefined || value === '') {
return null
}
return /^\d+$/.test(String(value)) ? Number(value) : value
},
resetCreate() {
this.createForm = {
avatar: null,
sName: null,
sGender: '男',
sBirth: null,
orgName: null,
dutyLevel: null,
sPhone: null,
sEmail: null,
idCard: null,
faceRef: null,
remark: null
}
// 重置头像组件预览
this.user = { avatar: '' }
this.$set(this.createForm, "avatar", null);
// 还原为新增模式
this.isEdit = false
this.createTitle = '新增学员主档'
if (this.$refs.createForm) {
this.resetForm('createForm')
}
},
// 接收子组件上传成功抛出的头像地址:写入新增表单,随其他学员信息一并提交入库
handleAvatarSuccess(imgUrl) {
if (!imgUrl) {
return
}
this.$set(this.createForm, 'avatar', imgUrl)
// 校验通过后清除该项的校验提示
if (this.$refs.createForm) {
this.$refs.createForm.clearValidate('avatar')
}
},
// 头像地址解析:补全 baseApi,空值使用默认头像
avatarUrl(avatar) {
if (!avatar) {
return require('@/assets/images/profile.jpg')
}
if (avatar.indexOf('http') === 0 || avatar.indexOf('data:') === 0) {
return avatar
}
return process.env.VUE_APP_BASE_API + avatar
},
submitCreate() {
this.$refs.createForm.validate(valid => {
/*this.$refs.createForm.validate(valid => {
if (!valid) {
return
}
this.submitting = true
addStudent(this.createForm).then(() => {
this.$modal.msgSuccess('新增成功')
// 头像地址由子组件上传后写入 createForm.avatar,与其他学员信息一并提交
const payload = Object.assign({}, this.createForm, {
avatar: this.createForm.avatar || (this.user && this.user.avatar) || null
})
// 修改模式调用 PUT /train/student,新增模式调用 POST /train/student
const submitRequest = this.isEdit
? request({ url: '/train/student', method: 'put', data: payload })
: addStudent(payload)
submitRequest.then(() => {
this.$modal.msgSuccess(this.isEdit ? '修改成功' : '新增成功')
this.createOpen = false
this.resetCreate();
this.getList()
}).finally(() => {
this.submitting = false
})
})*/
this.$refs.createForm.validate(valid => {
if (valid) {
if (this.createForm.sId != null) {
updateStudent(this.createForm).then(response => {
this.$modal.msgSuccess('修改成功')
this.createOpen = false
this.getList()
})
} else {
addStudent(this.createForm).then(response => {
this.$modal.msgSuccess('新增成功')
this.createOpen = false
this.getList()
})
}
}
})
},
handleDetail(row) {
getStudent(row.sId).then(response => {
this.detail = response.data || {}
//console.log(this.deptArr);
this.$set(this.detail, "orgName", this.formatOrgName(this.detail.orgName));
this.detailOpen = true
})
},
@@ -467,6 +702,17 @@ export default {
}
return labels[sourceType] || '-'
},
// 所属单位:orgName 为数字或 deptArr 中存在映射时取 deptArr 对应值,否则显示 orgName 本身
formatOrgName(orgName) {
if (orgName === null || orgName === undefined || orgName === '') return '-'
const map = this.deptArr || {}
const mapped = map[orgName]
const isNumber = /^\d+$/.test(String(orgName))
if (isNumber || mapped !== undefined) {
return (mapped !== undefined && mapped !== null && mapped !== '') ? mapped : orgName
}
return orgName
},
formatGender(gender) {
if (gender === '0' || gender === 0 || gender === '男') return '男'
if (gender === '1' || gender === 1 || gender === '女') return '女'
@@ -476,8 +722,26 @@ export default {
}
</script>
<style scoped>
<style scoped lang="scss">
.student-detail ::v-deep .el-form-item {
margin-bottom: 8px;
}
.student-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
vertical-align: middle;
background: #f5f7fa;
}
.student-avatar .image-slot {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: #c0c4cc;
font-size: 18px;
}
</style>
@@ -0,0 +1,233 @@
<template>
<div>
<div class="user-info-head" @click="editCropper()"><img :src="options.img" title="点击上传头像" class="img-circle img-lg" @error="handleAvatarError"></div>
<el-dialog :title="title" :visible.sync="open" width="800px" append-to-body @opened="modalOpened" @close="closeDialog">
<el-row>
<el-col :xs="24" :md="12" :style="{height: '350px'}">
<vue-cropper
v-if="visible"
ref="cropper"
:img="options.img"
:info="true"
:auto-crop="options.autoCrop"
:auto-crop-width="options.autoCropWidth"
:auto-crop-height="options.autoCropHeight"
:fixed-box="options.fixedBox"
:output-type="options.outputType"
@realTime="realTime"
/>
</el-col>
<el-col :xs="24" :md="12" :style="{height: '350px'}">
<div class="avatar-upload-preview">
<img :src="previews.url" :style="previews.img">
</div>
</el-col>
</el-row>
<br>
<el-row>
<el-col :lg="2" :sm="3" :xs="3">
<el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload">
<el-button size="small">
选择
<i class="el-icon-upload el-icon--right" />
</el-button>
</el-upload>
</el-col>
<el-col :lg="{span: 1, offset: 2}" :sm="2" :xs="2">
<el-button icon="el-icon-plus" size="small" @click="changeScale(1)" />
</el-col>
<el-col :lg="{span: 1, offset: 1}" :sm="2" :xs="2">
<el-button icon="el-icon-minus" size="small" @click="changeScale(-1)" />
</el-col>
<el-col :lg="{span: 1, offset: 1}" :sm="2" :xs="2">
<el-button icon="el-icon-refresh-left" size="small" @click="rotateLeft()" />
</el-col>
<el-col :lg="{span: 1, offset: 1}" :sm="2" :xs="2">
<el-button icon="el-icon-refresh-right" size="small" @click="rotateRight()" />
</el-col>
<el-col :lg="{span: 2, offset: 6}" :sm="2" :xs="2">
<el-button type="primary" size="small" @click="uploadImg()">提 交</el-button>
</el-col>
</el-row>
</el-dialog>
</div>
</template>
<script>
import { VueCropper } from 'vue-cropper'
import request from '@/utils/request'
import { debounce } from '@/utils'
// 默认头像
const DEFAULT_AVATAR = require('@/assets/images/profile.jpg')
export default {
components: { VueCropper },
props: {
user: {
type: Object,
default: () => ({})
}
},
data() {
return {
// 是否显示弹出层
open: false,
// 是否显示cropper
visible: false,
// 弹出层标题
title: '修改头像',
options: {
img: DEFAULT_AVATAR, // 裁剪图片的地址
autoCrop: true, // 是否默认生成截图框
autoCropWidth: 200, // 默认生成截图框宽度
autoCropHeight: 200, // 默认生成截图框高度
fixedBox: true, // 固定截图框大小 不允许改变
outputType: 'png' // 默认生成截图为PNG格式
},
previews: {},
resizeHandler: null,
// 当前生效的头像地址(本地上传成功后优先于此值,避免被关闭弹窗重置)
currentAvatar: ''
}
},
watch: {
// 跟随传入学员的头像地址变化
'user.avatar': {
immediate: true,
handler(val) {
this.currentAvatar = val || ''
this.options.img = this.resolveAvatar(val)
}
}
},
methods: {
// 头像地址解析:相对路径补全 baseApi,空值使用默认头像
// withStamp=true 时追加时间戳,强制浏览器重新加载,避免缓存导致图片不刷新
resolveAvatar(avatar, withStamp) {
if (!avatar) {
return DEFAULT_AVATAR
}
if (avatar.indexOf('http') === 0 || avatar.indexOf('data:') === 0) {
return avatar
}
let url = process.env.VUE_APP_BASE_API + avatar
if (withStamp) {
url += (url.indexOf('?') === -1 ? '?' : '&') + 't=' + new Date().getTime()
}
return url
},
handleAvatarError() {
this.options.img = DEFAULT_AVATAR
},
// 编辑头像
editCropper() {
this.open = true
},
// 打开弹出层结束时的回调
modalOpened() {
this.visible = true
if (!this.resizeHandler) {
this.resizeHandler = debounce(() => {
this.refresh()
}, 100)
}
window.addEventListener('resize', this.resizeHandler)
},
// 刷新组件
refresh() {
this.$refs.cropper.refresh()
},
// 覆盖默认的上传行为
requestUpload() {
},
// 向左旋转
rotateLeft() {
this.$refs.cropper.rotateLeft()
},
// 向右旋转
rotateRight() {
this.$refs.cropper.rotateRight()
},
// 图片缩放
changeScale(num) {
num = num || 1
this.$refs.cropper.changeScale(num)
},
// 上传预处理
beforeUpload(file) {
if (file.type.indexOf('image/') === -1) {
this.$modal.msgError('文件格式错误,请上传图片类型,如:JPG,PNG后缀的文件。')
} else {
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => {
this.options.img = reader.result
}
}
},
// 上传图片:走通用上传 /common/upload,只存文件不修改 sys_user.avatar
uploadImg() {
this.$refs.cropper.getCropBlob(data => {
const formData = new FormData()
// 使用带文件名的 File,保证后端能识别后缀
const uploadFile = new File([data], 'avatar.png', { type: 'image/png' })
formData.append('file', uploadFile)
request({
url: '/common/upload',
method: 'post',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
}).then(response => {
// 入库用相对路径,如 /profile/upload/2026/09/28/xxx.png
const imgUrl = response.fileName || response.url
// 记录当前生效头像,防止弹窗关闭时被旧值覆盖
this.currentAvatar = imgUrl
// 追加时间戳强制刷新,避免浏览器缓存
this.options.img = this.resolveAvatar(imgUrl, true)
// 先抛给父组件更新 user.avatar,再关闭弹窗
this.$emit('success', imgUrl)
this.$modal.msgSuccess('上传成功')
this.open = false
this.visible = false
})
})
},
// 实时预览
realTime(data) {
this.previews = data
},
// 关闭窗口:优先使用本地已上传的头像,其次取父组件传入的 user.avatar
closeDialog() {
this.options.img = this.resolveAvatar(this.currentAvatar || (this.user && this.user.avatar))
this.visible = false
window.removeEventListener('resize', this.resizeHandler)
}
}
}
</script>
<style scoped lang="scss">
.user-info-head {
position: relative;
display: inline-block;
height: 120px;
}
.user-info-head:hover:after {
content: '+';
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
color: #eee;
background: rgba(0, 0, 0, 0.5);
font-size: 24px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
cursor: pointer;
line-height: 110px;
border-radius: 50%;
}
</style>
+187 -13
View File
@@ -10,12 +10,13 @@
/>
</el-form-item>
<el-form-item label="所属单位" prop="orgName">
<el-input
<!-- <el-input
v-model="queryParams.orgName"
placeholder="请输入所属单位"
clearable
@keyup.enter.native="handleQuery"
/>
/>-->
<treeselect style="width: 210px;" v-model="queryParams.orgName" :options="deptOptions" :normalizer="normalizer" :show-count="true" placeholder="请选择归属部门" />
</el-form-item>
<el-form-item label="职务职称" prop="jobTitle">
<el-input
@@ -25,14 +26,30 @@
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="联系电话" prop="tPhone">
<el-form-item label="专业方向" prop="major">
<el-input
v-model="queryParams.major"
placeholder="请输入专业方向"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="主讲课程" prop="courses">
<el-input
v-model="queryParams.courses"
placeholder="请输入主讲课程"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<!-- <el-form-item label="联系电话" prop="tPhone">
<el-input
v-model="queryParams.tPhone"
placeholder="请输入联系电话"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
</el-form-item>-->
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="教师状态" clearable>
<el-option label="正常" :value="1" />
@@ -72,21 +89,37 @@
<el-table v-loading="loading" :data="teacherList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="头像" align="center" width="80">
<template slot-scope="scope">
<el-image
:src="avatarUrl(scope.row.avatar)"
:preview-src-list="[avatarUrl(scope.row.avatar)]"
fit="cover"
class="teacher-avatar"
>
<div slot="error" class="image-slot">
<i class="el-icon-picture-outline" />
</div>
</el-image>
</template>
</el-table-column>
<el-table-column label="教师姓名" align="center" prop="tName" min-width="120" />
<el-table-column label="性别" align="center" prop="tGender" width="80">
<template slot-scope="scope">
<span>{{ formatGender(scope.row.tGender) }}</span>
</template>
</el-table-column>
<el-table-column label="出生日期" align="center" prop="tBirth" width="120">
<!-- <el-table-column label="出生日期" align="center" prop="tBirth" width="120">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.tBirth, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
</el-table-column>-->
<el-table-column label="所属单位" align="center" prop="orgName" :show-overflow-tooltip="true" />
<el-table-column label="职务/职称" align="center" prop="jobTitle" :show-overflow-tooltip="true" />
<el-table-column label="联系电话" align="center" prop="tPhone" width="130" />
<el-table-column label="邮箱" align="center" prop="tEmail" :show-overflow-tooltip="true" />
<el-table-column label="专业方向" align="center" prop="major" :show-overflow-tooltip="true" />
<el-table-column label="主讲课程" align="center" prop="courses" :show-overflow-tooltip="true" />
<!-- <el-table-column label="联系电话" align="center" prop="tPhone" width="130" />
<el-table-column label="邮箱" align="center" prop="tEmail" :show-overflow-tooltip="true" />-->
<el-table-column label="状态" align="center" prop="status" width="90">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
@@ -106,7 +139,7 @@
<el-tag v-else type="info" size="mini">未关联</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
<!-- <el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />-->
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="210">
<template slot-scope="scope">
<el-button
@@ -154,8 +187,11 @@
/>
<!-- 添加或修改教师信息对话框 -->
<el-dialog :title="title" :visible.sync="open" width="550px" append-to-body>
<el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="90px">
<el-form-item label="教师头像" prop="avatar">
<studentAvatar :user="user" @success="handleAvatarSuccess" />
</el-form-item>
<el-form-item label="教师姓名" prop="tName">
<el-input v-model="form.tName" placeholder="请输入教师姓名" maxlength="50" show-word-limit />
</el-form-item>
@@ -175,11 +211,27 @@
/>
</el-form-item>
<el-form-item label="所属单位" prop="orgName">
<el-input v-model="form.orgName" placeholder="请输入所属单位/部门" maxlength="100" />
<!-- <el-input v-model="form.orgName" placeholder="请输入所属单位/部门" maxlength="100" />-->
<treeselect v-model="form.orgName" :options="deptOptions" :normalizer="normalizer" :show-count="true" placeholder="请选择所属单位" />
</el-form-item>
<el-form-item label="职务/职称" prop="jobTitle">
<el-input v-model="form.jobTitle" placeholder="请输入职务/职称" maxlength="50" />
</el-form-item>
<el-form-item label="专业方向" prop="major">
<el-input v-model="form.major" placeholder="请输入专业方向" maxlength="100" />
</el-form-item>
<el-form-item label="主讲课程" prop="courses">
<el-input v-model="form.courses" placeholder="请输入主讲课程,多门课程可用逗号分隔" maxlength="200" />
</el-form-item>
<el-form-item label="从教时间" prop="startTeaching">
<el-date-picker
v-model="form.startTeaching"
clearable
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择从教时间"
/>
</el-form-item>
<el-form-item label="联系电话" prop="tPhone">
<el-input v-model="form.tPhone" placeholder="请输入11位手机号码" maxlength="20" />
</el-form-item>
@@ -266,9 +318,15 @@
<script>
import { listTeacher, getTeacher, delTeacher, addTeacher, updateTeacher, getTeacherBindableUsers, bindTeacherUser, unbindTeacherUser } from '@/api/train/teacher'
import {deptTreeSelect} from "@/api/system/user";
import Treeselect from "@riophae/vue-treeselect";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
import {listDept} from "@/api/system/dept";
// 复用学员头像上传组件(通用:props.user 传入 {avatar},上传成功后抛出 success 事件)
import studentAvatar from '@/views/train/student/studentAvatar'
export default {
name: 'Teacher',
components: { Treeselect, studentAvatar },
data() {
return {
// 遮罩层
@@ -305,7 +363,11 @@ export default {
tPhone: null,
status: null
},
// 表单参数
// 部门树选项
deptOptions: undefined,
deptArr: {},
// 头像上传组件用的教师对象(avatar 为入库的相对地址)
user: { avatar: '' },
form: {},
// 表单校验
rules: {
@@ -318,12 +380,20 @@ export default {
],
tEmail: [
{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }
],
major: [
{ max: 100, message: '专业方向长度不能超过100个字符', trigger: 'blur' }
],
courses: [
{ max: 200, message: '主讲课程长度不能超过200个字符', trigger: 'blur' }
]
}
}
},
created() {
this.mapDept()
this.getList()
this.getDeptTree()
},
methods: {
/** 查询教师信息列表 */
@@ -332,9 +402,52 @@ export default {
listTeacher(this.queryParams).then(response => {
this.teacherList = response.rows
this.total = response.total
this.teacherList.forEach(stud => {
let studOrgName = this.formatOrgName(stud.orgName);
//console.log(stud.orgName +"--"+ studOrgName);
this.$set(stud, "orgName", studOrgName);
});
}).finally(() => {
this.loading = false
})
},
/** 查询部门下拉树结构 */
getDeptTree() {
deptTreeSelect().then(response => {
this.deptOptions = response.data;
});
},
// 筛选节点
filterNode(value, data) {
if (!value) return true;
return data.label.indexOf(value) !== -1;
},
// 节点单击事件
handleNodeClick(data) {
this.createForm.orgName = data.id;
this.handleQuery();
},
// 数据格式化函数,把后端返回部门树形转成treeselect需要格式
normalizer(node) {
return {
id: node.id,
label: node.label,
children: node.children
}
},
mapDept() {
listDept().then(response => {
let deptList = response.data || [];
let deptArr = {};
deptList.forEach(dept => {
if (dept && dept.deptId !== undefined && dept.deptId !== null) {
deptArr[dept.deptId] = dept.deptName;
}
});
this.deptArr = deptArr;
});
},
// 取消按钮
cancel() {
this.open = false
@@ -344,18 +457,46 @@ export default {
reset() {
this.form = {
tId: null,
avatar: null,
tName: null,
tGender: '男',
tBirth: null,
orgName: null,
jobTitle: null,
major: null,
courses: null,
startTeaching: null,
tPhone: null,
tEmail: null,
status: 1,
remark: null
}
// 重置头像组件预览
this.user = { avatar: '' }
this.resetForm('form')
},
/** 头像上传成功回调:相对地址写入表单,随教师信息一并提交 */
handleAvatarSuccess(imgUrl) {
if (!imgUrl) {
return
}
this.$set(this.form, 'avatar', imgUrl)
// 同步更新头像组件的预览
this.user = Object.assign({}, this.user, { avatar: imgUrl })
if (this.$refs.form) {
this.$refs.form.clearValidate('avatar')
}
},
/** 头像地址解析:补全 baseApi,空值使用默认头像 */
avatarUrl(avatar) {
if (!avatar) {
return require('@/assets/images/profile.jpg')
}
if (avatar.indexOf('http') === 0 || avatar.indexOf('data:') === 0) {
return avatar
}
return process.env.VUE_APP_BASE_API + avatar
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
@@ -383,6 +524,8 @@ export default {
const tId = row.tId || this.ids[0]
getTeacher(tId).then(response => {
this.form = response.data
// 回填头像预览
this.user = { avatar: (this.form && this.form.avatar) || '' }
this.open = true
this.title = '修改教师信息'
})
@@ -472,6 +615,17 @@ export default {
this.$modal.msgSuccess("删除成功");
}).catch(() => {});
},
// 所属单位:orgName 为数字或 deptArr 中存在映射时取 deptArr 对应值,否则显示 orgName 本身
formatOrgName(orgName) {
if (orgName === null || orgName === undefined || orgName === '') return '-'
const map = this.deptArr || {}
const mapped = map[orgName]
const isNumber = /^\d+$/.test(String(orgName))
if (isNumber || mapped !== undefined) {
return (mapped !== undefined && mapped !== null && mapped !== '') ? mapped : orgName
}
return orgName
},
formatGender(gender) {
if (gender === '0' || gender === 0 || gender === '男') return '男'
if (gender === '1' || gender === 1 || gender === '女') return '女'
@@ -480,3 +634,23 @@ export default {
}
}
</script>
<style scoped lang="scss">
.teacher-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
vertical-align: middle;
background: #f5f7fa;
}
.teacher-avatar .image-slot {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: #c0c4cc;
font-size: 18px;
}
</style>