This commit is contained in:
zlwu committed 2025-08-13 22:16:46 +08:00
commit 14d53f60b0
23 files changed
+4260 -72

No files matched your search

@@ -163,9 +163,9 @@ mybatis-plus:
mapperLocations: classpath*:mapper/**/*Mapper.xml
# 实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage: org.dromara.**.domain
# 临时禁用SQL日志(减少系统负担)
# configuration:
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 启用SQL日志输出,用于调试数据库插入问题
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
dbConfig:
# 主键类型
+6
View File
@@ -104,6 +104,12 @@
<artifactId>ruoyi-workflow</artifactId>
</dependency>
<!-- 学工管理模块 - 用于获取学生基础信息 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-xg</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,101 @@
package org.dromara.zsjy.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.zsjy.domain.bo.ZsjyInternshipDailyAddBo;
import org.dromara.zsjy.domain.bo.ZsjyInternshipDailyQueryBo;
import org.dromara.zsjy.domain.vo.ZsjyInternshipDailyVo;
import org.dromara.zsjy.service.IZsjyInternshipDailyService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 实习日常管理Controller
*
* @author system
* @date 2025-01-03
*/
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/zsjy/internship/daily")
@Tag(name = "实习日常管理", description = "学生实习日常打卡签到管理")
public class ZsjyInternshipDailyController extends BaseController {
private final IZsjyInternshipDailyService dailyService;
/**
* 查询实习日常记录列表
*/
@Operation(summary = "查询实习日常记录列表", description = "班主任查看学生实习日常记录")
@SaCheckPermission("zsjy:internshipdaily:list")
@GetMapping("/list")
public TableDataInfo<ZsjyInternshipDailyVo> list(@Validated ZsjyInternshipDailyQueryBo queryBo, PageQuery pageQuery) {
log.info("=== Controller层调试 ===");
log.info("接口被调用: /zsjy/internship/daily/list");
log.info("查询参数: {}", queryBo);
log.info("分页参数: {}", pageQuery);
return dailyService.queryPageList(queryBo, pageQuery);
}
/**
* 学生签到打卡
*/
@Operation(summary = "学生签到打卡", description = "学生进行实习日常签到打卡")
@SaCheckPermission("zsjy:internshipdaily:checkin")
@PostMapping("/checkin")
public R<Void> checkIn(@Validated(AddGroup.class) @RequestBody ZsjyInternshipDailyAddBo addBo) {
log.info("=== Controller层调试 ===");
log.info("接口被调用: /zsjy/internship/daily/checkin");
log.info("签到参数: {}", addBo);
return dailyService.checkIn(addBo);
}
/**
* 获取学生考勤日历
*/
@Operation(summary = "获取学生考勤日历", description = "查看指定学生的考勤日历数据")
@SaCheckPermission("zsjy:internshipdaily:calendar")
@GetMapping("/calendar/{studentId}")
public R<List<Map<String, Object>>> getCalendar(
@Parameter(description = "学生ID", required = true) @PathVariable Long studentId,
@Parameter(description = "年份") @RequestParam(required = false) Integer year,
@Parameter(description = "月份") @RequestParam(required = false) Integer month) {
log.info("=== Controller层调试 ===");
log.info("接口被调用: /zsjy/internship/daily/calendar/{}", studentId);
log.info("查询年月: {}-{}", year, month);
return dailyService.getCalendarData(studentId, year, month);
}
/**
* 获取考勤统计数据
*/
@Operation(summary = "获取考勤统计数据", description = "查看实习生考勤统计分析")
@SaCheckPermission("zsjy:internshipdaily:statistics")
@GetMapping("/statistics")
public R<Map<String, Object>> getStatistics(@Validated ZsjyInternshipDailyQueryBo queryBo) {
log.info("=== Controller层调试 ===");
log.info("接口被调用: /zsjy/internship/daily/statistics");
log.info("统计参数: {}", queryBo);
return dailyService.getStatistics(queryBo);
}
}
@@ -0,0 +1,99 @@
package org.dromara.zsjy.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import java.io.Serial;
import java.math.BigDecimal;
import java.sql.Time;
import java.util.Date;
/**
* 实习日常管理实体对象 zsjy_internship_daily
*
* @author system
* @date 2025-01-03
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("zsjy_internship_daily")
public class ZsjyInternshipDaily extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 日常记录ID
*/
@TableId(value = "daily_id")
private Long dailyId;
/**
* 实习ID
*/
private Long internshipId;
/**
* 学生ID
*/
private Long studentId;
/**
* 打卡日期
*/
private Date checkDate;
/**
* 签到时间
*/
private Time checkInTime;
/**
* 签到位置
*/
private String checkInLocation;
/**
* 纬度
*/
private BigDecimal latitude;
/**
* 经度
*/
private BigDecimal longitude;
/**
* 日总结
*/
private String dailySummary;
/**
* 图片附件(JSON格式)
*/
private String images;
/**
* 是否异常(0正常 1异常)
*/
private String isException;
/**
* 异常原因
*/
private String exceptionReason;
/**
* 异常处理状态(0待处理 1已处理)
*/
private String exceptionStatus;
/**
* 状态(0正常 1停用)
*/
private String status;
}
@@ -0,0 +1,163 @@
package org.dromara.zsjy.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import java.io.Serial;
import java.math.BigDecimal;
import java.util.Date;
/**
* 实习信息实体对象 zsjy_internship_info
*
* @author system
* @date 2025-01-03
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("zsjy_internship_info")
public class ZsjyInternshipInfo extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 实习信息ID
*/
@TableId(value = "internship_id")
private Long internshipId;
/**
* 学生ID
*/
private Long studentId;
/**
* 学号
*/
private String studentNo;
/**
* 学生姓名
*/
private String studentName;
/**
* 性别
*/
private String gender;
/**
* 学院ID
*/
private Long collegeId;
/**
* 专业ID
*/
private Long majorId;
/**
* 班级ID
*/
private Long classId;
/**
* 联系电话
*/
private String phone;
/**
* 实习单位名称
*/
private String companyName;
/**
* 实习单位地址
*/
private String companyAddress;
/**
* 行业类型
*/
private String companyIndustry;
/**
* 单位规模
*/
private String companyScale;
/**
* 单位联系人
*/
private String companyContact;
/**
* 联系人电话
*/
private String companyPhone;
/**
* 岗位名称
*/
private String positionName;
/**
* 岗位职责
*/
private String positionDuties;
/**
* 实习津贴
*/
private BigDecimal internshipAllowance;
/**
* 工作时长
*/
private String workHours;
/**
* 住宿地址
*/
private String accommodationAddress;
/**
* 住宿类型
*/
private String accommodationType;
/**
* 住宿费用
*/
private BigDecimal accommodationCost;
/**
* 实习开始日期
*/
private Date startDate;
/**
* 实习结束日期
*/
private Date endDate;
/**
* 实际结束日期
*/
private Date actualEndDate;
/**
* 状态(0正常 1停用)
*/
private String status;
/**
* 实习状态(1进行中 2已完成 3中断)
*/
private String internshipStatus;
}
@@ -0,0 +1,167 @@
package org.dromara.zsjy.domain.bo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.dromara.common.core.validate.AddGroup;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Positive;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 实习信息新增业务对象 InternshipInfoAddBO
*
* @author system
* @date 2025-01-03
*/
@Data
@Schema(description = "实习信息新增业务对象")
public class InternshipInfoAddBO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 学生ID
*/
@Schema(description = "学生ID")
@NotNull(message = "学生ID不能为空", groups = {AddGroup.class})
private Long studentId;
/**
* 实习状态:1-进行中,2-已完成,3-已暂停,4-已终止
*/
@Schema(description = "实习状态:1-进行中,2-已完成,3-已暂停,4-已终止")
@NotBlank(message = "实习状态不能为空", groups = {AddGroup.class})
@Pattern(regexp = "^[1-4]$", message = "实习状态只能是1-进行中,2-已完成,3-已暂停,4-已终止", groups = {AddGroup.class})
private String internshipStatus;
/**
* 实习单位名称
*/
@Schema(description = "实习单位名称")
@NotBlank(message = "实习单位名称不能为空", groups = {AddGroup.class})
private String companyName;
/**
* 实习单位地址
*/
@Schema(description = "实习单位地址")
private String companyAddress;
/**
* 单位联系人
*/
@Schema(description = "单位联系人")
private String companyContact;
/**
* 联系人电话
*/
@Schema(description = "联系人电话")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "联系人电话格式不正确", groups = {AddGroup.class})
private String companyPhone;
/**
* 行业类型
*/
@Schema(description = "行业类型")
private String companyIndustry;
/**
* 单位规模
*/
@Schema(description = "单位规模")
private String companyScale;
/**
* 实习岗位
*/
@Schema(description = "实习岗位")
@NotBlank(message = "实习岗位不能为空", groups = {AddGroup.class})
private String internshipPosition;
/**
* 实习薪资
*/
@Schema(description = "实习薪资")
@Positive(message = "实习薪资必须大于0", groups = {AddGroup.class})
private BigDecimal internshipAllowance;
/**
* 实习开始时间
*/
@Schema(description = "实习开始时间")
@JsonFormat(pattern = "yyyy-MM-dd")
@NotNull(message = "实习开始时间不能为空", groups = {AddGroup.class})
private Date internshipStartDate;
/**
* 实习结束时间
*/
@Schema(description = "实习结束时间")
@JsonFormat(pattern = "yyyy-MM-dd")
private Date internshipEndDate;
/**
* 联系人姓名
*/
@Schema(description = "联系人姓名")
private String contactPersonName;
/**
* 联系人电话
*/
@Schema(description = "联系人电话")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "联系人电话格式不正确", groups = {AddGroup.class})
private String contactPersonPhone;
/**
* 岗位职责
*/
@Schema(description = "岗位职责")
private String positionDuties;
/**
* 工作时长
*/
@Schema(description = "工作时长")
private String workHours;
/**
* 住宿地址
*/
@Schema(description = "住宿地址")
private String accommodationAddress;
/**
* 住宿类型
*/
@Schema(description = "住宿类型")
private String accommodationType;
/**
* 住宿费用
*/
@Schema(description = "住宿费用")
private BigDecimal accommodationCost;
/**
* 实习协议状态:1-已签署,2-未签署
*/
@Schema(description = "实习协议状态:1-已签署,2-未签署")
@Pattern(regexp = "^[1-2]$", message = "实习协议状态只能是1-已签署,2-未签署", groups = {AddGroup.class})
private String agreementStatus;
/**
* 备注信息
*/
@Schema(description = "备注信息")
private String remark;
}
@@ -0,0 +1,174 @@
package org.dromara.zsjy.domain.bo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.dromara.common.core.validate.EditGroup;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Positive;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 实习信息修改业务对象 InternshipInfoEditBO
*
* @author system
* @date 2025-01-03
*/
@Data
@Schema(description = "实习信息修改业务对象")
public class InternshipInfoEditBO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 实习信息ID
*/
@Schema(description = "实习信息ID")
@NotNull(message = "实习信息ID不能为空", groups = {EditGroup.class})
private Long internshipId;
/**
* 学生ID
*/
@Schema(description = "学生ID")
@NotNull(message = "学生ID不能为空", groups = {EditGroup.class})
private Long studentId;
/**
* 实习状态:1-进行中,2-已完成,3-已暂停,4-已终止
*/
@Schema(description = "实习状态:1-进行中,2-已完成,3-已暂停,4-已终止")
@NotBlank(message = "实习状态不能为空", groups = {EditGroup.class})
@Pattern(regexp = "^[1-4]$", message = "实习状态只能是1-进行中,2-已完成,3-已暂停,4-已终止", groups = {EditGroup.class})
private String internshipStatus;
/**
* 实习单位名称
*/
@Schema(description = "实习单位名称")
@NotBlank(message = "实习单位名称不能为空", groups = {EditGroup.class})
private String companyName;
/**
* 实习单位地址
*/
@Schema(description = "实习单位地址")
private String companyAddress;
/**
* 单位联系人
*/
@Schema(description = "单位联系人")
private String companyContact;
/**
* 联系人电话
*/
@Schema(description = "联系人电话")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "联系人电话格式不正确", groups = {EditGroup.class})
private String companyPhone;
/**
* 行业类型
*/
@Schema(description = "行业类型")
private String companyIndustry;
/**
* 单位规模
*/
@Schema(description = "单位规模")
private String companyScale;
/**
* 实习岗位
*/
@Schema(description = "实习岗位")
@NotBlank(message = "实习岗位不能为空", groups = {EditGroup.class})
private String internshipPosition;
/**
* 实习薪资
*/
@Schema(description = "实习薪资")
@Positive(message = "实习薪资必须大于0", groups = {EditGroup.class})
private BigDecimal internshipAllowance;
/**
* 实习开始时间
*/
@Schema(description = "实习开始时间")
@JsonFormat(pattern = "yyyy-MM-dd")
@NotNull(message = "实习开始时间不能为空", groups = {EditGroup.class})
private Date internshipStartDate;
/**
* 实习结束时间
*/
@Schema(description = "实习结束时间")
@JsonFormat(pattern = "yyyy-MM-dd")
private Date internshipEndDate;
/**
* 联系人姓名
*/
@Schema(description = "联系人姓名")
private String contactPersonName;
/**
* 联系人电话
*/
@Schema(description = "联系人电话")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "联系人电话格式不正确", groups = {EditGroup.class})
private String contactPersonPhone;
/**
* 岗位职责
*/
@Schema(description = "岗位职责")
private String positionDuties;
/**
* 工作时长
*/
@Schema(description = "工作时长")
private String workHours;
/**
* 住宿地址
*/
@Schema(description = "住宿地址")
private String accommodationAddress;
/**
* 住宿类型
*/
@Schema(description = "住宿类型")
private String accommodationType;
/**
* 住宿费用
*/
@Schema(description = "住宿费用")
private BigDecimal accommodationCost;
/**
* 实习协议状态:1-已签署,2-未签署
*/
@Schema(description = "实习协议状态:1-已签署,2-未签署")
@Pattern(regexp = "^[1-2]$", message = "实习协议状态只能是1-已签署,2-未签署", groups = {EditGroup.class})
private String agreementStatus;
/**
* 备注信息
*/
@Schema(description = "备注信息")
private String remark;
}
@@ -0,0 +1,119 @@
package org.dromara.zsjy.domain.bo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.io.Serial;
import java.math.BigDecimal;
import java.sql.Time;
import java.util.Date;
/**
* 实习日常管理新增业务对象 ZsjyInternshipDailyAddBo
*
* @author system
* @date 2025-01-03
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "实习日常管理新增业务对象")
public class ZsjyInternshipDailyAddBo extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 实习ID
*/
@Schema(description = "实习ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "实习ID不能为空", groups = {AddGroup.class})
private Long internshipId;
/**
* 学生ID
*/
@Schema(description = "学生ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "学生ID不能为空", groups = {AddGroup.class})
private Long studentId;
/**
* 打卡日期
*/
@Schema(description = "打卡日期", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "打卡日期不能为空", groups = {AddGroup.class})
private Date checkDate;
/**
* 签到时间
*/
@Schema(description = "签到时间")
private Time checkInTime;
/**
* 签到位置
*/
@Schema(description = "签到位置")
@Size(max = 200, message = "签到位置长度不能超过200个字符")
private String checkInLocation;
/**
* 纬度
*/
@Schema(description = "纬度")
private BigDecimal latitude;
/**
* 经度
*/
@Schema(description = "经度")
private BigDecimal longitude;
/**
* 日总结
*/
@Schema(description = "日总结")
private String dailySummary;
/**
* 图片附件(JSON格式)
*/
@Schema(description = "图片附件")
private String images;
/**
* 是否异常(0正常 1异常)
*/
@Schema(description = "是否异常")
private String isException;
/**
* 异常原因
*/
@Schema(description = "异常原因")
@Size(max = 500, message = "异常原因长度不能超过500个字符")
private String exceptionReason;
/**
* 异常处理状态(0待处理 1已处理)
*/
@Schema(description = "异常处理状态")
private String exceptionStatus;
/**
* 状态(0正常 1停用)
*/
@Schema(description = "状态")
private String status;
/**
* 备注
*/
@Schema(description = "备注")
private String remark;
}
@@ -0,0 +1,85 @@
package org.dromara.zsjy.domain.bo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import java.io.Serial;
import java.util.Date;
/**
* 实习日常管理查询业务对象 ZsjyInternshipDailyQueryBo
*
* @author system
* @date 2025-01-03
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "实习日常管理查询业务对象")
public class ZsjyInternshipDailyQueryBo extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 学生ID
*/
@Schema(description = "学生ID")
private Long studentId;
/**
* 学号
*/
@Schema(description = "学号")
private String studentNo;
/**
* 学生姓名
*/
@Schema(description = "学生姓名")
private String studentName;
/**
* 实习ID
*/
@Schema(description = "实习ID")
private Long internshipId;
/**
* 打卡开始日期
*/
@Schema(description = "打卡开始日期")
private Date checkDateStart;
/**
* 打卡结束日期
*/
@Schema(description = "打卡结束日期")
private Date checkDateEnd;
/**
* 是否异常(0正常 1异常)
*/
@Schema(description = "是否异常")
private String isException;
/**
* 异常处理状态(0待处理 1已处理)
*/
@Schema(description = "异常处理状态")
private String exceptionStatus;
/**
* 状态(0正常 1停用)
*/
@Schema(description = "状态")
private String status;
/**
* 公司名称
*/
@Schema(description = "公司名称")
private String companyName;
}
@@ -0,0 +1,164 @@
package org.dromara.zsjy.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.sql.Time;
import java.util.Date;
/**
* 实习日常管理视图对象 ZsjyInternshipDailyVo
*
* @author system
* @date 2025-01-03
*/
@Data
@Schema(description = "实习日常管理视图对象")
public class ZsjyInternshipDailyVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 日常记录ID
*/
@Schema(description = "日常记录ID")
private Long dailyId;
/**
* 实习ID
*/
@Schema(description = "实习ID")
private Long internshipId;
/**
* 学生ID
*/
@Schema(description = "学生ID")
private Long studentId;
/**
* 打卡日期
*/
@Schema(description = "打卡日期")
@JsonFormat(pattern = "yyyy-MM-dd")
private Date checkDate;
/**
* 签到时间
*/
@Schema(description = "签到时间")
@JsonFormat(pattern = "HH:mm:ss")
private Time checkInTime;
/**
* 签到位置
*/
@Schema(description = "签到位置")
private String checkInLocation;
/**
* 纬度
*/
@Schema(description = "纬度")
private BigDecimal latitude;
/**
* 经度
*/
@Schema(description = "经度")
private BigDecimal longitude;
/**
* 日总结
*/
@Schema(description = "日总结")
private String dailySummary;
/**
* 图片附件(JSON格式)
*/
@Schema(description = "图片附件")
private String images;
/**
* 是否异常(0正常 1异常)
*/
@Schema(description = "是否异常")
private String isException;
/**
* 异常原因
*/
@Schema(description = "异常原因")
private String exceptionReason;
/**
* 异常处理状态(0待处理 1已处理)
*/
@Schema(description = "异常处理状态")
private String exceptionStatus;
/**
* 状态(0正常 1停用)
*/
@Schema(description = "状态")
private String status;
/**
* 备注
*/
@Schema(description = "备注")
private String remark;
// ============ VO类基础字段规范(强制性) ============
/**
* 创建者
*/
@Schema(description = "创建者")
private Long createBy;
/**
* 创建时间
*/
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 更新者
*/
@Schema(description = "更新者")
private Long updateBy;
/**
* 更新时间
*/
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
// ============ 扩展字段(用于页面显示) ============
/**
* 学生姓名
*/
@Schema(description = "学生姓名")
private String studentName;
/**
* 学号
*/
@Schema(description = "学号")
private String studentNo;
/**
* 公司名称
*/
@Schema(description = "公司名称")
private String companyName;
}
@@ -0,0 +1,64 @@
package org.dromara.zsjy.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.dromara.zsjy.domain.ZsjyInternshipDaily;
import org.dromara.zsjy.domain.bo.ZsjyInternshipDailyQueryBo;
import org.dromara.zsjy.domain.vo.ZsjyInternshipDailyVo;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* 实习日常管理Mapper接口
*
* @author system
* @date 2025-01-03
*/
@Mapper
public interface ZsjyInternshipDailyMapper extends BaseMapper<ZsjyInternshipDaily> {
/**
* 查询实习日常记录列表
*
* @param page 分页参数(可选)
* @param queryBo 查询条件
* @return 实习日常记录列表
*/
List<ZsjyInternshipDailyVo> selectDailyList(@Param("page") Page<ZsjyInternshipDailyVo> page,
@Param("queryBo") ZsjyInternshipDailyQueryBo queryBo);
/**
* 根据学生ID获取考勤日历数据
*
* @param studentId 学生ID
* @param startDate 开始日期
* @param endDate 结束日期
* @return 考勤日历数据
*/
List<Map<String, Object>> selectCalendarData(@Param("studentId") Long studentId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate);
/**
* 获取考勤统计数据
*
* @param queryBo 查询条件
* @return 统计数据
*/
Map<String, Object> selectStatistics(@Param("queryBo") ZsjyInternshipDailyQueryBo queryBo);
/**
* 检查学生当日是否已签到
*
* @param studentId 学生ID
* @param checkDate 打卡日期
* @return 是否已签到
*/
boolean existsByStudentAndDate(@Param("studentId") Long studentId,
@Param("checkDate") Date checkDate);
}
@@ -0,0 +1,16 @@
package org.dromara.zsjy.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.dromara.zsjy.domain.ZsjyInternshipInfo;
import org.apache.ibatis.annotations.Mapper;
/**
* 实习信息Mapper接口
*
* @author system
* @date 2025-01-03
*/
@Mapper
public interface ZsjyInternshipInfoMapper extends BaseMapper<ZsjyInternshipInfo> {
}
@@ -0,0 +1,64 @@
package org.dromara.zsjy.service;
import org.dromara.common.core.domain.R;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.zsjy.domain.bo.ZsjyInternshipDailyAddBo;
import org.dromara.zsjy.domain.bo.ZsjyInternshipDailyQueryBo;
import org.dromara.zsjy.domain.vo.ZsjyInternshipDailyVo;
import java.util.List;
import java.util.Map;
/**
* 实习日常管理Service接口
*
* @author system
* @date 2025-01-03
*/
public interface IZsjyInternshipDailyService {
/**
* 查询实习日常记录分页列表
*
* @param queryBo 查询条件
* @param pageQuery 分页参数
* @return 实习日常记录分页列表
*/
TableDataInfo<ZsjyInternshipDailyVo> queryPageList(ZsjyInternshipDailyQueryBo queryBo, PageQuery pageQuery);
/**
* 查询实习日常记录列表(不分页)
*
* @param queryBo 查询条件
* @return 实习日常记录列表
*/
List<ZsjyInternshipDailyVo> queryList(ZsjyInternshipDailyQueryBo queryBo);
/**
* 学生签到打卡
*
* @param addBo 签到信息
* @return 签到结果
*/
R<Void> checkIn(ZsjyInternshipDailyAddBo addBo);
/**
* 根据学生ID获取考勤日历数据
*
* @param studentId 学生ID
* @param year 年份
* @param month 月份
* @return 考勤日历数据
*/
R<List<Map<String, Object>>> getCalendarData(Long studentId, Integer year, Integer month);
/**
* 获取考勤统计数据
*
* @param queryBo 查询条件
* @return 统计数据
*/
R<Map<String, Object>> getStatistics(ZsjyInternshipDailyQueryBo queryBo);
}
@@ -14,6 +14,9 @@ import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.xg.domain.vo.XgStudentMsgVo;
import org.dromara.xg.service.IXgStudentMsgService;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.zsjy.domain.bo.InternshipInfoQueryBO;
import org.dromara.zsjy.domain.bo.InternshipInfoAddBO;
import org.dromara.zsjy.domain.bo.InternshipInfoEditBO;
@@ -41,6 +44,7 @@ public class InternshipInfoServiceImpl implements IInternshipInfoService {
private final InternshipInfoMapper internshipInfoMapper;
private final ZsjyInternshipInfoMapper zsjyInternshipInfoMapper;
private final IXgStudentMsgService xgStudentMsgService;
/**
* 查询学生实习信息分页列表(班主任专用)
@@ -266,35 +270,104 @@ public class InternshipInfoServiceImpl implements IInternshipInfoService {
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean insertInternshipInfo(InternshipInfoAddBO addBO) {
log.info("新增实习信息,参数:{}", addBO);
log.info("=== 新增实习信息开始 ===");
log.info("1. 接收到的参数:{}", addBO);
// 检查该学生是否已经有实习信息
LambdaQueryWrapper<ZsjyInternshipInfo> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ZsjyInternshipInfo::getStudentId, addBO.getStudentId());
ZsjyInternshipInfo existingInfo = zsjyInternshipInfoMapper.selectOne(queryWrapper);
log.info("2. 检查学生是否已有实习信息,结果:{}", existingInfo != null ? "已存在" : "不存在");
if (ObjectUtil.isNotNull(existingInfo)) {
log.warn("学生ID:{} 已存在实习信息,不能重复添加", addBO.getStudentId());
throw new RuntimeException("该学生已存在实习信息,不能重复添加");
}
// 根据学生ID查询学生基础信息
log.info("3. 查询学生基础信息...");
XgStudentMsgVo studentInfo = getStudentInfo(addBO.getStudentId());
log.info("4. 学生信息查询结果:学号={}, 姓名={}, 学院ID={}, 专业ID={}, 班级ID={}, 手机号={}",
studentInfo.getStudentNo(), studentInfo.getName(),
studentInfo.getCollegeId(), studentInfo.getMajorId(),
studentInfo.getClassId(), studentInfo.getPhone());
// 构建实习信息实体
ZsjyInternshipInfo internshipInfo = new ZsjyInternshipInfo();
BeanUtil.copyProperties(addBO, internshipInfo);
log.info("5. 开始字段映射...");
// 映射字段名称差异
internshipInfo.setPositionName(addBO.getInternshipPosition());
internshipInfo.setStartDate(addBO.getInternshipStartDate());
internshipInfo.setEndDate(addBO.getInternshipEndDate());
internshipInfo.setCompanyContact(addBO.getContactPersonName());
internshipInfo.setCompanyPhone(addBO.getContactPersonPhone());
internshipInfo.setStatus(addBO.getAgreementStatus());
// 学生基础信息映射(数据库必填字段)
internshipInfo.setStudentId(addBO.getStudentId());
internshipInfo.setStudentNo(studentInfo.getStudentNo()); // 从学生信息获取
internshipInfo.setStudentName(studentInfo.getName()); // 从学生信息获取
internshipInfo.setGender(studentInfo.getGender()); // 从学生信息获取
// 类型转换:String -> Long(处理可能的空值)
internshipInfo.setCollegeId(StringUtils.isNotBlank(studentInfo.getCollegeId()) ?
Long.parseLong(studentInfo.getCollegeId()) : null); // 从学生信息获取
internshipInfo.setMajorId(StringUtils.isNotBlank(studentInfo.getMajorId()) ?
Long.parseLong(studentInfo.getMajorId()) : null); // 从学生信息获取
internshipInfo.setClassId(StringUtils.isNotBlank(studentInfo.getClassId()) ?
Long.parseLong(studentInfo.getClassId()) : null); // 从学生信息获取
internshipInfo.setPhone(studentInfo.getPhone()); // 从学生信息获取
// 实习相关信息映射(来自BO)
internshipInfo.setInternshipStatus(addBO.getInternshipStatus());
internshipInfo.setCompanyName(addBO.getCompanyName());
internshipInfo.setCompanyAddress(addBO.getCompanyAddress());
internshipInfo.setInternshipAllowance(addBO.getInternshipAllowance());
internshipInfo.setPositionName(addBO.getInternshipPosition()); // internshipPosition -> positionName
internshipInfo.setStartDate(addBO.getInternshipStartDate()); // internshipStartDate -> startDate
internshipInfo.setEndDate(addBO.getInternshipEndDate()); // internshipEndDate -> endDate
// 公司联系信息映射(优先使用BO中的新字段,兼容旧字段)
internshipInfo.setCompanyContact(StringUtils.isNotBlank(addBO.getCompanyContact()) ?
addBO.getCompanyContact() : addBO.getContactPersonName()); // 优先使用companyContact
internshipInfo.setCompanyPhone(StringUtils.isNotBlank(addBO.getCompanyPhone()) ?
addBO.getCompanyPhone() : addBO.getContactPersonPhone()); // 优先使用companyPhone
// 新增字段映射(从BO中获取真实值)
internshipInfo.setCompanyIndustry(addBO.getCompanyIndustry()); // 行业类型
internshipInfo.setCompanyScale(addBO.getCompanyScale()); // 单位规模
internshipInfo.setPositionDuties(addBO.getPositionDuties()); // 岗位职责
internshipInfo.setWorkHours(addBO.getWorkHours()); // 工作时长
internshipInfo.setAccommodationAddress(addBO.getAccommodationAddress()); // 住宿地址
internshipInfo.setAccommodationType(addBO.getAccommodationType()); // 住宿类型
internshipInfo.setAccommodationCost(addBO.getAccommodationCost()); // 住宿费用
internshipInfo.setStatus(addBO.getAgreementStatus()); // agreementStatus -> status
// actualEndDate保持null,因为实习还未结束
log.info("6. 映射后的实体对象:");
log.info(" - studentId: {}", internshipInfo.getStudentId());
log.info(" - studentNo: {}", internshipInfo.getStudentNo());
log.info(" - studentName: {}", internshipInfo.getStudentName());
log.info(" - collegeId: {}", internshipInfo.getCollegeId());
log.info(" - majorId: {}", internshipInfo.getMajorId());
log.info(" - classId: {}", internshipInfo.getClassId());
log.info(" - phone: {}", internshipInfo.getPhone());
log.info(" - internshipStatus: {}", internshipInfo.getInternshipStatus());
log.info(" - companyName: {}", internshipInfo.getCompanyName());
log.info(" - companyAddress: {}", internshipInfo.getCompanyAddress());
log.info(" - positionName: {}", internshipInfo.getPositionName());
log.info(" - internshipAllowance: {}", internshipInfo.getInternshipAllowance());
log.info(" - startDate: {}", internshipInfo.getStartDate());
log.info(" - endDate: {}", internshipInfo.getEndDate());
log.info(" - companyContact: {}", internshipInfo.getCompanyContact());
log.info(" - companyPhone: {}", internshipInfo.getCompanyPhone());
log.info(" - status: {}", internshipInfo.getStatus());
// 插入数据
log.info("7. 开始执行数据库插入操作...");
int result = zsjyInternshipInfoMapper.insert(internshipInfo);
log.info("新增实习信息结果:{}", result > 0 ? "成功" : "失败");
log.info("8. 数据库插入结果:{} (1表示成功,0表示失败)", result);
log.info("9. 插入后的主键ID:{}", internshipInfo.getInternshipId());
return result > 0;
boolean success = result > 0;
log.info("=== 新增实习信息结束,结果:{} ===", success ? "成功" : "失败");
return success;
}
/**
@@ -323,8 +396,22 @@ public class InternshipInfoServiceImpl implements IInternshipInfoService {
internshipInfo.setPositionName(editBO.getInternshipPosition());
internshipInfo.setStartDate(editBO.getInternshipStartDate());
internshipInfo.setEndDate(editBO.getInternshipEndDate());
internshipInfo.setCompanyContact(editBO.getContactPersonName());
internshipInfo.setCompanyPhone(editBO.getContactPersonPhone());
// 公司联系信息映射(优先使用BO中的新字段,兼容旧字段)
internshipInfo.setCompanyContact(StringUtils.isNotBlank(editBO.getCompanyContact()) ?
editBO.getCompanyContact() : editBO.getContactPersonName()); // 优先使用companyContact
internshipInfo.setCompanyPhone(StringUtils.isNotBlank(editBO.getCompanyPhone()) ?
editBO.getCompanyPhone() : editBO.getContactPersonPhone()); // 优先使用companyPhone
// 新增字段映射(从BO中获取真实值)
internshipInfo.setCompanyIndustry(editBO.getCompanyIndustry()); // 行业类型
internshipInfo.setCompanyScale(editBO.getCompanyScale()); // 单位规模
internshipInfo.setPositionDuties(editBO.getPositionDuties()); // 岗位职责
internshipInfo.setWorkHours(editBO.getWorkHours()); // 工作时长
internshipInfo.setAccommodationAddress(editBO.getAccommodationAddress()); // 住宿地址
internshipInfo.setAccommodationType(editBO.getAccommodationType()); // 住宿类型
internshipInfo.setAccommodationCost(editBO.getAccommodationCost()); // 住宿费用
internshipInfo.setStatus(editBO.getAgreementStatus());
// 更新数据
@@ -356,4 +443,20 @@ public class InternshipInfoServiceImpl implements IInternshipInfoService {
return result > 0;
}
/**
* 根据学生ID查询学生基础信息
*
* @param studentId 学生ID
* @return 学生信息
*/
private XgStudentMsgVo getStudentInfo(Long studentId) {
log.info("根据学生ID查询学生信息:{}", studentId);
XgStudentMsgVo studentInfo = xgStudentMsgService.queryByUserId(studentId);
if (studentInfo == null) {
log.error("未找到学生信息,学生ID:{}", studentId);
throw new ServiceException("未找到学生信息,学生ID:" + studentId);
}
return studentInfo;
}
}
@@ -0,0 +1,164 @@
package org.dromara.zsjy.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.zsjy.domain.ZsjyInternshipDaily;
import org.dromara.zsjy.domain.bo.ZsjyInternshipDailyAddBo;
import org.dromara.zsjy.domain.bo.ZsjyInternshipDailyQueryBo;
import org.dromara.zsjy.domain.vo.ZsjyInternshipDailyVo;
import org.dromara.zsjy.mapper.ZsjyInternshipDailyMapper;
import org.dromara.zsjy.service.IZsjyInternshipDailyService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.sql.Time;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.util.*;
/**
* 实习日常管理Service业务层处理
*
* @author system
* @date 2025-01-03
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class ZsjyInternshipDailyServiceImpl implements IZsjyInternshipDailyService {
private final ZsjyInternshipDailyMapper baseMapper;
/**
* 查询实习日常记录分页列表
*/
@Override
public TableDataInfo<ZsjyInternshipDailyVo> queryPageList(ZsjyInternshipDailyQueryBo queryBo, PageQuery pageQuery) {
Page<ZsjyInternshipDailyVo> page = pageQuery.build();
List<ZsjyInternshipDailyVo> list = baseMapper.selectDailyList(page, queryBo);
return TableDataInfo.build(page);
}
/**
* 查询实习日常记录列表(不分页)
*/
@Override
public List<ZsjyInternshipDailyVo> queryList(ZsjyInternshipDailyQueryBo queryBo) {
return baseMapper.selectDailyList(null, queryBo);
}
/**
* 学生签到打卡
*/
@Override
@Transactional(rollbackFor = Exception.class)
public R<Void> checkIn(ZsjyInternshipDailyAddBo addBo) {
try {
// 1. 验证学生当日是否已签到
boolean exists = baseMapper.existsByStudentAndDate(addBo.getStudentId(), addBo.getCheckDate());
if (exists) {
return R.fail("今日已签到,请勿重复签到");
}
// 2. 构建实体对象
ZsjyInternshipDaily entity = MapstructUtils.convert(addBo, ZsjyInternshipDaily.class);
// 3. 设置签到时间为当前时间
if (ObjectUtil.isNull(entity.getCheckInTime())) {
entity.setCheckInTime(Time.valueOf(DateUtil.format(new Date(), "HH:mm:ss")));
}
// 4. 设置默认值
if (ObjectUtil.isNull(entity.getIsException())) {
entity.setIsException("0"); // 默认正常
}
if (ObjectUtil.isNull(entity.getExceptionStatus())) {
entity.setExceptionStatus("0"); // 默认待处理
}
if (ObjectUtil.isNull(entity.getStatus())) {
entity.setStatus("0"); // 默认正常
}
// 5. 保存签到记录
boolean result = baseMapper.insert(entity) > 0;
if (!result) {
return R.fail("签到失败,请稍后重试");
}
log.info("学生[{}]于{}成功签到", addBo.getStudentId(), addBo.getCheckDate());
return R.ok("签到成功");
} catch (Exception e) {
log.error("学生签到失败: {}", e.getMessage(), e);
throw new ServiceException("签到失败: " + e.getMessage());
}
}
/**
* 根据学生ID获取考勤日历数据
*/
@Override
public R<List<Map<String, Object>>> getCalendarData(Long studentId, Integer year, Integer month) {
try {
// 1. 参数校验
if (ObjectUtil.isNull(studentId)) {
return R.fail("学生ID不能为空");
}
if (ObjectUtil.isNull(year) || ObjectUtil.isNull(month)) {
return R.fail("年份和月份不能为空");
}
// 2. 计算日期范围
YearMonth yearMonth = YearMonth.of(year, month);
Date startDate = DateUtil.beginOfMonth(DateUtil.parse(yearMonth + "-01"));
Date endDate = DateUtil.endOfMonth(startDate);
// 3. 查询考勤数据
List<Map<String, Object>> calendarData = baseMapper.selectCalendarData(studentId, startDate, endDate);
return R.ok(calendarData);
} catch (Exception e) {
log.error("获取考勤日历数据失败: {}", e.getMessage(), e);
return R.fail("获取考勤日历数据失败: " + e.getMessage());
}
}
/**
* 获取考勤统计数据
*/
@Override
public R<Map<String, Object>> getStatistics(ZsjyInternshipDailyQueryBo queryBo) {
try {
// 1. 查询统计数据
Map<String, Object> statistics = baseMapper.selectStatistics(queryBo);
// 2. 补充统计信息
if (ObjectUtil.isNull(statistics)) {
statistics = new HashMap<>();
statistics.put("totalDays", 0);
statistics.put("attendanceDays", 0);
statistics.put("exceptionDays", 0);
statistics.put("pendingExceptions", 0);
statistics.put("attendanceRate", 0.0);
}
return R.ok(statistics);
} catch (Exception e) {
log.error("获取考勤统计数据失败: {}", e.getMessage(), e);
return R.fail("获取考勤统计数据失败: " + e.getMessage());
}
}
}
@@ -0,0 +1,168 @@
<?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="org.dromara.zsjy.mapper.ZsjyInternshipDailyMapper">
<!-- 实习日常记录结果映射 -->
<resultMap type="org.dromara.zsjy.domain.vo.ZsjyInternshipDailyVo" id="ZsjyInternshipDailyVoResult">
<id property="dailyId" column="daily_id" />
<result property="internshipId" column="internship_id" />
<result property="studentId" column="student_id" />
<result property="checkDate" column="check_date" />
<result property="checkInTime" column="check_in_time" />
<result property="checkInLocation" column="check_in_location" />
<result property="latitude" column="latitude" />
<result property="longitude" column="longitude" />
<result property="dailySummary" column="daily_summary" />
<result property="images" column="images" />
<result property="isException" column="is_exception" />
<result property="exceptionReason" column="exception_reason" />
<result property="exceptionStatus" column="exception_status" />
<result property="status" column="status" />
<result property="remark" column="remark" />
<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="studentName" column="student_name" />
<result property="studentNo" column="student_no" />
<result property="companyName" column="company_name" />
</resultMap>
<!-- 基础SQL片段 -->
<sql id="selectZsjyInternshipDailyVo">
SELECT
d.daily_id,
d.internship_id,
d.student_id,
d.check_date,
d.check_in_time,
d.check_in_location,
d.latitude,
d.longitude,
d.daily_summary,
d.images,
d.is_exception,
d.exception_reason,
d.exception_status,
d.status,
d.remark,
d.create_by,
d.create_time,
d.update_by,
d.update_time,
-- 关联查询扩展字段
i.student_name,
i.student_no,
i.company_name
FROM zsjy_internship_daily d
LEFT JOIN zsjy_internship_info i ON d.internship_id = i.internship_id
</sql>
<!-- 查询实习日常记录列表 -->
<select id="selectDailyList" parameterType="org.dromara.zsjy.domain.bo.ZsjyInternshipDailyQueryBo" resultMap="ZsjyInternshipDailyVoResult">
<include refid="selectZsjyInternshipDailyVo"/>
<where>
d.deleted = 0
<if test="queryBo.studentId != null">
AND d.student_id = #{queryBo.studentId}
</if>
<if test="queryBo.studentNo != null and queryBo.studentNo != ''">
AND i.student_no LIKE CONCAT('%', #{queryBo.studentNo}, '%')
</if>
<if test="queryBo.studentName != null and queryBo.studentName != ''">
AND i.student_name LIKE CONCAT('%', #{queryBo.studentName}, '%')
</if>
<if test="queryBo.internshipId != null">
AND d.internship_id = #{queryBo.internshipId}
</if>
<if test="queryBo.checkDateStart != null">
AND d.check_date &gt;= #{queryBo.checkDateStart}
</if>
<if test="queryBo.checkDateEnd != null">
AND d.check_date &lt;= #{queryBo.checkDateEnd}
</if>
<if test="queryBo.isException != null and queryBo.isException != ''">
AND d.is_exception = #{queryBo.isException}
</if>
<if test="queryBo.exceptionStatus != null and queryBo.exceptionStatus != ''">
AND d.exception_status = #{queryBo.exceptionStatus}
</if>
<if test="queryBo.status != null and queryBo.status != ''">
AND d.status = #{queryBo.status}
</if>
<if test="queryBo.companyName != null and queryBo.companyName != ''">
AND i.company_name LIKE CONCAT('%', #{queryBo.companyName}, '%')
</if>
</where>
ORDER BY d.check_date DESC, d.create_time DESC
</select>
<!-- 根据学生ID获取考勤日历数据 -->
<select id="selectCalendarData" resultType="java.util.Map">
SELECT
DATE_FORMAT(check_date, '%Y-%m-%d') as date,
CASE
WHEN check_in_time IS NOT NULL THEN '1'
WHEN is_exception = '1' THEN '2'
ELSE '0'
END as status,
check_in_time,
is_exception,
exception_reason,
daily_summary
FROM zsjy_internship_daily
WHERE student_id = #{studentId}
AND deleted = 0
<if test="startDate != null">
AND check_date &gt;= #{startDate}
</if>
<if test="endDate != null">
AND check_date &lt;= #{endDate}
</if>
ORDER BY check_date ASC
</select>
<!-- 获取考勤统计数据 -->
<select id="selectStatistics" resultType="java.util.Map">
SELECT
COUNT(*) as totalDays,
SUM(CASE WHEN check_in_time IS NOT NULL THEN 1 ELSE 0 END) as attendanceDays,
SUM(CASE WHEN is_exception = '1' THEN 1 ELSE 0 END) as exceptionDays,
SUM(CASE WHEN is_exception = '1' AND exception_status = '0' THEN 1 ELSE 0 END) as pendingExceptions,
ROUND(
SUM(CASE WHEN check_in_time IS NOT NULL THEN 1 ELSE 0 END) * 100.0 /
NULLIF(COUNT(*), 0), 2
) as attendanceRate
FROM zsjy_internship_daily d
LEFT JOIN zsjy_internship_info i ON d.internship_id = i.internship_id
<where>
d.deleted = 0
<if test="queryBo.studentId != null">
AND d.student_id = #{queryBo.studentId}
</if>
<if test="queryBo.internshipId != null">
AND d.internship_id = #{queryBo.internshipId}
</if>
<if test="queryBo.checkDateStart != null">
AND d.check_date &gt;= #{queryBo.checkDateStart}
</if>
<if test="queryBo.checkDateEnd != null">
AND d.check_date &lt;= #{queryBo.checkDateEnd}
</if>
<if test="queryBo.companyName != null and queryBo.companyName != ''">
AND i.company_name LIKE CONCAT('%', #{queryBo.companyName}, '%')
</if>
</where>
</select>
<!-- 检查学生当日是否已签到 -->
<select id="existsByStudentAndDate" resultType="boolean">
SELECT COUNT(*) > 0
FROM zsjy_internship_daily
WHERE student_id = #{studentId}
AND check_date = #{checkDate}
AND deleted = 0
</select>
</mapper>
+46
View File
@@ -0,0 +1,46 @@
import request from '@/utils/request'
// 查询实习日常记录列表
export function listInternshipDaily(query) {
return request({
url: '/zsjy/internship/daily/list',
method: 'get',
params: query
})
}
// 学生签到打卡
export function checkIn(data) {
return request({
url: '/zsjy/internship/daily/checkin',
method: 'post',
data: data
})
}
// 新增实习日常记录
export function addInternshipDaily(data) {
return request({
url: '/zsjy/internship/daily',
method: 'post',
data: data
})
}
// 获取学生考勤日历
export function getCalendar(studentId, params) {
return request({
url: `/zsjy/internship/daily/calendar/${studentId}`,
method: 'get',
params: params
})
}
// 获取考勤统计数据
export function getStatistics(query) {
return request({
url: '/zsjy/internship/daily/statistics',
method: 'get',
params: query
})
}
+115 -45
View File
@@ -841,6 +841,31 @@ export interface InternshipInfoVO {
*/
companyAddress?: string;
/**
* 实习津贴
*/
internshipAllowance?: number;
/**
* 单位联系人
*/
contactPersonName?: string;
/**
* 联系人电话
*/
contactPersonPhone?: string;
/**
* 协议状态
*/
agreementStatus?: string;
/**
* 备注信息
*/
remark?: string;
/**
* 创建时间
*/
@@ -871,6 +896,16 @@ export interface InternshipInfoAddBO {
*/
studentId: string | number;
/**
* 学生姓名(前端显示用)
*/
studentName?: string;
/**
* 学生学号(前端显示用)
*/
studentNo?: string;
/**
* 实习状态(1进行中 2已完成 3中断)
*/
@@ -886,6 +921,16 @@ export interface InternshipInfoAddBO {
*/
companyAddress: string;
/**
* 单位联系人
*/
companyContact?: string;
/**
* 联系人电话
*/
companyPhone?: string;
/**
* 行业类型
*/
@@ -897,30 +942,40 @@ export interface InternshipInfoAddBO {
companyScale?: string;
/**
* 单位联系人
* 实习岗位
*/
companyContact: string;
internshipPosition: string;
/**
* 实习薪资
*/
internshipAllowance?: number;
/**
* 实习开始时间
*/
internshipStartDate: string;
/**
* 实习结束时间
*/
internshipEndDate: string;
/**
* 联系人姓名
*/
contactPersonName: string;
/**
* 联系人电话
*/
companyPhone: string;
/**
* 岗位名称
*/
positionName: string;
contactPersonPhone: string;
/**
* 岗位职责
*/
positionDuties?: string;
/**
* 实习津贴
*/
internshipAllowance?: number;
/**
* 工作时长
*/
@@ -942,19 +997,9 @@ export interface InternshipInfoAddBO {
accommodationCost?: number;
/**
* 实习开始日期
* 实习协议状态(1已签署 2未签署)
*/
startDate: string;
/**
* 实习结束日期
*/
endDate: string;
/**
* 实际结束日期
*/
actualEndDate?: string;
agreementStatus: string;
/**
* 备注
@@ -976,6 +1021,16 @@ export interface InternshipInfoEditBO {
*/
studentId: string | number;
/**
* 学生姓名(前端显示用)
*/
studentName?: string;
/**
* 学生学号(前端显示用)
*/
studentNo?: string;
/**
* 实习状态(1进行中 2已完成 3中断)
*/
@@ -991,6 +1046,16 @@ export interface InternshipInfoEditBO {
*/
companyAddress: string;
/**
* 单位联系人
*/
companyContact?: string;
/**
* 联系人电话
*/
companyPhone?: string;
/**
* 行业类型
*/
@@ -1002,30 +1067,40 @@ export interface InternshipInfoEditBO {
companyScale?: string;
/**
* 单位联系人
* 实习岗位
*/
companyContact: string;
internshipPosition: string;
/**
* 实习薪资
*/
internshipAllowance?: number;
/**
* 实习开始时间
*/
internshipStartDate: string;
/**
* 实习结束时间
*/
internshipEndDate: string;
/**
* 联系人姓名
*/
contactPersonName: string;
/**
* 联系人电话
*/
companyPhone: string;
/**
* 岗位名称
*/
positionName: string;
contactPersonPhone: string;
/**
* 岗位职责
*/
positionDuties?: string;
/**
* 实习津贴
*/
internshipAllowance?: number;
/**
* 工作时长
*/
@@ -1047,14 +1122,9 @@ export interface InternshipInfoEditBO {
accommodationCost?: number;
/**
* 实习开始日期
* 实习协议状态(1已签署 2未签署)
*/
startDate: string;
/**
* 实习结束日期
*/
endDate: string;
agreementStatus: string;
/**
* 实际结束日期
@@ -0,0 +1,695 @@
<template>
<div class="checkin-container">
<!-- 顶部状态卡片 -->
<el-card class="status-card" shadow="hover">
<div class="status-header">
<div class="user-info">
<el-avatar :size="60" :src="userInfo.avatar" icon="UserFilled" />
<div class="info-text">
<h3>{{ userInfo.studentName || userInfo.nickName }}</h3>
<p>学号:{{ userInfo.studentNumber || '未设置' }}</p>
<p>班级:{{ userInfo.className || '未设置' }}</p>
</div>
</div>
<div class="time-info">
<div class="current-time">{{ currentTime }}</div>
<div class="current-date">{{ currentDate }}</div>
</div>
</div>
</el-card>
<!-- 签到区域 -->
<el-row :gutter="20">
<el-col :span="12">
<el-card class="checkin-card" shadow="hover">
<template #header>
<div class="card-header">
<el-icon class="header-icon"><Clock /></el-icon>
<span>签到打卡</span>
</div>
</template>
<div class="checkin-content">
<!-- 签到状态显示 -->
<div class="status-display">
<div v-if="todayRecord.checkInTime" class="checked-in">
<el-icon class="status-icon success"><SuccessFilled /></el-icon>
<div class="status-text">
<h4>已签到</h4>
<p>{{ parseTime(todayRecord.checkInTime, '{h}:{i}') }}</p>
</div>
</div>
<div v-else class="not-checked-in">
<el-icon class="status-icon waiting"><Clock /></el-icon>
<div class="status-text">
<h4>未签到</h4>
<p>请点击下方按钮进行签到</p>
</div>
</div>
</div>
<!-- 签到按钮 -->
<div class="checkin-actions">
<el-button
type="primary"
size="large"
:disabled="todayRecord.checkInTime || loading"
:loading="loading"
@click="handleCheckIn"
class="checkin-btn"
>
<el-icon><Location /></el-icon>
{{ todayRecord.checkInTime ? '已签到' : '立即签到' }}
</el-button>
</div>
<!-- 位置信息 -->
<div class="location-info" v-if="locationInfo.address">
<el-icon><MapPin /></el-icon>
<span>{{ locationInfo.address }}</span>
</div>
</div>
</el-card>
</el-col>
<el-col :span="12">
<el-card class="checkout-card" shadow="hover">
<template #header>
<div class="card-header">
<el-icon class="header-icon"><Clock /></el-icon>
<span>签退打卡</span>
</div>
</template>
<div class="checkout-content">
<!-- 签退状态显示 -->
<div class="status-display">
<div v-if="todayRecord.checkOutTime" class="checked-out">
<el-icon class="status-icon success"><SuccessFilled /></el-icon>
<div class="status-text">
<h4>已签退</h4>
<p>{{ parseTime(todayRecord.checkOutTime, '{h}:{i}') }}</p>
</div>
</div>
<div v-else class="not-checked-out">
<el-icon class="status-icon waiting"><Clock /></el-icon>
<div class="status-text">
<h4>未签退</h4>
<p>{{ todayRecord.checkInTime ? '可以进行签退' : '请先签到' }}</p>
</div>
</div>
</div>
<!-- 签退按钮 -->
<div class="checkout-actions">
<el-button
type="success"
size="large"
:disabled="!todayRecord.checkInTime || todayRecord.checkOutTime || loading"
:loading="loading"
@click="handleCheckOut"
class="checkout-btn"
>
<el-icon><Location /></el-icon>
{{ todayRecord.checkOutTime ? '已签退' : '立即签退' }}
</el-button>
</div>
<!-- 工作时长 -->
<div class="duration-info" v-if="todayRecord.workDuration">
<el-icon><Timer /></el-icon>
<span>工作时长:{{ todayRecord.workDuration }}小时</span>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 工作内容填写 -->
<el-card class="work-content-card" shadow="hover" v-if="todayRecord.checkInTime">
<template #header>
<div class="card-header">
<el-icon class="header-icon"><Edit /></el-icon>
<span>今日工作内容</span>
</div>
</template>
<div class="work-content">
<el-form ref="workFormRef" :model="workForm" label-width="100px">
<el-form-item label="工作内容" prop="workContent">
<el-input
v-model="workForm.workContent"
type="textarea"
:rows="4"
placeholder="请详细描述今日的工作内容..."
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="workForm.remark"
type="textarea"
:rows="2"
placeholder="可填写工作心得、遇到的问题等..."
maxlength="200"
show-word-limit
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveWorkContent" :loading="saving">
<el-icon><DocumentAdd /></el-icon>
保存工作内容
</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
<!-- 最近记录 -->
<el-card class="recent-records-card" shadow="hover">
<template #header>
<div class="card-header">
<el-icon class="header-icon"><List /></el-icon>
<span>最近考勤记录</span>
</div>
</template>
<div class="recent-records">
<el-table :data="recentRecords" style="width: 100%">
<el-table-column label="日期" width="120">
<template #default="scope">
{{ parseTime(scope.row.checkDate, '{m}-{d}') }}
</template>
</el-table-column>
<el-table-column label="签到时间" width="100">
<template #default="scope">
{{ scope.row.checkInTime ? parseTime(scope.row.checkInTime, '{h}:{i}') : '-' }}
</template>
</el-table-column>
<el-table-column label="签退时间" width="100">
<template #default="scope">
{{ scope.row.checkOutTime ? parseTime(scope.row.checkOutTime, '{h}:{i}') : '-' }}
</template>
</el-table-column>
<el-table-column label="工作时长" width="100">
<template #default="scope">
{{ scope.row.workDuration ? scope.row.workDuration + 'h' : '-' }}
</template>
</el-table-column>
<el-table-column label="状态" width="80">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)" size="small">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="工作内容" show-overflow-tooltip>
<template #default="scope">
{{ scope.row.workContent || '-' }}
</template>
</el-table-column>
</el-table>
</div>
</el-card>
</div>
</template>
<script setup name="InternshipCheckin">
import { checkIn } from "@/api/zsjy/internshipDaily";
import { parseTime } from "@/utils/ruoyi";
const { proxy } = getCurrentInstance();
// 响应式数据
const loading = ref(false);
const saving = ref(false);
const currentTime = ref('');
const currentDate = ref('');
const userInfo = ref({});
const todayRecord = ref({});
const recentRecords = ref([]);
const locationInfo = ref({});
// 工作内容表单
const workForm = ref({
workContent: '',
remark: ''
});
// 获取用户信息
const getUserInfo = () => {
// 从store或接口获取用户信息
userInfo.value = {
studentName: '张三',
studentNumber: '20230001',
className: '计算机科学与技术2023-1班',
avatar: ''
};
};
// 获取今日考勤记录
const getTodayRecord = () => {
// 调用接口获取今日考勤记录
const today = new Date().toISOString().slice(0, 10);
// 这里应该调用实际的API
todayRecord.value = {
checkDate: today,
checkInTime: null,
checkOutTime: null,
workDuration: null,
status: '0'
};
};
// 获取最近考勤记录
const getRecentRecords = () => {
// 调用接口获取最近7天的考勤记录
recentRecords.value = [
{
checkDate: '2024-01-15',
checkInTime: '2024-01-15 09:00:00',
checkOutTime: '2024-01-15 18:00:00',
workDuration: 8,
status: '1',
workContent: '参与项目开发,完成用户模块设计'
},
// 更多记录...
];
};
// 获取地理位置
const getLocation = () => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords;
locationInfo.value = {
latitude,
longitude,
address: '获取位置中...'
};
// 调用地图API获取详细地址
getAddressFromCoords(latitude, longitude);
},
(error) => {
console.error('获取位置失败:', error);
proxy.$modal.msgError('获取位置失败,请检查浏览器权限设置');
}
);
} else {
proxy.$modal.msgError('浏览器不支持地理位置功能');
}
};
// 根据坐标获取地址
const getAddressFromCoords = (lat, lng) => {
// 这里可以调用高德地图、百度地图等API
// 模拟地址信息
setTimeout(() => {
locationInfo.value.address = '浙江省杭州市西湖区某某大厦';
}, 1000);
};
// 处理签到
const handleCheckIn = async () => {
if (!locationInfo.value.latitude) {
proxy.$modal.msgError('请先获取位置信息');
getLocation();
return;
}
loading.value = true;
try {
const checkInData = {
checkInLocation: locationInfo.value.address,
latitude: locationInfo.value.latitude,
longitude: locationInfo.value.longitude,
checkInTime: new Date().toISOString()
};
await checkIn(checkInData);
proxy.$modal.msgSuccess('签到成功');
getTodayRecord(); // 刷新今日记录
getRecentRecords(); // 刷新最近记录
} catch (error) {
console.error('签到失败:', error);
proxy.$modal.msgError('签到失败,请重试');
} finally {
loading.value = false;
}
};
// 处理签退
const handleCheckOut = async () => {
if (!locationInfo.value.latitude) {
proxy.$modal.msgError('请先获取位置信息');
getLocation();
return;
}
loading.value = true;
try {
const checkOutData = {
checkOutLocation: locationInfo.value.address,
latitude: locationInfo.value.latitude,
longitude: locationInfo.value.longitude,
checkOutTime: new Date().toISOString()
};
// 调用签退接口
// await checkOut(checkOutData);
proxy.$modal.msgSuccess('签退成功');
getTodayRecord(); // 刷新今日记录
getRecentRecords(); // 刷新最近记录
} catch (error) {
console.error('签退失败:', error);
proxy.$modal.msgError('签退失败,请重试');
} finally {
loading.value = false;
}
};
// 保存工作内容
const saveWorkContent = async () => {
if (!workForm.value.workContent.trim()) {
proxy.$modal.msgError('请填写工作内容');
return;
}
saving.value = true;
try {
// 调用保存工作内容的接口
// await updateWorkContent(workForm.value);
proxy.$modal.msgSuccess('工作内容保存成功');
getTodayRecord(); // 刷新今日记录
} catch (error) {
console.error('保存失败:', error);
proxy.$modal.msgError('保存失败,请重试');
} finally {
saving.value = false;
}
};
// 获取状态类型
const getStatusType = (status) => {
const statusMap = {
'0': 'info',
'1': 'success',
'2': 'warning',
'3': 'danger'
};
return statusMap[status] || 'info';
};
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
'0': '未签到',
'1': '正常',
'2': '迟到',
'3': '早退'
};
return statusMap[status] || '';
};
// 更新时间
const updateTime = () => {
const now = new Date();
currentTime.value = now.toLocaleTimeString('zh-CN', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
currentDate.value = now.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
});
};
// 初始化
onMounted(() => {
getUserInfo();
getTodayRecord();
getRecentRecords();
getLocation();
updateTime();
// 每秒更新时间
setInterval(updateTime, 1000);
});
</script>
<style scoped lang="scss">
.checkin-container {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
.status-card {
margin-bottom: 20px;
border-radius: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
:deep(.el-card__body) {
padding: 24px;
}
.status-header {
display: flex;
justify-content: space-between;
align-items: center;
.user-info {
display: flex;
align-items: center;
gap: 16px;
.info-text {
h3 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
}
p {
margin: 2px 0;
opacity: 0.9;
font-size: 14px;
}
}
}
.time-info {
text-align: right;
.current-time {
font-size: 32px;
font-weight: bold;
line-height: 1;
margin-bottom: 4px;
}
.current-date {
font-size: 16px;
opacity: 0.9;
}
}
}
}
.checkin-card,
.checkout-card {
border-radius: 12px;
margin-bottom: 20px;
.card-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 16px;
.header-icon {
font-size: 18px;
color: #409eff;
}
}
.checkin-content,
.checkout-content {
text-align: center;
padding: 20px 0;
.status-display {
margin-bottom: 24px;
.checked-in,
.checked-out,
.not-checked-in,
.not-checked-out {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
.status-icon {
font-size: 48px;
&.success {
color: #67c23a;
}
&.waiting {
color: #909399;
}
}
.status-text {
h4 {
margin: 0 0 4px 0;
font-size: 18px;
font-weight: 600;
}
p {
margin: 0;
color: #909399;
font-size: 14px;
}
}
}
}
.checkin-actions,
.checkout-actions {
margin-bottom: 16px;
.checkin-btn,
.checkout-btn {
width: 200px;
height: 48px;
font-size: 16px;
border-radius: 24px;
font-weight: 600;
}
}
.location-info,
.duration-info {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
color: #606266;
font-size: 14px;
}
}
}
.work-content-card,
.recent-records-card {
border-radius: 12px;
margin-bottom: 20px;
.card-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 16px;
.header-icon {
font-size: 18px;
color: #409eff;
}
}
.work-content {
padding: 16px 0;
}
.recent-records {
:deep(.el-table) {
.el-table__header-wrapper {
th {
background-color: #f8f9fa;
color: #333;
font-weight: 600;
}
}
.el-table__row {
&:hover {
background-color: #f5f7fa;
}
}
}
}
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.checkin-container {
padding: 12px;
.status-header {
flex-direction: column;
text-align: center;
gap: 16px;
.user-info {
flex-direction: column;
text-align: center;
}
.time-info {
text-align: center;
.current-time {
font-size: 24px;
}
}
}
.checkin-btn,
.checkout-btn {
width: 100% !important;
}
}
}
/* 动画效果 */
.checkin-card,
.checkout-card,
.work-content-card,
.recent-records-card {
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
}
.checkin-btn,
.checkout-btn {
transition: all 0.3s ease;
&:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(64, 158, 255, 0.3);
}
}
</style>
@@ -0,0 +1,899 @@
<template>
<div class="p-2 page-container">
<!-- 搜索区域 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="100px">
<!-- 第一行:4个搜索条件 -->
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="学生姓名" prop="studentName">
<el-input v-model="queryParams.studentName" placeholder="请输入学生姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学号" prop="studentNumber">
<el-input v-model="queryParams.studentNumber" placeholder="请输入学号" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="实习单位" prop="companyName">
<el-input v-model="queryParams.companyName" placeholder="请输入实习单位" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="签到状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择签到状态" clearable style="width: 100%">
<el-option label="已签到" value="1" />
<el-option label="未签到" value="0" />
<el-option label="迟到" value="2" />
<el-option label="早退" value="3" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- 第二行:日期筛选 + 操作按钮 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="签到日期">
<el-date-picker
v-model="daterangeCheckTime"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
@change="handleQuery"
style="width: 100%"
></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12" class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 统计卡片区域 -->
<div class="mb-[10px]">
<el-row :gutter="16">
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-card-primary">
<div class="stat-content">
<div class="stat-icon">
<el-icon><User /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.totalStudents || 0 }}</div>
<div class="stat-label">实习学生总数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-card-success">
<div class="stat-content">
<div class="stat-icon">
<el-icon><SuccessFilled /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.todayCheckedIn || 0 }}</div>
<div class="stat-label">今日已签到</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-card-warning">
<div class="stat-content">
<div class="stat-icon">
<el-icon><WarningFilled /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.todayLate || 0 }}</div>
<div class="stat-label">今日迟到</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-card-info">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Clock /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.attendanceRate || '0%' }}</div>
<div class="stat-label">本月出勤率</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
<!-- 操作工具栏 -->
<el-card shadow="never" class="button-card">
<div class="card-header-vertical">
<div class="toolbar-title">实习日常管理</div>
<div class="button-group">
<el-button type="primary" icon="Plus" @click="handleAdd" v-hasPermi="['zsjy:internship:daily:add']">新增签到</el-button>
<el-button plain icon="Search" @click="showSearch = !showSearch">搜索</el-button>
<el-button type="primary" plain icon="Download" @click="handleExport" v-hasPermi="['zsjy:internship:daily:export']">导出</el-button>
</div>
</div>
</el-card>
<!-- 数据表格 -->
<el-card shadow="never" class="table-card">
<div class="table-container">
<el-table
ref="internshipDailyTableRef"
v-loading="loading"
:data="internshipDailyList"
row-key="id"
class="scroll-table"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="学生信息" min-width="180" align="left">
<template #default="scope">
<div class="student-info">
<div class="student-name">{{ scope.row.studentName }}</div>
<div class="student-number">学号:{{ scope.row.studentNumber }}</div>
<div class="student-class">班级:{{ scope.row.className }}</div>
</div>
</template>
</el-table-column>
<el-table-column label="实习单位" prop="companyName" min-width="150" align="left" show-overflow-tooltip />
<el-table-column label="签到时间" min-width="120" align="center">
<template #default="scope">
<div v-if="scope.row.checkInTime">
<div>{{ parseTime(scope.row.checkInTime, '{y}-{m}-{d}') }}</div>
<div class="time-detail">{{ parseTime(scope.row.checkInTime, '{h}:{i}') }}</div>
</div>
<span v-else class="text-danger">未签到</span>
</template>
</el-table-column>
<el-table-column label="签退时间" min-width="120" align="center">
<template #default="scope">
<div v-if="scope.row.checkOutTime">
<div>{{ parseTime(scope.row.checkOutTime, '{y}-{m}-{d}') }}</div>
<div class="time-detail">{{ parseTime(scope.row.checkOutTime, '{h}:{i}') }}</div>
</div>
<span v-else class="text-muted">未签退</span>
</template>
</el-table-column>
<el-table-column label="工作时长" min-width="100" align="center">
<template #default="scope">
<span v-if="scope.row.workDuration">{{ scope.row.workDuration }}小时</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="签到状态" min-width="100" align="center">
<template #default="scope">
<dict-tag :options="attendanceStatusOptions" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="签到地点" prop="checkInLocation" min-width="150" align="left" show-overflow-tooltip />
<el-table-column label="工作内容" prop="workContent" min-width="200" align="left" show-overflow-tooltip />
<el-table-column label="操作" width="120" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<div class="action-buttons">
<el-tooltip content="查看详情" placement="top">
<el-button link type="primary" icon="View" @click="handleView(scope.row)" />
</el-tooltip>
<el-tooltip content="查看日历" placement="top">
<el-button link type="success" icon="Calendar" @click="handleCalendar(scope.row)" />
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页组件 -->
<div class="pagination-container">
<pagination
v-show="total > 0"
:total="total"
:page="queryParams.pageNum"
:limit="queryParams.pageSize"
@pagination="getList"
/>
</div>
</el-card>
<!-- 详情对话框 -->
<el-dialog title="考勤详情" v-model="viewDialogVisible" width="600px" append-to-body>
<div v-if="currentRecord" class="detail-container">
<el-descriptions :column="2" border>
<el-descriptions-item label="学生姓名">{{ currentRecord.studentName }}</el-descriptions-item>
<el-descriptions-item label="学号">{{ currentRecord.studentNumber }}</el-descriptions-item>
<el-descriptions-item label="班级">{{ currentRecord.className }}</el-descriptions-item>
<el-descriptions-item label="实习单位">{{ currentRecord.companyName }}</el-descriptions-item>
<el-descriptions-item label="签到时间">
{{ currentRecord.checkInTime ? parseTime(currentRecord.checkInTime) : '未签到' }}
</el-descriptions-item>
<el-descriptions-item label="签退时间">
{{ currentRecord.checkOutTime ? parseTime(currentRecord.checkOutTime) : '未签退' }}
</el-descriptions-item>
<el-descriptions-item label="工作时长">
{{ currentRecord.workDuration ? currentRecord.workDuration + '小时' : '-' }}
</el-descriptions-item>
<el-descriptions-item label="签到状态">
<dict-tag :options="attendanceStatusOptions" :value="currentRecord.status" />
</el-descriptions-item>
<el-descriptions-item label="签到地点" :span="2">{{ currentRecord.checkInLocation || '-' }}</el-descriptions-item>
<el-descriptions-item label="工作内容" :span="2">
<div style="white-space: pre-wrap;">{{ currentRecord.workContent || '-' }}</div>
</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">
<div style="white-space: pre-wrap;">{{ currentRecord.remark || '-' }}</div>
</el-descriptions-item>
</el-descriptions>
</div>
<template #footer>
<div class="dialog-footer-right">
<el-button @click="viewDialogVisible = false">关闭</el-button>
</div>
</template>
</el-dialog>
<!-- 考勤日历对话框 -->
<el-dialog title="考勤日历" v-model="calendarDialogVisible" width="800px" append-to-body>
<div v-if="currentStudent" class="calendar-container">
<div class="calendar-header">
<h4>{{ currentStudent.studentName }}({{ currentStudent.studentNumber }})的考勤记录</h4>
<el-date-picker
v-model="calendarMonth"
type="month"
placeholder="选择月份"
format="YYYY年MM月"
value-format="YYYY-MM"
@change="handleMonthChange"
/>
</div>
<el-calendar v-model="calendarDate">
<template #date-cell="{ data }">
<div class="calendar-day">
<div class="day-number">{{ data.day.split('-').slice(2).join('') }}</div>
<div v-if="getAttendanceStatus(data.day)" class="attendance-status">
<el-tag
:type="getStatusTagType(getAttendanceStatus(data.day))"
size="small"
>
{{ getStatusText(getAttendanceStatus(data.day)) }}
</el-tag>
</div>
</div>
</template>
</el-calendar>
</div>
<template #footer>
<div class="dialog-footer-right">
<el-button @click="calendarDialogVisible = false">关闭</el-button>
</div>
</template>
</el-dialog>
<!-- 新增签到对话框 -->
<el-dialog title="新增签到记录" v-model="addDialogVisible" width="600px" append-to-body>
<el-form ref="addFormRef" :model="addForm" :rules="addFormRules" label-width="100px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="学生姓名" prop="studentName">
<el-input v-model="addForm.studentName" placeholder="请输入学生姓名" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学号" prop="studentNumber">
<el-input v-model="addForm.studentNumber" placeholder="请输入学号" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="班级" prop="className">
<el-input v-model="addForm.className" placeholder="请输入班级" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="实习单位" prop="companyName">
<el-input v-model="addForm.companyName" placeholder="请输入实习单位" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="签到时间" prop="checkInTime">
<el-date-picker
v-model="addForm.checkInTime"
type="datetime"
placeholder="选择签到时间"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="签退时间" prop="checkOutTime">
<el-date-picker
v-model="addForm.checkOutTime"
type="datetime"
placeholder="选择签退时间"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="签到状态" prop="status">
<el-select v-model="addForm.status" placeholder="请选择签到状态" style="width: 100%">
<el-option
v-for="item in attendanceStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工作时长" prop="workDuration">
<el-input-number
v-model="addForm.workDuration"
:min="0"
:max="24"
:precision="1"
placeholder="工作时长(小时)"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="签到地点" prop="checkInLocation">
<el-input v-model="addForm.checkInLocation" placeholder="请输入签到地点" />
</el-form-item>
<el-form-item label="工作内容" prop="workContent">
<el-input
v-model="addForm.workContent"
type="textarea"
:rows="3"
placeholder="请输入工作内容"
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="addForm.remark"
type="textarea"
:rows="2"
placeholder="请输入备注"
maxlength="200"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer-right">
<el-button @click="cancelAdd">取消</el-button>
<el-button type="primary" @click="submitAdd" :loading="addLoading">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="InternshipDaily">
import { listInternshipDaily, getCalendar, getStatistics, addInternshipDaily } from "@/api/zsjy/internshipDaily";
import { parseTime } from "@/utils/ruoyi";
const { proxy } = getCurrentInstance();
// 响应式数据
const internshipDailyList = ref([]);
const loading = ref(true);
const showSearch = ref(true);
const total = ref(0);
const daterangeCheckTime = ref([]);
const viewDialogVisible = ref(false);
const calendarDialogVisible = ref(false);
const currentRecord = ref(null);
const currentStudent = ref(null);
const calendarMonth = ref('');
const calendarDate = ref(new Date());
const calendarData = ref([]);
const statistics = ref({});
const addDialogVisible = ref(false);
const addLoading = ref(false);
// 新增表单数据
const addForm = ref({
studentName: '',
studentNumber: '',
className: '',
companyName: '',
checkInTime: '',
checkOutTime: '',
workDuration: null,
status: '1',
checkInLocation: '',
workContent: '',
remark: ''
});
// 新增表单验证规则
const addFormRules = ref({
studentName: [
{ required: true, message: '请输入学生姓名', trigger: 'blur' }
],
studentNumber: [
{ required: true, message: '请输入学号', trigger: 'blur' }
],
className: [
{ required: true, message: '请输入班级', trigger: 'blur' }
],
companyName: [
{ required: true, message: '请输入实习单位', trigger: 'blur' }
],
checkInTime: [
{ required: true, message: '请选择签到时间', trigger: 'change' }
],
status: [
{ required: true, message: '请选择签到状态', trigger: 'change' }
]
});
// 查询参数
const queryParams = ref({
pageNum: 1,
pageSize: 10,
studentName: '',
studentNumber: '',
companyName: '',
status: '',
checkTimeStart: '',
checkTimeEnd: ''
});
// 字典数据
const attendanceStatusOptions = ref([
{ label: '未签到', value: '0', elTagType: 'info' },
{ label: '已签到', value: '1', elTagType: 'success' },
{ label: '迟到', value: '2', elTagType: 'warning' },
{ label: '早退', value: '3', elTagType: 'danger' }
]);
// 查询列表
const getList = () => {
loading.value = true;
// 处理日期范围
if (daterangeCheckTime.value && daterangeCheckTime.value.length === 2) {
queryParams.value.checkTimeStart = daterangeCheckTime.value[0];
queryParams.value.checkTimeEnd = daterangeCheckTime.value[1];
} else {
queryParams.value.checkTimeStart = '';
queryParams.value.checkTimeEnd = '';
}
listInternshipDaily(queryParams.value).then(response => {
internshipDailyList.value = response.rows;
total.value = response.total;
loading.value = false;
});
};
// 获取统计数据
const getStatisticsData = () => {
getStatistics().then(response => {
statistics.value = response.data || {};
});
};
// 搜索按钮操作
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
// 重置按钮操作
const resetQuery = () => {
daterangeCheckTime.value = [];
proxy.resetForm("queryFormRef");
queryParams.value = {
pageNum: 1,
pageSize: 10,
studentName: '',
studentNumber: '',
companyName: '',
status: '',
checkTimeStart: '',
checkTimeEnd: ''
};
handleQuery();
};
// 多选框选中数据
const handleSelectionChange = (selection) => {
// 多选处理逻辑
};
// 新增按钮操作
const handleAdd = () => {
resetAddForm();
addDialogVisible.value = true;
};
// 重置新增表单
const resetAddForm = () => {
addForm.value = {
studentName: '',
studentNumber: '',
className: '',
companyName: '',
checkInTime: '',
checkOutTime: '',
workDuration: null,
status: '1',
checkInLocation: '',
workContent: '',
remark: ''
};
if (proxy.$refs.addFormRef) {
proxy.$refs.addFormRef.resetFields();
}
};
// 取消新增
const cancelAdd = () => {
addDialogVisible.value = false;
resetAddForm();
};
// 提交新增
const submitAdd = () => {
proxy.$refs.addFormRef.validate(valid => {
if (valid) {
addLoading.value = true;
addInternshipDaily(addForm.value).then(response => {
proxy.$modal.msgSuccess("新增签到记录成功");
addDialogVisible.value = false;
getList();
}).finally(() => {
addLoading.value = false;
});
}
});
};
// 查看详情
const handleView = (row) => {
currentRecord.value = { ...row };
viewDialogVisible.value = true;
};
// 查看考勤日历
const handleCalendar = (row) => {
currentStudent.value = { ...row };
calendarMonth.value = new Date().toISOString().slice(0, 7); // 当前月份
calendarDate.value = new Date();
calendarDialogVisible.value = true;
loadCalendarData();
};
// 月份变化处理
const handleMonthChange = (month) => {
calendarMonth.value = month;
loadCalendarData();
};
// 加载日历数据
const loadCalendarData = () => {
if (!currentStudent.value || !calendarMonth.value) return;
const params = {
month: calendarMonth.value
};
getCalendar(currentStudent.value.studentId, params).then(response => {
calendarData.value = response.data || [];
});
};
// 获取考勤状态
const getAttendanceStatus = (date) => {
const record = calendarData.value.find(item =>
item.checkDate === date
);
return record ? record.status : null;
};
// 获取状态标签类型
const getStatusTagType = (status) => {
const statusMap = {
'0': 'info',
'1': 'success',
'2': 'warning',
'3': 'danger'
};
return statusMap[status] || 'info';
};
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
'0': '未签到',
'1': '正常',
'2': '迟到',
'3': '早退'
};
return statusMap[status] || '';
};
// 导出按钮操作
const handleExport = () => {
proxy.download('zsjy/internship/daily/export', {
...queryParams.value
}, `实习日常记录_${new Date().getTime()}.xlsx`);
};
// 初始化
onMounted(() => {
getList();
getStatisticsData();
});
</script>
<style scoped lang="scss">
// 继承后勤管理的样式
.page-container {
.search-btn-container {
text-align: right;
}
.button-card {
margin-bottom: 20px;
.card-header-vertical {
display: flex;
justify-content: space-between;
align-items: center;
}
.toolbar-title {
font-size: 18px;
font-weight: bold;
color: #303133;
}
.button-group {
display: flex;
align-items: center;
gap: 10px;
}
}
.table-card {
.table-container {
overflow-x: auto;
}
.pagination-container {
display: flex;
justify-content: flex-end;
padding: 16px 20px 0 0 !important;
background-color: #fff;
margin-top: 0px !important;
}
}
}
/* 统计卡片样式 */
.stat-card {
border-radius: 8px;
transition: all 0.3s ease;
height: 90px;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.stat-content {
display: flex;
align-items: center;
padding: 5px 12px;
height: 100%;
box-sizing: border-box;
.stat-icon {
font-size: 24px;
margin-right: 12px;
opacity: 0.8;
}
.stat-info {
flex: 1;
.stat-value {
font-size: 20px;
font-weight: bold;
line-height: 1;
margin-bottom: 3px;
}
.stat-label {
font-size: 13px;
opacity: 0.7;
line-height: 1;
}
}
}
&.stat-card-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
&.stat-card-success {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
&.stat-card-warning {
background: linear-gradient(135deg, #ff9a56 0%, #ffad56 100%);
color: white;
}
&.stat-card-info {
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
color: #333;
}
}
/* 表格样式 */
.scroll-table {
:deep(.el-table__header-wrapper) {
.el-table__header {
th {
background-color: #f8f9fa;
color: #333;
font-weight: 600;
}
}
}
:deep(.el-table__body-wrapper) {
.el-table__row {
&:hover {
background-color: #f5f7fa;
}
}
}
}
/* 学生信息样式 */
.student-info {
.student-name {
font-weight: 600;
color: #303133;
margin-bottom: 2px;
}
.student-number,
.student-class {
font-size: 12px;
color: #909399;
line-height: 1.2;
}
}
/* 时间详情样式 */
.time-detail {
font-size: 12px;
color: #909399;
}
.text-danger {
color: #f56c6c;
}
.text-muted {
color: #909399;
}
/* 操作按钮样式 */
.action-buttons {
display: flex;
gap: 8px;
justify-content: center;
align-items: center;
}
/* 对话框样式 */
.dialog-footer-right {
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* 详情容器样式 */
.detail-container {
padding: 8px 0;
}
/* 日历相关样式 */
.calendar-container {
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h4 {
margin: 0;
color: #303133;
}
}
.calendar-day {
height: 60px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
.day-number {
font-size: 14px;
font-weight: 500;
margin-bottom: 4px;
}
.attendance-status {
.el-tag {
font-size: 10px;
padding: 0 4px;
height: 16px;
line-height: 16px;
}
}
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.stat-card {
margin-bottom: 10px;
}
.action-buttons {
flex-direction: column;
gap: 4px;
}
}
</style>
@@ -0,0 +1,366 @@
<template>
<el-dialog
v-model="visible"
title="新增实习信息"
width="80%"
:before-close="handleClose"
draggable
destroy-on-close
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" class="form-container">
<el-row :gutter="24">
<!-- 基本信息 -->
<el-col :span="24">
<div class="form-section-title">基本信息</div>
</el-col>
<el-col :span="12">
<el-form-item label="学生姓名" prop="studentName">
<el-input v-model="form.studentName" placeholder="学生姓名" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学号" prop="studentNo">
<el-input v-model="form.studentNo" placeholder="学号" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="实习状态" prop="internshipStatus">
<el-select v-model="form.internshipStatus" placeholder="请选择实习状态" style="width: 100%">
<el-option v-for="dict in internship_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="实习津贴" prop="internshipAllowance">
<el-input-number v-model="form.internshipAllowance" :min="0" :precision="2" controls-position="right" placeholder="实习津贴" style="width: 100%" />
</el-form-item>
</el-col>
<!-- 实习单位信息 -->
<el-col :span="24">
<div class="form-section-title">实习单位信息</div>
</el-col>
<el-col :span="12">
<el-form-item label="实习单位名称" prop="companyName">
<el-input v-model="form.companyName" placeholder="请输入实习单位名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="单位联系人" prop="companyContact">
<el-input v-model="form.companyContact" placeholder="请输入单位联系人" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="单位地址" prop="companyAddress">
<el-input v-model="form.companyAddress" placeholder="请输入单位地址" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系人电话" prop="companyPhone">
<el-input v-model="form.companyPhone" placeholder="请输入联系人电话" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="行业类型" prop="companyIndustry">
<el-input v-model="form.companyIndustry" placeholder="请输入行业类型" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="单位规模" prop="companyScale">
<el-select v-model="form.companyScale" placeholder="请选择单位规模" clearable style="width: 100%">
<el-option label="20人以下" value="1" />
<el-option label="20-99人" value="2" />
<el-option label="100-499人" value="3" />
<el-option label="500-999人" value="4" />
<el-option label="1000人以上" value="5" />
</el-select>
</el-form-item>
</el-col>
<!-- 岗位信息 -->
<el-col :span="24">
<div class="form-section-title">岗位信息</div>
</el-col>
<el-col :span="12">
<el-form-item label="岗位名称" prop="internshipPosition">
<el-input v-model="form.internshipPosition" placeholder="请输入岗位名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工作时长" prop="workHours">
<el-input v-model="form.workHours" placeholder="请输入工作时长" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="岗位职责" prop="positionDuties">
<el-input v-model="form.positionDuties" placeholder="请输入岗位职责" />
</el-form-item>
</el-col>
<!-- 实习周期 -->
<el-col :span="24">
<div class="form-section-title">实习周期</div>
</el-col>
<el-col :span="12">
<el-form-item label="实习开始日期" prop="internshipStartDate">
<el-date-picker
v-model="form.internshipStartDate"
type="date"
placeholder="请选择实习开始日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="实习结束日期" prop="internshipEndDate">
<el-date-picker
v-model="form.internshipEndDate"
type="date"
placeholder="请选择实习结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
<!-- 联系方式和其他信息 -->
<el-col :span="24">
<div class="form-section-title">联系方式和其他信息</div>
</el-col>
<el-col :span="12">
<el-form-item label="单位联系人" prop="contactPersonName">
<el-input v-model="form.contactPersonName" placeholder="请输入单位联系人" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系人电话" prop="contactPersonPhone">
<el-input v-model="form.contactPersonPhone" placeholder="请输入联系人电话" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="实习协议状态" prop="agreementStatus">
<el-select v-model="form.agreementStatus" placeholder="请选择实习协议状态" style="width: 100%">
<el-option label="已签署" value="1" />
<el-option label="未签署" value="2" />
</el-select>
</el-form-item>
</el-col>
<!-- 备注 -->
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注信息" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">保存</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup name="AddDialog" lang="ts">
import { ref, reactive, watch, getCurrentInstance, ComponentInternalInstance, toRefs } from 'vue';
import { ElMessage } from 'element-plus';
import { addInternshipInfo } from '@/api/zsjy/internship';
import { InternshipInfoAddBO } from '@/api/zsjy/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { internship_status } = toRefs<any>(proxy?.useDict('internship_status'));
// Props定义
interface Props {
modelValue: boolean;
studentInfo?: {
studentId: string;
studentName: string;
studentNo: string;
};
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
studentInfo: () => ({
studentId: '',
studentName: '',
studentNo: ''
})
});
// Emits定义
const emit = defineEmits<{
'update:modelValue': [value: boolean];
'success': [];
}>();
const visible = ref(false);
const formRef = ref();
const submitLoading = ref(false);
// 表单数据
const form = reactive<InternshipInfoAddBO>({
studentId: '',
studentName: '',
studentNo: '',
internshipStatus: '1',
companyName: '',
companyAddress: '',
companyIndustry: '',
companyScale: '',
internshipPosition: '',
positionDuties: '',
internshipAllowance: 0,
workHours: '',
accommodationAddress: '',
accommodationType: '',
accommodationCost: 0,
internshipStartDate: '',
internshipEndDate: '',
contactPersonName: '',
contactPersonPhone: '',
agreementStatus: '1',
remark: ''
});
// 表单验证规则
const rules = reactive({
internshipStatus: [{ required: true, message: '请选择实习状态', trigger: 'change' }],
companyName: [{ required: true, message: '请输入实习单位名称', trigger: 'blur' }],
companyAddress: [{ required: true, message: '请输入单位地址', trigger: 'blur' }],
internshipPosition: [{ required: true, message: '请输入岗位名称', trigger: 'blur' }],
contactPersonName: [{ required: true, message: '请输入单位联系人', trigger: 'blur' }],
contactPersonPhone: [
{ required: true, message: '请输入联系人电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
internshipStartDate: [{ required: true, message: '请选择实习开始日期', trigger: 'change' }],
internshipEndDate: [{ required: true, message: '请选择实习结束日期', trigger: 'change' }],
agreementStatus: [{ required: true, message: '请选择实习协议状态', trigger: 'change' }]
});
// 监听弹窗显示状态
watch(
() => props.modelValue,
(newVal) => {
visible.value = newVal;
if (newVal && props.studentInfo) {
form.studentId = props.studentInfo.studentId;
form.studentName = props.studentInfo.studentName;
form.studentNo = props.studentInfo.studentNo;
}
}
);
// 监听visible变化,同步到父组件
watch(
() => visible.value,
(newVal) => {
emit('update:modelValue', newVal);
}
);
// 重置表单
const resetForm = () => {
Object.assign(form, {
studentId: '',
studentName: '',
studentNo: '',
internshipStatus: '1',
companyName: '',
companyAddress: '',
companyIndustry: '',
companyScale: '',
internshipPosition: '',
positionDuties: '',
internshipAllowance: 0,
workHours: '',
accommodationAddress: '',
accommodationType: '',
accommodationCost: 0,
internshipStartDate: '',
internshipEndDate: '',
contactPersonName: '',
contactPersonPhone: '',
agreementStatus: '1',
remark: ''
});
formRef.value?.clearValidate();
};
// 关闭弹窗
const handleClose = () => {
visible.value = false;
resetForm();
};
// 提交表单
const handleSubmit = async () => {
try {
await formRef.value.validate();
submitLoading.value = true;
console.log('=== 前端提交实习信息 ===');
console.log('1. 表单验证通过');
console.log('2. 提交的数据:', JSON.stringify(form, null, 2));
const response = await addInternshipInfo(form);
console.log('3. 后端响应:', response);
console.log('4. 响应状态码:', response.code);
console.log('5. 响应消息:', response.msg);
if (response.code === 200) {
console.log('6. 新增成功');
ElMessage.success('新增实习信息成功');
handleClose();
emit('success');
} else {
console.error('7. 新增失败,后端返回错误:', response.msg);
ElMessage.error(response.msg || '新增实习信息失败');
}
} catch (error) {
console.error('=== 前端提交失败 ===');
console.error('错误对象:', error);
console.error('错误信息:', error.message);
console.error('错误响应:', error.response);
ElMessage.error('新增实习信息失败');
} finally {
submitLoading.value = false;
console.log('=== 前端提交流程结束 ===');
}
};
</script>
<style lang="scss" scoped>
.form-container {
max-height: 60vh;
overflow-y: auto;
padding: 20px 0;
}
.form-section-title {
font-size: 16px;
font-weight: bold;
color: #303133;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #409eff;
}
.dialog-footer {
text-align: right;
}
:deep(.el-dialog__body) {
padding-top: 10px;
padding-bottom: 10px;
}
</style>
@@ -0,0 +1,437 @@
<template>
<el-dialog
v-model="visible"
title="编辑实习信息"
width="80%"
:before-close="handleClose"
draggable
destroy-on-close
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" class="form-container" v-loading="loading">
<el-row :gutter="24">
<!-- 基本信息 -->
<el-col :span="24">
<div class="form-section-title">基本信息</div>
</el-col>
<el-col :span="12">
<el-form-item label="学生姓名" prop="studentName">
<el-input v-model="form.studentName" placeholder="学生姓名" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学号" prop="studentNo">
<el-input v-model="form.studentNo" placeholder="学号" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="实习状态" prop="internshipStatus">
<el-select v-model="form.internshipStatus" placeholder="请选择实习状态" style="width: 100%">
<el-option v-for="dict in internship_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="实习津贴" prop="internshipAllowance">
<el-input-number v-model="form.internshipAllowance" :min="0" :precision="2" controls-position="right" placeholder="实习津贴" style="width: 100%" />
</el-form-item>
</el-col>
<!-- 实习单位信息 -->
<el-col :span="24">
<div class="form-section-title">实习单位信息</div>
</el-col>
<el-col :span="12">
<el-form-item label="实习单位名称" prop="companyName">
<el-input v-model="form.companyName" placeholder="请输入实习单位名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="单位联系人" prop="companyContact">
<el-input v-model="form.companyContact" placeholder="请输入单位联系人" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="单位地址" prop="companyAddress">
<el-input v-model="form.companyAddress" placeholder="请输入单位地址" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系人电话" prop="companyPhone">
<el-input v-model="form.companyPhone" placeholder="请输入联系人电话" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="行业类型" prop="companyIndustry">
<el-input v-model="form.companyIndustry" placeholder="请输入行业类型" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="单位规模" prop="companyScale">
<el-select v-model="form.companyScale" placeholder="请选择单位规模" clearable style="width: 100%">
<el-option label="20人以下" value="1" />
<el-option label="20-99人" value="2" />
<el-option label="100-499人" value="3" />
<el-option label="500-999人" value="4" />
<el-option label="1000人以上" value="5" />
</el-select>
</el-form-item>
</el-col>
<!-- 岗位信息 -->
<el-col :span="24">
<div class="form-section-title">岗位信息</div>
</el-col>
<!-- 岗位名称 -->
<el-col :span="12">
<el-form-item label="岗位名称" prop="internshipPosition">
<el-input v-model="form.internshipPosition" placeholder="请输入岗位名称" />
</el-form-item>
</el-col>
<!-- 岗位职责 -->
<el-col :span="12">
<el-form-item label="岗位职责" prop="positionDuties">
<el-input v-model="form.positionDuties" placeholder="请输入岗位职责" />
</el-form-item>
</el-col>
<!-- 实习津贴 -->
<el-col :span="12">
<el-form-item label="实习津贴" prop="internshipAllowance">
<el-input-number v-model="form.internshipAllowance" :min="0" :precision="2" placeholder="请输入实习津贴" style="width: 100%" />
</el-form-item>
</el-col>
<!-- 工作时长 -->
<el-col :span="12">
<el-form-item label="工作时长" prop="workHours">
<el-input v-model="form.workHours" placeholder="请输入工作时长" />
</el-form-item>
</el-col>
</el-row>
<!-- 第四分区:实习时间信息 -->
<el-divider content-position="left">实习时间信息</el-divider>
<el-row :gutter="20">
<!-- 实习开始日期 -->
<el-col :span="12">
<el-form-item label="实习开始日期" prop="internshipStartDate">
<el-date-picker
v-model="form.internshipStartDate"
type="date"
placeholder="请选择实习开始日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
<!-- 实习结束日期 -->
<el-col :span="12">
<el-form-item label="实习结束日期" prop="internshipEndDate">
<el-date-picker
v-model="form.internshipEndDate"
type="date"
placeholder="请选择实习结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
<!-- 实际结束日期 -->
<el-col :span="12">
<el-form-item label="实际结束日期" prop="actualEndDate">
<el-date-picker
v-model="form.actualEndDate"
type="date"
placeholder="请选择实际结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 第五分区:联系方式和其他信息 -->
<el-divider content-position="left">联系方式和其他信息</el-divider>
<el-row :gutter="20">
<!-- 单位联系人 -->
<el-col :span="12">
<el-form-item label="单位联系人" prop="contactPersonName">
<el-input v-model="form.contactPersonName" placeholder="请输入单位联系人" />
</el-form-item>
</el-col>
<!-- 联系人电话 -->
<el-col :span="12">
<el-form-item label="联系人电话" prop="contactPersonPhone">
<el-input v-model="form.contactPersonPhone" placeholder="请输入联系人电话" />
</el-form-item>
</el-col>
<!-- 实习协议状态 -->
<el-col :span="12">
<el-form-item label="实习协议状态" prop="agreementStatus">
<el-select v-model="form.agreementStatus" placeholder="请选择实习协议状态" style="width: 100%">
<el-option label="已签署" value="1" />
<el-option label="未签署" value="2" />
</el-select>
</el-form-item>
</el-col>
<!-- 备注 -->
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注信息" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">保存</el-button>
<el-button type="danger" @click="handleDelete">删除</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup name="EditDialog" lang="ts">
import { ref, reactive, watch, getCurrentInstance, ComponentInternalInstance, toRefs } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { getInternshipInfo, updateInternshipInfo, delInternshipInfo } from '@/api/zsjy/internship';
import { InternshipInfoEditBO } from '@/api/zsjy/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { internship_status } = toRefs<any>(proxy?.useDict('internship_status'));
// Props定义
interface Props {
modelValue: boolean;
studentId?: string;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
studentId: ''
});
// Emits定义
const emit = defineEmits<{
'update:modelValue': [value: boolean];
'success': [];
}>();
const visible = ref(false);
const formRef = ref();
const loading = ref(false);
const submitLoading = ref(false);
// 表单数据
const form = reactive<InternshipInfoEditBO>({
internshipId: '',
studentId: '',
studentName: '',
studentNo: '',
internshipStatus: '1',
companyName: '',
companyAddress: '',
companyIndustry: '',
companyScale: '',
internshipPosition: '',
positionDuties: '',
internshipAllowance: 0,
workHours: '',
accommodationAddress: '',
accommodationType: '',
accommodationCost: 0,
internshipStartDate: '',
internshipEndDate: '',
contactPersonName: '',
contactPersonPhone: '',
agreementStatus: '1',
actualEndDate: '',
remark: ''
});
// 表单验证规则
const rules = reactive({
internshipStatus: [{ required: true, message: '请选择实习状态', trigger: 'change' }],
companyName: [{ required: true, message: '请输入实习单位名称', trigger: 'blur' }],
companyAddress: [{ required: true, message: '请输入单位地址', trigger: 'blur' }],
internshipPosition: [{ required: true, message: '请输入岗位名称', trigger: 'blur' }],
contactPersonName: [{ required: true, message: '请输入单位联系人', trigger: 'blur' }],
contactPersonPhone: [
{ required: true, message: '请输入联系人电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
internshipStartDate: [{ required: true, message: '请选择实习开始日期', trigger: 'change' }],
internshipEndDate: [{ required: true, message: '请选择实习结束日期', trigger: 'change' }],
agreementStatus: [{ required: true, message: '请选择实习协议状态', trigger: 'change' }]
});
// 监听弹窗显示状态
watch(() => props.modelValue, (newVal) => {
visible.value = newVal;
if (newVal && props.studentId) {
getInternshipDetail();
}
});
watch(visible, (newVal) => {
emit('update:modelValue', newVal);
});
// 获取实习信息详情
const getInternshipDetail = async () => {
if (!props.studentId) {
ElMessage.error('参数错误');
return;
}
loading.value = true;
try {
const res = await getInternshipInfo(props.studentId);
const data = res.data;
// 填充表单数据
Object.assign(form, {
internshipId: data.internshipId,
studentId: data.studentId,
studentName: data.studentName,
studentNo: data.studentNo,
internshipStatus: data.internshipStatus,
companyName: data.companyName,
companyAddress: data.companyAddress,
companyIndustry: data.companyIndustry,
companyScale: data.companyScale,
internshipPosition: data.internshipPosition || data.positionName,
positionDuties: data.positionDuties,
internshipAllowance: data.internshipAllowance,
workHours: data.workHours,
accommodationAddress: data.accommodationAddress,
accommodationType: data.accommodationType,
accommodationCost: data.accommodationCost,
internshipStartDate: data.internshipStartDate || data.startDate,
internshipEndDate: data.internshipEndDate || data.endDate,
contactPersonName: data.contactPersonName || data.companyContact,
contactPersonPhone: data.contactPersonPhone || data.companyPhone,
agreementStatus: data.agreementStatus || '1',
actualEndDate: data.actualEndDate,
remark: data.remark
});
} catch (error) {
console.error('获取实习信息失败:', error);
ElMessage.error('获取实习信息失败');
handleClose();
} finally {
loading.value = false;
}
};
// 重置表单
const resetForm = () => {
Object.assign(form, {
internshipId: '',
studentId: '',
studentName: '',
studentNo: '',
internshipStatus: '1',
companyName: '',
companyAddress: '',
companyIndustry: '',
companyScale: '',
companyContact: '',
companyPhone: '',
positionName: '',
positionDuties: '',
internshipAllowance: 0,
workHours: '',
accommodationAddress: '',
accommodationType: '',
accommodationCost: 0,
startDate: '',
endDate: '',
actualEndDate: '',
remark: ''
});
formRef.value?.clearValidate();
};
// 关闭弹窗
const handleClose = () => {
visible.value = false;
resetForm();
};
// 提交表单
const handleSubmit = async () => {
try {
await formRef.value.validate();
submitLoading.value = true;
await updateInternshipInfo(form);
ElMessage.success('修改实习信息成功');
handleClose();
emit('success');
} catch (error) {
console.error('修改实习信息失败:', error);
ElMessage.error('修改实习信息失败');
} finally {
submitLoading.value = false;
}
};
// 删除实习信息
const handleDelete = async () => {
try {
await ElMessageBox.confirm('确认删除该实习信息?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
await delInternshipInfo(form.internshipId);
ElMessage.success('删除实习信息成功');
handleClose();
emit('success');
} catch (error) {
if (error === 'cancel') return;
console.error('删除实习信息失败:', error);
ElMessage.error('删除实习信息失败');
}
};
</script>
<style lang="scss" scoped>
.form-container {
max-height: 60vh;
overflow-y: auto;
padding: 20px 0;
}
.form-section-title {
font-size: 16px;
font-weight: bold;
color: #303133;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #409eff;
}
.dialog-footer {
text-align: right;
}
:deep(.el-dialog__body) {
padding-top: 10px;
padding-bottom: 10px;
}
</style>
@@ -93,7 +93,7 @@
<div class="statistics-card internshiping">
<div class="statistics-content">
<div class="statistics-number">{{ statisticsData.internshipCount || 0 }}</div>
<div class="statistics-label">实习中</div>
<div class="statistics-label">已实习</div>
</div>
<div class="statistics-icon">
<el-icon><Loading /></el-icon>
@@ -103,9 +103,9 @@
<el-col :span="6">
<div class="statistics-card completed">
<div class="statistics-content">
<div class="statistics-number">{{ statisticsData.internshipCount || 0 }}</div>
<div class="statistics-label">已完成</div>
<div class="statistics-rate">实习率:{{ statisticsData.internshipRate || '0.00%' }}</div>
<div class="statistics-number">{{ statisticsData.internshipRate || '0.00%' }}</div>
<div class="statistics-label">实习率</div>
<div class="statistics-description">已实习学生占比</div>
</div>
<div class="statistics-icon">
<el-icon><CircleCheck /></el-icon>
@@ -194,17 +194,17 @@
<!-- 学生实习详情对话框 -->
<el-dialog title="学生实习详情" v-model="dialog.visible" width="800px" append-to-body>
<el-dialog title="学生实习详情" v-model="dialog.visible" width="900px" append-to-body>
<div v-if="studentDetail" class="student-detail">
<el-descriptions title="基本信息" :column="2" border>
<el-descriptions-item label="学号">{{ studentDetail.studentNo }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ studentDetail.studentName }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ studentDetail.gender }}</el-descriptions-item>
<el-descriptions-item label="手机号">{{ studentDetail.mobile }}</el-descriptions-item>
<el-descriptions-item label="手机号">{{ studentDetail.phone }}</el-descriptions-item>
<el-descriptions-item label="学院">{{ studentDetail.collegeName }}</el-descriptions-item>
<el-descriptions-item label="专业">{{ studentDetail.majorName }}</el-descriptions-item>
<el-descriptions-item label="班级">{{ studentDetail.className }}</el-descriptions-item>
<el-descriptions-item label="班主任">{{ studentDetail.classMasterName }}</el-descriptions-item>
<el-descriptions-item label="班主任">{{ studentDetail.classTeacherName }}</el-descriptions-item>
</el-descriptions>
<el-descriptions title="实习信息" :column="2" border class="mt-4">
@@ -212,17 +212,29 @@
<span v-if="studentDetail.internshipStatus === 'null' || !studentDetail.internshipStatus" class="status-tag not-started">未实习</span>
<dict-tag v-else :options="internship_status" :value="studentDetail.internshipStatus" />
</el-descriptions-item>
<el-descriptions-item label="实习公司">{{ studentDetail.internshipCompany || '-' }}</el-descriptions-item>
<el-descriptions-item label="实习公司">{{ studentDetail.companyName || '-' }}</el-descriptions-item>
<el-descriptions-item label="实习岗位">{{ studentDetail.internshipPosition || '-' }}</el-descriptions-item>
<el-descriptions-item label="实习地点">{{ studentDetail.internshipLocation || '-' }}</el-descriptions-item>
<el-descriptions-item label="实习地点">{{ studentDetail.companyAddress || '-' }}</el-descriptions-item>
<el-descriptions-item label="实习开始时间">
<span v-if="studentDetail.internshipStartTime">{{ parseTime(studentDetail.internshipStartTime, '{y}-{m}-{d}') }}</span>
<span v-if="studentDetail.internshipStartDate">{{ parseTime(studentDetail.internshipStartDate, '{y}-{m}-{d}') }}</span>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="实习结束时间">
<span v-if="studentDetail.internshipEndTime">{{ parseTime(studentDetail.internshipEndTime, '{y}-{m}-{d}') }}</span>
<span v-if="studentDetail.internshipEndDate">{{ parseTime(studentDetail.internshipEndDate, '{y}-{m}-{d}') }}</span>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="单位联系人">{{ studentDetail.contactPersonName || '-' }}</el-descriptions-item>
<el-descriptions-item label="联系人电话">{{ studentDetail.contactPersonPhone || '-' }}</el-descriptions-item>
<el-descriptions-item label="实习津贴">
<span v-if="studentDetail.internshipAllowance && studentDetail.internshipAllowance > 0">{{ studentDetail.internshipAllowance }}元/月</span>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="协议状态">
<el-tag v-if="studentDetail.agreementStatus === '1'" type="success">已签署</el-tag>
<el-tag v-else-if="studentDetail.agreementStatus === '2'" type="warning">未签署</el-tag>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ studentDetail.remark || '-' }}</el-descriptions-item>
</el-descriptions>
</div>
<template #footer>
@@ -498,6 +510,12 @@ const cancel = () => {
opacity: 0.8;
margin-top: 2px;
}
.statistics-description {
font-size: 12px;
opacity: 0.8;
margin-top: 2px;
}
}
.statistics-icon {