Просмотр исходного кода

Merge branch 'master' of http://47.99.164.66:3000/hzdyjs-xg/all-project

zlwu 1 год назад
Родитель
Сommit
c00199b2cf
29 измененных файлов с 3713 добавлено и 340 удалено
  1. 18 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/ZsjyInternshipDailyController.java
  2. 131 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/ZsjyInternshipVisitController.java
  3. 173 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/ZsjyInternshipVisit.java
  4. 6 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/bo/ZsjyInternshipDailyQueryBo.java
  5. 131 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/bo/ZsjyInternshipVisitBo.java
  6. 86 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/bo/ZsjyInternshipVisitQueryBo.java
  7. 201 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/vo/ZsjyInternshipVisitVo.java
  8. 20 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/mapper/ZsjyInternshipDailyMapper.java
  9. 40 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/mapper/ZsjyInternshipVisitMapper.java
  10. 10 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/IZsjyInternshipDailyService.java
  11. 54 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/IZsjyInternshipVisitService.java
  12. 104 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/impl/ZsjyInternshipDailyServiceImpl.java
  13. 116 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/impl/ZsjyInternshipVisitServiceImpl.java
  14. 56 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/resources/mapper/ZsjyInternshipDailyMapper.xml
  15. 162 0
      backend/ruoyi-modules/ruoyi-zsjy/src/main/resources/mapper/ZsjyInternshipVisitMapper.xml
  16. 1 1
      mobile/src/api/internship.js
  17. 751 191
      mobile/src/pages/internship/index.vue
  18. 6 6
      web/src/api/zsjy/internshipDaily.js
  19. 85 0
      web/src/api/zsjy/internshipVisit.ts
  20. 290 0
      web/src/api/zsjy/types.ts
  21. 88 4
      web/src/components/ImageUpload/index.vue
  22. 15 2
      web/src/views/jcbt/workstudy/xgreview/README.md
  23. 35 10
      web/src/views/jcbt/workstudy/xgreview/开发总结.md
  24. 3 3
      web/src/views/zsjy/internship/daily/checkin.vue
  25. 44 85
      web/src/views/zsjy/internship/daily/index.vue
  26. 9 10
      web/src/views/zsjy/internship/msgmanage/components/AddDialog.vue
  27. 17 15
      web/src/views/zsjy/internship/msgmanage/components/EditDialog.vue
  28. 5 13
      web/src/views/zsjy/internship/msgmanage/index.vue
  29. 1056 0
      web/src/views/zsjy/internship/visit/index.vue

+ 18 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/ZsjyInternshipDailyController.java

@@ -99,4 +99,22 @@ public class ZsjyInternshipDailyController extends BaseController {
         return dailyService.getStatistics(queryBo);
     }
 
+    /**
+     * 获取学生个人月度考勤统计(移动端专用)
+     */
+    @Operation(summary = "获取学生月度统计", description = "获取指定学生的月度考勤统计数据")
+    @SaCheckPermission("zsjy:internshipdaily:statistics")
+    @GetMapping("/monthly-statistics")
+    public R<Map<String, Object>> getStudentMonthlyStatistics(
+            @Parameter(description = "学生ID", required = true) @RequestParam Long studentId,
+            @Parameter(description = "年份", required = true) @RequestParam Integer year,
+            @Parameter(description = "月份", required = true) @RequestParam Integer month) {
+
+        log.info("=== Controller层调试 ===");
+        log.info("接口被调用: /zsjy/internship/daily/monthly-statistics");
+        log.info("学生ID: {}, 年月: {}-{}", studentId, year, month);
+
+        return dailyService.getStudentMonthlyStatistics(studentId, year, month);
+    }
+
 }

+ 131 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/controller/ZsjyInternshipVisitController.java

@@ -0,0 +1,131 @@
+package org.dromara.zsjy.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Parameters;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.excel.utils.ExcelUtil;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+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.ZsjyInternshipVisitBo;
+import org.dromara.zsjy.domain.bo.ZsjyInternshipVisitQueryBo;
+import org.dromara.zsjy.domain.vo.ZsjyInternshipVisitVo;
+import org.dromara.zsjy.service.IZsjyInternshipVisitService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 班主任走访Controller
+ *
+ * @author system
+ * @date 2025-01-03
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/zsjy/internshipVisit")
+@Tag(name = "班主任走访管理", description = "班主任走访管理")
+public class ZsjyInternshipVisitController extends BaseController {
+
+    private final IZsjyInternshipVisitService zsjyInternshipVisitService;
+
+    /**
+     * 查询班主任走访列表
+     */
+    @Operation(summary = "查询班主任走访列表")
+    @SaCheckPermission("zsjy:internshipVisit:list")
+    @GetMapping("/list")
+    public TableDataInfo<ZsjyInternshipVisitVo> list(ZsjyInternshipVisitQueryBo bo, PageQuery pageQuery) {
+        return zsjyInternshipVisitService.queryPageList(bo, pageQuery);
+    }
+
+    /**
+     * 导出班主任走访列表
+     */
+    @Operation(summary = "导出班主任走访列表")
+    @SaCheckPermission("zsjy:internshipVisit:export")
+    @Log(title = "班主任走访", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(ZsjyInternshipVisitQueryBo bo, HttpServletResponse response) {
+        List<ZsjyInternshipVisitVo> list = zsjyInternshipVisitService.queryList(bo);
+        ExcelUtil.exportExcel(list, "班主任走访", ZsjyInternshipVisitVo.class, response);
+    }
+
+    /**
+     * 获取班主任走访详细信息
+     */
+    @Operation(summary = "获取班主任走访详细信息")
+    @SaCheckPermission("zsjy:internshipVisit:query")
+    @GetMapping("/{visitId}")
+    @Parameters({
+        @Parameter(name = "visitId", description = "走访记录ID", required = true)
+    })
+    public R<ZsjyInternshipVisitVo> getInfo(@NotNull(message = "走访记录ID不能为空")
+                                     @PathVariable Long visitId) {
+        return R.ok(zsjyInternshipVisitService.queryById(visitId));
+    }
+
+    /**
+     * 新增班主任走访
+     */
+    @Operation(summary = "新增班主任走访")
+    @SaCheckPermission("zsjy:internshipVisit:add")
+    @Log(title = "班主任走访", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping()
+    public R<Void> add(@Validated(AddGroup.class) @RequestBody ZsjyInternshipVisitBo bo) {
+        return toAjax(zsjyInternshipVisitService.insertByBo(bo));
+    }
+
+    /**
+     * 修改班主任走访
+     */
+    @Operation(summary = "修改班主任走访")
+    @SaCheckPermission("zsjy:internshipVisit:edit")
+    @Log(title = "班主任走访", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping()
+    public R<Void> edit(@Validated(EditGroup.class) @RequestBody ZsjyInternshipVisitBo bo) {
+        return toAjax(zsjyInternshipVisitService.updateByBo(bo));
+    }
+
+    /**
+     * 删除班主任走访
+     */
+    @Operation(summary = "删除班主任走访")
+    @SaCheckPermission("zsjy:internshipVisit:remove")
+    @Log(title = "班主任走访", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{visitIds}")
+    @Parameters({
+        @Parameter(name = "visitIds", description = "走访记录ID串", required = true)
+    })
+    public R<Void> remove(@NotEmpty(message = "走访记录ID不能为空")
+                          @PathVariable Long[] visitIds) {
+        return toAjax(zsjyInternshipVisitService.deleteWithValidByIds(List.of(visitIds), true));
+    }
+
+    /**
+     * 获取可选项列表(学生选择)
+     */
+    @Operation(summary = "获取可选项列表")
+    @SaCheckPermission("zsjy:internshipVisit:list")
+    @GetMapping("/options")
+    public R<List<ZsjyInternshipVisitVo>> getOptions() {
+        List<ZsjyInternshipVisitVo> list = zsjyInternshipVisitService.getStudentOptions();
+        return R.ok(list);
+    }
+}

+ 173 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/ZsjyInternshipVisit.java

@@ -0,0 +1,173 @@
+package org.dromara.zsjy.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+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.util.Date;
+
+/**
+ * 班主任走访实体对象 zsjy_internship_visit
+ *
+ * @author system
+ * @date 2025-01-03
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("zsjy_internship_visit")
+public class ZsjyInternshipVisit extends BaseEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 走访记录ID
+     */
+    @TableId(value = "visit_id", type = IdType.ASSIGN_ID)
+    private Long visitId;
+
+    /**
+     * 租户ID
+     */
+    @TableField("tenant_id")
+    private String tenantId;
+
+    /**
+     * 实习ID
+     */
+    @TableField("internship_id")
+    private Long internshipId;
+
+    /**
+     * 学生ID
+     */
+    @TableField("student_id")
+    private Long studentId;
+
+    /**
+     * 企业名称
+     */
+    @TableField("company_name")
+    private String companyName;
+
+    /**
+     * 企业地址
+     */
+    @TableField("company_address")
+    private String companyAddress;
+
+    /**
+     * 走访时间
+     */
+    @TableField("visit_date")
+    private Date visitDate;
+
+    /**
+     * 走访人员
+     */
+    @TableField("visitor_name")
+    private String visitorName;
+
+    /**
+     * 走访人员职务
+     */
+    @TableField("visitor_position")
+    private String visitorPosition;
+
+    /**
+     * 走访目的
+     */
+    @TableField("visit_purpose")
+    private String visitPurpose;
+
+    /**
+     * 企业情况
+     */
+    @TableField("company_situation")
+    private String companySituation;
+
+    /**
+     * 学生表现
+     */
+    @TableField("student_performance")
+    private String studentPerformance;
+
+    /**
+     * 发现问题
+     */
+    @TableField("problems_found")
+    private String problemsFound;
+
+    /**
+     * 改进建议
+     */
+    @TableField("suggestions")
+    private String suggestions;
+
+    /**
+     * 走访总结
+     */
+    @TableField("visit_summary")
+    private String visitSummary;
+
+    /**
+     * 现场照片(JSON格式)
+     */
+    @TableField("visit_images")
+    private String visitImages;
+
+    /**
+     * 走访文件附件(JSON格式)
+     */
+    @TableField("visit_files")
+    private String visitFiles;
+
+    /**
+     * 走访状态:数据库存储数字编码(0-草稿 1-已提交 2-已审核)
+     */
+    @TableField("status")
+    private String status;
+
+    /**
+     * 备注
+     */
+    @TableField("remark")
+    private String remark;
+
+    // ==================== 扩展字段(数据库中不存在,通过关联查询获取) ====================
+
+    /**
+     * 学生姓名 (通过student_id关联查询获取)
+     */
+    @TableField(exist = false)
+    private String studentName;
+
+    /**
+     * 学号 (通过student_id关联查询获取)
+     */
+    @TableField(exist = false)
+    private String studentNumber;
+
+    /**
+     * 班级名称 (通过student_id关联查询获取)
+     */
+    @TableField(exist = false)
+    private String className;
+
+    /**
+     * 专业名称 (通过student_id关联查询获取)
+     */
+    @TableField(exist = false)
+    private String majorName;
+
+    /**
+     * 学院名称 (通过student_id关联查询获取)
+     */
+    @TableField(exist = false)
+    private String collegeName;
+}

+ 6 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/bo/ZsjyInternshipDailyQueryBo.java

@@ -82,4 +82,10 @@ public class ZsjyInternshipDailyQueryBo extends BaseEntity {
     @Schema(description = "公司名称")
     private String companyName;
 
+    /**
+     * 班主任ID(用于权限控制)
+     */
+    @Schema(description = "班主任ID")
+    private Long headTeacherId;
+
 }

+ 131 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/bo/ZsjyInternshipVisitBo.java

@@ -0,0 +1,131 @@
+package org.dromara.zsjy.domain.bo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+import org.dromara.zsjy.domain.ZsjyInternshipVisit;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import java.util.Date;
+
+/**
+ * 班主任走访业务对象 ZsjyInternshipVisitBo
+ *
+ * @author system
+ * @date 2025-01-03
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = ZsjyInternshipVisit.class, reverseConvertGenerate = false)
+public class ZsjyInternshipVisitBo extends BaseEntity {
+
+    /**
+     * 走访记录ID
+     */
+    @NotNull(message = "走访记录ID不能为空", groups = { EditGroup.class })
+    private Long visitId;
+
+    /**
+     * 租户编号
+     */
+    private String tenantId;
+
+    /**
+     * 实习ID
+     */
+    @NotNull(message = "实习ID不能为空", groups = { AddGroup.class, EditGroup.class })
+    private Long internshipId;
+
+    /**
+     * 学生ID
+     */
+    @NotNull(message = "学生ID不能为空", groups = { AddGroup.class, EditGroup.class })
+    private Long studentId;
+
+    /**
+     * 企业名称
+     */
+    @NotBlank(message = "企业名称不能为空", groups = { AddGroup.class, EditGroup.class })
+    private String companyName;
+
+    /**
+     * 企业地址
+     */
+    private String companyAddress;
+
+    /**
+     * 走访时间
+     */
+    @NotNull(message = "走访时间不能为空", groups = { AddGroup.class, EditGroup.class })
+    private Date visitDate;
+
+    /**
+     * 走访人员
+     */
+    @NotBlank(message = "走访人员不能为空", groups = { AddGroup.class, EditGroup.class })
+    private String visitorName;
+
+    /**
+     * 走访人员职务
+     */
+    private String visitorPosition;
+
+    /**
+     * 走访目的
+     */
+    @NotBlank(message = "走访目的不能为空", groups = { AddGroup.class, EditGroup.class })
+    private String visitPurpose;
+
+    /**
+     * 企业情况
+     */
+    private String companySituation;
+
+    /**
+     * 学生表现
+     */
+    private String studentPerformance;
+
+    /**
+     * 发现问题
+     */
+    private String problemsFound;
+
+    /**
+     * 改进建议
+     */
+    private String suggestions;
+
+    /**
+     * 走访总结
+     */
+    private String visitSummary;
+
+    /**
+     * 现场照片(JSON格式)
+     */
+    private String visitImages;
+
+    /**
+     * 走访文件附件(JSON格式)
+     */
+    private String visitFiles;
+
+    /**
+     * 走访状态:数据库存储数字编码(0-草稿 1-已提交 2-已审核)
+     */
+    @Pattern(regexp = "^[0-2]$",
+             message = "走访状态只能是:0(草稿)、1(已提交)、2(已审核)",
+             groups = { AddGroup.class, EditGroup.class })
+    private String status;
+
+    /**
+     * 备注
+     */
+    private String remark;
+}

+ 86 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/bo/ZsjyInternshipVisitQueryBo.java

@@ -0,0 +1,86 @@
+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.util.Date;
+
+/**
+ * 班主任走访查询业务对象 ZsjyInternshipVisitQueryBo
+ *
+ * @author system
+ * @date 2025-01-03
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Schema(description = "班主任走访查询业务对象")
+public class ZsjyInternshipVisitQueryBo extends BaseEntity {
+
+    /**
+     * 走访记录ID
+     */
+    @Schema(description = "走访记录ID")
+    private Long visitId;
+
+    /**
+     * 实习ID
+     */
+    @Schema(description = "实习ID")
+    private Long internshipId;
+
+    /**
+     * 学生ID
+     */
+    @Schema(description = "学生ID")
+    private Long studentId;
+
+    /**
+     * 学生姓名
+     */
+    @Schema(description = "学生姓名")
+    private String studentName;
+
+    /**
+     * 学号
+     */
+    @Schema(description = "学号")
+    private String studentNumber;
+
+    /**
+     * 班级名称
+     */
+    @Schema(description = "班级名称")
+    private String className;
+
+    /**
+     * 企业名称
+     */
+    @Schema(description = "企业名称")
+    private String companyName;
+
+    /**
+     * 走访人员
+     */
+    @Schema(description = "走访人员")
+    private String visitorName;
+
+    /**
+     * 走访状态:数据库存储数字编码(0-草稿 1-已提交 2-已审核),前端通过字典显示为中文
+     */
+    @Schema(description = "走访状态")
+    private String status;
+
+    /**
+     * 走访开始时间
+     */
+    @Schema(description = "走访开始时间")
+    private Date visitStartDate;
+
+    /**
+     * 走访结束时间
+     */
+    @Schema(description = "走访结束时间")
+    private Date visitEndDate;
+}

+ 201 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/domain/vo/ZsjyInternshipVisitVo.java

@@ -0,0 +1,201 @@
+package org.dromara.zsjy.domain.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.github.linpeilie.annotations.AutoMapper;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.dromara.zsjy.domain.ZsjyInternshipVisit;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 班主任走访视图对象 ZsjyInternshipVisitVo
+ *
+ * @author system
+ * @date 2025-01-03
+ */
+@Data
+@AutoMapper(target = ZsjyInternshipVisit.class)
+@Schema(description = "班主任走访视图对象")
+public class ZsjyInternshipVisitVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 走访记录ID
+     */
+    @Schema(description = "走访记录ID")
+    private Long visitId;
+
+    /**
+     * 租户编号
+     */
+    @Schema(description = "租户编号")
+    private String tenantId;
+
+    /**
+     * 实习ID
+     */
+    @Schema(description = "实习ID")
+    private Long internshipId;
+
+    /**
+     * 学生ID
+     */
+    @Schema(description = "学生ID")
+    private Long studentId;
+
+    /**
+     * 企业名称
+     */
+    @Schema(description = "企业名称")
+    private String companyName;
+
+    /**
+     * 企业地址
+     */
+    @Schema(description = "企业地址")
+    private String companyAddress;
+
+    /**
+     * 走访时间
+     */
+    @Schema(description = "走访时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date visitDate;
+
+    /**
+     * 走访人员
+     */
+    @Schema(description = "走访人员")
+    private String visitorName;
+
+    /**
+     * 走访人员职务
+     */
+    @Schema(description = "走访人员职务")
+    private String visitorPosition;
+
+    /**
+     * 走访目的
+     */
+    @Schema(description = "走访目的")
+    private String visitPurpose;
+
+    /**
+     * 企业情况
+     */
+    @Schema(description = "企业情况")
+    private String companySituation;
+
+    /**
+     * 学生表现
+     */
+    @Schema(description = "学生表现")
+    private String studentPerformance;
+
+    /**
+     * 发现问题
+     */
+    @Schema(description = "发现问题")
+    private String problemsFound;
+
+    /**
+     * 改进建议
+     */
+    @Schema(description = "改进建议")
+    private String suggestions;
+
+    /**
+     * 走访总结
+     */
+    @Schema(description = "走访总结")
+    private String visitSummary;
+
+    /**
+     * 现场照片(JSON格式)
+     */
+    @Schema(description = "现场照片")
+    private String visitImages;
+
+    /**
+     * 走访文件附件(JSON格式)
+     */
+    @Schema(description = "走访文件附件")
+    private String visitFiles;
+
+    /**
+     * 走访状态:数据库存储数字编码(0-草稿 1-已提交 2-已审核),前端通过字典显示为中文
+     */
+    @Schema(description = "走访状态")
+    private String status;
+
+    /**
+     * 备注
+     */
+    @Schema(description = "备注")
+    private String remark;
+
+    // ==================== 关联查询字段 ====================
+
+    /**
+     * 学生姓名
+     */
+    @Schema(description = "学生姓名")
+    private String studentName;
+
+    /**
+     * 学号
+     */
+    @Schema(description = "学号")
+    private String studentNumber;
+
+    /**
+     * 班级名称
+     */
+    @Schema(description = "班级名称")
+    private String className;
+
+    /**
+     * 专业名称
+     */
+    @Schema(description = "专业名称")
+    private String majorName;
+
+    /**
+     * 学院名称
+     */
+    @Schema(description = "学院名称")
+    private String collegeName;
+
+    // ==================== 统一基础字段(强制性,避免MyBatis映射错误) ====================
+
+    /**
+     * 创建者
+     */
+    @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;
+}

+ 20 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/mapper/ZsjyInternshipDailyMapper.java

@@ -61,4 +61,24 @@ public interface ZsjyInternshipDailyMapper extends BaseMapper<ZsjyInternshipDail
     boolean existsByStudentAndDate(@Param("studentId") Long studentId,
                                   @Param("checkDate") Date checkDate);
 
+    /**
+     * 获取学生个人月度考勤统计
+     *
+     * @param studentId 学生ID
+     * @param year 年份
+     * @param month 月份
+     * @return 学生月度统计数据
+     */
+    Map<String, Object> selectStudentMonthlyStatistics(@Param("studentId") Long studentId,
+                                                      @Param("year") Integer year,
+                                                      @Param("month") Integer month);
+
+    /**
+     * 统计班主任管理的班级数量
+     *
+     * @param teacherId 班主任ID
+     * @return 班级数量
+     */
+    Long countClassesByTeacherId(@Param("teacherId") Long teacherId);
+
 }

+ 40 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/mapper/ZsjyInternshipVisitMapper.java

@@ -0,0 +1,40 @@
+package org.dromara.zsjy.mapper;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.zsjy.domain.ZsjyInternshipVisit;
+import org.dromara.zsjy.domain.bo.ZsjyInternshipVisitQueryBo;
+import org.dromara.zsjy.domain.vo.ZsjyInternshipVisitVo;
+
+import java.util.List;
+
+/**
+ * 班主任走访Mapper接口
+ *
+ * @author system
+ * @date 2025-01-03
+ */
+public interface ZsjyInternshipVisitMapper extends BaseMapperPlus<ZsjyInternshipVisit, ZsjyInternshipVisitVo> {
+
+    /**
+     * 根据ID查询班主任走访详情(包含关联学生信息)
+     * @param visitId 走访记录ID
+     * @return 走访详情VO
+     */
+    ZsjyInternshipVisitVo selectById(Long visitId);
+
+    /**
+     * 查询班主任走访列表(包含关联学生信息)
+     * @param bo 查询条件
+     * @return 走访记录列表
+     */
+    List<ZsjyInternshipVisitVo> queryList(ZsjyInternshipVisitQueryBo bo);
+
+    /**
+     * 分页查询班主任走访列表(包含关联学生信息)
+     * @param page 分页参数
+     * @param bo 查询条件
+     * @return 走访记录分页结果
+     */
+    Page<ZsjyInternshipVisitVo> queryPageList(Page<?> page, ZsjyInternshipVisitQueryBo bo);
+}

+ 10 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/IZsjyInternshipDailyService.java

@@ -61,4 +61,14 @@ public interface IZsjyInternshipDailyService {
      */
     R<Map<String, Object>> getStatistics(ZsjyInternshipDailyQueryBo queryBo);
 
+    /**
+     * 获取学生个人月度考勤统计
+     *
+     * @param studentId 学生ID
+     * @param year 年份
+     * @param month 月份
+     * @return 学生月度统计数据
+     */
+    R<Map<String, Object>> getStudentMonthlyStatistics(Long studentId, Integer year, Integer month);
+
 }

+ 54 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/IZsjyInternshipVisitService.java

@@ -0,0 +1,54 @@
+package org.dromara.zsjy.service;
+
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.zsjy.domain.bo.ZsjyInternshipVisitBo;
+import org.dromara.zsjy.domain.bo.ZsjyInternshipVisitQueryBo;
+import org.dromara.zsjy.domain.vo.ZsjyInternshipVisitVo;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 班主任走访Service接口
+ *
+ * @author system
+ * @date 2025-01-03
+ */
+public interface IZsjyInternshipVisitService {
+
+    /**
+     * 查询班主任走访
+     */
+    ZsjyInternshipVisitVo queryById(Long visitId);
+
+    /**
+     * 查询班主任走访列表
+     */
+    TableDataInfo<ZsjyInternshipVisitVo> queryPageList(ZsjyInternshipVisitQueryBo bo, PageQuery pageQuery);
+
+    /**
+     * 查询班主任走访列表
+     */
+    List<ZsjyInternshipVisitVo> queryList(ZsjyInternshipVisitQueryBo bo);
+
+    /**
+     * 新增班主任走访
+     */
+    Boolean insertByBo(ZsjyInternshipVisitBo bo);
+
+    /**
+     * 修改班主任走访
+     */
+    Boolean updateByBo(ZsjyInternshipVisitBo bo);
+
+    /**
+     * 校验并批量删除班主任走访信息
+     */
+    Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
+
+    /**
+     * 获取学生选项列表(用于下拉选择)
+     */
+    List<ZsjyInternshipVisitVo> getStudentOptions();
+}

+ 104 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/impl/ZsjyInternshipDailyServiceImpl.java

@@ -48,8 +48,19 @@ public class ZsjyInternshipDailyServiceImpl implements IZsjyInternshipDailyServi
      */
     @Override
     public TableDataInfo<ZsjyInternshipDailyVo> queryPageList(ZsjyInternshipDailyQueryBo queryBo, PageQuery pageQuery) {
+        log.info("=== 实习日常查询后端调试日志 ===");
+        log.info("1. 查询参数: {}", queryBo);
+        log.info("2. 分页参数: pageNum={}, pageSize={}", pageQuery.getPageNum(), pageQuery.getPageSize());
+
+        // 设置数据权限
+        setDataPermission(queryBo);
+        log.info("3. 设置数据权限后的查询参数: {}", queryBo);
+        log.info("3.1. 权限控制参数: headTeacherId={}", queryBo.getHeadTeacherId());
+
         Page<ZsjyInternshipDailyVo> page = pageQuery.build();
         List<ZsjyInternshipDailyVo> list = baseMapper.selectDailyList(page, queryBo);
+        log.info("4. 查询到的数据条数: {}", list != null ? list.size() : "null");
+
         return TableDataInfo.build(list);
     }
 
@@ -58,6 +69,9 @@ public class ZsjyInternshipDailyServiceImpl implements IZsjyInternshipDailyServi
      */
     @Override
     public List<ZsjyInternshipDailyVo> queryList(ZsjyInternshipDailyQueryBo queryBo) {
+        // 设置数据权限
+        setDataPermission(queryBo);
+
         return baseMapper.selectDailyList(null, queryBo);
     }
 
@@ -202,4 +216,94 @@ public class ZsjyInternshipDailyServiceImpl implements IZsjyInternshipDailyServi
         }
     }
 
+    /**
+     * 获取学生个人月度考勤统计
+     */
+    @Override
+    public R<Map<String, Object>> getStudentMonthlyStatistics(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("年月参数不能为空");
+            }
+            if (year < 2020 || year > 2030) {
+                return R.fail("年份参数不合法");
+            }
+            if (month < 1 || month > 12) {
+                return R.fail("月份参数不合法");
+            }
+
+            // 2. 查询学生月度统计数据
+            Map<String, Object> statistics = baseMapper.selectStudentMonthlyStatistics(studentId, year, month);
+
+            // 3. 补充默认值
+            if (ObjectUtil.isNull(statistics)) {
+                statistics = new HashMap<>();
+                statistics.put("totalDays", 0);
+                statistics.put("checkedDays", 0);
+                statistics.put("missedDays", 0);
+                statistics.put("exceptionDays", 0);
+            }
+
+            // 4. 确保数据类型正确
+            statistics.put("totalDays", ObjectUtil.defaultIfNull(statistics.get("totalDays"), 0));
+            statistics.put("checkedDays", ObjectUtil.defaultIfNull(statistics.get("checkedDays"), 0));
+            statistics.put("missedDays", ObjectUtil.defaultIfNull(statistics.get("missedDays"), 0));
+            statistics.put("exceptionDays", ObjectUtil.defaultIfNull(statistics.get("exceptionDays"), 0));
+
+            log.info("学生{}的{}-{}月度统计: {}", studentId, year, month, statistics);
+            return R.ok(statistics);
+
+        } catch (Exception e) {
+            log.error("获取学生月度统计数据失败: studentId={}, year={}, month={}, error={}",
+                     studentId, year, month, e.getMessage(), e);
+            return R.fail("获取学生月度统计数据失败: " + e.getMessage());
+        }
+    }
+
+        /**
+     * 设置数据权限(班主任只能查看自己班级的学生数据)
+     */
+    private void setDataPermission(ZsjyInternshipDailyQueryBo queryBo) {
+        try {
+            // 获取当前登录用户ID
+            Long currentUserId = LoginHelper.getUserId();
+            log.info("=== 权限控制调试 ===");
+            log.info("当前登录用户ID: {}", currentUserId);
+
+            if (ObjectUtil.isNotNull(currentUserId)) {
+                // 检查该用户是否是班主任
+                Long classTeacherCount = baseMapper.countClassesByTeacherId(currentUserId);
+                log.info("用户ID {}作为班主任管理的班级数量: {}", currentUserId, classTeacherCount);
+
+                if (classTeacherCount > 0) {
+                    // 是班主任,设置权限限制
+                    queryBo.setHeadTeacherId(currentUserId);
+                    log.info("✅ 权限判断:用户是班主任,设置headTeacherId={}", currentUserId);
+                } else {
+                    // 不是班主任,设置不存在的班主任ID,使其无法查看任何数据
+                    log.info("❌ 权限判断:用户不是班主任,设置权限限制");
+
+                    // 设置一个不存在的班主任ID(-1),这样SQL查询结果为空
+                    queryBo.setHeadTeacherId(-1L);
+                    log.info("🔒 设置数据权限:非班主任用户无法查看数据,headTeacherId=-1");
+                }
+
+                // 输出最终的权限设置
+                log.info("🎯 最终权限设置:queryBo.headTeacherId = {}", queryBo.getHeadTeacherId());
+            } else {
+                log.warn("⚠️ 未获取到当前用户ID,设置权限限制");
+                queryBo.setHeadTeacherId(-1L);
+            }
+        } catch (Exception e) {
+            log.error("❌ 设置数据权限失败:{}", e.getMessage(), e);
+            // 异常情况下,设置权限限制以确保数据安全
+            queryBo.setHeadTeacherId(-1L);
+            log.warn("🔒 异常情况下设置权限限制,headTeacherId=-1");
+        }
+    }
+
 }

+ 116 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/java/org/dromara/zsjy/service/impl/ZsjyInternshipVisitServiceImpl.java

@@ -0,0 +1,116 @@
+package org.dromara.zsjy.service.impl;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.zsjy.domain.ZsjyInternshipVisit;
+import org.dromara.zsjy.domain.bo.ZsjyInternshipVisitBo;
+import org.dromara.zsjy.domain.bo.ZsjyInternshipVisitQueryBo;
+import org.dromara.zsjy.domain.vo.ZsjyInternshipVisitVo;
+import org.dromara.zsjy.mapper.ZsjyInternshipVisitMapper;
+import org.dromara.zsjy.service.IZsjyInternshipVisitService;
+import org.springframework.stereotype.Service;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 班主任走访Service业务层处理
+ *
+ * @author system
+ * @date 2025-01-03
+ */
+@RequiredArgsConstructor
+@Service
+public class ZsjyInternshipVisitServiceImpl implements IZsjyInternshipVisitService {
+
+    private final ZsjyInternshipVisitMapper baseMapper;
+
+    /**
+     * 查询班主任走访
+     */
+    @Override
+    public ZsjyInternshipVisitVo queryById(Long visitId){
+        return baseMapper.selectById(visitId);
+    }
+
+        /**
+     * 查询班主任走访列表
+     */
+    @Override
+    public TableDataInfo<ZsjyInternshipVisitVo> queryPageList(ZsjyInternshipVisitQueryBo bo, PageQuery pageQuery) {
+        // 使用自定义分页查询方法获取包含学生信息的完整数据
+        Page<ZsjyInternshipVisitVo> result = baseMapper.queryPageList(pageQuery.build(), bo);
+        return TableDataInfo.build(result);
+    }
+
+    /**
+     * 查询班主任走访列表
+     */
+    @Override
+    public List<ZsjyInternshipVisitVo> queryList(ZsjyInternshipVisitQueryBo bo) {
+        // 使用自定义查询方法获取包含学生信息的完整数据
+        return baseMapper.queryList(bo);
+    }
+
+    /**
+     * 新增班主任走访
+     */
+    @Override
+    public Boolean insertByBo(ZsjyInternshipVisitBo bo) {
+        ZsjyInternshipVisit add = MapstructUtils.convert(bo, ZsjyInternshipVisit.class);
+        validEntityBeforeSave(add);
+        boolean flag = baseMapper.insert(add) > 0;
+        if (flag) {
+            bo.setVisitId(add.getVisitId());
+        }
+        return flag;
+    }
+
+    /**
+     * 修改班主任走访
+     */
+    @Override
+    public Boolean updateByBo(ZsjyInternshipVisitBo bo) {
+        ZsjyInternshipVisit update = MapstructUtils.convert(bo, ZsjyInternshipVisit.class);
+        validEntityBeforeSave(update);
+        return baseMapper.updateById(update) > 0;
+    }
+
+    /**
+     * 保存前的数据校验
+     */
+    private void validEntityBeforeSave(ZsjyInternshipVisit entity){
+        // TODO 做一些数据校验,如唯一约束
+    }
+
+    /**
+     * 批量删除班主任走访
+     */
+    @Override
+    public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
+        if(isValid){
+            // TODO 做一些业务上的校验,判断是否需要校验
+        }
+        return baseMapper.deleteByIds(ids) > 0;
+    }
+
+    /**
+     * 获取学生选项列表(用于下拉选择)
+     */
+    @Override
+    public List<ZsjyInternshipVisitVo> getStudentOptions() {
+        // TODO 实现获取有实习信息的学生列表
+        LambdaQueryWrapper<ZsjyInternshipVisit> lqw = Wrappers.lambdaQuery();
+        lqw.select(ZsjyInternshipVisit::getStudentId, ZsjyInternshipVisit::getInternshipId);
+        lqw.groupBy(ZsjyInternshipVisit::getStudentId);
+        return baseMapper.selectVoList(lqw);
+    }
+}

+ 56 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/resources/mapper/ZsjyInternshipDailyMapper.xml

@@ -104,6 +104,15 @@
             <if test="queryBo.companyName != null and queryBo.companyName != ''">
                 AND i.company_name LIKE CONCAT('%', #{queryBo.companyName}, '%')
             </if>
+            <!-- 权限控制:班主任只能查看自己班级的学生数据 -->
+            <if test="queryBo.headTeacherId != null">
+                AND EXISTS (
+                    SELECT 1 FROM xg_student_msg s
+                    WHERE s.id = d.student_id
+                      AND s.class_teacher_id = #{queryBo.headTeacherId}
+                      AND s.deleted = 0
+                )
+            </if>
         </where>
         ORDER BY d.check_date DESC, d.create_time DESC
     </select>
@@ -175,6 +184,44 @@
             ) as attendanceRate
     </select>
 
+        <!-- 获取学生个人月度考勤统计 -->
+    <select id="selectStudentMonthlyStatistics" resultType="java.util.Map">
+        <![CDATA[
+        SELECT
+            -- 本月工作日总天数(简化计算,按22天计算)
+            22 as totalDays,
+
+            -- 已签到天数
+            (SELECT COUNT(*)
+             FROM zsjy_internship_daily
+             WHERE student_id = #{studentId}
+               AND deleted = 0
+               AND YEAR(check_date) = #{year}
+               AND MONTH(check_date) = #{month}
+               AND check_in_time IS NOT NULL) as checkedDays,
+
+            -- 异常天数
+            (SELECT COUNT(*)
+             FROM zsjy_internship_daily
+             WHERE student_id = #{studentId}
+               AND deleted = 0
+               AND YEAR(check_date) = #{year}
+               AND MONTH(check_date) = #{month}
+               AND is_exception = '1') as exceptionDays,
+
+            -- 未签到天数(22 - 已签到 - 异常)
+            (22 - 
+             (SELECT COUNT(*)
+              FROM zsjy_internship_daily
+              WHERE student_id = #{studentId}
+                AND deleted = 0
+                AND YEAR(check_date) = #{year}
+                AND MONTH(check_date) = #{month}
+                AND (check_in_time IS NOT NULL OR is_exception = '1'))
+            ) as missedDays
+        ]]>
+    </select>
+
     <!-- 检查学生当日是否已签到 -->
     <select id="existsByStudentAndDate" resultType="boolean">
         SELECT COUNT(*) > 0
@@ -184,4 +231,13 @@
           AND deleted = 0
     </select>
 
+    <!-- 统计班主任管理的班级数量 -->
+    <select id="countClassesByTeacherId" resultType="java.lang.Long">
+        SELECT COUNT(DISTINCT s.class_id)
+        FROM xg_student_msg s
+        WHERE s.class_teacher_id = #{teacherId}
+          AND s.deleted = 0
+          AND s.class_id IS NOT NULL
+    </select>
+
 </mapper>

+ 162 - 0
backend/ruoyi-modules/ruoyi-zsjy/src/main/resources/mapper/ZsjyInternshipVisitMapper.xml

@@ -0,0 +1,162 @@
+<?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.ZsjyInternshipVisitMapper">
+
+    <resultMap type="org.dromara.zsjy.domain.vo.ZsjyInternshipVisitVo" id="ZsjyInternshipVisitResult">
+        <result property="visitId"              column="visit_id"               />
+        <result property="tenantId"             column="tenant_id"              />
+        <result property="internshipId"         column="internship_id"          />
+        <result property="studentId"            column="student_id"             />
+        <result property="companyName"          column="company_name"           />
+        <result property="companyAddress"       column="company_address"        />
+        <result property="visitDate"            column="visit_date"             />
+        <result property="visitorName"          column="visitor_name"           />
+        <result property="visitorPosition"      column="visitor_position"       />
+        <result property="visitPurpose"         column="visit_purpose"          />
+        <result property="companySituation"     column="company_situation"      />
+        <result property="studentPerformance"   column="student_performance"    />
+        <result property="problemsFound"        column="problems_found"         />
+        <result property="suggestions"          column="suggestions"            />
+        <result property="visitSummary"         column="visit_summary"          />
+        <result property="visitImages"          column="visit_images"           />
+        <result property="visitFiles"           column="visit_files"            />
+        <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="studentNumber"        column="student_number"         />
+        <result property="className"            column="class_name"             />
+        <result property="majorName"            column="major_name"             />
+        <result property="collegeName"          column="college_name"           />
+    </resultMap>
+
+    <sql id="selectZsjyInternshipVisitVo">
+        select
+            v.visit_id,
+            v.tenant_id,
+            v.internship_id,
+            v.student_id,
+            v.company_name,
+            v.company_address,
+            v.visit_date,
+            v.visitor_name,
+            v.visitor_position,
+            v.visit_purpose,
+            v.company_situation,
+            v.student_performance,
+            v.problems_found,
+            v.suggestions,
+            v.visit_summary,
+            v.visit_images,
+            v.visit_files,
+            v.status,
+            v.remark,
+            v.create_by,
+            v.create_time,
+            v.update_by,
+            v.update_time,
+            -- 关联查询学生信息
+            s.name as student_name,
+            s.student_no as student_number,
+            s.class_name as class_name,
+            s.major_name as major_name,
+            s.college_name as college_name
+        from zsjy_internship_visit v
+        left join zsjy_internship_info ii on v.internship_id = ii.internship_id and ii.tenant_id = '000000'
+        left join xg_student_msg s on v.student_id = s.id and s.tenant_id = '000000' and s.deleted = 0
+    </sql>
+
+    <select id="queryList" parameterType="org.dromara.zsjy.domain.bo.ZsjyInternshipVisitQueryBo" resultMap="ZsjyInternshipVisitResult">
+        <include refid="selectZsjyInternshipVisitVo"/>
+        <where>
+            v.deleted = 0 and v.tenant_id = '000000'
+            <if test="visitId != null">
+                and v.visit_id = #{visitId}
+            </if>
+            <if test="internshipId != null">
+                and v.internship_id = #{internshipId}
+            </if>
+            <if test="studentId != null">
+                and v.student_id = #{studentId}
+            </if>
+            <if test="studentName != null and studentName != ''">
+                and s.name like concat('%', #{studentName}, '%')
+            </if>
+            <if test="studentNumber != null and studentNumber != ''">
+                and s.student_no like concat('%', #{studentNumber}, '%')
+            </if>
+            <if test="className != null and className != ''">
+                and s.class_name like concat('%', #{className}, '%')
+            </if>
+            <if test="companyName != null and companyName != ''">
+                and v.company_name like concat('%', #{companyName}, '%')
+            </if>
+            <if test="visitorName != null and visitorName != ''">
+                and v.visitor_name like concat('%', #{visitorName}, '%')
+            </if>
+            <if test="status != null and status != ''">
+                and v.status = #{status}
+            </if>
+            <if test="visitStartDate != null">
+                and v.visit_date &gt;= #{visitStartDate}
+            </if>
+            <if test="visitEndDate != null">
+                and v.visit_date &lt;= #{visitEndDate}
+            </if>
+        </where>
+        order by v.visit_date desc, v.create_time desc
+    </select>
+
+    <select id="queryPageList" parameterType="org.dromara.zsjy.domain.bo.ZsjyInternshipVisitQueryBo" resultMap="ZsjyInternshipVisitResult">
+        <include refid="selectZsjyInternshipVisitVo"/>
+        <where>
+            v.deleted = 0 and v.tenant_id = '000000'
+            <if test="bo.visitId != null">
+                and v.visit_id = #{bo.visitId}
+            </if>
+            <if test="bo.internshipId != null">
+                and v.internship_id = #{bo.internshipId}
+            </if>
+            <if test="bo.studentId != null">
+                and v.student_id = #{bo.studentId}
+            </if>
+            <if test="bo.studentName != null and bo.studentName != ''">
+                and s.name like concat('%', #{bo.studentName}, '%')
+            </if>
+            <if test="bo.studentNumber != null and bo.studentNumber != ''">
+                and s.student_no like concat('%', #{bo.studentNumber}, '%')
+            </if>
+            <if test="bo.className != null and bo.className != ''">
+                and s.class_name like concat('%', #{bo.className}, '%')
+            </if>
+            <if test="bo.companyName != null and bo.companyName != ''">
+                and v.company_name like concat('%', #{bo.companyName}, '%')
+            </if>
+            <if test="bo.visitorName != null and bo.visitorName != ''">
+                and v.visitor_name like concat('%', #{bo.visitorName}, '%')
+            </if>
+            <if test="bo.status != null and bo.status != ''">
+                and v.status = #{bo.status}
+            </if>
+            <if test="bo.visitStartDate != null">
+                and v.visit_date &gt;= #{bo.visitStartDate}
+            </if>
+            <if test="bo.visitEndDate != null">
+                and v.visit_date &lt;= #{bo.visitEndDate}
+            </if>
+        </where>
+        order by v.visit_date desc, v.create_time desc
+    </select>
+
+    <select id="selectById" parameterType="Long" resultMap="ZsjyInternshipVisitResult">
+        <include refid="selectZsjyInternshipVisitVo"/>
+        where v.visit_id = #{visitId} and v.deleted = 0 and v.tenant_id = '000000'
+    </select>
+
+</mapper>

+ 1 - 1
mobile/src/api/internship.js

@@ -63,7 +63,7 @@ export const getCalendarData = (studentId, year, month) => {
  */
 export const getStatistics = (params) => {
     return request({
-        url: '/zsjy/internship/daily/statistics',
+        url: '/zsjy/internship/daily/monthly-statistics',
         method: 'GET',
         params
     });

Разница между файлами не показана из-за своего большого размера
+ 751 - 191
mobile/src/pages/internship/index.vue


+ 6 - 6
web/src/api/zsjy/internshipDaily.js

@@ -1,4 +1,4 @@
-import request from '@/utils/request'
+import request from '@/utils/request';
 
 // 查询实习日常记录列表
 export function listInternshipDaily(query) {
@@ -6,7 +6,7 @@ export function listInternshipDaily(query) {
     url: '/zsjy/internship/daily/list',
     method: 'get',
     params: query
-  })
+  });
 }
 
 // 学生签到打卡
@@ -15,7 +15,7 @@ export function checkIn(data) {
     url: '/zsjy/internship/daily/checkin',
     method: 'post',
     data: data
-  })
+  });
 }
 
 // 新增实习日常记录
@@ -24,7 +24,7 @@ export function addInternshipDaily(data) {
     url: '/zsjy/internship/daily',
     method: 'post',
     data: data
-  })
+  });
 }
 
 // 获取学生考勤日历
@@ -33,7 +33,7 @@ export function getCalendar(studentId, params) {
     url: `/zsjy/internship/daily/calendar/${studentId}`,
     method: 'get',
     params: params
-  })
+  });
 }
 
 // 获取考勤统计数据
@@ -42,5 +42,5 @@ export function getStatistics(query) {
     url: '/zsjy/internship/daily/statistics',
     method: 'get',
     params: query
-  })
+  });
 }

+ 85 - 0
web/src/api/zsjy/internshipVisit.ts

@@ -0,0 +1,85 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { InternshipVisitVO, InternshipVisitQuery, InternshipVisitAddBO, InternshipVisitEditBO } from '@/api/zsjy/types';
+import { TableDataInfo, R } from '@/api/types';
+
+/**
+ * 查询班主任走访记录列表
+ * @param query 查询参数
+ */
+export const listInternshipVisit = (query?: InternshipVisitQuery): AxiosPromise<TableDataInfo<InternshipVisitVO>> => {
+  return request({
+    url: '/zsjy/internshipVisit/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询班主任走访记录详细信息
+ * @param visitId 走访记录ID
+ */
+export const getInternshipVisit = (visitId: string | number): AxiosPromise<R<InternshipVisitVO>> => {
+  return request({
+    url: `/zsjy/internshipVisit/${visitId}`,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增班主任走访记录
+ * @param data 走访记录信息
+ */
+export const addInternshipVisit = (data: InternshipVisitAddBO): AxiosPromise<R<void>> => {
+  return request({
+    url: '/zsjy/internshipVisit',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改班主任走访记录
+ * @param data 走访记录信息
+ */
+export const updateInternshipVisit = (data: InternshipVisitEditBO): AxiosPromise<R<void>> => {
+  return request({
+    url: '/zsjy/internshipVisit',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除班主任走访记录
+ * @param visitIds 走访记录ID
+ */
+export const delInternshipVisit = (visitIds: string | number | Array<string | number>): AxiosPromise<R<void>> => {
+  return request({
+    url: `/zsjy/internshipVisit/${visitIds}`,
+    method: 'delete'
+  });
+};
+
+/**
+ * 导出班主任走访记录列表
+ * @param query 查询参数
+ */
+export const exportInternshipVisit = (query: InternshipVisitQuery) => {
+  return request({
+    url: '/zsjy/internshipVisit/export',
+    method: 'post',
+    data: query,
+    responseType: 'blob'
+  });
+};
+
+/**
+ * 获取可选项列表(学生选择)
+ */
+export const getInternshipVisitOptions = (): AxiosPromise<R<InternshipVisitVO[]>> => {
+  return request({
+    url: '/zsjy/internshipVisit/options',
+    method: 'get'
+  });
+};

+ 290 - 0
web/src/api/zsjy/types.ts

@@ -1195,6 +1195,16 @@ export interface InternshipInfoQuery {
    * 实习开始日期-结束
    */
   internshipStartDateEnd?: string;
+
+  /**
+   * 班主任ID(用于数据权限控制)
+   */
+  classTeacherId?: string | number;
+
+  /**
+   * 辅导员ID(用于数据权限控制)
+   */
+  counselorId?: string | number;
 }
 
 /**
@@ -1261,3 +1271,283 @@ export interface InternshipStatisticsVO {
    */
   internshipRate?: string;
 }
+
+/**
+ * 班主任走访记录 VO
+ */
+export interface InternshipVisitVO {
+  /**
+   * 走访记录ID
+   */
+  visitId?: string | number;
+
+  /**
+   * 实习记录ID
+   */
+  internshipId?: string | number;
+
+  /**
+   * 学生ID
+   */
+  studentId?: string | number;
+
+  /**
+   * 学生姓名
+   */
+  studentName?: string;
+
+  /**
+   * 学号
+   */
+  studentNumber?: string;
+
+  /**
+   * 班级名称
+   */
+  className?: string;
+
+  /**
+   * 专业名称
+   */
+  majorName?: string;
+
+  /**
+   * 学院名称
+   */
+  collegeName?: string;
+
+  /**
+   * 企业名称
+   */
+  companyName?: string;
+
+  /**
+   * 企业地址
+   */
+  companyAddress?: string;
+
+  /**
+   * 走访日期
+   */
+  visitDate?: string;
+
+  /**
+   * 走访人员
+   */
+  visitorName?: string;
+
+  /**
+   * 走访人员职务
+   */
+  visitorPosition?: string;
+
+  /**
+   * 走访目的
+   */
+  visitPurpose?: string;
+
+  /**
+   * 企业情况
+   */
+  companySituation?: string;
+
+  /**
+   * 学生表现
+   */
+  studentPerformance?: string;
+
+  /**
+   * 发现问题
+   */
+  problemsFound?: string;
+
+  /**
+   * 改进建议
+   */
+  suggestions?: string;
+
+  /**
+   * 走访总结
+   */
+  visitSummary?: string;
+
+  /**
+   * 现场照片(JSON格式)
+   */
+  visitImages?: string;
+
+  /**
+   * 走访文件附件(JSON格式)
+   */
+  visitFiles?: string;
+
+  /**
+   * 状态(0-草稿 1-已提交 2-已审核)
+   */
+  status?: string;
+
+  /**
+   * 备注
+   */
+  remark?: string;
+
+  /**
+   * 创建时间
+   */
+  createTime?: string;
+
+  /**
+   * 创建者
+   */
+  createBy?: number;
+
+  /**
+   * 更新时间
+   */
+  updateTime?: string;
+
+  /**
+   * 更新者
+   */
+  updateBy?: number;
+}
+
+/**
+ * 班主任走访记录查询参数
+ */
+export interface InternshipVisitQuery extends PageQuery {
+  /**
+   * 学生姓名
+   */
+  studentName?: string;
+
+  /**
+   * 学号
+   */
+  studentNumber?: string;
+
+  /**
+   * 企业名称
+   */
+  companyName?: string;
+
+  /**
+   * 走访人员
+   */
+  visitorName?: string;
+
+  /**
+   * 走访日期-开始
+   */
+  visitDateBegin?: string;
+
+  /**
+   * 走访日期-结束
+   */
+  visitDateEnd?: string;
+
+  /**
+   * 状态
+   */
+  status?: string;
+}
+
+/**
+ * 班主任走访记录新增参数
+ */
+export interface InternshipVisitAddBO {
+  /**
+   * 实习记录ID
+   */
+  internshipId?: string | number;
+
+  /**
+   * 学生ID
+   */
+  studentId?: string | number;
+
+  /**
+   * 企业名称
+   */
+  companyName?: string;
+
+  /**
+   * 企业地址
+   */
+  companyAddress?: string;
+
+  /**
+   * 走访日期
+   */
+  visitDate?: string;
+
+  /**
+   * 走访人员
+   */
+  visitorName?: string;
+
+  /**
+   * 走访人员职务
+   */
+  visitorPosition?: string;
+
+  /**
+   * 走访目的
+   */
+  visitPurpose?: string;
+
+  /**
+   * 企业情况
+   */
+  companySituation?: string;
+
+  /**
+   * 学生表现
+   */
+  studentPerformance?: string;
+
+  /**
+   * 发现问题
+   */
+  problemsFound?: string;
+
+  /**
+   * 改进建议
+   */
+  suggestions?: string;
+
+  /**
+   * 走访总结
+   */
+  visitSummary?: string;
+
+  /**
+   * 现场照片(JSON格式)
+   */
+  visitImages?: string;
+
+  /**
+   * 走访文件附件(JSON格式)
+   */
+  visitFiles?: string;
+
+  /**
+   * 状态
+   */
+  status?: string;
+
+  /**
+   * 备注
+   */
+  remark?: string;
+}
+
+/**
+ * 班主任走访记录编辑参数
+ */
+export interface InternshipVisitEditBO extends InternshipVisitAddBO {
+  /**
+   * 走访记录ID
+   */
+  visitId?: string | number;
+}

+ 88 - 4
web/src/components/ImageUpload/index.vue

@@ -455,14 +455,38 @@ const handleBeforeUpload = (file: any) => {
     }
   }
 
+  // 设置超时处理 - 如果60秒后仍在loading状态,自动关闭并提示
+  const timeoutId = setTimeout(() => {
+    if (number.value > 0) {
+      console.warn('上传超时,自动关闭loading');
+      (proxy as any)?.$modal.closeLoading();
+      (proxy as any)?.$modal.msgError('上传超时,请检查网络连接或重试');
+      number.value = 0; // 重置计数器
+    }
+  }, 60000); // 60秒超时
+
   //压缩图片,开启压缩并且大于指定的压缩大小时才压缩
   if (props.compressSupport && file.size / 1024 > props.compressTargetSize) {
     (proxy as any)?.$modal.loading('正在上传图片,请稍候...');
     number.value++;
-    return compressAccurately(file, props.compressTargetSize);
+
+    // 压缩图片时的错误处理
+    try {
+      return compressAccurately(file, props.compressTargetSize);
+    } catch (compressionError) {
+      console.error('图片压缩失败:', compressionError);
+      clearTimeout(timeoutId);
+      (proxy as any)?.$modal.closeLoading();
+      (proxy as any)?.$modal.msgError('图片压缩失败,请重试');
+      number.value--;
+      return false;
+    }
   } else {
     (proxy as any)?.$modal.loading('正在上传图片,请稍候...');
     number.value++;
+
+    // 保存timeout ID以便在上传完成时清除
+    (file as any).uploadTimeoutId = timeoutId;
   }
 };
 
@@ -476,6 +500,11 @@ const handleUploadSuccess = (res: any, file: any) => {
   console.log('handleUploadSuccess:res', res);
   console.log('handleUploadSuccess:file', file);
 
+  // 清除上传超时处理
+  if ((file as any).uploadTimeoutId) {
+    clearTimeout((file as any).uploadTimeoutId);
+  }
+
   if (res.code === 200) {
     const uploadItem = {
       name: res.data.fileName,
@@ -488,7 +517,9 @@ const handleUploadSuccess = (res: any, file: any) => {
   } else {
     number.value--;
     (proxy as any)?.$modal.closeLoading();
-    (proxy as any)?.$modal.msgError(res.msg);
+    // 提供更详细的错误信息
+    const errorMsg = res.msg || '上传失败,请重试';
+    (proxy as any)?.$modal.msgError(errorMsg);
     imageUploadRef.value?.handleRemove(file);
     uploadedSuccessfully();
   }
@@ -657,9 +688,62 @@ const uploadedSuccessfully = async () => {
 };
 
 // 上传失败
-const handleUploadError = () => {
-  (proxy as any)?.$modal.msgError('上传图片失败');
+const handleUploadError = (error: any, file: any) => {
+  console.error('上传失败详细信息:', error);
+
+  // 清除上传超时处理
+  if (file && (file as any).uploadTimeoutId) {
+    clearTimeout((file as any).uploadTimeoutId);
+  }
+
+  // 确保关闭loading
   (proxy as any)?.$modal.closeLoading();
+
+  // 减少计数器
+  number.value--;
+
+  // 根据错误类型提供不同的错误信息
+  let errorMessage = '上传图片失败';
+
+  if (error.response) {
+    const status = error.response.status;
+    const responseData = error.response.data;
+
+    switch (status) {
+      case 403:
+        errorMessage = '没有文件上传权限,请联系管理员';
+        break;
+      case 413:
+        errorMessage = '文件太大,超过服务器限制';
+        break;
+      case 500:
+        errorMessage = '服务器内部错误,请稍后重试';
+        break;
+      case 504:
+        errorMessage = '上传超时,请检查网络连接';
+        break;
+      default:
+        if (responseData && responseData.msg) {
+          errorMessage = responseData.msg;
+        } else if (responseData && typeof responseData === 'string') {
+          errorMessage = `上传失败: ${responseData}`;
+        } else {
+          errorMessage = `上传失败,错误代码: ${status}`;
+        }
+    }
+  } else if (error.request) {
+    // 网络错误
+    if (error.code === 'ECONNABORTED' || error.message.includes('timeout')) {
+      errorMessage = '上传超时,请检查网络连接或文件大小';
+    } else {
+      errorMessage = '网络连接错误,请检查网络设置';
+    }
+  } else if (error.message) {
+    errorMessage = `上传失败: ${error.message}`;
+  }
+
+  console.error('上传错误信息:', errorMessage);
+  (proxy as any)?.$modal.msgError(errorMessage);
 };
 
 // 预览

+ 15 - 2
web/src/views/jcbt/workstudy/xgreview/README.md

@@ -7,12 +7,14 @@
 ## 功能特性
 
 ### 1. 统计概览
+
 - 待审核申请数量
-- 已通过申请数量  
+- 已通过申请数量
 - 已拒绝申请数量
 - 总处理数量
 
 ### 2. 待审核申请管理
+
 - 查看待审核申请列表
 - 支持多条件搜索(岗位名称、学生姓名、学号、学院名称、申请日期)
 - 单个申请审核(通过/拒绝)
@@ -20,6 +22,7 @@
 - 查看申请详情
 
 ### 3. 所有申请查看
+
 - 查看所有申请记录
 - 支持按状态筛选
 - 导出申请数据
@@ -40,16 +43,19 @@ xgreview/
 ## 技术特点
 
 ### 1. 响应式设计
+
 - 使用Vue 3 Composition API
 - TypeScript类型支持
 - Element Plus UI组件库
 
 ### 2. 数据管理
+
 - 基于学工处审核API
 - 支持分页查询
 - 实时数据更新
 
 ### 3. 用户体验
+
 - 加载状态提示
 - 操作反馈消息
 - 响应式布局
@@ -57,6 +63,7 @@ xgreview/
 ## API接口
 
 ### 主要接口
+
 - `listPendingApplications` - 获取待审核申请列表
 - `listStudentAffairsApplications` - 获取所有申请列表
 - `getStudentAffairsApplication` - 获取申请详情
@@ -68,15 +75,18 @@ xgreview/
 ## 使用说明
 
 ### 1. 访问路径
+
 ```
 /jcbt/workstudy/xgreview
 ```
 
 ### 2. 权限要求
+
 - 需要学工处审核权限
 - 支持批量操作权限
 
 ### 3. 操作流程
+
 1. 查看统计概览,了解待处理申请数量
 2. 在"待审核申请"标签页中查看待处理申请
 3. 使用搜索功能筛选特定申请
@@ -100,16 +110,19 @@ xgreview/
 ## 开发说明
 
 ### 1. 组件复用
+
 本模块复用了学院审核模块的组件结构,但使用不同的API接口和字段。
 
 ### 2. 样式规范
+
 遵循项目的前端开发规范,使用统一的颜色主题和组件样式。
 
 ### 3. 错误处理
+
 包含完整的错误处理机制,提供用户友好的错误提示。
 
 ## 更新日志
 
 - **2025-01-03**:创建学工处审核模块
 - **2025-01-03**:实现基础审核功能
-- **2025-01-03**:添加统计概览和批量操作 
+- **2025-01-03**:添加统计概览和批量操作

+ 35 - 10
web/src/views/jcbt/workstudy/xgreview/开发总结.md

@@ -7,12 +7,14 @@
 ## ✅ 已完成功能
 
 ### 1. 主页面 (index.vue)
+
 - ✅ 统计卡片展示(待审核、已通过、已拒绝、总计)
 - ✅ 标签页切换(待审核申请、所有申请)
 - ✅ 响应式布局和美观的UI设计
 - ✅ 与后勤管理模块保持一致的样式风格
 
 ### 2. 待审核申请组件 (PendingApplications.vue)
+
 - ✅ 多条件搜索表单(岗位名称、学生姓名、学号、学院名称、申请日期)
 - ✅ 批量操作按钮(批量通过、批量拒绝)
 - ✅ 数据表格展示(包含学院审核状态)
@@ -22,6 +24,7 @@
 - ✅ 分页功能
 
 ### 3. 所有申请组件 (AllApplications.vue)
+
 - ✅ 扩展搜索条件(包含学工处审核状态)
 - ✅ 完整申请列表展示
 - ✅ 学院审核和学工处审核状态对比
@@ -29,6 +32,7 @@
 - ✅ 审核操作(针对待审核状态)
 
 ### 4. 申请详情组件 (ApplicationDetail.vue)
+
 - ✅ 完整的申请信息展示
 - ✅ 学院审核信息展示
 - ✅ 学工处审核信息展示
@@ -36,23 +40,27 @@
 - ✅ 立即审核功能
 
 ### 5. 路由配置 (jcbt.ts)
+
 - ✅ 创建jcbt模块路由配置
 - ✅ 包含学工处审核模块路由
 - ✅ 支持懒加载
 
 ### 6. 文档说明
+
 - ✅ README.md - 详细的功能说明和使用指南
 - ✅ 开发总结.md - 开发过程和成果总结
 
 ## 🏗️ 技术架构
 
 ### 前端技术栈
+
 - **Vue 3.5.13** + **TypeScript 5.8.3**
 - **Element Plus** UI组件库
 - **Composition API** 响应式编程
 - **SCSS** 样式预处理器
 
 ### 组件结构
+
 ```
 xgreview/
 ├── index.vue                    # 主页面
@@ -66,6 +74,7 @@ xgreview/
 ```
 
 ### API接口
+
 - 基于 `@/api/jcbt/workStudyApplicationStudentAffairsReview.ts`
 - 支持加密请求(AES+RSA)
 - 完整的CRUD操作接口
@@ -73,16 +82,19 @@ xgreview/
 ## 🎨 设计特点
 
 ### 1. 视觉一致性
+
 - 与学院审核模块保持完全一致的视觉风格
 - 使用相同的颜色主题和组件样式
 - 统一的布局结构和交互模式
 
 ### 2. 用户体验
+
 - 响应式设计,支持不同屏幕尺寸
 - 加载状态提示和操作反馈
 - 直观的操作流程和清晰的界面布局
 
 ### 3. 功能完整性
+
 - 覆盖完整的审核业务流程
 - 支持单个和批量操作
 - 提供详细的数据展示和操作记录
@@ -90,53 +102,61 @@ xgreview/
 ## 🔧 开发规范遵循
 
 ### 1. 前端开发规范
+
 - ✅ 遵循 `.cursor/cursorrules-frontend.md` 规范
 - ✅ 使用Vue 3 Composition API
 - ✅ TypeScript类型定义完整
 - ✅ 组件化开发,代码复用
 
 ### 2. 项目结构规范
+
 - ✅ 页面代码存放在 `web/src/views/jcbt/workstudy/xgreview/`
 - ✅ API代码存放在 `web/src/api/jcbt/`
 - ✅ 路由配置存放在 `web/src/router/`
 
 ### 3. 代码质量
+
 - ✅ 完整的错误处理机制
 - ✅ 清晰的代码注释和文档
 - ✅ 遵循Vue 3最佳实践
 
 ## 📊 功能对比
 
-| 功能模块 | 学院审核 | 学工处审核 | 状态 |
-|---------|---------|-----------|------|
-| 统计概览 | ✅ | ✅ | 完全一致 |
-| 待审核列表 | ✅ | ✅ | 完全一致 |
-| 所有申请 | ✅ | ✅ | 完全一致 |
-| 申请详情 | ✅ | ✅ | 完全一致 |
-| 审核操作 | ✅ | ✅ | 完全一致 |
-| 批量操作 | ✅ | ✅ | 完全一致 |
-| 导出功能 | ✅ | ✅ | 完全一致 |
+| 功能模块   | 学院审核 | 学工处审核 | 状态     |
+| ---------- | -------- | ---------- | -------- |
+| 统计概览   | ✅       | ✅         | 完全一致 |
+| 待审核列表 | ✅       | ✅         | 完全一致 |
+| 所有申请   | ✅       | ✅         | 完全一致 |
+| 申请详情   | ✅       | ✅         | 完全一致 |
+| 审核操作   | ✅       | ✅         | 完全一致 |
+| 批量操作   | ✅       | ✅         | 完全一致 |
+| 导出功能   | ✅       | ✅         | 完全一致 |
 
 ## 🚀 部署说明
 
 ### 1. 文件位置
+
 所有文件已正确放置在指定目录:
+
 - 主页面:`web/src/views/jcbt/workstudy/xgreview/index.vue`
 - 组件:`web/src/views/jcbt/workstudy/xgreview/components/`
 - 路由:`web/src/router/jcbt.ts`
 
 ### 2. 访问路径
+
 ```
 /jcbt/workstudy/xgreview
 ```
 
 ### 3. 权限要求
+
 - 需要学工处审核权限
 - 支持批量操作权限
 
 ## 🔍 测试建议
 
 ### 1. 功能测试
+
 - 统计数据展示
 - 搜索和筛选功能
 - 审核操作流程
@@ -144,11 +164,13 @@ xgreview/
 - 导出功能
 
 ### 2. 兼容性测试
+
 - 不同浏览器兼容性
 - 不同屏幕尺寸响应式
 - 权限控制验证
 
 ### 3. 性能测试
+
 - 大数据量加载性能
 - 操作响应速度
 - 内存使用情况
@@ -156,16 +178,19 @@ xgreview/
 ## 📝 后续优化建议
 
 ### 1. 功能增强
+
 - 添加审核历史记录
 - 支持审核意见模板
 - 增加数据统计图表
 
 ### 2. 性能优化
+
 - 实现虚拟滚动
 - 添加数据缓存
 - 优化API请求
 
 ### 3. 用户体验
+
 - 添加操作引导
 - 支持快捷键操作
 - 增加个性化设置
@@ -186,4 +211,4 @@ xgreview/
 **开发完成时间**:2025-01-03  
 **开发状态**:✅ 完成  
 **代码质量**:⭐⭐⭐⭐⭐  
-**文档完整性**:⭐⭐⭐⭐⭐ 
+**文档完整性**:⭐⭐⭐⭐⭐

+ 3 - 3
web/src/views/zsjy/internship/daily/checkin.vue

@@ -216,8 +216,8 @@
 </template>
 
 <script setup name="InternshipCheckin">
-import { checkIn } from "@/api/zsjy/internshipDaily";
-import { parseTime } from "@/utils/ruoyi";
+import { checkIn } from '@/api/zsjy/internshipDaily';
+import { parseTime } from '@/utils/ruoyi';
 
 const { proxy } = getCurrentInstance();
 
@@ -273,7 +273,7 @@ const getRecentRecords = () => {
       workDuration: 8,
       status: '1',
       workContent: '参与项目开发,完成用户模块设计'
-    },
+    }
     // 更多记录...
   ];
 };

+ 44 - 85
web/src/views/zsjy/internship/daily/index.vue

@@ -212,13 +212,7 @@
 
       <!-- 分页组件 -->
       <div class="pagination-container">
-        <pagination
-          v-show="total > 0"
-          :total="total"
-          :page="queryParams.pageNum"
-          :limit="queryParams.pageSize"
-          @pagination="getList"
-        />
+        <pagination v-show="total > 0" :total="total" :page="queryParams.pageNum" :limit="queryParams.pageSize" @pagination="getList" />
       </div>
     </el-card>
 
@@ -244,10 +238,10 @@
           </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>
+            <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>
+            <div style="white-space: pre-wrap">{{ currentRecord.remark || '-' }}</div>
           </el-descriptions-item>
         </el-descriptions>
       </div>
@@ -277,10 +271,7 @@
             <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"
-                >
+                <el-tag :type="getStatusTagType(getAttendanceStatus(data.day))" size="small">
                   {{ getStatusText(getAttendanceStatus(data.day)) }}
                 </el-tag>
               </div>
@@ -350,25 +341,13 @@
           <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-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-input-number v-model="addForm.workDuration" :min="0" :max="24" :precision="1" placeholder="工作时长(小时)" style="width: 100%" />
             </el-form-item>
           </el-col>
         </el-row>
@@ -376,24 +355,10 @@
           <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-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-input v-model="addForm.remark" type="textarea" :rows="2" placeholder="请输入备注" maxlength="200" show-word-limit />
         </el-form-item>
       </el-form>
       <template #footer>
@@ -447,24 +412,12 @@ const addForm = ref({
 
 // 新增表单验证规则
 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' }
-  ]
+  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' }]
 });
 
 // 查询参数
@@ -500,7 +453,7 @@ const getList = () => {
     queryParams.value.checkTimeEnd = '';
   }
 
-  listInternshipDaily(queryParams.value).then(response => {
+  listInternshipDaily(queryParams.value).then((response) => {
     internshipDailyList.value = response.rows;
     total.value = response.total;
     loading.value = false;
@@ -510,13 +463,15 @@ const getList = () => {
 // 获取统计数据
 const getStatisticsData = () => {
   console.log('正在获取统计数据...');
-  getStatistics().then(response => {
-    console.log('统计数据响应:', response);
-    statistics.value = response.data || {};
-    console.log('设置的统计数据:', statistics.value);
-  }).catch(error => {
-    console.error('获取统计数据失败:', error);
-  });
+  getStatistics()
+    .then((response) => {
+      console.log('统计数据响应:', response);
+      statistics.value = response.data || {};
+      console.log('设置的统计数据:', statistics.value);
+    })
+    .catch((error) => {
+      console.error('获取统计数据失败:', error);
+    });
 };
 
 // 搜索按钮操作
@@ -528,7 +483,7 @@ const handleQuery = () => {
 // 重置按钮操作
 const resetQuery = () => {
   daterangeCheckTime.value = [];
-  proxy.resetForm("queryFormRef");
+  proxy.resetForm('queryFormRef');
   queryParams.value = {
     pageNum: 1,
     pageSize: 10,
@@ -581,17 +536,19 @@ const cancelAdd = () => {
 
 // 提交新增
 const submitAdd = () => {
-  proxy.$refs.addFormRef.validate(valid => {
+  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;
-      });
+      addInternshipDaily(addForm.value)
+        .then((response) => {
+          proxy.$modal.msgSuccess('新增签到记录成功');
+          addDialogVisible.value = false;
+          getList();
+        })
+        .finally(() => {
+          addLoading.value = false;
+        });
     }
   });
 };
@@ -625,16 +582,14 @@ const loadCalendarData = () => {
     month: calendarMonth.value
   };
 
-  getCalendar(currentStudent.value.studentId, params).then(response => {
+  getCalendar(currentStudent.value.studentId, params).then((response) => {
     calendarData.value = response.data || [];
   });
 };
 
 // 获取考勤状态
 const getAttendanceStatus = (date) => {
-  const record = calendarData.value.find(item =>
-    item.checkDate === date
-  );
+  const record = calendarData.value.find((item) => item.checkDate === date);
   return record ? record.status : null;
 };
 
@@ -662,9 +617,13 @@ const getStatusText = (status) => {
 
 // 导出按钮操作
 const handleExport = () => {
-  proxy.download('zsjy/internship/daily/export', {
-    ...queryParams.value
-  }, `实习日常记录_${new Date().getTime()}.xlsx`);
+  proxy.download(
+    'zsjy/internship/daily/export',
+    {
+      ...queryParams.value
+    },
+    `实习日常记录_${new Date().getTime()}.xlsx`
+  );
 };
 
 // 初始化

+ 9 - 10
web/src/views/zsjy/internship/msgmanage/components/AddDialog.vue

@@ -1,12 +1,5 @@
 <template>
-  <el-dialog
-    v-model="visible"
-    title="新增实习信息"
-    width="80%"
-    :before-close="handleClose"
-    draggable
-    destroy-on-close
-  >
+  <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">
         <!-- 基本信息 -->
@@ -32,7 +25,14 @@
         </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-input-number
+              v-model="form.internshipAllowance"
+              :min="0"
+              :precision="2"
+              controls-position="right"
+              placeholder="实习津贴"
+              style="width: 100%"
+            />
           </el-form-item>
         </el-col>
 
@@ -363,4 +363,3 @@ const handleSubmit = async () => {
   padding-bottom: 10px;
 }
 </style>
- 

+ 17 - 15
web/src/views/zsjy/internship/msgmanage/components/EditDialog.vue

@@ -1,12 +1,5 @@
 <template>
-  <el-dialog
-    v-model="visible"
-    title="编辑实习信息"
-    width="80%"
-    :before-close="handleClose"
-    draggable
-    destroy-on-close
-  >
+  <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">
         <!-- 基本信息 -->
@@ -32,7 +25,14 @@
         </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-input-number
+              v-model="form.internshipAllowance"
+              :min="0"
+              :precision="2"
+              controls-position="right"
+              placeholder="实习津贴"
+              style="width: 100%"
+            />
           </el-form-item>
         </el-col>
 
@@ -277,12 +277,15 @@ const rules = reactive({
 });
 
 // 监听弹窗显示状态
-watch(() => props.modelValue, (newVal) => {
-  visible.value = newVal;
-  if (newVal && props.studentId) {
-    getInternshipDetail();
+watch(
+  () => props.modelValue,
+  (newVal) => {
+    visible.value = newVal;
+    if (newVal && props.studentId) {
+      getInternshipDetail();
+    }
   }
-});
+);
 
 watch(visible, (newVal) => {
   emit('update:modelValue', newVal);
@@ -434,4 +437,3 @@ const handleDelete = async () => {
   padding-bottom: 10px;
 }
 </style>
- 

+ 5 - 13
web/src/views/zsjy/internship/msgmanage/index.vue

@@ -191,8 +191,6 @@
       </div>
     </el-card>
 
-
-
     <!-- 学生实习详情对话框 -->
     <el-dialog title="学生实习详情" v-model="dialog.visible" width="900px" append-to-body>
       <div v-if="studentDetail" class="student-detail">
@@ -226,7 +224,9 @@
           <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-if="studentDetail.internshipAllowance && studentDetail.internshipAllowance > 0"
+              >{{ studentDetail.internshipAllowance }}元/月</span
+            >
             <span v-else>-</span>
           </el-descriptions-item>
           <el-descriptions-item label="协议状态">
@@ -245,18 +245,10 @@
     </el-dialog>
 
     <!-- 新增弹窗 -->
-    <AddDialog
-      v-model="showAddDialog"
-      :student-info="selectedStudentInfo"
-      @success="handleDialogSuccess"
-    />
+    <AddDialog v-model="showAddDialog" :student-info="selectedStudentInfo" @success="handleDialogSuccess" />
 
     <!-- 编辑弹窗 -->
-    <EditDialog
-      v-model="showEditDialog"
-      :student-id="selectedStudentId"
-      @success="handleDialogSuccess"
-    />
+    <EditDialog v-model="showEditDialog" :student-id="selectedStudentId" @success="handleDialogSuccess" />
   </div>
 </template>
 

+ 1056 - 0
web/src/views/zsjy/internship/visit/index.vue

@@ -0,0 +1,1056 @@
+<template>
+  <div class="app-container">
+    <!-- 统计卡片 -->
+    <el-row :gutter="16" class="stats-row">
+      <el-col :xl="6" :lg="12" :md="12" :sm="12" :xs="24">
+        <div class="stat-card total">
+          <div class="stat-icon">
+            <el-icon><User /></el-icon>
+          </div>
+          <div class="stat-content">
+            <div class="stat-number">{{ statistics.total }}</div>
+            <div class="stat-label">走访总数</div>
+          </div>
+        </div>
+      </el-col>
+      <el-col :xl="6" :lg="12" :md="12" :sm="12" :xs="24">
+        <div class="stat-card month">
+          <div class="stat-icon">
+            <el-icon><Calendar /></el-icon>
+          </div>
+          <div class="stat-content">
+            <div class="stat-number">{{ statistics.monthCount }}</div>
+            <div class="stat-label">本月走访</div>
+          </div>
+        </div>
+      </el-col>
+      <el-col :xl="6" :lg="12" :md="12" :sm="12" :xs="24">
+        <div class="stat-card completed">
+          <div class="stat-icon">
+            <el-icon><CircleCheck /></el-icon>
+          </div>
+          <div class="stat-content">
+            <div class="stat-number">{{ statistics.completedCount }}</div>
+            <div class="stat-label">已完成</div>
+          </div>
+        </div>
+      </el-col>
+      <el-col :xl="6" :lg="12" :md="12" :sm="12" :xs="24">
+        <div class="stat-card pending">
+          <div class="stat-icon">
+            <el-icon><Clock /></el-icon>
+          </div>
+          <div class="stat-content">
+            <div class="stat-number">{{ statistics.pendingCount }}</div>
+            <div class="stat-label">待走访</div>
+          </div>
+        </div>
+      </el-col>
+    </el-row>
+
+    <!-- 搜索区域 -->
+    <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">
+            <!-- 搜索条件 -->
+            <el-row :gutter="20">
+              <el-col :span="6">
+                <el-form-item label="学生信息" prop="studentName">
+                  <el-input
+                    v-model="queryParams.studentName"
+                    placeholder="请输入学生姓名或学号"
+                    clearable
+                    style="width: 100%"
+                    @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 style="width: 100%" @keyup.enter="handleQuery" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item label="走访人" prop="visitorName">
+                  <el-input v-model="queryParams.visitorName" placeholder="请输入走访人姓名" clearable style="width: 100%" @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 v-for="dict in visitStatusOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+            </el-row>
+
+            <!-- 第二行:日期范围 + 操作按钮 -->
+            <el-row :gutter="20">
+              <el-col :span="12">
+                <el-form-item label="走访日期" prop="visitDateRange">
+                  <el-date-picker
+                    v-model="queryParams.visitDateRange"
+                    type="daterange"
+                    range-separator="至"
+                    start-placeholder="开始日期"
+                    end-placeholder="结束日期"
+                    value-format="YYYY-MM-DD"
+                    style="width: 100%"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <div 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-button type="info" plain icon="Search" @click="showSearch = !showSearch">
+                      {{ showSearch ? '隐藏' : '显示' }}搜索
+                    </el-button>
+                  </el-form-item>
+                </div>
+              </el-col>
+            </el-row>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <!-- 操作按钮栏 -->
+    <el-card shadow="hover" class="button-card">
+      <template #header>
+        <div class="card-header-vertical">
+          <div class="toolbar-title">班主任走访列表</div>
+          <div class="button-group">
+            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['zsjy:internshipVisit:add']">新增走访</el-button>
+            <el-button type="success" plain icon="Download" @click="handleExport" v-hasPermi="['zsjy:internshipVisit:export']">导出</el-button>
+            <el-button type="info" plain icon="Search" @click="showSearch = !showSearch">
+              {{ showSearch ? '隐藏' : '显示' }}搜索
+            </el-button>
+            <el-tooltip content="刷新" placement="top">
+              <el-button icon="Refresh" circle @click="getList" />
+            </el-tooltip>
+          </div>
+        </div>
+      </template>
+    </el-card>
+
+    <!-- 数据表格 -->
+    <el-card shadow="hover" class="table-card">
+      <div class="table-container">
+        <el-table
+          v-loading="loading"
+          :data="visitList"
+          row-key="visitId"
+          border
+          stripe
+          highlight-current-row
+          @selection-change="handleSelectionChange"
+        >
+          <el-table-column type="selection" width="50" align="center" />
+
+          <el-table-column label="学生信息" min-width="150" 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="走访人" prop="visitorName" min-width="100" align="center" />
+
+          <el-table-column label="走访日期" prop="visitDate" min-width="120" align="center">
+            <template #default="scope">
+              {{ parseTime(scope.row.visitDate, '{y}-{m}-{d}') }}
+            </template>
+          </el-table-column>
+
+
+
+          <el-table-column label="走访状态" prop="status" min-width="100" align="center">
+            <template #default="scope">
+              <dict-tag :options="visitStatusOptions" :value="scope.row.status || ''" />
+            </template>
+          </el-table-column>
+
+          <el-table-column label="创建时间" prop="createTime" min-width="160" align="center">
+            <template #default="scope">
+              {{ parseTime(scope.row.createTime) }}
+            </template>
+          </el-table-column>
+
+          <el-table-column label="操作" width="180" 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)" v-hasPermi="['zsjy:internshipVisit:query']" />
+                </el-tooltip>
+                <el-tooltip content="编辑" placement="top">
+                  <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['zsjy:internshipVisit:edit']" />
+                </el-tooltip>
+                <el-tooltip content="删除" placement="top">
+                  <el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['zsjy:internshipVisit:remove']" />
+                </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="800px" 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.visitorName }}</el-descriptions-item>
+          <el-descriptions-item label="走访日期">
+            {{ parseTime(currentRecord.visitDate, '{y}-{m}-{d}') }}
+          </el-descriptions-item>
+          <el-descriptions-item label="走访状态">
+            <dict-tag :options="visitStatusOptions" :value="currentRecord.status || ''" />
+          </el-descriptions-item>
+          <el-descriptions-item label="走访目的" :span="2">{{ currentRecord.visitPurpose || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="企业情况" :span="2">
+            <div style="white-space: pre-wrap">{{ currentRecord.companySituation || '-' }}</div>
+          </el-descriptions-item>
+          <el-descriptions-item label="学生表现" :span="2">
+            <div style="white-space: pre-wrap">{{ currentRecord.studentPerformance || '-' }}</div>
+          </el-descriptions-item>
+          <el-descriptions-item label="发现问题" :span="2">
+            <div style="white-space: pre-wrap">{{ currentRecord.problemsFound || '-' }}</div>
+          </el-descriptions-item>
+          <el-descriptions-item label="改进建议" :span="2">
+            <div style="white-space: pre-wrap">{{ currentRecord.suggestions || '-' }}</div>
+          </el-descriptions-item>
+          <el-descriptions-item label="走访总结" :span="2">
+            <div style="white-space: pre-wrap">{{ currentRecord.visitSummary || '-' }}</div>
+          </el-descriptions-item>
+          <el-descriptions-item label="备注" :span="2">
+            <div style="white-space: pre-wrap">{{ currentRecord.remark || '-' }}</div>
+          </el-descriptions-item>
+          <el-descriptions-item label="创建时间">
+            {{ parseTime(currentRecord.createTime) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="更新时间">
+            {{ parseTime(currentRecord.updateTime) }}
+          </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="formTitle" v-model="formDialogVisible" width="800px" append-to-body>
+      <el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="学生" prop="studentId" required>
+              <el-select
+                v-model="form.studentId"
+                placeholder="请选择学生"
+                filterable
+                remote
+                :remote-method="searchStudents"
+                :loading="studentLoading"
+                style="width: 100%"
+                @change="handleStudentChange"
+                @focus="handleStudentFocus"
+              >
+                <el-option
+                  v-for="student in studentOptions"
+                  :key="student.studentId"
+                  :label="`${student.studentName}(${student.studentNo})- ${student.className}`"
+                  :value="student.studentId"
+                />
+                <!-- 空数据提示 -->
+                <el-option
+                  v-if="studentOptions.length === 0 && !studentLoading"
+                  disabled
+                  label="暂无学生数据"
+                  value=""
+                />
+              </el-select>
+              <!-- 提示信息 -->
+              <div class="form-item-tip">
+                系统将自动显示您所管理班级的学生,支持输入姓名或学号搜索
+              </div>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="走访人" prop="visitorName" required>
+              <el-input v-model="form.visitorName" placeholder="请输入走访人姓名" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="走访日期" prop="visitDate" required>
+              <el-date-picker v-model="form.visitDate" type="date" placeholder="请选择走访日期" value-format="YYYY-MM-DD" style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="走访状态" prop="status" required>
+              <el-select v-model="form.status" placeholder="请选择走访状态" style="width: 100%">
+                <el-option v-for="dict in visitStatusOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="走访目的" prop="visitPurpose" required>
+          <el-input v-model="form.visitPurpose" placeholder="请输入走访目的" />
+        </el-form-item>
+        <el-form-item label="企业情况" prop="companySituation">
+          <el-input v-model="form.companySituation" type="textarea" :rows="3" placeholder="请输入企业基本情况" />
+        </el-form-item>
+        <el-form-item label="学生表现" prop="studentPerformance">
+          <el-input v-model="form.studentPerformance" type="textarea" :rows="3" placeholder="请输入学生在企业的表现情况" />
+        </el-form-item>
+        <el-form-item label="发现问题" prop="problemsFound">
+          <el-input v-model="form.problemsFound" type="textarea" :rows="3" placeholder="请输入发现的问题" />
+        </el-form-item>
+        <el-form-item label="改进建议" prop="suggestions">
+          <el-input v-model="form.suggestions" type="textarea" :rows="3" placeholder="请输入改进建议" />
+        </el-form-item>
+        <el-form-item label="走访总结" prop="visitSummary">
+          <el-input v-model="form.visitSummary" type="textarea" :rows="4" placeholder="请输入走访总结" />
+        </el-form-item>
+        <el-form-item label="现场照片" prop="visitImages">
+          <image-upload v-model="form.visitImages" :limit="10" :file-size="10" />
+          <div class="form-item-tip">支持上传jpg、png、gif格式,单张图片不超过2MB,总大小不超过10MB</div>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注信息" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer-right">
+          <el-button @click="formDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="submitForm" :loading="formLoading">确定</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts" name="InternshipVisit">
+import { ref, reactive, onMounted, computed, getCurrentInstance } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { parseTime } from '@/utils/ruoyi';
+import { getDicts } from '@/api/system/dict/data/index';
+import { useUserStore } from '@/store/modules/user';
+import {
+  listInternshipVisit,
+  getInternshipVisit,
+  addInternshipVisit,
+  updateInternshipVisit,
+  delInternshipVisit,
+  exportInternshipVisit
+} from '@/api/zsjy/internshipVisit';
+import { listInternshipInfo } from '@/api/zsjy/internship';
+import ImageUpload from '@/components/ImageUpload/index.vue';
+
+// 获取组件实例和store
+const { proxy } = getCurrentInstance() as any;
+const userStore = useUserStore();
+
+// 响应式数据
+const loading = ref(true);
+const showSearch = ref(true);
+const formLoading = ref(false);
+const studentLoading = ref(false);
+const ids = ref([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const visitList = ref([]);
+const formTitle = ref('');
+const viewDialogVisible = ref(false);
+const formDialogVisible = ref(false);
+const currentRecord = ref(null);
+
+// 统计数据
+const statistics = ref({
+  total: 0,
+  monthCount: 0,
+  completedCount: 0,
+  pendingCount: 0
+});
+
+// 字典数据
+const visitStatusOptions = ref([]);
+const studentOptions = ref([]);
+
+// 查询参数
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  studentName: undefined,
+  companyName: undefined,
+  visitorName: undefined,
+  status: undefined,
+  visitDateRange: [],
+  visitStartDate: undefined,
+  visitEndDate: undefined
+});
+
+// 表单数据
+const form = reactive({
+  visitId: undefined,
+  internshipId: undefined, // 实习ID
+  studentId: undefined,
+  studentName: undefined,
+  studentNumber: undefined,
+  className: undefined,
+  companyName: undefined,
+  visitorName: undefined,
+  visitDate: undefined,
+  status: '0', // 0-草稿(对应字典的planned)
+  visitPurpose: undefined,
+  companySituation: undefined,
+  studentPerformance: undefined,
+  problemsFound: undefined,
+  suggestions: undefined,
+  visitSummary: undefined,
+  visitImages: undefined,
+  remark: undefined
+});
+
+// 表单验证规则
+const formRules = {
+  studentId: [{ required: true, message: '请选择学生', trigger: 'change' }],
+  visitorName: [{ required: true, message: '请输入走访人姓名', trigger: 'blur' }],
+  visitDate: [{ required: true, message: '请选择走访日期', trigger: 'change' }],
+  visitPurpose: [{ required: true, message: '请输入走访目的', trigger: 'blur' }],
+  status: [{ required: true, message: '请选择走访状态', trigger: 'change' }]
+};
+
+// 引用
+const queryRef = ref();
+const formRef = ref();
+
+/** 查询走访记录列表 */
+function getList() {
+  loading.value = true;
+  const params = { ...queryParams } as any;
+  if (params.visitDateRange && params.visitDateRange.length === 2) {
+    params.visitStartDate = params.visitDateRange[0];
+    params.visitEndDate = params.visitDateRange[1];
+  }
+  delete params.visitDateRange;
+
+  listInternshipVisit(params).then((response) => {
+    console.log('API响应数据:', response); // 用于调试
+
+    // 健壮的响应处理逻辑
+    if (response && typeof response === 'object') {
+      // 情况1:直接的TableDataInfo格式 {total: number, rows: array}
+      if (response.rows !== undefined && response.total !== undefined) {
+        visitList.value = response.rows || [];
+        total.value = response.total || 0;
+      }
+      // 情况2:嵌套在data中的TableDataInfo格式 {data: {total: number, rows: array}}
+      else if (response.data && response.data.rows !== undefined && response.data.total !== undefined) {
+        visitList.value = response.data.rows || [];
+        total.value = response.data.total || 0;
+      }
+      // 情况3:直接返回数组格式
+      else if (Array.isArray(response)) {
+        visitList.value = response;
+        total.value = response.length;
+      }
+      // 情况4:嵌套的数组格式
+      else if (response.data && Array.isArray(response.data)) {
+        visitList.value = response.data;
+        total.value = response.data.length;
+      }
+      // 其他未知格式
+      else {
+        console.warn('未识别的API响应格式:', response);
+        visitList.value = [];
+        total.value = 0;
+      }
+    } else {
+      console.warn('API响应不是对象类型:', response);
+      visitList.value = [];
+      total.value = 0;
+    }
+
+    loading.value = false;
+    // 计算统计数据
+    calculateStatistics();
+  }).catch((error) => {
+    console.error('获取走访记录列表失败:', error);
+    visitList.value = [];
+    total.value = 0;
+    loading.value = false;
+    // 可以添加用户友好的错误提示
+    proxy?.$modal.msgError('获取数据失败,请稍后重试');
+  });
+}
+
+/** 计算统计数据 */
+function calculateStatistics() {
+  const now = new Date();
+  const currentMonth = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0');
+
+  statistics.value = {
+    total: visitList.value.length,
+    monthCount: visitList.value.filter((item) => item.visitDate && item.visitDate.startsWith(currentMonth)).length,
+    completedCount: visitList.value.filter((item) => item.status === '2').length, // 2-已审核
+    pendingCount: visitList.value.filter((item) => item.status === '0').length // 0-草稿
+  };
+}
+
+/** 搜索按钮操作 */
+function handleQuery() {
+  queryParams.pageNum = 1;
+  getList();
+}
+
+/** 重置按钮操作 */
+function resetQuery() {
+  queryRef.value.resetFields();
+  queryParams.visitDateRange = [];
+  handleQuery();
+}
+
+/** 多选框选中数据 */
+function handleSelectionChange(selection) {
+  ids.value = selection.map((item) => item.visitId);
+  single.value = selection.length !== 1;
+  multiple.value = !selection.length;
+}
+
+/** 新增按钮操作 */
+function handleAdd() {
+  reset();
+  formDialogVisible.value = true;
+  formTitle.value = '新增走访记录';
+  // 新增:自动加载当前班主任班级的学生列表
+  loadCurrentTeacherStudents();
+}
+
+/** 查看详情 */
+function handleView(row) {
+  currentRecord.value = { ...row };
+  viewDialogVisible.value = true;
+}
+
+/** 修改按钮操作 */
+function handleUpdate(row) {
+  reset();
+  const visitId = row.visitId;
+  getInternshipVisit(visitId).then((response) => {
+    Object.assign(form, response.data);
+    formDialogVisible.value = true;
+    formTitle.value = '修改走访记录';
+  });
+}
+
+/** 提交按钮 */
+function submitForm() {
+  formRef.value.validate((valid) => {
+    if (valid) {
+      // 检查实习ID是否已设置
+      if (!form.internshipId) {
+        ElMessage.error('请先选择学生,系统将自动设置实习ID');
+        return;
+      }
+
+      console.log('提交表单数据:', form);
+      formLoading.value = true;
+
+      if (form.visitId != undefined) {
+        updateInternshipVisit(form)
+          .then((response) => {
+            ElMessage.success('修改成功');
+            formDialogVisible.value = false;
+            getList();
+          })
+          .finally(() => {
+            formLoading.value = false;
+          });
+      } else {
+        addInternshipVisit(form)
+          .then((response) => {
+            ElMessage.success('新增成功');
+            formDialogVisible.value = false;
+            getList();
+          })
+          .finally(() => {
+            formLoading.value = false;
+          });
+      }
+    }
+  });
+}
+
+/** 删除按钮操作 */
+function handleDelete(row) {
+  const visitIds = row.visitId || ids.value;
+  ElMessageBox.confirm('是否确认删除走访记录编号为"' + visitIds + '"的数据项?')
+    .then(function () {
+      return delInternshipVisit(visitIds);
+    })
+    .then(() => {
+      getList();
+      ElMessage.success('删除成功');
+    })
+    .catch(() => {});
+}
+
+/** 导出按钮操作 */
+function handleExport() {
+  const params = { ...queryParams } as any;
+  if (params.visitDateRange && params.visitDateRange.length === 2) {
+    params.visitStartDate = params.visitDateRange[0];
+    params.visitEndDate = params.visitDateRange[1];
+  }
+  delete params.visitDateRange;
+
+  ElMessageBox.confirm('是否确认导出所有走访记录数据项?')
+    .then(function () {
+      return exportInternshipVisit(params);
+    })
+    .then((response) => {
+      // 处理下载
+      const blob = new Blob([response.data]);
+      const url = window.URL.createObjectURL(blob);
+      const link = document.createElement('a');
+      link.href = url;
+      link.download = '班主任走访记录_' + parseTime(new Date(), '{y}{m}{d}{h}{i}{s}') + '.xlsx';
+      link.click();
+      window.URL.revokeObjectURL(url);
+    })
+    .catch(() => {});
+}
+
+/** 搜索学生 */
+function searchStudents(query) {
+  studentLoading.value = true;
+
+  // 获取当前登录用户的班主任ID
+  const teacherId = userStore.userId;
+
+  // 构建查询参数
+  const params: any = {
+    pageNum: 1,
+    pageSize: 100,
+    classTeacherId: teacherId // 始终传递班主任ID
+  };
+
+  // 如果有搜索关键词,添加搜索条件
+  if (query && query.trim() !== '') {
+    const trimmedQuery = query.trim();
+    // 支持按姓名或学号搜索
+    if (/^\d+$/.test(trimmedQuery)) {
+      // 纯数字,按学号搜索
+      params.studentNo = trimmedQuery;
+    } else {
+      // 包含字母或中文,按姓名搜索
+      params.studentName = trimmedQuery;
+    }
+  }
+
+  console.log('搜索学生参数:', params);
+
+  listInternshipInfo(params)
+    .then((response) => {
+      if (response.rows && Array.isArray(response.rows)) {
+        studentOptions.value = response.rows;
+        console.log('搜索到学生数据:', response.rows.length, '条');
+        console.log('学生数据示例:', response.rows.slice(0, 3));
+      } else {
+        studentOptions.value = [];
+        console.warn('未搜索到学生数据');
+      }
+      studentLoading.value = false;
+    })
+    .catch((error) => {
+      console.error('搜索学生失败:', error);
+      studentOptions.value = [];
+      studentLoading.value = false;
+      ElMessage.error('搜索学生失败,请稍后重试');
+    });
+}
+
+/** 学生选择变化 */
+function handleStudentChange(studentId) {
+  const student = studentOptions.value.find((item) => item.studentId === studentId);
+  if (student) {
+    console.log('选择的学生完整数据:', student);
+    form.studentName = student.studentName;
+    form.studentNumber = student.studentNo;
+    form.className = student.className;
+    form.companyName = student.companyName;
+
+    // 尝试多种可能的实习ID字段名
+    if (student.internshipId) {
+      form.internshipId = student.internshipId;
+    } else if (student.id) {
+      form.internshipId = student.id;
+    } else if (student.internshipInfoId) {
+      form.internshipId = student.internshipInfoId;
+    } else {
+      console.warn('未找到实习ID字段,学生数据:', student);
+      form.internshipId = undefined;
+    }
+
+    console.log('设置后的表单数据:', {
+      studentId: form.studentId,
+      internshipId: form.internshipId,
+      studentName: form.studentName
+    });
+  }
+}
+
+/** 学生选择器获得焦点时的处理 */
+function handleStudentFocus() {
+  // 如果当前没有学生数据,自动加载一次
+  if (studentOptions.value.length === 0) {
+    loadCurrentTeacherStudents();
+  }
+}
+
+/** 表单重置 */
+function reset() {
+  Object.assign(form, {
+    visitId: undefined,
+    internshipId: undefined, // 实习ID
+    studentId: undefined,
+    studentName: undefined,
+    studentNumber: undefined,
+    className: undefined,
+    companyName: undefined,
+    visitorName: undefined,
+    visitDate: undefined,
+    status: '0', // 0-草稿(对应字典的planned)
+    visitPurpose: undefined,
+    companySituation: undefined,
+    studentPerformance: undefined,
+    problemsFound: undefined,
+    suggestions: undefined,
+    visitSummary: undefined,
+    visitImages: undefined,
+    remark: undefined
+  });
+  formRef.value?.resetFields();
+}
+
+/** 初始化字典数据 */
+function initDictData() {
+  // 获取走访状态字典
+  getDicts('zsjy_visit_status').then((response) => {
+    visitStatusOptions.value = response.data.map(item => ({
+      label: item.dictLabel,
+      value: item.dictValue
+    }));
+  });
+
+
+}
+
+
+
+/** 加载当前班主任班级的学生列表 */
+function loadCurrentTeacherStudents() {
+  const teacherId = userStore.userId;
+  if (teacherId) {
+    studentLoading.value = true;
+    console.log('开始加载班主任ID为', teacherId, '的学生列表');
+
+    listInternshipInfo({
+      pageNum: 1,
+      pageSize: 100, // 加载尽可能多的学生
+      classTeacherId: teacherId
+    })
+      .then((response) => {
+        console.log('API响应完整结构:', response);
+        console.log('response.data:', response.data);
+        console.log('response直接访问:', response);
+
+        // 根据msgmanage/index.vue的用法,直接使用response.rows
+        if (response.rows && Array.isArray(response.rows)) {
+          studentOptions.value = response.rows;
+          console.log('成功加载学生数据:', response.rows.length, '条');
+          console.log('学生数据示例:', response.rows.slice(0, 3));
+        } else {
+          studentOptions.value = [];
+          console.warn('未获取到学生数据,响应结构:', response);
+          console.warn('response.rows类型:', typeof response.rows);
+        }
+        studentLoading.value = false;
+      })
+      .catch((error) => {
+        console.error('加载学生数据失败:', error);
+        studentOptions.value = [];
+        studentLoading.value = false;
+        ElMessage.error('加载学生数据失败,请稍后重试');
+      });
+  } else {
+    console.warn('未获取到班主任ID');
+    ElMessage.warning('未获取到班主任ID,无法加载学生列表');
+  }
+}
+
+// 组件挂载
+onMounted(() => {
+  initDictData();
+  getList();
+  // 预加载当前班主任班级的学生列表
+  loadCurrentTeacherStudents();
+});
+</script>
+
+<style scoped>
+/* 搜索按钮容器样式 */
+.search-btn-container {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  height: 100%;
+}
+
+/* 按钮卡片样式 */
+.button-card {
+  margin-bottom: 16px;
+}
+
+.form-item-tip {
+  font-size: 12px;
+  color: #909399;
+  line-height: 1.4;
+  margin-top: 4px;
+}
+
+.card-header-vertical {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  width: 100%;
+}
+
+.toolbar-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.button-group {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+}
+
+.table-card {
+  margin-bottom: 16px;
+}
+
+/* 统计卡片样式 */
+.stats-row {
+  margin-bottom: 20px;
+}
+
+.stat-card {
+  background: white;
+  border-radius: 8px;
+  padding: 20px;
+  display: flex;
+  align-items: center;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  transition: all 0.3s ease;
+  margin-bottom: 16px;
+}
+
+.stat-card:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
+}
+
+.stat-icon {
+  width: 60px;
+  height: 60px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 16px;
+  font-size: 24px;
+  color: white;
+}
+
+.stat-card.total .stat-icon {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
+.stat-card.month .stat-icon {
+  background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+}
+
+.stat-card.completed .stat-icon {
+  background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+}
+
+.stat-card.pending .stat-icon {
+  background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
+}
+
+.stat-content {
+  flex: 1;
+}
+
+.stat-number {
+  font-size: 28px;
+  font-weight: bold;
+  color: #303133;
+  margin-bottom: 4px;
+}
+
+.stat-label {
+  font-size: 14px;
+  color: #909399;
+}
+
+/* 主卡片样式 */
+.main-card {
+  margin-top: 20px;
+}
+
+.main-card .el-card__body {
+  padding: 20px;
+}
+
+/* 搜索容器样式 */
+.search-container {
+  background: #f8f9fa;
+  padding: 16px;
+  border-radius: 6px;
+  margin-bottom: 16px;
+}
+
+/* 工具栏样式 */
+.toolbar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16px;
+  padding: 0 4px;
+}
+
+.toolbar-left {
+  display: flex;
+  gap: 12px;
+}
+
+.toolbar-right {
+  display: flex;
+  gap: 8px;
+}
+
+/* 表格容器样式 */
+.table-container {
+  border-radius: 6px;
+  overflow: hidden;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.el-table {
+  border-radius: 6px;
+}
+
+.el-table th {
+  background-color: #f5f7fa;
+  color: #606266;
+  font-weight: 600;
+}
+
+.el-table .el-table__row:hover {
+  background-color: #f5f7fa;
+}
+
+/* 分页容器样式 */
+.pagination-container {
+  display: flex;
+  justify-content: center;
+  margin-top: 20px;
+  padding: 16px 0;
+}
+
+/* 学生信息样式 */
+.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;
+  }
+}
+
+/* 操作按钮样式 */
+.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;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+  .stat-card {
+    margin-bottom: 10px;
+  }
+
+  .toolbar {
+    flex-direction: column;
+    gap: 12px;
+    align-items: stretch;
+  }
+
+  .toolbar-left,
+  .toolbar-right {
+    justify-content: center;
+  }
+
+  .action-buttons {
+    flex-direction: column;
+    gap: 4px;
+  }
+}
+</style>

Некоторые файлы не были показаны из-за большого количества измененных файлов