心理健康2.0

This commit is contained in:
李旭明 committed 2025-09-16 10:34:38 +08:00
1 parent 067957256d
commit 3b492479b5
63 files changed
+10170 -534

No files matched your search

+6
View File
@@ -93,6 +93,12 @@
<artifactId>ruoyi-xg</artifactId>
</dependency>
<!-- 系统模块 - 用于访问用户信息 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-system</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,113 @@
package org.dromara.stgl.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
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.stgl.domain.StglAnnualReviewAudit;
import org.dromara.stgl.domain.bo.StglAnnualReviewApprovalBo;
import org.dromara.stgl.service.IStglAnnualReviewAuditService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.util.List;
/**
* 社团年审审批
*
* @author ruoyi
* @date 2024-01-01
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/stgl/annualReviewAudit")
public class StglAnnualReviewAuditController extends BaseController {
private final IStglAnnualReviewAuditService stglAnnualReviewAuditService;
/**
* 查询社团年审审批详细
*/
@SaCheckPermission("stgl:annualReviewAudit:query")
@GetMapping("/{id}")
public R<StglAnnualReviewAudit> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(stglAnnualReviewAuditService.queryById(id));
}
/**
* 根据年审记录ID查询审批记录
*/
@SaCheckPermission("stgl:annualReviewAudit:query")
@GetMapping("/byAnnualReview/{annualReviewId}")
public R<List<StglAnnualReviewAudit>> getByAnnualReviewId(@NotNull(message = "年审记录ID不能为空")
@PathVariable Long annualReviewId) {
return R.ok(stglAnnualReviewAuditService.queryByAnnualReviewId(annualReviewId));
}
/**
* 根据审批人ID查询待审批记录
*/
@SaCheckPermission("stgl:annualReviewAudit:query")
@GetMapping("/pending/{approverId}")
public R<List<StglAnnualReviewAudit>> getPendingByApproverId(@NotNull(message = "审批人ID不能为空")
@PathVariable Long approverId) {
return R.ok(stglAnnualReviewAuditService.queryPendingByApproverId(approverId));
}
/**
* 新增社团年审审批
*/
@SaCheckPermission("stgl:annualReviewAudit:add")
@Log(title = "社团年审审批", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody StglAnnualReviewAudit stglAnnualReviewAudit) {
return toAjax(stglAnnualReviewAuditService.insertByBo(stglAnnualReviewAudit));
}
/**
* 修改社团年审审批
*/
@SaCheckPermission("stgl:annualReviewAudit:edit")
@Log(title = "社团年审审批", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody StglAnnualReviewAudit stglAnnualReviewAudit) {
return toAjax(stglAnnualReviewAuditService.updateByBo(stglAnnualReviewAudit));
}
/**
* 删除社团年审审批
*/
@SaCheckPermission("stgl:annualReviewAudit:remove")
@Log(title = "社团年审审批", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(stglAnnualReviewAuditService.deleteWithValidByIds(List.of(ids), true));
}
/**
* 审批年审记录
*/
@SaCheckPermission("stgl:annualReviewAudit:approve")
@Log(title = "社团年审审批", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PostMapping("/approve")
public R<Void> approve(@Validated @RequestBody StglAnnualReviewApprovalBo approvalBo) {
return toAjax(stglAnnualReviewAuditService.approveAnnualReview(approvalBo));
}
}
@@ -0,0 +1,179 @@
package org.dromara.stgl.controller;
import java.util.List;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.web.core.BaseController;
import org.dromara.common.mybatis.core.page.PageQuery;
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.log.enums.BusinessType;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.stgl.domain.vo.StglAuditorVo;
import org.dromara.stgl.domain.bo.StglAuditorBo;
import org.dromara.stgl.service.IStglAuditorService;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.system.service.ISysUserService;
import org.dromara.system.domain.vo.SysUserVo;
/**
* 社团管理审核人配置
*
* @author ruoyi
* @date 2024-01-01
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/stgl/auditor")
public class StglAuditorController extends BaseController {
private final IStglAuditorService stglAuditorService;
private final ISysUserService sysUserService;
/**
* 查询社团管理审核人配置列表
*/
@SaCheckPermission("stgl:auditor:list")
@GetMapping("/list")
public TableDataInfo<StglAuditorVo> list(StglAuditorBo bo, PageQuery pageQuery) {
return stglAuditorService.queryPageList(bo, pageQuery);
}
/**
* 导出社团管理审核人配置列表
*/
@SaCheckPermission("stgl:auditor:export")
@Log(title = "社团管理审核人配置", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(StglAuditorBo bo, HttpServletResponse response) {
List<StglAuditorVo> list = stglAuditorService.queryList(bo);
ExcelUtil.exportExcel(list, "社团管理审核人配置", StglAuditorVo.class, response);
}
/**
* 获取社团管理审核人配置详细信息
*/
@SaCheckPermission("stgl:auditor:query")
@GetMapping("/{id}")
public R<StglAuditorVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(stglAuditorService.queryById(id));
}
/**
* 新增社团管理审核人配置
*/
@SaCheckPermission("stgl:auditor:add")
@Log(title = "社团管理审核人配置", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody StglAuditorBo bo) {
return toAjax(stglAuditorService.insertByBo(bo));
}
/**
* 修改社团管理审核人配置
*/
@SaCheckPermission("stgl:auditor:edit")
@Log(title = "社团管理审核人配置", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody StglAuditorBo bo) {
return toAjax(stglAuditorService.updateByBo(bo));
}
/**
* 删除社团管理审核人配置
*/
@SaCheckPermission("stgl:auditor:remove")
@Log(title = "社团管理审核人配置", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(stglAuditorService.deleteWithValidByIds(List.of(ids)));
}
/**
* 批量添加审核人
*/
@SaCheckPermission("stgl:auditor:add")
@Log(title = "批量添加审核人", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping("/batchAdd")
public R<Void> batchAdd(@RequestBody BatchAddRequest request) {
return toAjax(stglAuditorService.batchAddAuditors(request.getUserIds(), request.getRemark()));
}
/**
* 查询我配置的审核人列表
*/
@SaCheckPermission("stgl:auditor:list")
@GetMapping("/myConfig")
public TableDataInfo<StglAuditorVo> myConfigList(StglAuditorBo bo, PageQuery pageQuery) {
return stglAuditorService.queryMyConfigList(bo, pageQuery);
}
/**
* 批量启用审核人
*/
@SaCheckPermission("stgl:auditor:edit")
@Log(title = "启用审核人", businessType = BusinessType.UPDATE)
@PostMapping("/enable")
public R<Void> enable(@RequestBody List<Long> ids) {
return toAjax(stglAuditorService.enableAuditors(ids));
}
/**
* 批量停用审核人
*/
@SaCheckPermission("stgl:auditor:edit")
@Log(title = "停用审核人", businessType = BusinessType.UPDATE)
@PostMapping("/disable")
public R<Void> disable(@RequestBody List<Long> ids) {
return toAjax(stglAuditorService.disableAuditors(ids));
}
/**
* 根据部门ID获取用户列表
*/
@SaCheckPermission("stgl:auditor:list")
@GetMapping("/users/{deptId}")
public R<List<SysUserVo>> listUserByDeptId(@PathVariable String deptId) {
List<SysUserVo> users = sysUserService.selectUserListByDept(Long.valueOf(deptId));
return R.ok(users);
}
/**
* 批量添加请求对象
*/
public static class BatchAddRequest {
private List<Long> userIds;
private String remark;
public List<Long> getUserIds() {
return userIds;
}
public void setUserIds(List<Long> userIds) {
this.userIds = userIds;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
}
}
@@ -0,0 +1,90 @@
package org.dromara.stgl.domain;
import org.dromara.common.tenant.core.TenantEntity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.math.BigDecimal;
import java.util.Date;
/**
* 社团年审审批对象 stgl_annual_review_audit
*
* @author ruoyi
* @date 2024-01-01
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("stgl_annual_review_audit")
public class StglAnnualReviewAudit extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId
private Long id;
/**
* 删除标志(0代表存在 1代表删除)
*/
@TableLogic
private Integer deleted;
/**
* 年审记录ID
*/
private Long annualReviewId;
/**
* 节点代码(advisor_approve/college_approve/youth_league_approve)
*/
private String nodeCode;
/**
* 节点名称(指导老师审批/学院审批/校团委审批)
*/
private String nodeName;
/**
* 节点顺序(1.1,1.2,2,3)
*/
private BigDecimal nodeOrder;
/**
* 节点状态(pending/waiting/approved)
*/
@TableField("audit_status")
private String nodeStatus;
/**
* 审批人ID
*/
private Long approverId;
/**
* 审批人姓名
*/
private String approverName;
/**
* 审批结果(通过/不通过)
*/
private String approveResult;
/**
* 审批意见
*/
@TableField("audit_opinion")
private String approveOpinion;
/**
* 审批时间
*/
@TableField("audit_time")
private Date approveTime;
}
@@ -0,0 +1,86 @@
package org.dromara.stgl.domain;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.tenant.core.TenantEntity;
import java.io.Serial;
import java.io.Serializable;
/**
* 社团管理审核人配置对象 stgl_auditor
*
* @author ruoyi
* @date 2024-01-01
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("stgl_auditor")
public class StglAuditor extends TenantEntity implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id")
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 用户姓名
*/
private String userName;
/**
* 用户昵称
*/
private String nickName;
/**
* 部门ID
*/
private Long deptId;
/**
* 部门名称
*/
private String deptName;
/**
* 联系电话
*/
private String phoneNumber;
/**
* 邮箱地址
*/
private String email;
/**
* 状态(1正常 0停用)
*/
private String status;
/**
* 备注说明
*/
private String remark;
/**
* 配置人ID
*/
private Long configBy;
/**
* 配置人姓名
*/
private String configByName;
}
@@ -0,0 +1,39 @@
package org.dromara.stgl.domain;
import lombok.Data;
/**
* 社团管理用户和部门完整信息
* 用于批量添加审核人时获取完整的用户数据
*
* @author ruoyi
* @date 2025-01-03
*/
@Data
public class StglUserWithDeptInfo {
/** 用户ID */
private Long userId;
/** 用户账号 */
private String userName;
/** 用户昵称 */
private String nickName;
/** 部门ID */
private Long deptId;
/** 部门名称 */
private String deptName;
/** 手机号码 */
private String phonenumber;
/** 用户邮箱 */
private String email;
/** 帐号状态(1正常 0停用) */
private String status;
}
@@ -0,0 +1,60 @@
package org.dromara.stgl.domain.bo;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
/**
* 社团年审审批请求业务对象
*
* @author system
* @date 2025-01-03
*/
@Data
public class StglAnnualReviewApprovalBo {
/**
* 审批记录ID
*/
@NotNull(message = "审批记录ID不能为空")
private Long auditId;
/**
* 审批结果
*/
@NotBlank(message = "审批结果不能为空")
private String approveResult;
/**
* 审批意见
*/
private String approveOpinion;
/**
* 下一步审核人列表
*/
private List<NextAuditor> nextAuditors;
/**
* 下一步审核人信息
*/
@Data
public static class NextAuditor {
/**
* 用户ID
*/
private Long userId;
/**
* 用户名
*/
private String userName;
/**
* 昵称
*/
private String nickName;
}
}
@@ -0,0 +1,113 @@
package org.dromara.stgl.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.stgl.domain.StglAuditor;
import jakarta.validation.constraints.*;
import java.util.Date;
import java.util.List;
/**
* 社团管理审核人配置业务对象 stgl_auditor
*
* @author ruoyi
* @date 2024-01-01
*/
@Data
@EqualsAndHashCode(callSuper = true)
@AutoMapper(target = StglAuditor.class, reverseConvertGenerate = false)
public class StglAuditorBo extends BaseEntity {
/**
* 主键ID
*/
@NotNull(message = "主键ID不能为空", groups = { EditGroup.class })
private Long id;
/**
* 用户ID
*/
@NotNull(message = "用户ID不能为空", groups = { AddGroup.class, EditGroup.class })
private Long userId;
/**
* 用户姓名
*/
@NotBlank(message = "用户姓名不能为空", groups = { AddGroup.class, EditGroup.class })
private String userName;
/**
* 用户昵称
*/
@NotBlank(message = "用户昵称不能为空", groups = { AddGroup.class, EditGroup.class })
private String nickName;
/**
* 部门ID
*/
@NotNull(message = "部门ID不能为空", groups = { AddGroup.class, EditGroup.class })
private Long deptId;
/**
* 部门名称
*/
@NotBlank(message = "部门名称不能为空", groups = { AddGroup.class, EditGroup.class })
private String deptName;
/**
* 联系电话
*/
private String phoneNumber;
/**
* 邮箱地址
*/
private String email;
/**
* 状态(1正常 0停用)
*/
@NotBlank(message = "状态不能为空", groups = { AddGroup.class, EditGroup.class })
private String status;
/**
* 备注说明
*/
private String remark;
/**
* 配置人ID
*/
private Long configBy;
/**
* 配置人姓名
*/
private String configByName;
/**
* 分页大小
*/
private Integer pageSize;
/**
* 页码
*/
private Integer pageNum;
/**
* 搜索值
*/
private String searchValue;
/**
* 创建时间范围
*/
private List<Date> createTimeRange;
}
@@ -0,0 +1,133 @@
package org.dromara.stgl.domain.vo;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.excel.convert.ExcelDictConvert;
import org.dromara.stgl.domain.StglAuditor;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 社团管理审核人配置视图对象 stgl_auditor
*
* @author ruoyi
* @date 2024-01-01
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = StglAuditor.class)
public class StglAuditorVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@ExcelProperty(value = "主键ID")
private Long id;
/**
* 用户ID
*/
@ExcelProperty(value = "用户ID")
private Long userId;
/**
* 用户姓名
*/
@ExcelProperty(value = "用户姓名")
private String userName;
/**
* 用户昵称
*/
@ExcelProperty(value = "用户昵称")
private String nickName;
/**
* 部门ID
*/
@ExcelProperty(value = "部门ID")
private Long deptId;
/**
* 部门名称
*/
@ExcelProperty(value = "部门名称")
private String deptName;
/**
* 联系电话
*/
@ExcelProperty(value = "联系电话")
private String phoneNumber;
/**
* 邮箱地址
*/
@ExcelProperty(value = "邮箱地址")
private String email;
/**
* 状态(1正常 0停用)
*/
@ExcelProperty(value = "状态", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "sys_normal_disable")
private String status;
/**
* 备注说明
*/
@ExcelProperty(value = "备注说明")
private String remark;
/**
* 配置人ID
*/
private Long configBy;
/**
* 配置人姓名
*/
@ExcelProperty(value = "配置人")
private String configByName;
/**
* 租户编号
*/
private String tenantId;
/**
* 创建部门
*/
private Long createDept;
/**
* 创建者ID
*/
private Long createBy;
/**
* 创建时间
*/
@ExcelProperty(value = "创建时间")
private Date createTime;
/**
* 更新者ID
*/
private Long updateBy;
/**
* 更新时间
*/
@ExcelProperty(value = "更新时间")
private Date updateTime;
}
@@ -0,0 +1,40 @@
package org.dromara.stgl.mapper;
import org.dromara.stgl.domain.StglAnnualReviewAudit;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import java.util.List;
/**
* 社团年审审批Mapper接口
*
* @author ruoyi
* @date 2024-01-01
*/
public interface StglAnnualReviewAuditMapper extends BaseMapperPlus<StglAnnualReviewAudit, StglAnnualReviewAudit> {
/**
* 根据年审记录ID查询审批记录
*
* @param annualReviewId 年审记录ID
* @return 审批记录列表
*/
List<StglAnnualReviewAudit> selectByAnnualReviewId(Long annualReviewId);
/**
* 根据年审记录ID和节点代码查询审批记录
*
* @param annualReviewId 年审记录ID
* @param nodeCode 节点代码
* @return 审批记录列表
*/
List<StglAnnualReviewAudit> selectByAnnualReviewIdAndNodeCode(Long annualReviewId, String nodeCode);
/**
* 根据审批人ID查询待审批记录
*
* @param approverId 审批人ID
* @return 审批记录列表
*/
List<StglAnnualReviewAudit> selectPendingByApproverId(Long approverId);
}
@@ -0,0 +1,66 @@
package org.dromara.stgl.mapper;
import org.dromara.stgl.domain.StglAuditor;
import org.dromara.stgl.domain.vo.StglAuditorVo;
import org.dromara.stgl.domain.StglUserWithDeptInfo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 社团管理审核人配置Mapper接口
*
* @author ruoyi
* @date 2024-01-01
*/
public interface StglAuditorMapper extends BaseMapperPlus<StglAuditor, StglAuditorVo> {
/**
* 查询社团管理审核人配置
*
* @param id 社团管理审核人配置主键
* @return 社团管理审核人配置
*/
StglAuditorVo selectStglAuditorVoById(Long id);
/**
* 查询社团管理审核人配置列表
*
* @param stglAuditor 社团管理审核人配置
* @return 社团管理审核人配置集合
*/
List<StglAuditorVo> selectStglAuditorVoList(StglAuditor stglAuditor);
/**
* 根据用户ID检查是否已存在审核人配置
*
* @param userId 用户ID
* @return 记录数
*/
int checkUserExists(@Param("userId") Long userId);
/**
* 批量启用审核人
*
* @param ids 审核人ID列表
* @return 影响行数
*/
int enableAuditorsByIds(@Param("ids") List<Long> ids);
/**
* 批量停用审核人
*
* @param ids 审核人ID列表
* @return 影响行数
*/
int disableAuditorsByIds(@Param("ids") List<Long> ids);
/**
* 查询完整的用户信息(包含部门信息)用于批量添加
*
* @param userIds 用户ID列表
* @return 用户和部门完整信息列表
*/
List<StglUserWithDeptInfo> selectUsersWithDeptInfo(@Param("userIds") List<Long> userIds);
}
@@ -0,0 +1,104 @@
package org.dromara.stgl.service;
import org.dromara.stgl.domain.StglAnnualReviewAudit;
import org.dromara.stgl.domain.bo.StglAnnualReviewApprovalBo;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import java.util.Collection;
import java.util.List;
/**
* 社团年审审批Service接口
*
* @author ruoyi
* @date 2024-01-01
*/
public interface IStglAnnualReviewAuditService {
/**
* 查询社团年审审批
*
* @param id 社团年审审批主键
* @return 社团年审审批
*/
StglAnnualReviewAudit queryById(Long id);
/**
* 根据年审记录ID查询审批记录
*
* @param annualReviewId 年审记录ID
* @return 审批记录列表
*/
List<StglAnnualReviewAudit> queryByAnnualReviewId(Long annualReviewId);
/**
* 根据年审记录ID和节点代码查询审批记录
*
* @param annualReviewId 年审记录ID
* @param nodeCode 节点代码
* @return 审批记录列表
*/
List<StglAnnualReviewAudit> queryByAnnualReviewIdAndNodeCode(Long annualReviewId, String nodeCode);
/**
* 根据审批人ID查询待审批记录
*
* @param approverId 审批人ID
* @return 审批记录列表
*/
List<StglAnnualReviewAudit> queryPendingByApproverId(Long approverId);
/**
* 新增社团年审审批
*
* @param stglAnnualReviewAudit 社团年审审批
* @return 结果
*/
Boolean insertByBo(StglAnnualReviewAudit stglAnnualReviewAudit);
/**
* 修改社团年审审批
*
* @param stglAnnualReviewAudit 社团年审审批
* @return 结果
*/
Boolean updateByBo(StglAnnualReviewAudit stglAnnualReviewAudit);
/**
* 校验并批量删除社团年审审批信息
*
* @param ids 需要删除的社团年审审批主键集合
* @param isValid 是否校验,true-删除前校验,false-不校验
* @return 结果
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 创建指导老师审批记录
*
* @param annualReviewId 年审记录ID
* @param advisorIds 指导老师ID列表
* @param advisorNames 指导老师姓名列表
* @return 结果
*/
Boolean createAdvisorApprovalRecords(Long annualReviewId, List<Long> advisorIds, List<String> advisorNames);
/**
* 审批年审记录
*
* @param auditId 审批记录ID
* @param approveResult 审批结果
* @param approveOpinion 审批意见
* @return 结果
*/
Boolean approveAnnualReview(Long auditId, String approveResult, String approveOpinion);
/**
* 审批年审记录(支持动态审核人选择)
*
* @param approvalBo 审批请求对象
* @return 结果
*/
Boolean approveAnnualReview(StglAnnualReviewApprovalBo approvalBo);
}
@@ -0,0 +1,102 @@
package org.dromara.stgl.service;
import org.dromara.stgl.domain.vo.StglAuditorVo;
import org.dromara.stgl.domain.bo.StglAuditorBo;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import java.util.Collection;
import java.util.List;
/**
* 社团管理审核人配置Service接口
*
* @author ruoyi
* @date 2024-01-01
*/
public interface IStglAuditorService {
/**
* 查询社团管理审核人配置
*
* @param id 主键
* @return 社团管理审核人配置
*/
StglAuditorVo queryById(Long id);
/**
* 查询社团管理审核人配置列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 社团管理审核人配置集合
*/
TableDataInfo<StglAuditorVo> queryPageList(StglAuditorBo bo, PageQuery pageQuery);
/**
* 查询社团管理审核人配置列表
*
* @param bo 查询条件
* @return 社团管理审核人配置集合
*/
List<StglAuditorVo> queryList(StglAuditorBo bo);
/**
* 新增社团管理审核人配置
*
* @param bo 社团管理审核人配置
* @return 结果
*/
Boolean insertByBo(StglAuditorBo bo);
/**
* 修改社团管理审核人配置
*
* @param bo 社团管理审核人配置
* @return 结果
*/
Boolean updateByBo(StglAuditorBo bo);
/**
* 校验并批量删除社团管理审核人配置信息
*
* @param ids 需要删除的主键集合
* @return 结果
*/
Boolean deleteWithValidByIds(Collection<Long> ids);
/**
* 批量添加审核人
*
* @param userIds 用户ID列表
* @param remark 备注
* @return 结果
*/
Boolean batchAddAuditors(List<Long> userIds, String remark);
/**
* 查询我配置的审核人列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 审核人列表
*/
TableDataInfo<StglAuditorVo> queryMyConfigList(StglAuditorBo bo, PageQuery pageQuery);
/**
* 批量启用审核人
*
* @param ids 审核人ID列表
* @return 结果
*/
Boolean enableAuditors(List<Long> ids);
/**
* 批量停用审核人
*
* @param ids 审核人ID列表
* @return 结果
*/
Boolean disableAuditors(List<Long> ids);
}
@@ -0,0 +1,409 @@
package org.dromara.stgl.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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.stgl.domain.StglAnnualReviewAudit;
import org.dromara.stgl.domain.StglClubAnnualReview;
import org.dromara.stgl.domain.bo.StglAnnualReviewApprovalBo;
import org.dromara.stgl.mapper.StglAnnualReviewAuditMapper;
import org.dromara.stgl.mapper.StglClubAnnualReviewMapper;
import org.dromara.stgl.service.IStglAnnualReviewAuditService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* 社团年审审批Service业务层处理
*
* @author ruoyi
* @date 2024-01-01
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class StglAnnualReviewAuditServiceImpl implements IStglAnnualReviewAuditService {
private final StglAnnualReviewAuditMapper baseMapper;
private final StglClubAnnualReviewMapper annualReviewMapper;
/**
* 查询社团年审审批
*/
@Override
public StglAnnualReviewAudit queryById(Long id) {
return baseMapper.selectById(id);
}
/**
* 根据年审记录ID查询审批记录
*/
@Override
public List<StglAnnualReviewAudit> queryByAnnualReviewId(Long annualReviewId) {
return baseMapper.selectByAnnualReviewId(annualReviewId);
}
/**
* 根据年审记录ID和节点代码查询审批记录
*/
@Override
public List<StglAnnualReviewAudit> queryByAnnualReviewIdAndNodeCode(Long annualReviewId, String nodeCode) {
return baseMapper.selectByAnnualReviewIdAndNodeCode(annualReviewId, nodeCode);
}
/**
* 根据审批人ID查询待审批记录
*/
@Override
public List<StglAnnualReviewAudit> queryPendingByApproverId(Long approverId) {
return baseMapper.selectPendingByApproverId(approverId);
}
/**
* 新增社团年审审批
*/
@Override
public Boolean insertByBo(StglAnnualReviewAudit bo) {
StglAnnualReviewAudit add = BeanUtil.toBean(bo, StglAnnualReviewAudit.class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setId(add.getId());
}
return flag;
}
/**
* 修改社团年审审批
*/
@Override
public Boolean updateByBo(StglAnnualReviewAudit bo) {
StglAnnualReviewAudit update = BeanUtil.toBean(bo, StglAnnualReviewAudit.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(StglAnnualReviewAudit entity) {
// TODO 做一些数据校验,如唯一约束
}
/**
* 批量删除社团年审审批
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
// TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteByIds(ids) > 0;
}
/**
* 创建指导老师审批记录
*/
@Override
@Transactional
public Boolean createAdvisorApprovalRecords(Long annualReviewId, List<Long> advisorIds, List<String> advisorNames) {
if (ObjectUtil.isEmpty(advisorIds) || ObjectUtil.isEmpty(advisorNames) || advisorIds.size() != advisorNames.size()) {
return false;
}
// 删除已存在的指导老师审批记录
LambdaQueryWrapper<StglAnnualReviewAudit> wrapper = Wrappers.lambdaQuery();
wrapper.eq(StglAnnualReviewAudit::getAnnualReviewId, annualReviewId)
.eq(StglAnnualReviewAudit::getNodeCode, "advisor_approve");
baseMapper.delete(wrapper);
// 创建新的指导老师审批记录
for (int i = 0; i < advisorIds.size(); i++) {
StglAnnualReviewAudit audit = new StglAnnualReviewAudit();
audit.setAnnualReviewId(annualReviewId);
audit.setNodeCode("advisor_approve");
audit.setNodeName("指导老师审批");
// 设置节点顺序:单个指导老师为1.0,多个指导老师为1.1, 1.2, 1.3...
if (advisorIds.size() == 1) {
audit.setNodeOrder(new BigDecimal("1.0"));
} else {
audit.setNodeOrder(new BigDecimal("1." + (i + 1)));
}
// 设置节点状态:第一个为pending,其他为waiting
audit.setNodeStatus(i == 0 ? "pending" : "waiting");
audit.setApproverId(advisorIds.get(i));
audit.setApproverName(advisorNames.get(i));
baseMapper.insert(audit);
}
return true;
}
/**
* 审批年审记录
*/
@Override
@Transactional
public Boolean approveAnnualReview(Long auditId, String approveResult, String approveOpinion) {
// 1. 获取当前审批记录
StglAnnualReviewAudit currentAudit = baseMapper.selectById(auditId);
if (currentAudit == null || !"pending".equals(currentAudit.getNodeStatus())) {
return false;
}
// 2. 更新当前审批记录
currentAudit.setNodeStatus("approved");
currentAudit.setApproveResult(approveResult);
currentAudit.setApproveOpinion(approveOpinion);
currentAudit.setApproveTime(new Date());
baseMapper.updateById(currentAudit);
// 3. 如果审批不通过,直接更新年审记录状态为4(审核不通过)
if ("不通过".equals(approveResult)) {
StglClubAnnualReview annualReview = annualReviewMapper.selectById(currentAudit.getAnnualReviewId());
if (annualReview != null) {
annualReview.setStatus("4");
annualReviewMapper.updateById(annualReview);
}
return true;
}
// 4. 如果审批通过,检查是否有下一个待审批的同环节审批人
List<StglAnnualReviewAudit> samePhaseAudits = baseMapper.selectByAnnualReviewIdAndNodeCode(
currentAudit.getAnnualReviewId(), currentAudit.getNodeCode());
// 查找下一个waiting状态的审批记录
StglAnnualReviewAudit nextAudit = null;
for (StglAnnualReviewAudit audit : samePhaseAudits) {
if ("waiting".equals(audit.getNodeStatus()) &&
audit.getNodeOrder().compareTo(currentAudit.getNodeOrder()) > 0) {
if (nextAudit == null || audit.getNodeOrder().compareTo(nextAudit.getNodeOrder()) < 0) {
nextAudit = audit;
}
}
}
// 5. 如果有下一个审批人,将其状态改为pending
if (nextAudit != null) {
nextAudit.setNodeStatus("pending");
baseMapper.updateById(nextAudit);
} else {
// 6. 如果同环节所有审批人都审批通过,检查是否是第一个审批节点,决定是否更新状态为2(审核中)
boolean allApproved = samePhaseAudits.stream()
.allMatch(audit -> "approved".equals(audit.getNodeStatus()) && "通过".equals(audit.getApproveResult()));
if (allApproved) {
// 只有第一个审批节点(node_order=1.1)通过时才设置为2-审核中
if (isFirstApprovalNode(currentAudit)) {
StglClubAnnualReview annualReview = annualReviewMapper.selectById(currentAudit.getAnnualReviewId());
if (annualReview != null) {
annualReview.setStatus("2");
annualReviewMapper.updateById(annualReview);
log.info("第一个审批节点通过,年审记录状态更新为2-审核中,年审记录ID: {}", annualReview.getId());
}
} else {
log.info("非第一个审批节点通过,保持当前状态,当前节点: {}, node_order: {}",
currentAudit.getNodeName(), currentAudit.getNodeOrder());
}
}
}
return true;
}
/**
* 审批年审记录(支持动态审核人选择)
*/
@Override
@Transactional
public Boolean approveAnnualReview(StglAnnualReviewApprovalBo approvalBo) {
// 调用原有方法进行基础审批
Boolean result = approveAnnualReview(approvalBo.getAuditId(),
approvalBo.getApproveResult(),
approvalBo.getApproveOpinion());
// 如果审批通过且有指定下一步审核人,创建新的审批记录
if (result && "通过".equals(approvalBo.getApproveResult()) &&
approvalBo.getNextAuditors() != null && !approvalBo.getNextAuditors().isEmpty()) {
// 获取当前审批记录
StglAnnualReviewAudit currentAudit = baseMapper.selectById(approvalBo.getAuditId());
if (currentAudit != null) {
// 创建下一步审核人的审批记录
createNextStepAuditRecords(currentAudit.getAnnualReviewId(), approvalBo.getNextAuditors());
}
} else if (result && "通过".equals(approvalBo.getApproveResult()) &&
(approvalBo.getNextAuditors() == null || approvalBo.getNextAuditors().isEmpty())) {
// 如果审批通过且没有下一步审核人,检查是否是校团委最后节点,决定是否结束流程
StglAnnualReviewAudit currentAudit = baseMapper.selectById(approvalBo.getAuditId());
if (currentAudit != null && isSchoolCommitteeLastNode(currentAudit)) {
// 校团委最后节点审批通过,流程结束,将年审记录状态设置为3(审核通过)
StglClubAnnualReview annualReview = annualReviewMapper.selectById(currentAudit.getAnnualReviewId());
if (annualReview != null) {
annualReview.setStatus("3");
annualReviewMapper.updateById(annualReview);
log.info("校团委最后节点审批通过,年审流程结束,年审记录ID: {}, 状态更新为: 3", annualReview.getId());
}
}
}
return result;
}
/**
* 创建下一步审批记录
*/
private void createNextStepAuditRecords(Long annualReviewId, List<StglAnnualReviewApprovalBo.NextAuditor> nextAuditors) {
// 查询已有的审批记录,确定下一个审批层级
List<StglAnnualReviewAudit> existingAudits = baseMapper.selectByAnnualReviewId(annualReviewId);
// 确定下一个审批层级
int nextLevel = determineNextApprovalLevel(existingAudits);
for (int i = 0; i < nextAuditors.size(); i++) {
StglAnnualReviewApprovalBo.NextAuditor auditor = nextAuditors.get(i);
StglAnnualReviewAudit nextAudit = new StglAnnualReviewAudit();
nextAudit.setAnnualReviewId(annualReviewId);
nextAudit.setNodeCode(getNodeCodeByLevel(nextLevel));
nextAudit.setNodeName(getNodeNameByLevel(nextLevel));
// 根据层级和顺序设置node_order
if (nextAuditors.size() == 1 && nextLevel == 3) {
// 校团委审批单个审核人为3.0
nextAudit.setNodeOrder(new BigDecimal(nextLevel + ".0"));
} else {
// 其他情况按 层级.序号 格式设置
nextAudit.setNodeOrder(new BigDecimal(nextLevel + "." + (i + 1)));
}
nextAudit.setApproverId(auditor.getUserId());
nextAudit.setApproverName(auditor.getNickName() != null ? auditor.getNickName() : auditor.getUserName());
nextAudit.setNodeStatus(i == 0 ? "pending" : "waiting"); // 第一个为pending,其余为waiting
nextAudit.setCreateTime(new Date());
baseMapper.insert(nextAudit);
}
}
/**
* 确定下一个审批层级
*/
private int determineNextApprovalLevel(List<StglAnnualReviewAudit> existingAudits) {
int maxLevel = 1; // 默认从指导老师审批层级开始
for (StglAnnualReviewAudit audit : existingAudits) {
BigDecimal nodeOrder = audit.getNodeOrder();
if (nodeOrder != null) {
int level = nodeOrder.intValue(); // 获取整数部分作为层级
if (level > maxLevel) {
maxLevel = level;
}
}
}
return maxLevel + 1; // 下一个层级
}
/**
* 根据层级获取节点代码
*/
private String getNodeCodeByLevel(int level) {
switch (level) {
case 1:
return "advisor_approve";
case 2:
return "college_approve";
case 3:
return "youth_league_approve";
default:
return "college_approve"; // 默认为二级学院审批
}
}
/**
* 根据层级获取节点名称
*/
private String getNodeNameByLevel(int level) {
switch (level) {
case 1:
return "指导老师审批";
case 2:
return "二级学院审批";
case 3:
return "校团委审批";
default:
return "二级学院审批"; // 默认为二级学院审批
}
}
/**
* 判断是否为第一个审批节点(node_order=1.1)
*/
private boolean isFirstApprovalNode(StglAnnualReviewAudit currentAudit) {
// 检查node_order是否为1.1
return currentAudit != null &&
currentAudit.getNodeOrder() != null &&
currentAudit.getNodeOrder().compareTo(new BigDecimal("1.1")) == 0;
}
/**
* 判断当前是否是校团委审批的最后一个节点
*/
private boolean isSchoolCommitteeLastNode(StglAnnualReviewAudit currentAudit) {
if (currentAudit == null || currentAudit.getNodeOrder() == null) {
return false;
}
String currentNodeName = currentAudit.getNodeName();
BigDecimal currentNodeOrder = currentAudit.getNodeOrder();
// 检查是否是校团委审批节点且node_order是3.X格式
boolean isSchoolCommitteeNode = "校团委审批".equals(currentNodeName);
boolean isNodeOrder3X = currentNodeOrder.compareTo(new BigDecimal("3")) >= 0 &&
currentNodeOrder.compareTo(new BigDecimal("4")) < 0;
if (!isSchoolCommitteeNode || !isNodeOrder3X) {
return false;
}
// 获取同一环节的所有节点
List<StglAnnualReviewAudit> samePhaseNodes = baseMapper.selectByAnnualReviewIdAndNodeCode(
currentAudit.getAnnualReviewId(), currentAudit.getNodeCode());
if (samePhaseNodes.isEmpty()) {
return false;
}
// 找出同一环节中最大的nodeOrder
BigDecimal maxNodeOrder = samePhaseNodes.stream()
.map(StglAnnualReviewAudit::getNodeOrder)
.filter(order -> order != null)
.max(BigDecimal::compareTo)
.orElse(BigDecimal.ZERO);
// 当前节点的nodeOrder等于同环节最大nodeOrder时,表示是最后一个节点
boolean isLastNode = currentNodeOrder.compareTo(maxNodeOrder) == 0;
log.info("校团委最后节点判断: 节点名称={}, nodeOrder={}, 是否校团委={}, 是否3.X={}, 是否最后节点={}",
currentNodeName, currentNodeOrder, isSchoolCommitteeNode, isNodeOrder3X, isLastNode);
return isLastNode;
}
}
@@ -0,0 +1,266 @@
package org.dromara.stgl.service.impl;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.dromara.stgl.domain.bo.StglAuditorBo;
import org.dromara.stgl.domain.vo.StglAuditorVo;
import org.dromara.stgl.domain.StglAuditor;
import org.dromara.stgl.domain.StglUserWithDeptInfo;
import org.dromara.stgl.mapper.StglAuditorMapper;
import org.dromara.stgl.service.IStglAuditorService;
import org.dromara.system.service.ISysUserService;
import org.dromara.system.domain.vo.SysUserVo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.core.exception.ServiceException;
import java.util.List;
import java.util.Map;
import java.util.Collection;
import java.util.ArrayList;
import java.util.stream.Collectors;
/**
* 社团管理审核人配置Service业务层处理
*
* @author ruoyi
* @date 2024-01-01
*/
@RequiredArgsConstructor
@Service
public class StglAuditorServiceImpl implements IStglAuditorService {
private final StglAuditorMapper baseMapper;
private final ISysUserService sysUserService;
/**
* 查询社团管理审核人配置
*/
@Override
public StglAuditorVo queryById(Long id) {
StglAuditor entity = baseMapper.selectById(id);
if (entity == null) {
return null;
}
return MapstructUtils.convert(entity, StglAuditorVo.class);
}
/**
* 查询社团管理审核人配置列表
*/
@Override
public TableDataInfo<StglAuditorVo> queryPageList(StglAuditorBo bo, PageQuery pageQuery) {
// 直接使用查询条件构建器
LambdaQueryWrapper<StglAuditor> lqw = buildQueryWrapper(bo);
Page<StglAuditor> entityPage = baseMapper.selectPage(pageQuery.build(), lqw);
// 转换为 VO 对象
List<StglAuditorVo> voList = new ArrayList<>();
for (StglAuditor entity : entityPage.getRecords()) {
StglAuditorVo vo = MapstructUtils.convert(entity, StglAuditorVo.class);
voList.add(vo);
}
// 构建分页结果
Page<StglAuditorVo> voPage = new Page<>(entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal());
voPage.setRecords(voList);
return TableDataInfo.build(voPage);
}
/**
* 查询社团管理审核人配置列表
*/
@Override
public List<StglAuditorVo> queryList(StglAuditorBo bo) {
LambdaQueryWrapper<StglAuditor> lqw = buildQueryWrapper(bo);
List<StglAuditor> entityList = baseMapper.selectList(lqw);
// 转换为 VO 对象
List<StglAuditorVo> voList = new ArrayList<>();
for (StglAuditor entity : entityList) {
StglAuditorVo vo = MapstructUtils.convert(entity, StglAuditorVo.class);
voList.add(vo);
}
return voList;
}
private LambdaQueryWrapper<StglAuditor> buildQueryWrapper(StglAuditorBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<StglAuditor> lqw = Wrappers.<StglAuditor>lambdaQuery();
lqw.eq(bo.getUserId() != null, StglAuditor::getUserId, bo.getUserId());
lqw.like(StringUtils.isNotBlank(bo.getUserName()), StglAuditor::getUserName, bo.getUserName());
lqw.like(StringUtils.isNotBlank(bo.getNickName()), StglAuditor::getNickName, bo.getNickName());
lqw.eq(bo.getDeptId() != null, StglAuditor::getDeptId, bo.getDeptId());
lqw.eq(StringUtils.isNotBlank(bo.getStatus()), StglAuditor::getStatus, bo.getStatus());
lqw.eq(bo.getConfigBy() != null, StglAuditor::getConfigBy, bo.getConfigBy());
lqw.between(params.get("beginCreateTime") != null && params.get("endCreateTime") != null,
StglAuditor::getCreateTime, params.get("beginCreateTime"), params.get("endCreateTime"));
lqw.orderByDesc(StglAuditor::getCreateTime);
return lqw;
}
/**
* 新增社团管理审核人配置
*/
@Override
public Boolean insertByBo(StglAuditorBo bo) {
StglAuditor add = MapstructUtils.convert(bo, StglAuditor.class);
validEntityBeforeSave(add);
// 设置配置人信息
Long currentUserId = LoginHelper.getUserId();
add.setConfigBy(currentUserId);
// 获取当前用户信息设置配置人姓名
try {
SysUserVo currentUser = sysUserService.selectUserById(currentUserId);
if (currentUser != null) {
add.setConfigByName(currentUser.getUserName());
}
} catch (Exception e) {
// 如果获取失败,使用默认值
add.setConfigByName("系统管理员");
}
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setId(add.getId());
}
return flag;
}
/**
* 修改社团管理审核人配置
*/
@Override
public Boolean updateByBo(StglAuditorBo bo) {
StglAuditor update = MapstructUtils.convert(bo, StglAuditor.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(StglAuditor entity) {
if (entity.getUserId() != null) {
// 检查用户是否已存在(新增时或修改用户ID时)
if (entity.getId() == null) {
// 新增时检查
int count = baseMapper.checkUserExists(entity.getUserId());
if (count > 0) {
throw new ServiceException("该用户已配置为审核人,不能重复添加");
}
}
}
}
/**
* 批量删除社团管理审核人配置
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids) {
return baseMapper.deleteBatchIds(ids) > 0;
}
/**
* 批量添加审核人
*/
@Override
public Boolean batchAddAuditors(List<Long> userIds, String remark) {
if (userIds == null || userIds.isEmpty()) {
throw new ServiceException("用户ID列表不能为空");
}
Long currentUserId = LoginHelper.getUserId();
String currentUserName = "系统管理员";
// 获取当前用户信息
try {
SysUserVo currentUser = sysUserService.selectUserById(currentUserId);
if (currentUser != null) {
currentUserName = currentUser.getUserName();
}
} catch (Exception e) {
// 使用默认值
}
// 1. 获取完整的用户信息(包含部门信息)
List<StglUserWithDeptInfo> users = baseMapper.selectUsersWithDeptInfo(userIds);
if (users.isEmpty()) {
throw new ServiceException("没有找到有效的用户信息");
}
// 2. 过滤已存在的审核人
List<Long> existingUserIds = baseMapper.selectList(Wrappers.<StglAuditor>lambdaQuery()
.in(StglAuditor::getUserId, userIds))
.stream().map(StglAuditor::getUserId).collect(Collectors.toList());
// 3. 构建新增审核人列表
List<StglAuditor> auditorList = new ArrayList<>();
for (StglUserWithDeptInfo user : users) {
if (!existingUserIds.contains(user.getUserId())) {
StglAuditor auditor = new StglAuditor();
auditor.setUserId(user.getUserId());
auditor.setUserName(user.getUserName());
auditor.setNickName(user.getNickName());
auditor.setDeptId(user.getDeptId());
auditor.setDeptName(user.getDeptName() != null ? user.getDeptName() : "");
auditor.setPhoneNumber(user.getPhonenumber());
auditor.setEmail(user.getEmail());
auditor.setStatus("1"); // 默认正常状态
auditor.setRemark(remark);
auditor.setConfigBy(currentUserId);
auditor.setConfigByName(currentUserName);
auditorList.add(auditor);
}
}
// 4. 批量保存
if (auditorList.isEmpty()) {
throw new ServiceException("所有用户都已存在,无需重复添加");
}
return baseMapper.insertBatch(auditorList);
}
/**
* 查询我配置的审核人列表
*/
@Override
public TableDataInfo<StglAuditorVo> queryMyConfigList(StglAuditorBo bo, PageQuery pageQuery) {
bo.setConfigBy(LoginHelper.getUserId());
return queryPageList(bo, pageQuery);
}
/**
* 批量启用审核人
*/
@Override
public Boolean enableAuditors(List<Long> ids) {
if (ids == null || ids.isEmpty()) {
return false;
}
return baseMapper.enableAuditorsByIds(ids) > 0;
}
/**
* 批量停用审核人
*/
@Override
public Boolean disableAuditors(List<Long> ids) {
if (ids == null || ids.isEmpty()) {
return false;
}
return baseMapper.disableAuditorsByIds(ids) > 0;
}
}
@@ -24,8 +24,10 @@ import org.dromara.stgl.mapper.StglClubAnnualReviewActivityMapper;
import org.dromara.stgl.service.IStglClubAnnualReviewService;
import org.dromara.stgl.service.IStglActivityService;
import org.dromara.stgl.service.IStglClubService;
import org.dromara.stgl.service.IStglAnnualReviewAuditService;
import org.dromara.stgl.domain.bo.StglActivityBo;
import org.dromara.stgl.domain.vo.StglClubVo;
import org.dromara.stgl.domain.StglClub;
import java.util.List;
import java.util.Map;
@@ -60,6 +62,7 @@ public class StglClubAnnualReviewServiceImpl implements IStglClubAnnualReviewSer
private final StglClubAnnualReviewActivityMapper activityMapper;
private final IStglActivityService stglActivityService;
private final IStglClubService stglClubService;
private final IStglAnnualReviewAuditService auditService;
private final ObjectMapper objectMapper = new ObjectMapper();
/**
@@ -539,9 +542,82 @@ public class StglClubAnnualReviewServiceImpl implements IStglClubAnnualReviewSer
throw new RuntimeException("只能提交草稿状态的年审记录");
}
// 获取社团信息,提取指导老师信息
StglClubVo club = stglClubService.queryById(entity.getClubId());
if (club == null) {
throw new RuntimeException("社团信息不存在");
}
// 提取指导老师信息
List<Long> advisorIds = new ArrayList<>();
List<String> advisorNames = new ArrayList<>();
try {
// 优先从JSON字段读取指导老师数据
if (StringUtils.isNotBlank(club.getAdvisorsJson())) {
ObjectMapper mapper = new ObjectMapper();
List<Map<String, Object>> advisors = mapper.readValue(club.getAdvisorsJson(),
new com.fasterxml.jackson.core.type.TypeReference<List<Map<String, Object>>>() {});
for (Map<String, Object> advisor : advisors) {
// 提取用户ID和姓名
Object userIdObj = null;
String advisorName = null;
// 尝试直接从advisor对象获取userId和name(兼容旧格式)
if (advisor.containsKey("userId")) {
userIdObj = advisor.get("userId");
advisorName = (String) advisor.get("name");
}
// 尝试从teacher子对象获取userId和name(新格式)
else if (advisor.containsKey("teacher")) {
Object teacherObj = advisor.get("teacher");
if (teacherObj instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> teacher = (Map<String, Object>) teacherObj;
userIdObj = teacher.get("userId");
advisorName = (String) teacher.get("nickName");
if (StringUtils.isBlank(advisorName)) {
advisorName = (String) teacher.get("userName");
}
}
}
if (userIdObj != null && StringUtils.isNotBlank(advisorName)) {
Long userId = Long.valueOf(userIdObj.toString());
advisorIds.add(userId);
advisorNames.add(advisorName);
}
}
}
// 如果没有JSON数据,尝试从单一字段获取
if (advisorIds.isEmpty() && StringUtils.isNotBlank(club.getAdvisorName())) {
// 这种情况下无法获取用户ID,暂时跳过审批流程
log.warn("社团 {} 的指导老师信息不完整,无法创建审批记录", club.getClubName());
}
} catch (Exception e) {
log.error("解析指导老师信息失败", e);
throw new RuntimeException("解析指导老师信息失败");
}
// 更新年审记录状态
entity.setStatus("1");
entity.setReviewTime(new Date());
return baseMapper.updateById(entity) > 0;
boolean updateResult = baseMapper.updateById(entity) > 0;
// 如果有指导老师信息,创建审批记录
if (!advisorIds.isEmpty() && updateResult) {
try {
auditService.createAdvisorApprovalRecords(id, advisorIds, advisorNames);
} catch (Exception e) {
log.error("创建审批记录失败", e);
throw new RuntimeException("创建审批记录失败");
}
}
return updateResult;
}
/**
@@ -0,0 +1,56 @@
<?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.stgl.mapper.StglAnnualReviewAuditMapper">
<resultMap type="org.dromara.stgl.domain.StglAnnualReviewAudit" id="StglAnnualReviewAuditResult">
<result property="id" column="id" />
<result property="tenantId" column="tenant_id" />
<result property="createDept" column="create_dept" />
<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="deleted" column="deleted" />
<result property="annualReviewId" column="annual_review_id" />
<result property="nodeCode" column="node_code" />
<result property="nodeName" column="node_name" />
<result property="nodeOrder" column="node_order" />
<result property="nodeStatus" column="audit_status" />
<result property="approverId" column="approver_id" />
<result property="approverName" column="approver_name" />
<result property="approveResult" column="approve_result" />
<result property="approveOpinion" column="audit_opinion" />
<result property="approveTime" column="audit_time" />
</resultMap>
<sql id="selectStglAnnualReviewAuditVo">
select id, tenant_id, create_dept, create_by, create_time, update_by, update_time, deleted,
annual_review_id, node_code, node_name, node_order, audit_status,
approver_id, approver_name, approve_result, audit_opinion, audit_time
from stgl_annual_review_audit
</sql>
<!-- 根据年审记录ID查询审批记录 -->
<select id="selectByAnnualReviewId" parameterType="Long" resultMap="StglAnnualReviewAuditResult">
<include refid="selectStglAnnualReviewAuditVo"/>
where annual_review_id = #{annualReviewId} and deleted = 0
order by node_order
</select>
<!-- 根据年审记录ID和节点代码查询审批记录 -->
<select id="selectByAnnualReviewIdAndNodeCode" resultMap="StglAnnualReviewAuditResult">
<include refid="selectStglAnnualReviewAuditVo"/>
where annual_review_id = #{annualReviewId} and node_code = #{nodeCode} and deleted = 0
order by node_order
</select>
<!-- 根据审批人ID查询待审批记录 -->
<select id="selectPendingByApproverId" parameterType="Long" resultMap="StglAnnualReviewAuditResult">
<include refid="selectStglAnnualReviewAuditVo"/>
where approver_id = #{approverId} and audit_status = 'pending' and deleted = 0
order by create_time desc
</select>
</mapper>
@@ -0,0 +1,132 @@
<?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.stgl.mapper.StglAuditorMapper">
<resultMap type="org.dromara.stgl.domain.vo.StglAuditorVo" id="StglAuditorResult">
<result property="id" column="id" />
<result property="userId" column="user_id" />
<result property="userName" column="user_name" />
<result property="nickName" column="nick_name" />
<result property="deptId" column="dept_id" />
<result property="deptName" column="dept_name" />
<result property="phoneNumber" column="phone_number" />
<result property="email" column="email" />
<result property="status" column="status" />
<result property="remark" column="remark" />
<result property="configBy" column="config_by" />
<result property="configByName" column="config_by_name" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
</resultMap>
<sql id="selectStglAuditorVo">
SELECT
sa.id,
sa.user_id,
sa.user_name,
sa.nick_name,
sa.dept_id,
sa.dept_name,
sa.phone_number,
sa.email,
sa.status,
sa.remark,
sa.config_by,
sa.config_by_name,
sa.create_by,
sa.create_time,
sa.update_by,
sa.update_time
FROM stgl_auditor sa
</sql>
<select id="selectStglAuditorVoList" parameterType="org.dromara.stgl.domain.StglAuditor" resultMap="StglAuditorResult">
<include refid="selectStglAuditorVo"/>
<where>
sa.deleted = 0
<if test="userId != null">
AND sa.user_id = #{userId}
</if>
<if test="userName != null and userName != ''">
AND sa.user_name LIKE CONCAT('%', #{userName}, '%')
</if>
<if test="nickName != null and nickName != ''">
AND sa.nick_name LIKE CONCAT('%', #{nickName}, '%')
</if>
<if test="deptId != null">
AND sa.dept_id = #{deptId}
</if>
<if test="status != null and status != ''">
AND sa.status = #{status}
</if>
<if test="configBy != null">
AND sa.config_by = #{configBy}
</if>
<if test="params.beginCreateTime != null and params.beginCreateTime != ''">
AND sa.create_time &gt;= #{params.beginCreateTime}
</if>
<if test="params.endCreateTime != null and params.endCreateTime != ''">
AND sa.create_time &lt;= #{params.endCreateTime}
</if>
</where>
ORDER BY sa.create_time DESC
</select>
<select id="selectStglAuditorVoById" parameterType="Long" resultMap="StglAuditorResult">
<include refid="selectStglAuditorVo"/>
<where>
sa.id = #{id} AND sa.deleted = 0
</where>
</select>
<select id="checkUserExists" parameterType="Long" resultType="int">
SELECT COUNT(1) FROM stgl_auditor
WHERE user_id = #{userId} AND deleted = 0
</select>
<update id="enableAuditorsByIds">
UPDATE stgl_auditor
SET status = '1', update_time = NOW()
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
AND deleted = 0
</update>
<update id="disableAuditorsByIds">
UPDATE stgl_auditor
SET status = '0', update_time = NOW()
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
AND deleted = 0
</update>
<!-- 查询完整的用户信息(包含部门信息)用于批量添加 -->
<select id="selectUsersWithDeptInfo" resultType="org.dromara.stgl.domain.StglUserWithDeptInfo">
select
u.user_id as userId,
u.user_name as userName,
u.nick_name as nickName,
u.dept_id as deptId,
d.dept_name as deptName,
u.phonenumber as phonenumber,
u.email as email,
u.status as status
from sys_user u
left join sys_dept d on u.dept_id = d.dept_id
where u.status = '0'
and u.user_id in
<foreach item="userId" collection="userIds" open="(" separator="," close=")">
#{userId}
</foreach>
order by u.dept_id, u.user_name
</select>
</mapper>
@@ -6,6 +6,7 @@ import org.dromara.common.web.core.BaseController;
import org.dromara.xljk.domain.bo.XljkConsultationAppointmentBo;
import org.dromara.xljk.domain.vo.XljkConsultationAppointmentVo;
import org.dromara.xljk.domain.vo.TimeSlotDetailVo;
import org.dromara.xljk.domain.vo.ScheduleWithStatusVo;
import org.dromara.xljk.service.IXljkConsultationAppointmentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
@@ -161,52 +162,10 @@ public class XljkConsultationAppointmentController extends BaseController {
// 从请求体中获取参数
String status = requestBody.get("status").toString();
Date actualVisitTime = null;
Integer communicationDuration = null;
String consultationContent = "";
String counselorSuggestion = "";
// 处理actualVisitTime
if (requestBody.get("actualVisitTime") != null) {
try {
String actualVisitTimeStr = requestBody.get("actualVisitTime").toString();
log.info("解析实际到访时间字符串: {}", actualVisitTimeStr);
// 尝试多种日期格式
SimpleDateFormat[] dateFormats = {
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"),
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"),
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"),
new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"),
new SimpleDateFormat("yyyy-MM-dd")
};
boolean parsed = false;
for (SimpleDateFormat sdf : dateFormats) {
try {
actualVisitTime = sdf.parse(actualVisitTimeStr);
parsed = true;
log.info("成功解析实际到访时间: {}, 使用格式: {}", actualVisitTime, sdf.toPattern());
break;
} catch (Exception e) {
// 继续尝试下一个格式
}
}
if (!parsed) {
log.error("无法解析实际到访时间: {}", actualVisitTimeStr);
// 使用当前时间作为默认值
actualVisitTime = new Date();
log.info("使用当前时间作为默认值: {}", actualVisitTime);
}
} catch (Exception e) {
log.error("解析actualVisitTime失败", e);
// 使用当前时间作为默认值
actualVisitTime = new Date();
log.info("使用当前时间作为默认值: {}", actualVisitTime);
}
}
// 处理communicationDuration
if (requestBody.get("communicationDuration") != null) {
try {
@@ -227,8 +186,47 @@ public class XljkConsultationAppointmentController extends BaseController {
counselorSuggestion = requestBody.get("counselorSuggestion").toString();
}
boolean result = service.recordConsultation(id, actualVisitTime, communicationDuration,
consultationContent, counselorSuggestion, status);
// 处理心理病史相关字段
Integer psychologicalHistory = null;
String psychologicalHistoryDetail = "";
String basicSituation = "";
String majorLifeEvents = "";
Integer needReferral = null;
if (requestBody.get("psychologicalHistory") != null) {
try {
psychologicalHistory = Integer.parseInt(requestBody.get("psychologicalHistory").toString());
} catch (Exception e) {
log.error("解析psychologicalHistory失败", e);
}
}
if (requestBody.get("psychologicalHistoryDetail") != null) {
psychologicalHistoryDetail = requestBody.get("psychologicalHistoryDetail").toString();
}
if (requestBody.get("basicSituation") != null) {
basicSituation = requestBody.get("basicSituation").toString();
}
if (requestBody.get("majorLifeEvents") != null) {
majorLifeEvents = requestBody.get("majorLifeEvents").toString();
}
if (requestBody.get("needReferral") != null) {
try {
needReferral = Integer.parseInt(requestBody.get("needReferral").toString());
} catch (Exception e) {
log.error("解析needReferral失败", e);
}
}
log.info("解析的心理病史相关字段: psychologicalHistory={}, psychologicalHistoryDetail={}, basicSituation={}, majorLifeEvents={}, needReferral={}",
psychologicalHistory, psychologicalHistoryDetail, basicSituation, majorLifeEvents, needReferral);
boolean result = service.recordConsultation(id, communicationDuration,
consultationContent, counselorSuggestion, status,
psychologicalHistory, psychologicalHistoryDetail, basicSituation, majorLifeEvents, needReferral);
if (result) {
return R.ok(true);
} else {
@@ -284,7 +282,7 @@ public class XljkConsultationAppointmentController extends BaseController {
XSSFSheet sheet = workbook.createSheet("预约记录");
// 表头
String[] headers = {"序号", "学生姓名", "学号", "院系", "专业", "班级", "咨询原因", "咨询师", "预约日期", "预约时段", "状态", "咨询结果", "实际到访时间", "沟通时长", "咨询内容", "咨询师建议"};
String[] headers = {"序号", "学生姓名", "学号", "院系", "专业", "班级", "咨询主题", "咨询原因", "咨询师", "预约日期", "预约时段", "状态", "咨询结果", "沟通时长", "心理病史", "心理病史详情", "基本情况", "重大生活事件", "本次咨询过程", "是否需要转介", "本学期到访次数"};
XSSFRow headerRow = sheet.createRow(0);
for (int i = 0; i < headers.length; i++) {
headerRow.createCell(i).setCellValue(headers[i]);
@@ -303,6 +301,7 @@ public class XljkConsultationAppointmentController extends BaseController {
row.createCell(col++).setCellValue(vo.getCollegeName());
row.createCell(col++).setCellValue(vo.getMajorName());
row.createCell(col++).setCellValue(vo.getClassName());
row.createCell(col++).setCellValue(vo.getConsultTheme() != null ? vo.getConsultTheme() : ""); // 咨询主题
row.createCell(col++).setCellValue(vo.getReason());
row.createCell(col++).setCellValue(vo.getCounselorName());
row.createCell(col++).setCellValue(vo.getAppointmentDate() != null ? dateFormat.format(vo.getAppointmentDate()) : "");
@@ -315,15 +314,43 @@ public class XljkConsultationAppointmentController extends BaseController {
case 1: status = "已到访"; break;
case 2: status = "已取消"; break;
case 3: status = "未到访"; break;
case 4: status = "待审核"; break;
case 5: status = "已驳回"; break;
default: status = "未知状态";
}
}
row.createCell(col++).setCellValue(status);
row.createCell(col++).setCellValue(status); // 咨询结果
row.createCell(col++).setCellValue(vo.getActualVisitTime() != null ? dateTimeFormat.format(vo.getActualVisitTime()) : "");
row.createCell(col++).setCellValue(vo.getCommunicationDuration() != null ? vo.getCommunicationDuration().toString() : "");
row.createCell(col++).setCellValue(vo.getConsultationContent());
row.createCell(col++).setCellValue(vo.getCounselorSuggestion());
row.createCell(col++).setCellValue(vo.getCommunicationDuration() != null ? vo.getCommunicationDuration().toString() + "分钟" : "");
// 心理病史
String psychologicalHistory = "";
if (vo.getPsychologicalHistory() != null) {
psychologicalHistory = vo.getPsychologicalHistory() == 2 ? "是" : "否";
}
row.createCell(col++).setCellValue(psychologicalHistory);
// 心理病史详情
row.createCell(col++).setCellValue(vo.getPsychologicalHistoryDetail() != null ? vo.getPsychologicalHistoryDetail() : "");
// 基本情况
row.createCell(col++).setCellValue(vo.getBasicSituation() != null ? vo.getBasicSituation() : "");
// 重大生活事件
row.createCell(col++).setCellValue(vo.getMajorLifeEvents() != null ? vo.getMajorLifeEvents() : "");
// 本次咨询过程(原咨询内容)
row.createCell(col++).setCellValue(vo.getConsultationContent() != null ? vo.getConsultationContent() : "");
// 是否需要转介
String needReferral = "";
if (vo.getNeedReferral() != null) {
needReferral = vo.getNeedReferral() == 2 ? "是" : "否";
}
row.createCell(col++).setCellValue(needReferral);
// 本学期到访次数
row.createCell(col++).setCellValue(vo.getSemesterVisitCount() != null ? vo.getSemesterVisitCount().toString() + "次" : "0次");
}
// 写入字节流
@@ -350,6 +377,15 @@ public class XljkConsultationAppointmentController extends BaseController {
.body(data);
}
/**
* 获取日历视图的排班数据(带预约状态)
*/
@GetMapping("/calendar-schedules")
public R<List<ScheduleWithStatusVo>> getCalendarSchedules(@RequestParam String startDate, @RequestParam String endDate) {
List<ScheduleWithStatusVo> schedules = service.getCalendarSchedules(startDate, endDate);
return R.ok(schedules);
}
/**
* 转义CSV字段中的特殊字符
*/
@@ -14,6 +14,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.dromara.xljk.domain.vo.XljkConsultationScheduleSummaryVo;
@RestController
@@ -57,10 +58,14 @@ public class XljkConsultationScheduleController {
batchBo.setCounselorId(Long.valueOf(requestMap.get("counselorId").toString()));
}
if (requestMap.containsKey("office")) {
batchBo.setOffice(requestMap.get("office").toString());
}
batchBo.setStartDate(requestMap.get("startDate").toString());
batchBo.setEndDate(requestMap.get("endDate").toString());
// 设置上午时段
// 设置中午时段(字段名保持morning以兼容前端)
if (requestMap.containsKey("morningEnabled")) {
batchBo.setMorningEnabled(Boolean.valueOf(requestMap.get("morningEnabled").toString()));
}
@@ -73,6 +78,26 @@ public class XljkConsultationScheduleController {
batchBo.setMorningEndTime(requestMap.get("morningEndTime").toString());
}
// 设置中午时段(新版本)
if (requestMap.containsKey("noonEnabled")) {
batchBo.setNoonEnabled(Boolean.valueOf(requestMap.get("noonEnabled").toString()));
}
if (requestMap.containsKey("noonTimeSlots")) {
List<Map<String, Object>> noonSlots = (List<Map<String, Object>>) requestMap.get("noonTimeSlots");
if (noonSlots != null) {
List<XljkConsultationScheduleBatchBo.TimeSlot> timeSlots = noonSlots.stream()
.map(slot -> {
XljkConsultationScheduleBatchBo.TimeSlot timeSlot = new XljkConsultationScheduleBatchBo.TimeSlot();
timeSlot.setStartTime(slot.get("startTime").toString());
timeSlot.setEndTime(slot.get("endTime").toString());
return timeSlot;
})
.collect(Collectors.toList());
batchBo.setNoonTimeSlots(timeSlots);
}
}
// 设置下午时段
if (requestMap.containsKey("afternoonEnabled")) {
batchBo.setAfternoonEnabled(Boolean.valueOf(requestMap.get("afternoonEnabled").toString()));
@@ -86,6 +111,21 @@ public class XljkConsultationScheduleController {
batchBo.setAfternoonEndTime(requestMap.get("afternoonEndTime").toString());
}
if (requestMap.containsKey("afternoonTimeSlots")) {
List<Map<String, Object>> afternoonSlots = (List<Map<String, Object>>) requestMap.get("afternoonTimeSlots");
if (afternoonSlots != null) {
List<XljkConsultationScheduleBatchBo.TimeSlot> timeSlots = afternoonSlots.stream()
.map(slot -> {
XljkConsultationScheduleBatchBo.TimeSlot timeSlot = new XljkConsultationScheduleBatchBo.TimeSlot();
timeSlot.setStartTime(slot.get("startTime").toString());
timeSlot.setEndTime(slot.get("endTime").toString());
return timeSlot;
})
.collect(Collectors.toList());
batchBo.setAfternoonTimeSlots(timeSlots);
}
}
// 设置其他字段
if (requestMap.containsKey("status")) {
batchBo.setStatus(requestMap.get("status").toString());
@@ -166,10 +206,13 @@ public class XljkConsultationScheduleController {
*/
@GetMapping("/options/counselors")
public R<List<Map<String, Object>>> getCounselorOptions() {
String sql = "SELECT u.user_id as id, u.nick_name as name FROM sys_user u " +
String sql = "SELECT u.user_id as id, u.nick_name as name, c.office as office " +
"FROM sys_user u " +
"JOIN sys_user_role ur ON u.user_id = ur.user_id " +
"JOIN sys_role r ON ur.role_id = r.role_id " +
"WHERE r.role_key = 'xljk_consultant' AND u.del_flag = '0'";
"LEFT JOIN xljk_consulter c ON u.user_id = c.user_id AND c.deleted = 0 " +
"WHERE r.role_key = 'xljk_consultant' AND u.del_flag = '0' " +
"ORDER BY u.nick_name";
List<Map<String, Object>> counselorOptions = jdbcTemplate.queryForList(sql);
return R.ok(counselorOptions);
@@ -0,0 +1,179 @@
package org.dromara.xljk.controller;
import java.util.List;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.web.core.BaseController;
import org.dromara.common.mybatis.core.page.PageQuery;
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.log.enums.BusinessType;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.xljk.domain.vo.XljkConsulterVo;
import org.dromara.xljk.domain.bo.XljkConsulterBo;
import org.dromara.xljk.service.IXljkConsulterService;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.system.service.ISysUserService;
import org.dromara.system.domain.vo.SysUserVo;
/**
* 心理咨询师配置
*
* @author ruoyi
* @date 2024-01-01
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/xljk/consulter")
public class XljkConsulterController extends BaseController {
private final IXljkConsulterService xljkConsulterService;
private final ISysUserService sysUserService;
/**
* 查询心理咨询师配置列表
*/
@SaCheckPermission("xljk:consulter:list")
@GetMapping("/list")
public TableDataInfo<XljkConsulterVo> list(XljkConsulterBo bo, PageQuery pageQuery) {
return xljkConsulterService.queryPageList(bo, pageQuery);
}
/**
* 导出心理咨询师配置列表
*/
@SaCheckPermission("xljk:consulter:export")
@Log(title = "心理咨询师配置", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(XljkConsulterBo bo, HttpServletResponse response) {
List<XljkConsulterVo> list = xljkConsulterService.queryList(bo);
ExcelUtil.exportExcel(list, "心理咨询师配置", XljkConsulterVo.class, response);
}
/**
* 获取心理咨询师配置详细信息
*/
@SaCheckPermission("xljk:consulter:query")
@GetMapping("/{id}")
public R<XljkConsulterVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(xljkConsulterService.queryById(id));
}
/**
* 新增心理咨询师配置
*/
@SaCheckPermission("xljk:consulter:add")
@Log(title = "心理咨询师配置", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody XljkConsulterBo bo) {
return toAjax(xljkConsulterService.insertByBo(bo));
}
/**
* 修改心理咨询师配置
*/
@SaCheckPermission("xljk:consulter:edit")
@Log(title = "心理咨询师配置", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody XljkConsulterBo bo) {
return toAjax(xljkConsulterService.updateByBo(bo));
}
/**
* 删除心理咨询师配置
*/
@SaCheckPermission("xljk:consulter:remove")
@Log(title = "心理咨询师配置", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(xljkConsulterService.deleteWithValidByIds(List.of(ids)));
}
/**
* 批量添加咨询师
*/
@SaCheckPermission("xljk:consulter:add")
@Log(title = "批量添加咨询师", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping("/batchAdd")
public R<Void> batchAdd(@RequestBody BatchAddRequest request) {
return toAjax(xljkConsulterService.batchAddConsulters(request.getUserIds(), request.getRemark()));
}
/**
* 查询我配置的咨询师列表
*/
@SaCheckPermission("xljk:consulter:list")
@GetMapping("/myConfig")
public TableDataInfo<XljkConsulterVo> myConfigList(XljkConsulterBo bo, PageQuery pageQuery) {
return xljkConsulterService.queryMyConfigList(bo, pageQuery);
}
/**
* 批量启用咨询师
*/
@SaCheckPermission("xljk:consulter:edit")
@Log(title = "启用咨询师", businessType = BusinessType.UPDATE)
@PostMapping("/enable")
public R<Void> enable(@RequestBody List<Long> ids) {
return toAjax(xljkConsulterService.enableConsulters(ids));
}
/**
* 批量停用咨询师
*/
@SaCheckPermission("xljk:consulter:edit")
@Log(title = "停用咨询师", businessType = BusinessType.UPDATE)
@PostMapping("/disable")
public R<Void> disable(@RequestBody List<Long> ids) {
return toAjax(xljkConsulterService.disableConsulters(ids));
}
/**
* 根据部门ID获取用户列表
*/
@SaCheckPermission("xljk:consulter:list")
@GetMapping("/users/{deptId}")
public R<List<SysUserVo>> listUserByDeptId(@PathVariable String deptId) {
List<SysUserVo> users = sysUserService.selectUserListByDept(Long.valueOf(deptId));
return R.ok(users);
}
/**
* 批量添加请求对象
*/
public static class BatchAddRequest {
private List<Long> userIds;
private String remark;
public List<Long> getUserIds() {
return userIds;
}
public void setUserIds(List<Long> userIds) {
this.userIds = userIds;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
}
}
@@ -3,6 +3,7 @@ package org.dromara.xljk.domain;
import java.util.Date;
import java.io.Serializable;
import lombok.Data;
import com.baomidou.mybatisplus.annotation.TableField;
@Data
public class XljkConsultationAppointment implements Serializable {
@@ -21,9 +22,20 @@ public class XljkConsultationAppointment implements Serializable {
private String appointmentTime;
private String appointmentDetailTime; // 具体预约时间段,如"08:30-09:00"
private String reason;
@TableField("consult_theme")
private String consultTheme; // 咨询主题
private String status;
private Date actualVisitTime;
private Integer communicationDuration;
private String consultationContent;
private String counselorSuggestion;
// 新增心理健康相关字段
private String studentName; // 学生姓名(冗余字段)
private Integer psychologicalHistory; // 心理病史(1-否,2-是)
private String psychologicalHistoryDetail; // 心理病史详情
private String basicSituation; // 基本情况
private String majorLifeEvents; // 重大生活事件
private Integer needReferral; // 是否需要转介(1-不需要,2-需要)
}
@@ -15,8 +15,9 @@ public class XljkConsultationSchedule implements Serializable {
private Date updateTime;
private Integer deleted;
private Long counselorId;
private String office; // 办公室
private Date scheduleDate;
private String timeSlot; // 0上午 1下午
private String timeSlot; // 0中午 1下午
private String startTime; // 开始时间 HH:mm 格式
private String endTime; // 结束时间 HH:mm 格式
private String status; // 0正常 1停用
@@ -0,0 +1,97 @@
package org.dromara.xljk.domain;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.tenant.core.TenantEntity;
import java.io.Serial;
import java.io.Serializable;
/**
* 心理咨询师配置对象 xljk_consulter
*
* @author ruoyi
* @date 2024-01-01
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("xljk_consulter")
public class XljkConsulter extends TenantEntity implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id")
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 用户姓名
*/
private String userName;
/**
* 用户昵称
*/
private String nickName;
/**
* 部门ID
*/
private Long deptId;
/**
* 部门名称
*/
private String deptName;
/**
* 联系电话
*/
private String phoneNumber;
/**
* 办公室
*/
private String office;
/**
* 邮箱地址
*/
private String email;
/**
* 状态(1正常 0停用)
*/
private String status;
/**
* 备注说明
*/
private String remark;
/**
* 配置人ID
*/
private Long configBy;
/**
* 配置人姓名
*/
private String configByName;
/**
* 删除标志(0代表存在 1代表删除)
*/
@TableLogic
private Integer deleted;
}
@@ -0,0 +1,39 @@
package org.dromara.xljk.domain;
import lombok.Data;
/**
* 心理咨询用户和部门完整信息
* 用于批量添加咨询师时获取完整的用户数据
*
* @author ruoyi
* @date 2025-01-03
*/
@Data
public class XljkUserWithDeptInfo {
/** 用户ID */
private Long userId;
/** 用户账号 */
private String userName;
/** 用户昵称 */
private String nickName;
/** 部门ID */
private Long deptId;
/** 部门名称 */
private String deptName;
/** 手机号码 */
private String phonenumber;
/** 用户邮箱 */
private String email;
/** 帐号状态(1正常 0停用) */
private String status;
}
@@ -22,12 +22,19 @@ public class XljkConsultationAppointmentBo {
private String appointmentTime;
private String appointmentDetailTime; // 具体预约时间段,如"08:30-09:00"
private String reason;
private String consultTheme; // 咨询主题
private String status;
private Date actualVisitTime;
private Integer communicationDuration;
private String consultationContent;
private String counselorSuggestion;
// 新增心理健康相关字段
private Integer psychologicalHistory; // 心理病史(1-否,2-是)
private String psychologicalHistoryDetail; // 心理病史详情
private String basicSituation; // 基本情况
private String majorLifeEvents; // 重大生活事件
private Integer needReferral; // 是否需要转介(1-不需要,2-需要)
// 查询参数字段
private String studentName;
private String studentNo;
@@ -2,6 +2,7 @@ package org.dromara.xljk.domain.bo;
import lombok.Data;
import java.util.Date;
import java.util.List;
/**
* 批量创建预约时段的业务对象
@@ -13,6 +14,11 @@ public class XljkConsultationScheduleBatchBo {
*/
private Long counselorId;
/**
* 办公室
*/
private String office;
/**
* 开始日期
*/
@@ -24,35 +30,50 @@ public class XljkConsultationScheduleBatchBo {
private String endDate;
/**
* 是否启用上午时段
* 是否启用中午时段(字段名保持morningEnabled以兼容前端)
*/
private Boolean morningEnabled;
/**
* 上午开始时间 (HH:mm)
* 中午开始时间 (HH:mm)(字段名保持morningStartTime以兼容前端)
*/
private String morningStartTime;
/**
* 上午结束时间 (HH:mm)
* 中午结束时间 (HH:mm)(字段名保持morningEndTime以兼容前端)
*/
private String morningEndTime;
/**
* 是否启用中午时段
*/
private Boolean noonEnabled;
/**
* 中午时段列表
*/
private List<TimeSlot> noonTimeSlots;
/**
* 是否启用下午时段
*/
private Boolean afternoonEnabled;
/**
* 下午开始时间 (HH:mm)
* 下午开始时间 (HH:mm)(兼容旧版本)
*/
private String afternoonStartTime;
/**
* 下午结束时间 (HH:mm)
* 下午结束时间 (HH:mm)(兼容旧版本)
*/
private String afternoonEndTime;
/**
* 下午时段列表
*/
private List<TimeSlot> afternoonTimeSlots;
/**
* 状态 (0正常 1停用)
*/
@@ -67,4 +88,13 @@ public class XljkConsultationScheduleBatchBo {
* 批次ID,用于标识同一批次创建的预约时段
*/
private String batchId;
/**
* 时段内部类
*/
@Data
public static class TimeSlot {
private String startTime;
private String endTime;
}
}
@@ -14,8 +14,9 @@ public class XljkConsultationScheduleBo {
private Date updateTime;
private Integer deleted;
private Long counselorId;
private String office; // 办公室
private Date scheduleDate;
private String timeSlot; // 0上午 1下午
private String timeSlot; // 0中午 1下午
private String startTime; // 开始时间 HH:mm 格式
private String endTime; // 结束时间 HH:mm 格式
private String status;
@@ -0,0 +1,118 @@
package org.dromara.xljk.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.xljk.domain.XljkConsulter;
import jakarta.validation.constraints.*;
import java.util.Date;
import java.util.List;
/**
* 心理咨询师配置业务对象 xljk_consulter
*
* @author ruoyi
* @date 2024-01-01
*/
@Data
@EqualsAndHashCode(callSuper = true)
@AutoMapper(target = XljkConsulter.class, reverseConvertGenerate = false)
public class XljkConsulterBo extends BaseEntity {
/**
* 主键ID
*/
@NotNull(message = "主键ID不能为空", groups = { EditGroup.class })
private Long id;
/**
* 用户ID
*/
@NotNull(message = "用户ID不能为空", groups = { AddGroup.class, EditGroup.class })
private Long userId;
/**
* 用户姓名
*/
@NotBlank(message = "用户姓名不能为空", groups = { AddGroup.class, EditGroup.class })
private String userName;
/**
* 用户昵称
*/
@NotBlank(message = "用户昵称不能为空", groups = { AddGroup.class, EditGroup.class })
private String nickName;
/**
* 部门ID
*/
@NotNull(message = "部门ID不能为空", groups = { AddGroup.class, EditGroup.class })
private Long deptId;
/**
* 部门名称
*/
@NotBlank(message = "部门名称不能为空", groups = { AddGroup.class, EditGroup.class })
private String deptName;
/**
* 联系电话
*/
private String phoneNumber;
/**
* 办公室
*/
private String office;
/**
* 邮箱地址
*/
private String email;
/**
* 状态(1正常 0停用)
*/
@NotBlank(message = "状态不能为空", groups = { AddGroup.class, EditGroup.class })
private String status;
/**
* 备注说明
*/
private String remark;
/**
* 配置人ID
*/
private Long configBy;
/**
* 配置人姓名
*/
private String configByName;
/**
* 分页大小
*/
private Integer pageSize;
/**
* 页码
*/
private Integer pageNum;
/**
* 搜索值
*/
private String searchValue;
/**
* 创建时间范围
*/
private List<Date> createTimeRange;
}
@@ -0,0 +1,82 @@
package org.dromara.xljk.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
/**
* 带预约状态的排班数据视图对象
*/
@Data
public class ScheduleWithStatusVo {
/**
* 排班ID
*/
private Long scheduleId;
/**
* 咨询师ID
*/
private Long counselorId;
/**
* 咨询师姓名
*/
private String counselorName;
/**
* 排班日期
*/
@JsonFormat(pattern = "yyyy-MM-dd")
private String date;
/**
* 时段类型(0中午 1下午 2晚上)
*/
private String timeSlot;
/**
* 开始时间(HH:mm)
*/
private String startTime;
/**
* 结束时间(HH:mm)
*/
private String endTime;
/**
* 时段显示文本(如:中午 08:30-12:00)
*/
private String timeDisplay;
/**
* 是否已预约满
*/
private Boolean isFullyBooked;
/**
* 是否已过期
*/
private Boolean isExpired;
/**
* 可预约数量
*/
private Integer availableSlots;
/**
* 总预约数量
*/
private Integer totalSlots;
// 以下是基础字段,用于避免MyBatis映射错误
private Long createBy; // 创建者
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime; // 创建时间
private Long updateBy; // 更新者
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime; // 更新时间
}
@@ -30,14 +30,12 @@ public class XljkConsultationAppointmentVo {
private String appointmentTime;
private String appointmentDetailTime; // 具体预约时间段,如"08:30-09:00"
private String reason;
private String consultTheme; // 咨询主题
/**
* 状态(0待咨询 1已完成 2已取消 3未到访)
*/
private Integer status;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date actualVisitTime;
private Integer communicationDuration;
private String consultationContent;
private String counselorSuggestion;
@@ -51,4 +49,14 @@ public class XljkConsultationAppointmentVo {
// 新增院系和专业字段
private String collegeName; // 院系名称
private String majorName; // 专业名称
// 新增心理健康相关字段
private Integer psychologicalHistory; // 心理病史(1-否,2-是)
private String psychologicalHistoryDetail; // 心理病史详情
private String basicSituation; // 基本情况
private String majorLifeEvents; // 重大生活事件
private Integer needReferral; // 是否需要转介(1-不需要,2-需要)
// 本学期到访次数(计算字段,不存储在数据库)
private Integer semesterVisitCount;
}
@@ -18,11 +18,12 @@ public class XljkConsultationScheduleSummaryVo {
private Integer deleted;
private Long counselorId;
private String counselorName; // 咨询师姓名,用于前端显示
private String office; // 办公室,用于前端显示
// 汇总信息
private String dateRange; // 日期范围,如"2025-07-01 至 2025-07-10"
private Boolean morningEnabled; // 是否启用上午时段
private String morningTime; // 上午时间,如"08:30-12:00"
private Boolean morningEnabled; // 是否启用中午时段(字段名保持不变以兼容前端)
private String morningTime; // 中午时间,如"08:30-12:00"
private Boolean afternoonEnabled; // 是否启用下午时段
private String afternoonTime; // 下午时间,如"14:30-17:00"
private String status; // 状态
@@ -15,8 +15,9 @@ public class XljkConsultationScheduleVo {
private Integer deleted;
private Long counselorId;
private String counselorName; // 咨询师姓名,用于前端显示
private String office; // 办公室
private Date scheduleDate;
private String timeSlot; // 0上午 1下午
private String timeSlot; // 0中午 1下午
private String startTime; // 开始时间 HH:mm 格式
private String endTime; // 结束时间 HH:mm 格式
private String status;
@@ -0,0 +1,139 @@
package org.dromara.xljk.domain.vo;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.excel.convert.ExcelDictConvert;
import org.dromara.xljk.domain.XljkConsulter;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 心理咨询师配置视图对象 xljk_consulter
*
* @author ruoyi
* @date 2024-01-01
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = XljkConsulter.class)
public class XljkConsulterVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@ExcelProperty(value = "主键ID")
private Long id;
/**
* 用户ID
*/
@ExcelProperty(value = "用户ID")
private Long userId;
/**
* 用户姓名
*/
@ExcelProperty(value = "用户姓名")
private String userName;
/**
* 用户昵称
*/
@ExcelProperty(value = "用户昵称")
private String nickName;
/**
* 部门ID
*/
@ExcelProperty(value = "部门ID")
private Long deptId;
/**
* 部门名称
*/
@ExcelProperty(value = "部门名称")
private String deptName;
/**
* 联系电话
*/
@ExcelProperty(value = "联系电话")
private String phoneNumber;
/**
* 办公室
*/
@ExcelProperty(value = "办公室")
private String office;
/**
* 邮箱地址
*/
@ExcelProperty(value = "邮箱地址")
private String email;
/**
* 状态(1正常 0停用)
*/
@ExcelProperty(value = "状态", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "sys_normal_disable")
private String status;
/**
* 备注说明
*/
@ExcelProperty(value = "备注说明")
private String remark;
/**
* 配置人ID
*/
private Long configBy;
/**
* 配置人姓名
*/
@ExcelProperty(value = "配置人")
private String configByName;
/**
* 租户编号
*/
private String tenantId;
/**
* 创建部门
*/
private Long createDept;
/**
* 创建者ID
*/
private Long createBy;
/**
* 创建时间
*/
@ExcelProperty(value = "创建时间")
private Date createTime;
/**
* 更新者ID
*/
private Long updateBy;
/**
* 更新时间
*/
@ExcelProperty(value = "更新时间")
private Date updateTime;
}
@@ -0,0 +1,66 @@
package org.dromara.xljk.mapper;
import org.dromara.xljk.domain.XljkConsulter;
import org.dromara.xljk.domain.vo.XljkConsulterVo;
import org.dromara.xljk.domain.XljkUserWithDeptInfo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 心理咨询师配置Mapper接口
*
* @author ruoyi
* @date 2024-01-01
*/
public interface XljkConsulterMapper extends BaseMapperPlus<XljkConsulter, XljkConsulterVo> {
/**
* 查询心理咨询师配置
*
* @param id 心理咨询师配置主键
* @return 心理咨询师配置
*/
XljkConsulterVo selectXljkConsulterVoById(Long id);
/**
* 查询心理咨询师配置列表
*
* @param xljkConsulter 心理咨询师配置
* @return 心理咨询师配置集合
*/
List<XljkConsulterVo> selectXljkConsulterVoList(XljkConsulter xljkConsulter);
/**
* 根据用户ID检查是否已存在咨询师配置
*
* @param userId 用户ID
* @return 记录数
*/
int checkUserExists(@Param("userId") Long userId);
/**
* 批量启用咨询师
*
* @param ids 咨询师ID列表
* @return 影响行数
*/
int enableConsultersByIds(@Param("ids") List<Long> ids);
/**
* 批量停用咨询师
*
* @param ids 咨询师ID列表
* @return 影响行数
*/
int disableConsultersByIds(@Param("ids") List<Long> ids);
/**
* 查询完整的用户信息(包含部门信息)用于批量添加
*
* @param userIds 用户ID列表
* @return 用户和部门完整信息列表
*/
List<XljkUserWithDeptInfo> selectUsersWithDeptInfo(@Param("userIds") List<Long> userIds);
}
@@ -4,6 +4,7 @@ import org.dromara.xljk.domain.XljkConsultationAppointment;
import org.dromara.xljk.domain.bo.XljkConsultationAppointmentBo;
import org.dromara.xljk.domain.vo.XljkConsultationAppointmentVo;
import org.dromara.xljk.domain.vo.TimeSlotDetailVo;
import org.dromara.xljk.domain.vo.ScheduleWithStatusVo;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import java.util.List;
@@ -43,7 +44,7 @@ public interface IXljkConsultationAppointmentService {
/**
* 获取可用的详细时段列表(30分钟一个时段)
* @param date 日期
* @param timeSlot 时段类型(0上午 1下午)
* @param timeSlot 时段类型(0中午 1下午)
* @return 详细时段列表
*/
List<TimeSlotDetailVo> getAvailableDetailSlots(Date date, String timeSlot);
@@ -65,15 +66,21 @@ public interface IXljkConsultationAppointmentService {
/**
* 记录咨询结果
* @param id 预约ID
* @param actualVisitTime 实际到访时间
* @param communicationDuration 沟通时长
* @param consultationContent 咨询内容
* @param counselorSuggestion 咨询师建议
* @param status 状态(1已完成/3未到访)
* @param psychologicalHistory 心理病史(1-否,2-是)
* @param psychologicalHistoryDetail 心理病史详情
* @param basicSituation 基本情况
* @param majorLifeEvents 重大生活事件
* @param needReferral 是否需要转介(1-不需要,2-需要)
* @return 是否成功
*/
boolean recordConsultation(Long id, Date actualVisitTime, Integer communicationDuration,
String consultationContent, String counselorSuggestion, String status);
boolean recordConsultation(Long id, Integer communicationDuration,
String consultationContent, String counselorSuggestion, String status,
Integer psychologicalHistory, String psychologicalHistoryDetail,
String basicSituation, String majorLifeEvents, Integer needReferral);
/**
* 获取院系选项
@@ -95,4 +102,12 @@ public interface IXljkConsultationAppointmentService {
* @return 班级选项列表
*/
List<Map<String, Object>> getClassOptions(String collegeName, String majorName);
/**
* 获取日历视图的排班数据(带预约状态)
* @param startDate 开始日期
* @param endDate 结束日期
* @return 排班数据列表
*/
List<ScheduleWithStatusVo> getCalendarSchedules(String startDate, String endDate);
}
@@ -0,0 +1,102 @@
package org.dromara.xljk.service;
import org.dromara.xljk.domain.vo.XljkConsulterVo;
import org.dromara.xljk.domain.bo.XljkConsulterBo;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import java.util.Collection;
import java.util.List;
/**
* 心理咨询师配置Service接口
*
* @author ruoyi
* @date 2024-01-01
*/
public interface IXljkConsulterService {
/**
* 查询心理咨询师配置
*
* @param id 主键
* @return 心理咨询师配置
*/
XljkConsulterVo queryById(Long id);
/**
* 查询心理咨询师配置列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 心理咨询师配置集合
*/
TableDataInfo<XljkConsulterVo> queryPageList(XljkConsulterBo bo, PageQuery pageQuery);
/**
* 查询心理咨询师配置列表
*
* @param bo 查询条件
* @return 心理咨询师配置集合
*/
List<XljkConsulterVo> queryList(XljkConsulterBo bo);
/**
* 新增心理咨询师配置
*
* @param bo 心理咨询师配置
* @return 结果
*/
Boolean insertByBo(XljkConsulterBo bo);
/**
* 修改心理咨询师配置
*
* @param bo 心理咨询师配置
* @return 结果
*/
Boolean updateByBo(XljkConsulterBo bo);
/**
* 校验并批量删除心理咨询师配置信息
*
* @param ids 需要删除的主键集合
* @return 结果
*/
Boolean deleteWithValidByIds(Collection<Long> ids);
/**
* 批量添加咨询师
*
* @param userIds 用户ID列表
* @param remark 备注
* @return 结果
*/
Boolean batchAddConsulters(List<Long> userIds, String remark);
/**
* 查询我配置的咨询师列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 咨询师列表
*/
TableDataInfo<XljkConsulterVo> queryMyConfigList(XljkConsulterBo bo, PageQuery pageQuery);
/**
* 批量启用咨询师
*
* @param ids 咨询师ID列表
* @return 结果
*/
Boolean enableConsulters(List<Long> ids);
/**
* 批量停用咨询师
*
* @param ids 咨询师ID列表
* @return 结果
*/
Boolean disableConsulters(List<Long> ids);
}
@@ -10,6 +10,7 @@ import org.dromara.xljk.domain.XljkConsultationSchedule;
import org.dromara.xljk.domain.bo.XljkConsultationAppointmentBo;
import org.dromara.xljk.domain.vo.XljkConsultationAppointmentVo;
import org.dromara.xljk.domain.vo.TimeSlotDetailVo;
import org.dromara.xljk.domain.vo.ScheduleWithStatusVo;
import org.dromara.xljk.mapper.XljkConsultationAppointmentMapper;
import org.dromara.xljk.mapper.XljkConsultationScheduleMapper;
import org.dromara.xljk.service.IXljkConsultationAppointmentService;
@@ -38,6 +39,8 @@ import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.time.LocalDate;
import java.time.Month;
@Slf4j
@Service
@@ -71,9 +74,8 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation
BeanUtils.copyProperties(entity, vo);
// 记录关键字段
log.info("复制后的VO对象关键字段:status={}, actualVisitTime={}, communicationDuration={}, consultationContent长度={}, counselorSuggestion长度={}",
log.info("复制后的VO对象关键字段:status={}, communicationDuration={}, consultationContent长度={}, counselorSuggestion长度={}",
vo.getStatus(),
vo.getActualVisitTime(),
vo.getCommunicationDuration(),
vo.getConsultationContent() != null ? vo.getConsultationContent().length() : 0,
vo.getCounselorSuggestion() != null ? vo.getCounselorSuggestion().length() : 0);
@@ -128,23 +130,8 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation
// 再次检查关键字段
if (vo.getStatus() != null && vo.getStatus() == 1) {
log.info("已到访状态,检查咨询结果相关字段:actualVisitTime={}, communicationDuration={}",
vo.getActualVisitTime(), vo.getCommunicationDuration());
// 如果是已到访状态但实际到访时间为空,尝试从数据库直接查询
if (vo.getActualVisitTime() == null) {
log.warn("已到访状态但实际到访时间为空,尝试从数据库直接查询");
try {
String timeSql = "SELECT actual_visit_time FROM xljk_consultation_appointment WHERE id = " + id;
Date actualVisitTime = jdbcTemplate.queryForObject(timeSql, Date.class);
if (actualVisitTime != null) {
log.info("从数据库查询到实际到访时间: {}", actualVisitTime);
vo.setActualVisitTime(actualVisitTime);
}
} catch (Exception e) {
log.error("查询实际到访时间失败", e);
}
}
log.info("已到访状态,检查咨询结果相关字段:communicationDuration={}",
vo.getCommunicationDuration());
}
log.info("返回预约记录详情: {}", vo);
@@ -238,6 +225,9 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation
return resultList.stream().map(row -> {
XljkConsultationAppointmentVo vo = new XljkConsultationAppointmentVo();
// 获取学生ID用于计算本学期到访次数
Long studentId = row.get("student_id") != null ? ((Number) row.get("student_id")).longValue() : null;
// 设置基本字段
vo.setId(((Number) row.get("id")).longValue());
vo.setTenantId((String) row.get("tenant_id"));
@@ -291,11 +281,44 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation
vo.setStatus(0); // 默认为待咨询
}
vo.setActualVisitTime(convertToDate(row.get("actual_visit_time")));
vo.setCommunicationDuration(row.get("communication_duration") != null ? ((Number) row.get("communication_duration")).intValue() : null);
vo.setConsultationContent((String) row.get("consultation_content"));
vo.setCounselorSuggestion((String) row.get("counselor_suggestion"));
// 设置咨询主题字段
vo.setConsultTheme((String) row.get("consult_theme"));
// 设置心理健康相关字段
Object psychologicalHistoryObj = row.get("psychological_history");
if (psychologicalHistoryObj != null) {
if (psychologicalHistoryObj instanceof Number) {
vo.setPsychologicalHistory(((Number) psychologicalHistoryObj).intValue());
} else if (psychologicalHistoryObj instanceof String) {
try {
vo.setPsychologicalHistory(Integer.parseInt((String) psychologicalHistoryObj));
} catch (NumberFormatException e) {
vo.setPsychologicalHistory(null);
}
}
}
vo.setPsychologicalHistoryDetail((String) row.get("psychological_history_detail"));
vo.setBasicSituation((String) row.get("basic_situation"));
vo.setMajorLifeEvents((String) row.get("major_life_events"));
Object needReferralObj = row.get("need_referral");
if (needReferralObj != null) {
if (needReferralObj instanceof Number) {
vo.setNeedReferral(((Number) needReferralObj).intValue());
} else if (needReferralObj instanceof String) {
try {
vo.setNeedReferral(Integer.parseInt((String) needReferralObj));
} catch (NumberFormatException e) {
vo.setNeedReferral(null);
}
}
}
// 设置关联字段
vo.setStudentName((String) row.get("student_name"));
vo.setStudentNo((String) row.get("student_no"));
@@ -304,10 +327,69 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation
vo.setCollegeName((String) row.get("college_name"));
vo.setMajorName((String) row.get("major_name"));
// 计算本学期到访次数
if (studentId != null) {
vo.setSemesterVisitCount(calculateSemesterVisitCount(studentId));
} else {
vo.setSemesterVisitCount(0);
}
return vo;
}).collect(Collectors.toList());
}
/**
* 计算学生本学期到访次数
* 上学期:3月1日-8月31日
* 下学期:9月1日-2月29日
* @param studentId 学生ID
* @return 本学期到访次数
*/
private int calculateSemesterVisitCount(Long studentId) {
try {
// 获取当前日期
LocalDate now = LocalDate.now();
String startDate, endDate;
// 判断当前处于哪个学期
if (now.getMonthValue() >= 3 && now.getMonthValue() <= 8) {
// 上学期:3月1日-8月31日
startDate = now.getYear() + "-03-01";
endDate = now.getYear() + "-08-31";
} else if (now.getMonthValue() >= 9) {
// 下学期:9月1日-次年2月29日(当年9月到次年2月)
startDate = now.getYear() + "-09-01";
// 计算次年2月的最后一天
int nextYear = now.getYear() + 1;
LocalDate nextFeb = LocalDate.of(nextYear, Month.FEBRUARY, 1);
LocalDate lastDayOfFeb = nextFeb.withDayOfMonth(nextFeb.lengthOfMonth());
endDate = lastDayOfFeb.toString();
} else {
// 1月、2月属于上一学年的下学期
int prevYear = now.getYear() - 1;
startDate = prevYear + "-09-01";
// 当年2月的最后一天
LocalDate currentFeb = LocalDate.of(now.getYear(), Month.FEBRUARY, 1);
LocalDate lastDayOfFeb = currentFeb.withDayOfMonth(currentFeb.lengthOfMonth());
endDate = lastDayOfFeb.toString();
}
// 查询该学生在当前学期内状态为已到访(status=1)的记录数
String sql = "SELECT COUNT(*) FROM xljk_consultation_appointment " +
"WHERE student_id = ? AND status = 1 AND deleted = 0 " +
"AND appointment_date >= ? AND appointment_date <= ?";
Integer count = jdbcTemplate.queryForObject(sql, Integer.class, studentId, startDate, endDate);
log.debug("学生ID: {}, 学期时间段: {} - {}, 到访次数: {}", studentId, startDate, endDate, count);
return count != null ? count : 0;
} catch (Exception e) {
log.error("计算学生本学期到访次数失败,学生ID: {}", studentId, e);
return 0;
}
}
@Override
public boolean insert(XljkConsultationAppointmentBo bo) {
XljkConsultationAppointment entity = new XljkConsultationAppointment();
@@ -341,7 +423,7 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation
LambdaQueryWrapper<XljkConsultationAppointment> appointmentQueryWrapper = new LambdaQueryWrapper<>();
appointmentQueryWrapper.ge(XljkConsultationAppointment::getAppointmentDate, startDate)
.le(XljkConsultationAppointment::getAppointmentDate, endDate)
.in(XljkConsultationAppointment::getStatus, Arrays.asList("0", "1")); // 待咨询或已完成的
.in(XljkConsultationAppointment::getStatus, Arrays.asList("0", "1", "4")); // 待咨询、已完成或待审核的
List<XljkConsultationAppointment> appointments = mapper.selectList(appointmentQueryWrapper);
@@ -365,9 +447,10 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation
String dateStr = new SimpleDateFormat("yyyy-MM-dd").format(schedule.getScheduleDate());
String key = dateStr + "_" + schedule.getId();
// 检查该时段的可用详细时段
// 检查该时段的可用详细时段(用于时间判断,但总容量固定为1)
List<TimeSlotDetailVo> detailSlots = TimeSlotUtils.generateTimeSlots(schedule.getStartTime(), schedule.getEndTime());
int totalDetailSlots = detailSlots.size();
int totalDetailSlots = detailSlots.size(); // 保留用于时间过期判断
int totalSlots = 1; // 每个时段只能预约1个人
int bookedCount = 0;
boolean isExpired = false;
@@ -408,15 +491,11 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation
isExpired = false;
}
// 检查已预约的详细时段
// 检查已预约数量(按整个时段计算,不再按详细时段计算)
if (bookedDetailSlots.containsKey(key)) {
List<String> bookedTimes = bookedDetailSlots.get(key);
for (TimeSlotDetailVo slot : detailSlots) {
String detailTime = slot.getStartTime() + "-" + slot.getEndTime();
if (bookedTimes.contains(detailTime)) {
bookedCount++;
}
}
// 只要有任何预约,就算已预约1个(因为每个时段只能预约1个人)
bookedCount = bookedTimes.isEmpty() ? 0 : 1;
}
// 创建时段信息
@@ -427,8 +506,8 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation
slot.put("timeSlot", schedule.getTimeSlot());
slot.put("startTime", schedule.getStartTime());
slot.put("endTime", schedule.getEndTime());
slot.put("allBooked", bookedCount >= totalDetailSlots); // 是否全部约满
slot.put("availableCount", totalDetailSlots - bookedCount); // 可用数量
slot.put("allBooked", bookedCount >= totalSlots); // 是否全部约满
slot.put("availableCount", totalSlots - bookedCount); // 可用数量
slot.put("isExpired", isExpired); // 是否已过期
// 查询咨询师姓名
@@ -463,21 +542,34 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation
.eq(XljkConsultationSchedule::getTimeSlot, timeSlot)
.eq(XljkConsultationSchedule::getStatus, "0"); // 0表示正常可用
XljkConsultationSchedule schedule = scheduleMapper.selectOne(scheduleQueryWrapper);
// 修改:使用selectList()而不是selectOne(),因为可能有多条记录
List<XljkConsultationSchedule> schedules = scheduleMapper.selectList(scheduleQueryWrapper);
if (schedule == null) {
if (schedules == null || schedules.isEmpty()) {
// 该日期没有排班
return new ArrayList<>();
}
// 2. 生成30分钟时段列表
List<TimeSlotDetailVo> timeSlots = TimeSlotUtils.generateTimeSlots(schedule.getStartTime(), schedule.getEndTime());
// 2. 合并所有排班的时段,生成完整的30分钟时段列表
List<TimeSlotDetailVo> allTimeSlots = new ArrayList<>();
Set<String> uniqueTimeSlots = new LinkedHashSet<>(); // 用于去重,保持顺序
// 3. 查询已预约的时段
for (XljkConsultationSchedule schedule : schedules) {
List<TimeSlotDetailVo> timeSlots = TimeSlotUtils.generateTimeSlots(schedule.getStartTime(), schedule.getEndTime());
for (TimeSlotDetailVo slot : timeSlots) {
String slotKey = slot.getStartTime() + "-" + slot.getEndTime();
if (uniqueTimeSlots.add(slotKey)) {
allTimeSlots.add(slot);
}
}
}
// 3. 查询已预约的时段(查询所有相关排班的预约)
List<Long> scheduleIds = schedules.stream().map(XljkConsultationSchedule::getId).collect(Collectors.toList());
LambdaQueryWrapper<XljkConsultationAppointment> appointmentQueryWrapper = new LambdaQueryWrapper<>();
appointmentQueryWrapper.eq(XljkConsultationAppointment::getAppointmentDate, date)
.eq(XljkConsultationAppointment::getScheduleId, schedule.getId()) // 按排班ID过滤,确保只查询当前时段的预约
.in(XljkConsultationAppointment::getStatus, Arrays.asList("0", "1")); // 待咨询或已完成的
.in(XljkConsultationAppointment::getScheduleId, scheduleIds) // 按排班ID列表过滤
.in(XljkConsultationAppointment::getStatus, Arrays.asList("0", "1", "4")); // 待咨询、已完成或待审核的
List<XljkConsultationAppointment> appointments = mapper.selectList(appointmentQueryWrapper);
@@ -491,7 +583,7 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
for (TimeSlotDetailVo slot : timeSlots) {
for (TimeSlotDetailVo slot : allTimeSlots) {
String detailTime = slot.getStartTime() + "-" + slot.getEndTime();
// 检查是否已预约
@@ -532,7 +624,7 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation
}
}
return timeSlots;
return allTimeSlots;
}
@Override
@@ -582,7 +674,7 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation
XljkConsultationAppointment entity = new XljkConsultationAppointment();
BeanUtils.copyProperties(bo, entity);
entity.setStudentId(studentId); // 使用验证后的学生ID
entity.setStatus("0"); // 0表示待咨询
entity.setStatus("4"); // 4表示待审核
entity.setCreateTime(new Date()); // 设置创建时间
return mapper.insert(entity) > 0;
}
@@ -645,10 +737,6 @@ if (statusObj != null) {
vo.setScheduleId(((Number) row.get("schedule_id")).longValue());
}
if (row.get("actual_visit_time") != null) {
vo.setActualVisitTime(convertToDate(row.get("actual_visit_time")));
}
if (row.get("communication_duration") != null) {
vo.setCommunicationDuration(((Number) row.get("communication_duration")).intValue());
}
@@ -691,9 +779,11 @@ if (statusObj != null) {
@Override
@Transactional
public boolean recordConsultation(Long id, Date actualVisitTime, Integer communicationDuration,
String consultationContent, String counselorSuggestion, String status) {
log.info("记录咨询结果,ID: {}, 状态: {}, 实际到访时间: {}", id, status, actualVisitTime);
public boolean recordConsultation(Long id, Integer communicationDuration,
String consultationContent, String counselorSuggestion, String status,
Integer psychologicalHistory, String psychologicalHistoryDetail,
String basicSituation, String majorLifeEvents, Integer needReferral) {
log.info("记录咨询结果,ID: {}, 状态: {}, 心理病史: {}, 转介: {}", id, status, psychologicalHistory, needReferral);
XljkConsultationAppointment entity = mapper.selectById(id);
if (entity == null) {
@@ -717,29 +807,42 @@ if (statusObj != null) {
// 只有在已到访状态(1)时,才设置咨询结果相关字段
if (statusInt == 1) {
if (actualVisitTime == null) {
log.warn("实际到访时间为空,使用当前时间");
actualVisitTime = new Date();
}
entity.setActualVisitTime(actualVisitTime);
entity.setCommunicationDuration(communicationDuration != null ? communicationDuration : 30);
entity.setConsultationContent(consultationContent);
entity.setCounselorSuggestion(counselorSuggestion);
log.info("设置咨询结果相关字段:实际到访时间={}, 沟通时长={}, 咨询内容长度={}, 咨询师建议长度={}",
actualVisitTime,
// 设置心理病史相关字段
entity.setPsychologicalHistory(psychologicalHistory);
entity.setPsychologicalHistoryDetail(psychologicalHistoryDetail);
entity.setBasicSituation(basicSituation);
entity.setMajorLifeEvents(majorLifeEvents);
entity.setNeedReferral(needReferral);
log.info("设置咨询结果相关字段:沟通时长={}, 咨询内容长度={}, 咨询师建议长度={}",
communicationDuration,
consultationContent != null ? consultationContent.length() : 0,
counselorSuggestion != null ? counselorSuggestion.length() : 0);
log.info("设置心理病史相关字段:心理病史={}, 心理病史详情长度={}, 基本情况长度={}, 重大生活事件长度={}, 转介={}",
psychologicalHistory,
psychologicalHistoryDetail != null ? psychologicalHistoryDetail.length() : 0,
basicSituation != null ? basicSituation.length() : 0,
majorLifeEvents != null ? majorLifeEvents.length() : 0,
needReferral);
} else {
// 未到访状态,清空咨询结果相关字段
log.info("非已到访状态,清空咨询结果相关字段");
entity.setActualVisitTime(null);
entity.setCommunicationDuration(0);
entity.setConsultationContent("");
entity.setCounselorSuggestion("");
// 清空心理病史相关字段
entity.setPsychologicalHistory(null);
entity.setPsychologicalHistoryDetail("");
entity.setBasicSituation("");
entity.setMajorLifeEvents("");
entity.setNeedReferral(null);
// 如果是未到访状态(3),创建未到访记录
if (statusInt == 3) {
log.info("记录未到访,创建未到访记录");
@@ -1323,10 +1426,19 @@ if (statusObj != null) {
public TableDataInfo<XljkConsultationAppointmentVo> selectPageList(XljkConsultationAppointmentBo bo, PageQuery pageQuery) {
log.info("分页查询预约记录列表,页码: {}, 每页数量: {}", pageQuery.getPageNum(), pageQuery.getPageSize());
// 将变量定义移到try块外,确保catch块可以访问
StringBuilder sqlBuilder = new StringBuilder();
List<Object> params = new ArrayList<>();
try {
// 构建查询条件
StringBuilder sqlBuilder = new StringBuilder();
sqlBuilder.append("SELECT a.*, s.name AS student_name, s.student_no, ");
sqlBuilder.append("SELECT a.id, a.tenant_id, a.student_id, a.counselor_id, a.schedule_id, ");
sqlBuilder.append("a.appointment_date, a.appointment_time, a.appointment_detail_time, ");
sqlBuilder.append("a.reason, a.status, a.communication_duration, a.consultation_content, ");
sqlBuilder.append("a.counselor_suggestion, a.psychological_history, a.psychological_history_detail, ");
sqlBuilder.append("a.basic_situation, a.major_life_events, a.need_referral, ");
sqlBuilder.append("a.create_by, a.create_time, a.update_by, a.update_time, ");
sqlBuilder.append("s.name AS student_name, s.student_no, ");
sqlBuilder.append("s.college_name, s.major_name, s.class_name, ");
sqlBuilder.append("u.nick_name AS counselor_name ");
sqlBuilder.append("FROM xljk_consultation_appointment a ");
@@ -1334,8 +1446,6 @@ if (statusObj != null) {
sqlBuilder.append("LEFT JOIN sys_user u ON a.counselor_id = u.user_id ");
sqlBuilder.append("WHERE a.deleted = 0 ");
List<Object> params = new ArrayList<>();
// 添加查询条件
if (bo.getStudentName() != null && !bo.getStudentName().isEmpty()) {
sqlBuilder.append("AND s.name LIKE ? ");
@@ -1457,10 +1567,6 @@ if (statusObj != null) {
vo.setScheduleId(((Number) row.get("schedule_id")).longValue());
}
if (row.get("actual_visit_time") != null) {
vo.setActualVisitTime(convertToDate(row.get("actual_visit_time")));
}
if (row.get("communication_duration") != null) {
vo.setCommunicationDuration(((Number) row.get("communication_duration")).intValue());
}
@@ -1468,6 +1574,37 @@ if (statusObj != null) {
vo.setConsultationContent((String) row.get("consultation_content"));
vo.setCounselorSuggestion((String) row.get("counselor_suggestion"));
// 新增字段映射
Object psychologicalHistoryObj = row.get("psychological_history");
if (psychologicalHistoryObj != null) {
if (psychologicalHistoryObj instanceof Number) {
vo.setPsychologicalHistory(((Number) psychologicalHistoryObj).intValue());
} else if (psychologicalHistoryObj instanceof String) {
try {
vo.setPsychologicalHistory(Integer.parseInt((String) psychologicalHistoryObj));
} catch (NumberFormatException e) {
vo.setPsychologicalHistory(null);
}
}
}
vo.setPsychologicalHistoryDetail((String) row.get("psychological_history_detail"));
vo.setBasicSituation((String) row.get("basic_situation"));
vo.setMajorLifeEvents((String) row.get("major_life_events"));
Object needReferralObj = row.get("need_referral");
if (needReferralObj != null) {
if (needReferralObj instanceof Number) {
vo.setNeedReferral(((Number) needReferralObj).intValue());
} else if (needReferralObj instanceof String) {
try {
vo.setNeedReferral(Integer.parseInt((String) needReferralObj));
} catch (NumberFormatException e) {
vo.setNeedReferral(null);
}
}
}
// 其他通用字段
if (row.get("create_by") != null) {
vo.setCreateBy(((Number) row.get("create_by")).longValue());
@@ -1485,6 +1622,13 @@ if (statusObj != null) {
vo.setUpdateTime(convertToDate(row.get("update_time")));
}
// 计算本学期到访次数
if (vo.getStudentId() != null) {
vo.setSemesterVisitCount(calculateSemesterVisitCount(vo.getStudentId()));
} else {
vo.setSemesterVisitCount(0);
}
result.add(vo);
}
@@ -1496,12 +1640,194 @@ if (statusObj != null) {
return tableDataInfo;
} catch (Exception e) {
log.error("分页查询预约记录列表异常", e);
log.error("SQL: {}", sqlBuilder.toString());
log.error("参数: {}", params);
TableDataInfo<XljkConsultationAppointmentVo> emptyResult = new TableDataInfo<>();
emptyResult.setRows(new ArrayList<>());
emptyResult.setTotal(0);
emptyResult.setCode(200);
emptyResult.setMsg("查询失败");
emptyResult.setMsg("查询失败: " + e.getMessage());
return emptyResult;
}
}
@Override
public List<ScheduleWithStatusVo> getCalendarSchedules(String startDate, String endDate) {
try {
// 1. 查询指定日期范围内的所有排班数据
LambdaQueryWrapper<XljkConsultationSchedule> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.ge(XljkConsultationSchedule::getScheduleDate, startDate)
.le(XljkConsultationSchedule::getScheduleDate, endDate)
.eq(XljkConsultationSchedule::getStatus, "0") // 0表示正常可用
.orderByAsc(XljkConsultationSchedule::getScheduleDate)
.orderByAsc(XljkConsultationSchedule::getTimeSlot)
.orderByAsc(XljkConsultationSchedule::getStartTime);
List<XljkConsultationSchedule> schedules = scheduleMapper.selectList(queryWrapper);
if (schedules.isEmpty()) {
return new ArrayList<>();
}
// 2. 查询该日期范围内的所有预约记录
List<Long> scheduleIds = schedules.stream().map(XljkConsultationSchedule::getId).collect(Collectors.toList());
LambdaQueryWrapper<XljkConsultationAppointment> appointmentQueryWrapper = new LambdaQueryWrapper<>();
appointmentQueryWrapper.in(XljkConsultationAppointment::getScheduleId, scheduleIds)
.in(XljkConsultationAppointment::getStatus, Arrays.asList("0", "1", "4")); // 待咨询、已完成或待审核的
List<XljkConsultationAppointment> appointments = mapper.selectList(appointmentQueryWrapper);
// 3. 构建排班ID到预约记录的映射
Map<Long, List<XljkConsultationAppointment>> appointmentMap = appointments.stream()
.collect(Collectors.groupingBy(XljkConsultationAppointment::getScheduleId));
// 4. 获取咨询师信息映射
Map<Long, String> counselorMap = getCounselorNameMap();
// 5. 获取当前时间
Date now = new Date();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm");
// 6. 构建结果列表
List<ScheduleWithStatusVo> result = new ArrayList<>();
for (XljkConsultationSchedule schedule : schedules) {
ScheduleWithStatusVo vo = new ScheduleWithStatusVo();
// 基本信息
vo.setScheduleId(schedule.getId());
vo.setCounselorId(schedule.getCounselorId());
vo.setCounselorName(counselorMap.get(schedule.getCounselorId()));
vo.setDate(dateFormat.format(schedule.getScheduleDate()));
vo.setTimeSlot(schedule.getTimeSlot());
vo.setStartTime(schedule.getStartTime());
vo.setEndTime(schedule.getEndTime());
// 构建时段显示文本
String timeSlotText = getTimeSlotText(schedule.getTimeSlot());
vo.setTimeDisplay(timeSlotText + " " + schedule.getStartTime() + "-" + schedule.getEndTime());
// 计算预约状态
List<XljkConsultationAppointment> scheduleAppointments = appointmentMap.get(schedule.getId());
int bookedCount = scheduleAppointments != null ? scheduleAppointments.size() : 0;
// 每个时段只能预约1个人(无论时段长度)
// 生成30分钟时段用于详细时段显示,但总预约数量固定为1
List<TimeSlotDetailVo> timeSlots = TimeSlotUtils.generateTimeSlots(schedule.getStartTime(), schedule.getEndTime());
int totalSlots = 1; // 每个时段只能预约1个人
vo.setTotalSlots(totalSlots);
vo.setAvailableSlots(Math.max(0, totalSlots - bookedCount));
vo.setIsFullyBooked(bookedCount >= totalSlots);
// 检查是否已过期
boolean isExpired = false;
try {
Date scheduleDate = schedule.getScheduleDate();
Calendar calendar = Calendar.getInstance();
calendar.setTime(scheduleDate);
// 设置具体的结束时间
String[] endTimeParts = schedule.getEndTime().split(":");
calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(endTimeParts[0]));
calendar.set(Calendar.MINUTE, Integer.parseInt(endTimeParts[1]));
calendar.set(Calendar.SECOND, 0);
Date scheduleEndTime = calendar.getTime();
isExpired = scheduleEndTime.before(now);
} catch (Exception e) {
log.warn("检查时段过期状态失败: " + schedule.getId(), e);
}
vo.setIsExpired(isExpired);
// 设置基础字段
vo.setCreateBy(schedule.getCreateBy());
vo.setCreateTime(schedule.getCreateTime());
vo.setUpdateBy(schedule.getUpdateBy());
vo.setUpdateTime(schedule.getUpdateTime());
result.add(vo);
}
return result;
} catch (Exception e) {
log.error("获取日历排班数据失败", e);
return new ArrayList<>();
}
}
/**
* 获取时段类型文本
*/
private String getTimeSlotText(String timeSlot) {
switch (timeSlot) {
case "0": return "中午";
case "1": return "下午";
case "2": return "晚上";
default: return "未知";
}
}
/**
* 获取咨询师名称映射
*/
private Map<Long, String> getCounselorNameMap() {
try {
Map<Long, String> counselorMap = new HashMap<>();
// 1. 查询所有已创建排班的咨询师ID
LambdaQueryWrapper<XljkConsultationSchedule> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.select(XljkConsultationSchedule::getCounselorId)
.eq(XljkConsultationSchedule::getStatus, "0") // 只查询正常状态的
.groupBy(XljkConsultationSchedule::getCounselorId); // 去重
List<XljkConsultationSchedule> schedules = scheduleMapper.selectList(queryWrapper);
if (schedules.isEmpty()) {
return counselorMap;
}
// 2. 提取咨询师ID列表
List<Long> counselorIds = schedules.stream()
.map(XljkConsultationSchedule::getCounselorId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
if (counselorIds.isEmpty()) {
return counselorMap;
}
// 3. 批量查询咨询师信息
List<SysUserVo> counselors = userService.selectUserByIds(counselorIds, null);
// 4. 构建ID到姓名的映射
counselorMap = counselors.stream()
.filter(user -> user.getUserId() != null)
.collect(Collectors.toMap(
SysUserVo::getUserId,
user -> {
// 优先使用昵称,如果没有昵称则使用用户名
String name = user.getNickName();
if (StringUtils.isBlank(name)) {
name = user.getUserName();
}
if (StringUtils.isBlank(name)) {
name = "未知咨询师";
}
return name;
},
(existing, replacement) -> existing // 如果ID重复,保留第一个
));
log.info("成功加载咨询师信息映射,共{}个咨询师", counselorMap.size());
return counselorMap;
} catch (Exception e) {
log.error("获取咨询师信息失败", e);
return new HashMap<>();
}
}
}
@@ -13,6 +13,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.extern.slf4j.Slf4j;
import java.text.ParseException;
import java.text.SimpleDateFormat;
@@ -26,6 +27,7 @@ import java.util.UUID;
import java.util.stream.Collectors;
import java.util.LinkedHashMap;
@Slf4j
@Service
public class XljkConsultationScheduleServiceImpl implements IXljkConsultationScheduleService {
@Autowired
@@ -43,11 +45,16 @@ public class XljkConsultationScheduleServiceImpl implements IXljkConsultationSch
// 查询咨询师姓名
if (entity.getCounselorId() != null) {
String sql = "SELECT nick_name FROM sys_user WHERE user_id = ?";
List<Map<String, Object>> result = jdbcTemplate.queryForList(sql, entity.getCounselorId());
if (!result.isEmpty()) {
vo.setCounselorName((String) result.get(0).get("nick_name"));
try {
String counselorName = jdbcTemplate.queryForObject(sql, String.class, entity.getCounselorId());
vo.setCounselorName(counselorName);
} catch (Exception e) {
log.error("查询咨询师姓名失败,counselorId: {}", entity.getCounselorId(), e);
}
}
// office字段直接从排班记录中获取
vo.setOffice(entity.getOffice());
return vo;
}
@@ -65,11 +72,15 @@ public class XljkConsultationScheduleServiceImpl implements IXljkConsultationSch
// 查询咨询师姓名
if (entity.getCounselorId() != null) {
String sql = "SELECT nick_name FROM sys_user WHERE user_id = ?";
List<Map<String, Object>> result = jdbcTemplate.queryForList(sql, entity.getCounselorId());
if (!result.isEmpty()) {
vo.setCounselorName((String) result.get(0).get("nick_name"));
try {
String counselorName = jdbcTemplate.queryForObject(sql, String.class, entity.getCounselorId());
vo.setCounselorName(counselorName);
} catch (Exception e) {
log.error("查询咨询师姓名失败,counselorId: {}", entity.getCounselorId(), e);
}
}
// office字段直接从排班记录中获取
vo.setOffice(entity.getOffice());
return vo;
}).collect(Collectors.toList());
}
@@ -87,6 +98,11 @@ public class XljkConsultationScheduleServiceImpl implements IXljkConsultationSch
queryWrapper.eq(XljkConsultationSchedule::getCounselorId, bo.getCounselorId());
}
// 添加办公室筛选条件
if (bo.getOffice() != null && !bo.getOffice().isEmpty()) {
queryWrapper.like(XljkConsultationSchedule::getOffice, bo.getOffice());
}
// 按创建时间倒序查询所有记录
queryWrapper.orderByDesc(XljkConsultationSchedule::getCreateTime);
@@ -144,12 +160,17 @@ public class XljkConsultationScheduleServiceImpl implements IXljkConsultationSch
// 查询咨询师姓名
if (batchGroup.getCounselorId() != null) {
String sql = "SELECT nick_name FROM sys_user WHERE user_id = ?";
List<Map<String, Object>> result = jdbcTemplate.queryForList(sql, batchGroup.getCounselorId());
if (!result.isEmpty()) {
summary.setCounselorName((String) result.get(0).get("nick_name"));
try {
String counselorName = jdbcTemplate.queryForObject(sql, String.class, batchGroup.getCounselorId());
summary.setCounselorName(counselorName);
} catch (Exception e) {
log.error("查询咨询师姓名失败,counselorId: {}", batchGroup.getCounselorId(), e);
}
}
// office字段直接从排班记录中获取
summary.setOffice(batchGroup.getOffice());
// 确保创建时间不为空
if (summary.getCreateTime() == null) {
summary.setCreateTime(new Date());
@@ -226,12 +247,17 @@ public class XljkConsultationScheduleServiceImpl implements IXljkConsultationSch
// 查询咨询师姓名
if (firstSchedule.getCounselorId() != null) {
String sql = "SELECT nick_name FROM sys_user WHERE user_id = ?";
List<Map<String, Object>> result = jdbcTemplate.queryForList(sql, firstSchedule.getCounselorId());
if (!result.isEmpty()) {
summary.setCounselorName((String) result.get(0).get("nick_name"));
try {
String counselorName = jdbcTemplate.queryForObject(sql, String.class, firstSchedule.getCounselorId());
summary.setCounselorName(counselorName);
} catch (Exception e) {
log.error("查询咨询师姓名失败,counselorId: {}", firstSchedule.getCounselorId(), e);
}
}
// office字段直接从排班记录中获取
summary.setOffice(firstSchedule.getOffice());
summaryList.add(summary);
}
}
@@ -283,9 +309,12 @@ public class XljkConsultationScheduleServiceImpl implements IXljkConsultationSch
return 0;
}
// 检查是否至少启用了一个时段
if ((batchBo.getMorningEnabled() == null || !batchBo.getMorningEnabled()) &&
(batchBo.getAfternoonEnabled() == null || !batchBo.getAfternoonEnabled())) {
// 检查是否至少启用了一个时段(兼容新旧版本)
boolean hasMorning = (batchBo.getMorningEnabled() != null && batchBo.getMorningEnabled());
boolean hasNoon = (batchBo.getNoonEnabled() != null && batchBo.getNoonEnabled());
boolean hasAfternoon = (batchBo.getAfternoonEnabled() != null && batchBo.getAfternoonEnabled());
if (!hasMorning && !hasNoon && !hasAfternoon) {
return 0;
}
@@ -321,12 +350,13 @@ public class XljkConsultationScheduleServiceImpl implements IXljkConsultationSch
// 如果是周末且设置了跳过周末,则跳过
if (!(isWeekend && Boolean.TRUE.equals(batchBo.getSkipWeekends()))) {
// 创建上午时段
// 创建中午时段(字段名保持morning以兼容前端)
if (Boolean.TRUE.equals(batchBo.getMorningEnabled())) {
XljkConsultationSchedule morningSchedule = new XljkConsultationSchedule();
morningSchedule.setCounselorId(batchBo.getCounselorId());
morningSchedule.setOffice(batchBo.getOffice());
morningSchedule.setScheduleDate(currentDate);
morningSchedule.setTimeSlot("0"); // 上午
morningSchedule.setTimeSlot("0"); // 中午
morningSchedule.setStartTime(batchBo.getMorningStartTime());
morningSchedule.setEndTime(batchBo.getMorningEndTime());
morningSchedule.setStatus(batchBo.getStatus());
@@ -335,10 +365,45 @@ public class XljkConsultationScheduleServiceImpl implements IXljkConsultationSch
schedules.add(morningSchedule);
}
// 创建下午时段
// 创建中午时段(新版本)
if (Boolean.TRUE.equals(batchBo.getNoonEnabled()) && batchBo.getNoonTimeSlots() != null) {
for (XljkConsultationScheduleBatchBo.TimeSlot timeSlot : batchBo.getNoonTimeSlots()) {
XljkConsultationSchedule noonSchedule = new XljkConsultationSchedule();
noonSchedule.setCounselorId(batchBo.getCounselorId());
noonSchedule.setOffice(batchBo.getOffice());
noonSchedule.setScheduleDate(currentDate);
noonSchedule.setTimeSlot("0"); // 中午时段
noonSchedule.setStartTime(timeSlot.getStartTime());
noonSchedule.setEndTime(timeSlot.getEndTime());
noonSchedule.setStatus(batchBo.getStatus());
noonSchedule.setBatchId(batchId); // 设置批次ID
noonSchedule.setCreateTime(now); // 设置创建时间
schedules.add(noonSchedule);
}
}
// 创建下午时段(兼容新旧版本)
if (Boolean.TRUE.equals(batchBo.getAfternoonEnabled())) {
if (batchBo.getAfternoonTimeSlots() != null && !batchBo.getAfternoonTimeSlots().isEmpty()) {
// 新版本:支持多时段
for (XljkConsultationScheduleBatchBo.TimeSlot timeSlot : batchBo.getAfternoonTimeSlots()) {
XljkConsultationSchedule afternoonSchedule = new XljkConsultationSchedule();
afternoonSchedule.setCounselorId(batchBo.getCounselorId());
afternoonSchedule.setOffice(batchBo.getOffice());
afternoonSchedule.setScheduleDate(currentDate);
afternoonSchedule.setTimeSlot("1"); // 下午
afternoonSchedule.setStartTime(timeSlot.getStartTime());
afternoonSchedule.setEndTime(timeSlot.getEndTime());
afternoonSchedule.setStatus(batchBo.getStatus());
afternoonSchedule.setBatchId(batchId); // 设置批次ID
afternoonSchedule.setCreateTime(now); // 设置创建时间
schedules.add(afternoonSchedule);
}
} else if (batchBo.getAfternoonStartTime() != null && batchBo.getAfternoonEndTime() != null) {
// 旧版本:单时段
XljkConsultationSchedule afternoonSchedule = new XljkConsultationSchedule();
afternoonSchedule.setCounselorId(batchBo.getCounselorId());
afternoonSchedule.setOffice(batchBo.getOffice());
afternoonSchedule.setScheduleDate(currentDate);
afternoonSchedule.setTimeSlot("1"); // 下午
afternoonSchedule.setStartTime(batchBo.getAfternoonStartTime());
@@ -349,6 +414,7 @@ public class XljkConsultationScheduleServiceImpl implements IXljkConsultationSch
schedules.add(afternoonSchedule);
}
}
}
// 增加一天
calendar.add(Calendar.DAY_OF_MONTH, 1);
@@ -0,0 +1,329 @@
package org.dromara.xljk.service.impl;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.dromara.xljk.domain.bo.XljkConsulterBo;
import org.dromara.xljk.domain.vo.XljkConsulterVo;
import org.dromara.xljk.domain.XljkConsulter;
import org.dromara.xljk.domain.XljkUserWithDeptInfo;
import org.dromara.xljk.mapper.XljkConsulterMapper;
import org.dromara.xljk.service.IXljkConsulterService;
import org.dromara.system.service.ISysUserService;
import org.dromara.system.service.ISysRoleService;
import org.dromara.system.domain.vo.SysUserVo;
import org.dromara.system.domain.vo.SysRoleVo;
import org.dromara.system.domain.SysRole;
import org.dromara.system.domain.SysUserRole;
import org.dromara.system.domain.bo.SysRoleBo;
import org.dromara.system.mapper.SysUserRoleMapper;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.core.exception.ServiceException;
import java.util.List;
import java.util.Map;
import java.util.Collection;
import java.util.ArrayList;
import java.util.stream.Collectors;
/**
* 心理咨询师配置Service业务层处理
*
* @author ruoyi
* @date 2024-01-01
*/
@RequiredArgsConstructor
@Service
public class XljkConsulterServiceImpl implements IXljkConsulterService {
private final XljkConsulterMapper baseMapper;
private final ISysUserService sysUserService;
private final ISysRoleService sysRoleService;
private final SysUserRoleMapper sysUserRoleMapper;
/**
* 查询心理咨询师配置
*/
@Override
public XljkConsulterVo queryById(Long id) {
XljkConsulter entity = baseMapper.selectById(id);
if (entity == null) {
return null;
}
return MapstructUtils.convert(entity, XljkConsulterVo.class);
}
/**
* 查询心理咨询师配置列表
*/
@Override
public TableDataInfo<XljkConsulterVo> queryPageList(XljkConsulterBo bo, PageQuery pageQuery) {
// 直接使用查询条件构建器
LambdaQueryWrapper<XljkConsulter> lqw = buildQueryWrapper(bo);
Page<XljkConsulter> entityPage = baseMapper.selectPage(pageQuery.build(), lqw);
// 转换为 VO 对象
List<XljkConsulterVo> voList = new ArrayList<>();
for (XljkConsulter entity : entityPage.getRecords()) {
XljkConsulterVo vo = MapstructUtils.convert(entity, XljkConsulterVo.class);
voList.add(vo);
}
// 构建分页结果
Page<XljkConsulterVo> voPage = new Page<>(entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal());
voPage.setRecords(voList);
return TableDataInfo.build(voPage);
}
/**
* 查询心理咨询师配置列表
*/
@Override
public List<XljkConsulterVo> queryList(XljkConsulterBo bo) {
LambdaQueryWrapper<XljkConsulter> lqw = buildQueryWrapper(bo);
List<XljkConsulter> entityList = baseMapper.selectList(lqw);
// 转换为 VO 对象
List<XljkConsulterVo> voList = new ArrayList<>();
for (XljkConsulter entity : entityList) {
XljkConsulterVo vo = MapstructUtils.convert(entity, XljkConsulterVo.class);
voList.add(vo);
}
return voList;
}
private LambdaQueryWrapper<XljkConsulter> buildQueryWrapper(XljkConsulterBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<XljkConsulter> lqw = Wrappers.<XljkConsulter>lambdaQuery();
lqw.eq(bo.getUserId() != null, XljkConsulter::getUserId, bo.getUserId());
lqw.like(StringUtils.isNotBlank(bo.getUserName()), XljkConsulter::getUserName, bo.getUserName());
lqw.like(StringUtils.isNotBlank(bo.getNickName()), XljkConsulter::getNickName, bo.getNickName());
lqw.eq(bo.getDeptId() != null, XljkConsulter::getDeptId, bo.getDeptId());
lqw.eq(StringUtils.isNotBlank(bo.getStatus()), XljkConsulter::getStatus, bo.getStatus());
lqw.eq(bo.getConfigBy() != null, XljkConsulter::getConfigBy, bo.getConfigBy());
lqw.between(params.get("beginCreateTime") != null && params.get("endCreateTime") != null,
XljkConsulter::getCreateTime, params.get("beginCreateTime"), params.get("endCreateTime"));
lqw.orderByDesc(XljkConsulter::getCreateTime);
return lqw;
}
/**
* 新增心理咨询师配置
*/
@Override
public Boolean insertByBo(XljkConsulterBo bo) {
XljkConsulter add = MapstructUtils.convert(bo, XljkConsulter.class);
validEntityBeforeSave(add);
// 设置配置人信息
Long currentUserId = LoginHelper.getUserId();
add.setConfigBy(currentUserId);
// 获取当前用户信息设置配置人姓名
try {
SysUserVo currentUser = sysUserService.selectUserById(currentUserId);
if (currentUser != null) {
add.setConfigByName(currentUser.getUserName());
}
} catch (Exception e) {
// 如果获取失败,使用默认值
add.setConfigByName("系统管理员");
}
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setId(add.getId());
}
return flag;
}
/**
* 修改心理咨询师配置
*/
@Override
public Boolean updateByBo(XljkConsulterBo bo) {
XljkConsulter update = MapstructUtils.convert(bo, XljkConsulter.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(XljkConsulter entity) {
if (entity.getUserId() != null) {
// 检查用户是否已存在(新增时或修改用户ID时)
if (entity.getId() == null) {
// 新增时检查
int count = baseMapper.checkUserExists(entity.getUserId());
if (count > 0) {
throw new ServiceException("该用户已配置为咨询师,不能重复添加");
}
}
}
}
/**
* 批量删除心理咨询师配置
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids) {
return baseMapper.deleteBatchIds(ids) > 0;
}
/**
* 批量添加咨询师
*/
@Override
public Boolean batchAddConsulters(List<Long> userIds, String remark) {
if (userIds == null || userIds.isEmpty()) {
throw new ServiceException("用户ID列表不能为空");
}
Long currentUserId = LoginHelper.getUserId();
String currentUserName = "系统管理员";
// 获取当前用户信息
try {
SysUserVo currentUser = sysUserService.selectUserById(currentUserId);
if (currentUser != null) {
currentUserName = currentUser.getUserName();
}
} catch (Exception e) {
// 使用默认值
}
// 1. 获取完整的用户信息(包含部门信息)
List<XljkUserWithDeptInfo> users = baseMapper.selectUsersWithDeptInfo(userIds);
if (users.isEmpty()) {
throw new ServiceException("没有找到有效的用户信息");
}
// 2. 过滤已存在的咨询师
List<Long> existingUserIds = baseMapper.selectList(Wrappers.<XljkConsulter>lambdaQuery()
.in(XljkConsulter::getUserId, userIds))
.stream().map(XljkConsulter::getUserId).collect(Collectors.toList());
// 3. 构建新增咨询师列表
List<XljkConsulter> consulterList = new ArrayList<>();
for (XljkUserWithDeptInfo user : users) {
if (!existingUserIds.contains(user.getUserId())) {
XljkConsulter consulter = new XljkConsulter();
consulter.setUserId(user.getUserId());
consulter.setUserName(user.getUserName());
consulter.setNickName(user.getNickName());
consulter.setDeptId(user.getDeptId());
consulter.setDeptName(user.getDeptName() != null ? user.getDeptName() : "");
consulter.setPhoneNumber(user.getPhonenumber());
consulter.setEmail(user.getEmail());
consulter.setStatus("1"); // 默认正常状态
consulter.setRemark(remark);
consulter.setConfigBy(currentUserId);
consulter.setConfigByName(currentUserName);
consulterList.add(consulter);
}
}
// 4. 批量保存
if (consulterList.isEmpty()) {
throw new ServiceException("所有用户都已存在,无需重复添加");
}
boolean result = baseMapper.insertBatch(consulterList);
// 5. 为新添加的咨询师自动分配 xljk_consultant 角色
if (result) {
assignConsultantRole(consulterList);
}
return result;
}
/**
* 为咨询师分配 xljk_consultant 角色
*/
private void assignConsultantRole(List<XljkConsulter> consulterList) {
try {
// 1. 根据角色键查询角色信息
SysRoleBo roleBo = new SysRoleBo();
roleBo.setRoleKey("xljk_consultant");
List<SysRoleVo> roleList = sysRoleService.selectRoleList(roleBo);
if (roleList == null || roleList.isEmpty()) {
throw new ServiceException("未找到角色键为 'xljk_consultant' 的角色,请联系管理员创建该角色");
}
Long roleId = roleList.get(0).getRoleId();
// 2. 为每个用户分配角色
List<SysUserRole> userRoleList = new ArrayList<>();
for (XljkConsulter consulter : consulterList) {
// 检查用户是否已有该角色
Long count = sysUserRoleMapper.selectCount(
Wrappers.<SysUserRole>lambdaQuery()
.eq(SysUserRole::getUserId, consulter.getUserId())
.eq(SysUserRole::getRoleId, roleId)
);
// 如果用户还没有该角色,则添加
if (count == 0) {
SysUserRole userRole = new SysUserRole();
userRole.setUserId(consulter.getUserId());
userRole.setRoleId(roleId);
userRoleList.add(userRole);
}
}
// 3. 批量插入用户角色关联
if (!userRoleList.isEmpty()) {
sysUserRoleMapper.insertBatch(userRoleList);
System.out.println("成功为 " + userRoleList.size() + " 个用户分配了 'xljk_consultant' 角色");
}
} catch (Exception e) {
// 记录错误但不影响主流程
System.err.println("为咨询师分配角色失败: " + e.getMessage());
e.printStackTrace();
}
}
/**
* 查询我配置的咨询师列表
*/
@Override
public TableDataInfo<XljkConsulterVo> queryMyConfigList(XljkConsulterBo bo, PageQuery pageQuery) {
bo.setConfigBy(LoginHelper.getUserId());
return queryPageList(bo, pageQuery);
}
/**
* 批量启用咨询师
*/
@Override
public Boolean enableConsulters(List<Long> ids) {
if (ids == null || ids.isEmpty()) {
return false;
}
return baseMapper.enableConsultersByIds(ids) > 0;
}
/**
* 批量停用咨询师
*/
@Override
public Boolean disableConsulters(List<Long> ids) {
if (ids == null || ids.isEmpty()) {
return false;
}
return baseMapper.disableConsultersByIds(ids) > 0;
}
}
@@ -0,0 +1,98 @@
<?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.xljk.mapper.XljkConsulterMapper">
<resultMap type="org.dromara.xljk.domain.vo.XljkConsulterVo" id="XljkConsulterVoResult">
<result property="id" column="id"/>
<result property="userId" column="user_id"/>
<result property="userName" column="user_name"/>
<result property="nickName" column="nick_name"/>
<result property="deptId" column="dept_id"/>
<result property="deptName" column="dept_name"/>
<result property="phoneNumber" column="phone_number"/>
<result property="office" column="office"/>
<result property="email" column="email"/>
<result property="status" column="status"/>
<result property="remark" column="remark"/>
<result property="configBy" column="config_by"/>
<result property="configByName" column="config_by_name"/>
<result property="tenantId" column="tenant_id"/>
<result property="createDept" column="create_dept"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<sql id="selectXljkConsulterVo">
select id, user_id, user_name, nick_name, dept_id, dept_name, phone_number, office, email, status, remark, config_by, config_by_name, tenant_id, create_dept, create_by, create_time, update_by, update_time from xljk_consulter
</sql>
<select id="selectXljkConsulterVoById" parameterType="Long" resultMap="XljkConsulterVoResult">
<include refid="selectXljkConsulterVo"/>
where id = #{id}
</select>
<select id="selectXljkConsulterVoList" parameterType="org.dromara.xljk.domain.XljkConsulter" resultMap="XljkConsulterVoResult">
<include refid="selectXljkConsulterVo"/>
<where>
<if test="userId != null">and user_id = #{userId}</if>
<if test="userName != null and userName != ''">and user_name like concat('%', #{userName}, '%')</if>
<if test="nickName != null and nickName != ''">and nick_name like concat('%', #{nickName}, '%')</if>
<if test="deptId != null">and dept_id = #{deptId}</if>
<if test="status != null and status != ''">and status = #{status}</if>
<if test="configBy != null">and config_by = #{configBy}</if>
<if test="params.beginCreateTime != null and params.beginCreateTime != '' and params.endCreateTime != null and params.endCreateTime != ''">
and create_time between #{params.beginCreateTime} and #{params.endCreateTime}
</if>
</where>
order by create_time desc
</select>
<!-- 检查用户是否已存在 -->
<select id="checkUserExists" resultType="int">
SELECT COUNT(1) FROM xljk_consulter WHERE user_id = #{userId} AND deleted = 0
</select>
<!-- 批量启用咨询师 -->
<update id="enableConsultersByIds">
UPDATE xljk_consulter SET status = '1' WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
AND deleted = 0
</update>
<!-- 批量停用咨询师 -->
<update id="disableConsultersByIds">
UPDATE xljk_consulter SET status = '0' WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
AND deleted = 0
</update>
<!-- 查询完整的用户信息(包含部门信息)用于批量添加 -->
<select id="selectUsersWithDeptInfo" resultType="org.dromara.xljk.domain.XljkUserWithDeptInfo">
SELECT
u.user_id as userId,
u.user_name as userName,
u.nick_name as nickName,
u.dept_id as deptId,
d.dept_name as deptName,
u.phonenumber as phonenumber,
u.email as email,
u.status as status
FROM sys_user u
LEFT JOIN sys_dept d ON u.dept_id = d.dept_id
WHERE u.user_id IN
<foreach collection="userIds" item="userId" open="(" separator="," close=")">
#{userId}
</foreach>
AND u.del_flag = '0'
AND u.status = '0'
</select>
</mapper>
+23
View File
@@ -33,6 +33,19 @@ export const getAvailableDetailSlots = (date, timeSlot) => {
});
};
/**
* 获取日历视图的排班数据(带预约状态)- 直接从xljk_consultation_schedule表查询
* @param {string} startDate 开始日期 YYYY-MM-DD
* @param {string} endDate 结束日期 YYYY-MM-DD
*/
export const getCalendarSchedules = (startDate, endDate) => {
return request({
url: '/xljk/consultation/appointment/calendar-schedules',
method: 'GET',
params: { startDate, endDate }
});
};
// ===== 预约记录管理 API =====
/**
@@ -171,6 +184,16 @@ export const getClassOptions = (collegeName, majorName) => {
});
};
/**
* 获取咨询主题选项(字典数据)
*/
export const getConsultThemeOptions = () => {
return request({
url: '/system/dict/data/type/xljk_consult_theme',
method: 'GET'
});
};
// ===== 移动端专用辅助方法 =====
/**
+78 -13
View File
@@ -19,11 +19,11 @@
<view class="filter-section">
<view class="filter-tabs">
<view
v-for="(tab, index) in filterTabs"
v-for="(tab, index) in visibleFilterTabs"
:key="index"
class="filter-tab"
:class="{ active: activeFilterIndex === index }"
@click="switchFilter(index)"
:class="{ active: getOriginalTabIndex(index) === activeFilterIndex }"
@click="switchFilter(getOriginalTabIndex(index))"
>
<text class="tab-text">{{ tab.label }}</text>
<!-- 只有待咨询标签显示圆形标记 -->
@@ -71,12 +71,15 @@
></u-avatar>
</view>
<view class="counselor-info">
<view class="counselor-name-row">
<text class="counselor-name">{{ appointment.counselorName || '未知咨询师' }}</text>
<text class="appointment-office">{{ appointment.office || '暂无办公室信息' }}</text>
</view>
<text class="appointment-reason">{{ appointment.reason || '心理咨询' }}</text>
</view>
<!-- 取消按钮 -->
<view v-if="appointment.status === '0'" class="inline-action">
<view v-if="appointment.status === '0' || appointment.status === '4'" class="inline-action">
<button
class="cancel-button-inline"
@click.stop="handleCancelClick(appointment)"
@@ -113,7 +116,7 @@
</template>
<script>
import { getMyAppointments, cancelAppointment } from '@/api/xljk'
import { getMyAppointments, cancelAppointment, getCounselorOptions } from '@/api/xljk'
export default {
name: 'MyAppointments',
@@ -124,8 +127,10 @@ export default {
{ label: '全部', value: 'all', count: 0 },
{ label: '待咨询', value: '0', count: 0 },
{ label: '已完成', value: '1', count: 0 },
{ label: '已取消', value: '2', count: 0 },
{ label: '未到访', value: '3', count: 0 }
{ label: '已取消', value: '2', count: 0, hidden: true }, // 隐藏已取消标签
{ label: '未到访', value: '3', count: 0 },
{ label: '待审核', value: '4', count: 0, hidden: true }, // 隐藏待审核标签
{ label: '已驳回', value: '5', count: 0 }
],
activeFilterIndex: 0,
@@ -140,6 +145,12 @@ export default {
cancelingAppointmentId: null // 正在取消的预约ID
}
},
computed: {
// 获取可见的筛选标签(过滤掉隐藏的标签)
visibleFilterTabs() {
return this.filterTabs.filter(tab => !tab.hidden)
}
},
onLoad() {
// 初始化时重置取消相关状态
this.resetCancelState()
@@ -152,6 +163,12 @@ export default {
this.loadAppointments()
},
methods: {
// 获取可见标签在原始数组中的索引
getOriginalTabIndex(visibleIndex) {
const visibleTab = this.visibleFilterTabs[visibleIndex]
return this.filterTabs.findIndex(tab => tab.value === visibleTab.value)
},
// 返回上一页
goBack() {
uni.navigateBack()
@@ -203,14 +220,38 @@ export default {
response.data.forEach((item, index) => {
console.log(`🔍 [我的预约] 数据${index}: ID=${item.appointmentId}, status=${item.status} (${typeof item.status})`)
})
// 获取咨询师信息用于补充办公室字段
let counselors = []
try {
const counselorRes = await getCounselorOptions()
if (counselorRes.code === 200 && counselorRes.data) {
counselors = counselorRes.data
console.log('🔍 [我的预约] 获取到咨询师列表:', counselors)
}
} catch (error) {
console.error('获取咨询师信息失败:', error)
}
// 转换API数据格式
const appointments = response.data.map(item => {
// 补充办公室信息
let office = item.office || '暂无办公室信息'
if (item.counselorId && counselors.length > 0) {
const counselor = counselors.find(c => c.id === item.counselorId)
if (counselor && counselor.office) {
office = counselor.office
console.log('🔍 [我的预约] 补充咨询师办公室信息:', { counselorId: item.counselorId, office: counselor.office })
}
}
return {
id: item.id, // 修复:使用 id 而不是 appointmentId
appointmentDate: item.appointmentDate,
appointmentTime: item.appointmentDetailTime || item.appointmentTime,
counselorName: item.counselorName,
counselorAvatar: '',
office: office, // 使用补充后的办公室信息
reason: item.reason,
status: String(item.status), // 转换为字符串,便于筛选比较
originalData: item
@@ -249,14 +290,18 @@ export default {
const status1Count = this.allAppointments.filter(item => item.status === '1').length
const status2Count = this.allAppointments.filter(item => item.status === '2').length
const status3Count = this.allAppointments.filter(item => item.status === '3').length
const status4Count = this.allAppointments.filter(item => item.status === '4').length
const status5Count = this.allAppointments.filter(item => item.status === '5').length
console.log('🔍 [我的预约] 状态统计: 待咨询(0)=', status0Count, ', 已完成(1)=', status1Count, ', 已取消(2)=', status2Count, ', 未到访(3)=', status3Count)
console.log('🔍 [我的预约] 状态统计: 待咨询(0)=', status0Count, ', 已完成(1)=', status1Count, ', 已取消(2)=', status2Count, ', 未到访(3)=', status3Count, ', 待审核(4)=', status4Count, ', 已驳回(5)=', status5Count)
this.filterTabs[0].count = this.allAppointments.length // 全部
this.filterTabs[1].count = status0Count // 待咨询
this.filterTabs[2].count = status1Count // 已完成
this.filterTabs[3].count = status2Count // 已取消
this.filterTabs[4].count = status3Count // 未到访
this.filterTabs[5].count = status4Count // 待审核
this.filterTabs[6].count = status5Count // 已驳回
},
// 切换筛选器
@@ -288,6 +333,8 @@ export default {
'1': '已完成',
'2': '已取消',
'3': '未到访',
'4': '待审核',
'5': '已驳回',
// 兼容英文状态值
'pending': '待咨询',
'completed': '已完成',
@@ -304,6 +351,8 @@ export default {
'1': 'success',
'2': 'primary', // 已取消状态使用蓝色
'3': 'info', // 未到访状态使用灰色
'4': 'info', // 待审核状态使用灰色
'5': 'error', // 已驳回状态使用红色
// 兼容英文状态值
'pending': 'warning',
'completed': 'success',
@@ -321,7 +370,9 @@ export default {
'0': '暂无待咨询预约',
'1': '暂无已完成预约',
'2': '暂无已取消预约',
'3': '暂无未到访预约'
'3': '暂无未到访预约',
'4': '暂无待审核预约',
'5': '暂无已驳回预约'
}
return textMap[currentFilter.value] || '暂无数据'
},
@@ -355,9 +406,9 @@ export default {
cancelAppointment(appointment) {
console.log('点击取消预约按钮,预约信息:', appointment)
if (appointment.status !== '0') {
if (appointment.status !== '0' && appointment.status !== '4') {
uni.showToast({
title: '只能取消待咨询的预约',
title: '只能取消待咨询或待审核的预约',
icon: 'none'
})
return
@@ -670,12 +721,26 @@ export default {
.counselor-info {
flex: 1;
.counselor-name-row {
display: flex;
align-items: center;
margin-bottom: 6rpx;
.counselor-name {
font-size: 30rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 8rpx;
margin-right: 12rpx;
}
.appointment-office {
font-size: 24rpx;
color: #1890ff;
background: rgba(24, 144, 255, 0.1);
padding: 4rpx 8rpx;
border-radius: 8rpx;
flex-shrink: 0;
}
}
.appointment-reason {
+196 -82
View File
@@ -43,10 +43,10 @@
<!-- 分组时间段列表 -->
<view v-else-if="timeSlots.length > 0" class="grouped-time-slots">
<!-- 上午时段组 -->
<!-- 中午时段组 -->
<view v-if="groupedTimeSlots.morning.length > 0" class="time-group">
<view class="group-header">
<text class="group-title">上午时段</text>
<text class="group-title">中午时段</text>
</view>
<view class="slots-grid">
<view
@@ -133,6 +133,30 @@
<text class="detail-label">咨询师:</text>
<text class="detail-value">{{ selectedBookingSlot && selectedBookingSlot.counselorName }}</text>
</view>
<view class="booking-detail-item">
<text class="detail-label">办公室:</text>
<text class="detail-value">{{ selectedBookingSlot && selectedBookingSlot.office || '暂无信息' }}</text>
</view>
<!-- 咨询主题选择 -->
<view class="booking-detail-item">
<text class="detail-label required">咨询主题:</text>
</view>
<view class="theme-select-container">
<u-picker
:show="showThemePicker"
:columns="themeColumns"
@confirm="onThemeConfirm"
@cancel="showThemePicker = false"
keyName="dictLabel"
></u-picker>
<view class="theme-select-input" @click="showThemePicker = true">
<text class="theme-select-text" :class="{ placeholder: !selectedTheme }">
{{ selectedTheme ? selectedTheme.dictLabel : '请选择咨询主题' }}
</text>
<u-icon name="arrow-down" size="14" color="#c0c4cc"></u-icon>
</view>
</view>
<!-- 咨询原因输入框 -->
<view class="booking-detail-item reason-item">
@@ -158,7 +182,7 @@
</template>
<script>
import { getAvailableTimeSlots, getAvailableDetailSlots, submitAppointment } from '@/api/xljk'
import { getAvailableTimeSlots, getAvailableDetailSlots, getCalendarSchedules, submitAppointment, getConsultThemeOptions, getCounselorOptions } from '@/api/xljk'
export default {
name: 'XljkReservation',
@@ -182,7 +206,13 @@ export default {
showBookingModal: false,
selectedBookingSlot: null,
bookingLoading: false,
consultationReason: ''
consultationReason: '',
// 咨询主题相关
showThemePicker: false,
themeOptions: [],
themeColumns: [[]],
selectedTheme: null
}
},
onLoad() {
@@ -190,6 +220,7 @@ export default {
console.log('🔍 [预约页面] 开始初始化')
this.initWeekDates()
this.selectTodayAsDefault()
this.loadConsultThemeOptions()
console.log('🔍 [预约页面] 初始化完成')
} catch (error) {
console.error('🔍 [预约页面] 初始化失败:', error)
@@ -286,88 +317,104 @@ export default {
return `${date.getMonth() + 1}月${date.getDate()}日`
},
// 加载时间段数据 - 优化为加载详细时段
// 加载时间段数据 - 直接从xljk_consultation_schedule表获取
async loadTimeSlots(date) {
this.loadingTimeSlots = true
this.timeSlots = []
this.groupedTimeSlots = { morning: [], afternoon: [] }
try {
// 1. 先获取大时段列表 - 增加参数验证
// 参数验证
if (!date) {
throw new Error('日期参数无效')
}
console.log('🔍 开始获取大时段数据,日期:', date)
const bigSlotsResponse = await getAvailableTimeSlots(date, date)
console.log('🔍 大时段API返回:', bigSlotsResponse)
console.log('🔍 开始获取排班数据,日期:', date)
// 直接调用calendar-schedules接口获取排班数据(跟web端一样)
const response = await getCalendarSchedules(date, date)
console.log('🔍 排班数据API返回:', response)
// 验证API响应结构
if (!bigSlotsResponse || typeof bigSlotsResponse !== 'object') {
if (!response || typeof response !== 'object') {
throw new Error('API响应格式异常')
}
if (bigSlotsResponse.code === 200 && bigSlotsResponse.data && bigSlotsResponse.data.length > 0) {
// 2. 并行获取所有详细时段 - 增加数据安全性检查
const detailPromises = bigSlotsResponse.data
.filter(bigSlot => bigSlot && bigSlot.timeSlot) // 过滤无效数据
.map(bigSlot =>
getAvailableDetailSlots(date, bigSlot.timeSlot)
.then(response => ({
bigSlot,
detailSlots: response && response.data ? response.data : []
}))
.catch(error => {
console.error(`获取时段 ${bigSlot.timeSlot} 的详细数据失败:`, error)
return { bigSlot, detailSlots: [] }
})
if (response.code === 200 && response.data && Array.isArray(response.data)) {
// 如果排班数据缺少办公室信息,获取咨询师信息来补充
let counselors = []
const hasMissingOfficeInfo = response.data.some(schedule =>
(!schedule.office || schedule.office.trim() === '') && schedule.counselorId
)
const allDetailResults = await Promise.all(detailPromises)
console.log('🔍 详细时段API返回:', allDetailResults)
// 3. 扁平化并增强数据 - 增加数据安全性检查
const allDetailSlots = []
allDetailResults.forEach(({ bigSlot, detailSlots }) => {
// 确保 bigSlot 和 detailSlots 都是有效的
if (!bigSlot || !Array.isArray(detailSlots)) {
console.warn('无效的时段数据:', { bigSlot, detailSlots })
return
if (hasMissingOfficeInfo) {
try {
console.log('🔍 检测到缺少办公室信息,开始获取咨询师详细信息...')
const counselorRes = await getCounselorOptions()
if (counselorRes.code === 200 && counselorRes.data) {
counselors = counselorRes.data
console.log('🔍 获取到咨询师列表:', counselors)
}
} catch (error) {
console.error('获取咨询师信息失败:', error)
}
}
detailSlots.forEach(detail => {
// 处理排班数据,转换为移动端需要的格式
const scheduleSlots = []
response.data.forEach(schedule => {
// 确保必要的字段存在
if (!detail || !detail.timeSlot) {
console.warn('无效的详细时段数据:', detail)
if (!schedule || !schedule.startTime || !schedule.endTime) {
console.warn('无效的排班数据:', schedule)
return
}
try {
allDetailSlots.push({
id: `${bigSlot.scheduleId || 'unknown'}_${detail.timeSlot}`,
timeRange: detail.timeSlot,
available: !detail.isBooked && !detail.isExpired,
isBooked: Boolean(detail.isBooked),
isExpired: Boolean(detail.isExpired),
counselorName: bigSlot.counselorName || '未知咨询师',
counselorId: bigSlot.counselorId || null,
// 构建时间范围显示
const timeRange = `${schedule.startTime}-${schedule.endTime}`
// 判断是否可预约
const available = !schedule.isFullyBooked && !schedule.isExpired
// 补充办公室信息
let office = schedule.office || '暂无信息'
if ((!schedule.office || schedule.office.trim() === '') && schedule.counselorId && counselors.length > 0) {
const counselor = counselors.find(c => c.id === schedule.counselorId)
if (counselor && counselor.office) {
office = counselor.office
console.log('🔍 补充排班办公室信息:', { scheduleId: schedule.scheduleId, counselorId: schedule.counselorId, office: counselor.office })
}
}
scheduleSlots.push({
id: `schedule_${schedule.scheduleId}`,
timeRange: timeRange,
available: available,
isBooked: Boolean(schedule.isFullyBooked),
isExpired: Boolean(schedule.isExpired),
counselorName: schedule.counselorName || '未知咨询师',
counselorId: schedule.counselorId || null,
counselorAvatar: '',
office: office, // 使用补充后的办公室信息
dateText: this.selectedDateText || '',
originalData: {
...detail,
parentScheduleId: bigSlot.scheduleId || null,
counselorId: bigSlot.counselorId || null,
timeSlot: bigSlot.timeSlot || null
scheduleId: schedule.scheduleId,
counselorId: schedule.counselorId,
timeSlot: schedule.timeSlot,
startTime: schedule.startTime,
endTime: schedule.endTime,
available: available,
office: office // 在原始数据中也保存补充后的办公室信息
}
})
} catch (err) {
console.error('处理时段数据时出错:', err, { bigSlot, detail })
console.error('处理排班数据时出错:', err, schedule)
}
})
})
// 4. 按时间排序 - 增加安全性检查
allDetailSlots.sort((a, b) => {
// 按时间排序
scheduleSlots.sort((a, b) => {
try {
// 安全地获取时间用于排序
const timeA = (a.timeRange && typeof a.timeRange === 'string') ? a.timeRange.split('-')[0] : '00:00'
@@ -375,27 +422,27 @@ export default {
return timeA.localeCompare(timeB)
} catch (err) {
console.error('时间排序出错:', err, { a, b })
return 0 // 如果排序出错,保持原顺序
return 0
}
})
// 5. 按上午下午分组
this.groupAndSetTimeSlots(allDetailSlots)
// 按上午下午分组
this.groupAndSetTimeSlots(scheduleSlots)
console.log('🔍 最终处理的详细时段数据:', {
total: allDetailSlots.length,
console.log('🔍 最终处理的排班时段数据:', {
total: scheduleSlots.length,
morning: this.groupedTimeSlots.morning.length,
afternoon: this.groupedTimeSlots.afternoon.length
})
} else {
// 如果没有大时段数据
// 如果没有排班数据
this.timeSlots = []
this.groupedTimeSlots = { morning: [], afternoon: [] }
// 移除Toast提示,避免在没有排班时显示不必要的消息
console.log('🔍 当天无排班数据')
}
} catch (error) {
console.error('加载详细时段失败:', error)
console.error('加载排班时段失败:', error)
// 网络错误时显示友好提示
uni.showToast({
@@ -412,7 +459,7 @@ export default {
}
},
// 按上午下午分组时段 - 增加安全性检查
// 按时段分组 - 基于timeSlot字段分组(0-中午时段,1-下午时段)
groupAndSetTimeSlots(detailSlots) {
try {
// 确保输入是有效数组
@@ -423,35 +470,29 @@ export default {
return
}
// 按上午下午分组 - 增加安全性检查
// 基于timeSlot字段分组 - 增加安全性检查
const morningSlots = detailSlots.filter(slot => {
try {
if (!slot || !slot.timeRange || typeof slot.timeRange !== 'string') {
if (!slot || !slot.originalData || slot.originalData.timeSlot === undefined) {
console.warn('缺少timeSlot数据的时段:', slot)
return false
}
const timeParts = slot.timeRange.split(':')
if (timeParts.length === 0) {
return false
}
const hour = parseInt(timeParts[0])
return !isNaN(hour) && hour < 12
// timeSlot为0的是中午时段
return slot.originalData.timeSlot === 0 || slot.originalData.timeSlot === '0'
} catch (err) {
console.warn('处理上午时段时出错:', err, slot)
console.warn('处理中午时段时出错:', err, slot)
return false
}
})
const afternoonSlots = detailSlots.filter(slot => {
try {
if (!slot || !slot.timeRange || typeof slot.timeRange !== 'string') {
if (!slot || !slot.originalData || slot.originalData.timeSlot === undefined) {
console.warn('缺少timeSlot数据的时段:', slot)
return false
}
const timeParts = slot.timeRange.split(':')
if (timeParts.length === 0) {
return false
}
const hour = parseInt(timeParts[0])
return !isNaN(hour) && hour >= 12
// timeSlot为1的是下午时段
return slot.originalData.timeSlot === 1 || slot.originalData.timeSlot === '1'
} catch (err) {
console.warn('处理下午时段时出错:', err, slot)
return false
@@ -459,13 +500,15 @@ export default {
})
this.groupedTimeSlots = {
morning: morningSlots,
morning: morningSlots, // 实际上是中午时段
afternoon: afternoonSlots
}
// 保持原有的timeSlots格式兼容现有代码
this.timeSlots = [...morningSlots, ...afternoonSlots]
} catch (error) {
console.error('分组时段时出错:', error)
// 发生错误时设置为空数组
@@ -479,6 +522,39 @@ export default {
this.loadTimeSlots(this.selectedDate)
},
// 加载咨询主题选项
async loadConsultThemeOptions() {
try {
const response = await getConsultThemeOptions()
console.log('🔍 咨询主题字典数据:', response)
if (response.code === 200 && response.data && Array.isArray(response.data)) {
this.themeOptions = response.data // 直接使用所有字典数据
this.themeColumns = [this.themeOptions]
console.log('🔍 处理后的咨询主题选项:', this.themeOptions)
} else {
console.warn('咨询主题字典数据格式异常:', response)
this.themeOptions = []
this.themeColumns = [[]]
}
} catch (error) {
console.error('加载咨询主题选项失败:', error)
this.themeOptions = []
this.themeColumns = [[]]
// 不显示错误提示,避免影响用户体验
}
},
// 确认选择咨询主题
onThemeConfirm(value) {
console.log('🔍 选择的咨询主题:', value)
if (value && value.value && value.value.length > 0) {
this.selectedTheme = value.value[0]
console.log('🔍 设置选中的咨询主题:', this.selectedTheme)
}
this.showThemePicker = false
},
// 处理时间段点击
handleTimeSlotClick(slot) {
console.log('🔍 点击时间段:', slot)
@@ -500,6 +576,7 @@ export default {
}
this.selectedBookingSlot = slot
this.selectedTheme = null // 重置咨询主题选择
this.showBookingModal = true
console.log('🔍 设置弹窗显示状态:', this.showBookingModal)
console.log('🔍 选中的预约时段数据:', this.selectedBookingSlot)
@@ -509,6 +586,15 @@ export default {
async confirmBooking() {
if (this.bookingLoading) return
// 验证咨询主题
if (!this.selectedTheme || !this.selectedTheme.dictValue) {
uni.showToast({
title: '请选择咨询主题',
icon: 'none'
})
return
}
// 验证咨询原因
if (!this.consultationReason || this.consultationReason.trim() === '') {
uni.showToast({
@@ -567,12 +653,13 @@ export default {
// 构造预约数据
const appointmentData = {
studentId: studentId,
scheduleId: this.selectedBookingSlot.originalData.parentScheduleId,
scheduleId: this.selectedBookingSlot.originalData.scheduleId,
counselorId: this.selectedBookingSlot.originalData.counselorId,
appointmentDate: this.selectedDate,
appointmentTime: this.selectedBookingSlot.originalData.timeSlot,
appointmentDetailTime: this.selectedBookingSlot.timeRange,
reason: this.consultationReason.trim() // 使用用户输入的咨询原因
reason: this.consultationReason.trim(), // 使用用户输入的咨询原因
consultTheme: this.selectedTheme.dictValue // 咨询主题字典值
}
// 调用真实API提交预约
@@ -587,6 +674,7 @@ export default {
this.showBookingModal = false
this.selectedBookingSlot = null
this.consultationReason = '' // 重置咨询原因
this.selectedTheme = null // 重置咨询主题
// 刷新时间段数据
this.refreshTimeSlots()
@@ -625,6 +713,7 @@ export default {
this.showBookingModal = false
this.selectedBookingSlot = null
this.consultationReason = '' // 重置咨询原因
this.selectedTheme = null // 重置咨询主题
},
@@ -981,11 +1070,36 @@ export default {
}
}
.theme-select-container {
margin-bottom: 24rpx;
.theme-select-input {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 20rpx;
border: 2rpx solid #e0e0e0;
border-radius: 8rpx;
background-color: #fafafa;
min-height: 60rpx;
.theme-select-text {
font-size: 28rpx;
color: #333;
flex: 1;
&.placeholder {
color: #999;
}
}
}
}
.reason-input-container {
margin-bottom: 24rpx;
padding: 0 4rpx;
border: 2rpx solid #e0e0e0;
border-radius: 12rpx;
border-radius: 8rpx;
background-color: #fafafa;
}
+140
View File
@@ -0,0 +1,140 @@
import request from '@/utils/request'
import { AxiosPromise } from 'axios'
import { StglAuditorVO, StglAuditorForm, StglAuditorQuery } from './types'
/**
* 查询社团管理审核人配置列表
* @param query
* @returns {*}
*/
export const listStglAuditor = (query?: StglAuditorQuery): AxiosPromise<StglAuditorVO[]> => {
return request({
url: '/stgl/auditor/list',
method: 'get',
params: query
})
}
/**
* 查询社团管理审核人配置详细
* @param id
*/
export const getStglAuditor = (id: string | number): AxiosPromise<StglAuditorVO> => {
return request({
url: '/stgl/auditor/' + id,
method: 'get'
})
}
/**
* 新增社团管理审核人配置
* @param data
*/
export const addStglAuditor = (data: StglAuditorForm) => {
return request({
url: '/stgl/auditor',
method: 'post',
data: data
})
}
/**
* 修改社团管理审核人配置
* @param data
*/
export const updateStglAuditor = (data: StglAuditorForm) => {
return request({
url: '/stgl/auditor',
method: 'put',
data: data
})
}
/**
* 删除社团管理审核人配置
* @param id
*/
export const delStglAuditor = (id: string | number | Array<string | number>) => {
return request({
url: '/stgl/auditor/' + id,
method: 'delete'
})
}
/**
* 导出社团管理审核人配置
* @param query
* @returns {*}
*/
export const exportStglAuditor = (query?: StglAuditorQuery) => {
return request({
url: '/stgl/auditor/export',
method: 'post',
data: query,
responseType: 'blob'
})
}
/**
* 批量添加审核人
* @param userIds 用户ID列表
* @param remark 备注
*/
export const batchAddAuditors = (userIds: number[], remark?: string) => {
return request({
url: '/stgl/auditor/batchAdd',
method: 'post',
data: {
userIds,
remark
}
})
}
/**
* 查询我配置的审核人列表
* @param query
*/
export const listMyConfigAuditors = (query?: StglAuditorQuery): AxiosPromise<StglAuditorVO[]> => {
return request({
url: '/stgl/auditor/myConfig',
method: 'get',
params: query
})
}
/**
* 批量启用审核人
* @param ids
*/
export const enableAuditors = (ids: number[]) => {
return request({
url: '/stgl/auditor/enable',
method: 'post',
data: ids
})
}
/**
* 批量停用审核人
* @param ids
*/
export const disableAuditors = (ids: number[]) => {
return request({
url: '/stgl/auditor/disable',
method: 'post',
data: ids
})
}
/**
* 根据部门ID获取用户列表
* @param deptId
*/
export const listUserByDeptId = (deptId: string | number) => {
return request({
url: '/stgl/auditor/users/' + deptId,
method: 'get'
})
}
+184
View File
@@ -0,0 +1,184 @@
import { PageQuery } from '@/api/types'
export interface StglAuditorVO {
/**
* 主键ID
*/
id: string | number;
/**
* 用户ID
*/
userId: number;
/**
* 用户账号
*/
userName: string;
/**
* 用户昵称
*/
nickName: string;
/**
* 部门ID
*/
deptId: number;
/**
* 部门名称
*/
deptName: string;
/**
* 联系电话
*/
phoneNumber: string;
/**
* 邮箱地址
*/
email: string;
/**
* 状态(1正常 0停用)
*/
status: string;
/**
* 备注说明
*/
remark: string;
/**
* 配置人ID
*/
configBy: number;
/**
* 配置人姓名
*/
configByName: string;
/**
* 创建者
*/
createBy: number;
/**
* 创建时间
*/
createTime: string;
/**
* 更新者
*/
updateBy: number;
/**
* 更新时间
*/
updateTime: string;
}
export interface StglAuditorForm {
/**
* 主键ID
*/
id?: string | number;
/**
* 用户ID
*/
userId?: number;
/**
* 用户账号
*/
userName?: string;
/**
* 用户昵称
*/
nickName?: string;
/**
* 部门ID
*/
deptId?: number;
/**
* 部门名称
*/
deptName?: string;
/**
* 联系电话
*/
phoneNumber?: string;
/**
* 邮箱地址
*/
email?: string;
/**
* 状态(1正常 0停用)
*/
status?: string;
/**
* 备注说明
*/
remark?: string;
/**
* 配置人ID
*/
configBy?: number;
/**
* 配置人姓名
*/
configByName?: string;
}
export interface StglAuditorQuery extends PageQuery {
/**
* 用户ID
*/
userId?: number;
/**
* 用户账号
*/
userName?: string;
/**
* 用户昵称
*/
nickName?: string;
/**
* 部门ID
*/
deptId?: number;
/**
* 状态(1正常 0停用)
*/
status?: string;
/**
* 配置人ID
*/
configBy?: number;
/**
* 日期范围参数
*/
'params[beginCreateTime]'?: string;
'params[endCreateTime]'?: string;
}
@@ -0,0 +1,109 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { StglAnnualReviewAuditVO, StglAnnualReviewAuditForm, StglAnnualReviewAuditQuery } from './type';
/**
* 查询社团年审审批列表
* @param query
* @returns {*}
*/
export const listStglAnnualReviewAudit = (query?: StglAnnualReviewAuditQuery): AxiosPromise<StglAnnualReviewAuditVO[]> => {
return request({
url: '/stgl/annualReviewAudit/list',
method: 'get',
params: query
});
};
/**
* 查询社团年审审批详细
* @param id
*/
export const getStglAnnualReviewAudit = (id: string | number): AxiosPromise<StglAnnualReviewAuditVO> => {
return request({
url: '/stgl/annualReviewAudit/' + id,
method: 'get'
});
};
/**
* 根据年审记录ID查询审批记录
* @param annualReviewId
*/
export const getAnnualReviewAuditsByReviewId = (annualReviewId: string | number): AxiosPromise<StglAnnualReviewAuditVO[]> => {
return request({
url: '/stgl/annualReviewAudit/byAnnualReview/' + annualReviewId,
method: 'get'
});
};
/**
* 根据审批人ID查询待审批记录
* @param approverId
*/
export const getPendingAuditsByApproverId = (approverId: string | number): AxiosPromise<StglAnnualReviewAuditVO[]> => {
return request({
url: '/stgl/annualReviewAudit/pending/' + approverId,
method: 'get'
});
};
/**
* 新增社团年审审批
* @param data
*/
export const addStglAnnualReviewAudit = (data: StglAnnualReviewAuditForm) => {
return request({
url: '/stgl/annualReviewAudit',
method: 'post',
data: data
});
};
/**
* 修改社团年审审批
* @param data
*/
export const updateStglAnnualReviewAudit = (data: StglAnnualReviewAuditForm) => {
return request({
url: '/stgl/annualReviewAudit',
method: 'put',
data: data
});
};
/**
* 删除社团年审审批
* @param id
*/
export const delStglAnnualReviewAudit = (id: string | number | Array<string | number>) => {
return request({
url: '/stgl/annualReviewAudit/' + id,
method: 'delete'
});
};
/**
* 审批年审记录
* @param auditId 审批记录ID
* @param approveResult 审批结果
* @param approveOpinion 审批意见
* @param nextAuditors 下一步审核人列表(可选)
*/
export const approveAnnualReview = (
auditId: string | number,
approveResult: string,
approveOpinion: string,
nextAuditors?: Array<{userId: string | number; userName: string; nickName: string}>
) => {
return request({
url: '/stgl/annualReviewAudit/approve',
method: 'post',
data: {
auditId,
approveResult,
approveOpinion,
nextAuditors: nextAuditors || []
}
});
};
@@ -0,0 +1,182 @@
export interface StglAnnualReviewAuditVO {
/**
* 主键ID
*/
id: string | number;
/**
* 年审记录ID
*/
annualReviewId: string | number;
/**
* 节点代码(advisor_approve/college_approve/youth_league_approve)
*/
nodeCode: string;
/**
* 节点名称(指导老师审批/学院审批/校团委审批)
*/
nodeName: string;
/**
* 节点顺序(1.1,1.2,2,3)
*/
nodeOrder: number;
/**
* 节点状态(pending/waiting/approved)
*/
nodeStatus: string;
/**
* 审批人ID
*/
approverId: string | number;
/**
* 审批人姓名
*/
approverName: string;
/**
* 审批结果(通过/不通过)
*/
approveResult: string;
/**
* 审批意见
*/
approveOpinion: string;
/**
* 审批时间
*/
approveTime: string;
/**
* 创建者
*/
createBy: string | number;
/**
* 创建时间
*/
createTime: string;
/**
* 更新者
*/
updateBy: string | number;
/**
* 更新时间
*/
updateTime: string;
}
export interface StglAnnualReviewAuditForm {
/**
* 主键ID
*/
id?: string | number;
/**
* 年审记录ID
*/
annualReviewId?: string | number;
/**
* 节点代码(advisor_approve/college_approve/youth_league_approve)
*/
nodeCode?: string;
/**
* 节点名称(指导老师审批/学院审批/校团委审批)
*/
nodeName?: string;
/**
* 节点顺序(1.1,1.2,2,3)
*/
nodeOrder?: number;
/**
* 节点状态(pending/waiting/approved)
*/
nodeStatus?: string;
/**
* 审批人ID
*/
approverId?: string | number;
/**
* 审批人姓名
*/
approverName?: string;
/**
* 审批结果(通过/不通过)
*/
approveResult?: string;
/**
* 审批意见
*/
approveOpinion?: string;
/**
* 审批时间
*/
approveTime?: string;
}
export interface StglAnnualReviewAuditQuery extends PageQuery {
/**
* 年审记录ID
*/
annualReviewId?: string | number;
/**
* 节点代码
*/
nodeCode?: string;
/**
* 节点状态
*/
nodeStatus?: string;
/**
* 审批人ID
*/
approverId?: string | number;
/**
* 审批结果
*/
approveResult?: string;
/**
* 审批时间范围
*/
params?: any;
}
/**
* 审批节点状态选项
*/
export const AUDIT_NODE_STATUS_OPTIONS = [
{ label: '待审批', value: 'pending' },
{ label: '等待中', value: 'waiting' },
{ label: '已审批', value: 'approved' }
];
/**
* 审批结果选项
*/
export const APPROVE_RESULT_OPTIONS = [
{ label: '通过', value: '通过' },
{ label: '不通过', value: '不通过' }
];
+153 -2
View File
@@ -4,18 +4,23 @@ import {
XljkConsultationScheduleVO,
XljkConsultationScheduleForm,
XljkConsultationScheduleQuery,
XljkConsultationScheduleSummaryVO,
XljkConsultationAppointmentVO,
XljkConsultationAppointmentForm,
XljkConsultationAppointmentQuery,
AvailableTimeSlot,
TimeSlotDetail,
CounselorOption
CounselorOption,
ScheduleWithStatus,
XljkConsulterVO,
XljkConsulterForm,
XljkConsulterQuery
} from './types';
// ===== 预约时段管理 API =====
// 查询预约时段列表
export const listXljkSchedule = (query: XljkConsultationScheduleQuery): AxiosPromise<XljkConsultationScheduleVO[]> => {
export const listXljkSchedule = (query: XljkConsultationScheduleQuery): AxiosPromise<XljkConsultationScheduleSummaryVO[]> => {
return request({
url: '/xljk/consultation/schedule/list',
method: 'get',
@@ -102,6 +107,15 @@ export const getXljkAvailableTimeSlots = (startDate: string, endDate: string): A
});
};
// 获取日历视图的排班数据(带预约状态)
export const getXljkCalendarSchedules = (startDate: string, endDate: string): AxiosPromise<ScheduleWithStatus[]> => {
return request({
url: '/xljk/consultation/appointment/calendar-schedules',
method: 'get',
params: { startDate, endDate }
});
};
// 获取可用的详细时段列表(30分钟一个时段)
export const getXljkAvailableDetailSlots = (date: string, timeSlot: string): AxiosPromise<TimeSlotDetail[]> => {
return request({
@@ -213,3 +227,140 @@ export const getXljkConsultationClassOptions = (collegeName?: string, majorName?
params: { collegeName, majorName }
});
};
/**
* 查询心理咨询师配置列表
* @param query
* @returns {*}
*/
export const listXljkConsulter = (query?: XljkConsulterQuery): AxiosPromise<XljkConsulterVO[]> => {
return request({
url: '/xljk/consulter/list',
method: 'get',
params: query
})
}
/**
* 查询心理咨询师配置详细
* @param id
*/
export const getXljkConsulter = (id: string | number): AxiosPromise<XljkConsulterVO> => {
return request({
url: '/xljk/consulter/' + id,
method: 'get'
})
}
/**
* 新增心理咨询师配置
* @param data
*/
export const addXljkConsulter = (data: XljkConsulterForm) => {
return request({
url: '/xljk/consulter',
method: 'post',
data: data
})
}
/**
* 修改心理咨询师配置
* @param data
*/
export const updateXljkConsulter = (data: XljkConsulterForm) => {
return request({
url: '/xljk/consulter',
method: 'put',
data: data
})
}
/**
* 删除心理咨询师配置
* @param id
*/
export const delXljkConsulter = (id: string | number | Array<string | number>) => {
return request({
url: '/xljk/consulter/' + id,
method: 'delete'
})
}
/**
* 导出心理咨询师配置
* @param query
* @returns {*}
*/
export const exportXljkConsulter = (query?: XljkConsulterQuery) => {
return request({
url: '/xljk/consulter/export',
method: 'post',
data: query,
responseType: 'blob'
})
}
/**
* 批量添加咨询师
* @param userIds 用户ID列表
* @param remark 备注
*/
export const batchAddConsulters = (userIds: number[], remark?: string) => {
return request({
url: '/xljk/consulter/batchAdd',
method: 'post',
data: {
userIds,
remark
}
})
}
/**
* 查询我配置的咨询师列表
* @param query
*/
export const listMyConfigConsulters = (query?: XljkConsulterQuery): AxiosPromise<XljkConsulterVO[]> => {
return request({
url: '/xljk/consulter/myConfig',
method: 'get',
params: query
})
}
/**
* 批量启用咨询师
* @param ids
*/
export const enableConsulters = (ids: number[]) => {
return request({
url: '/xljk/consulter/enable',
method: 'post',
data: ids
})
}
/**
* 批量停用咨询师
* @param ids
*/
export const disableConsulters = (ids: number[]) => {
return request({
url: '/xljk/consulter/disable',
method: 'post',
data: ids
})
}
/**
* 根据部门ID获取用户列表
* @param deptId
*/
export const listUserByDeptId = (deptId: string | number) => {
return request({
url: '/xljk/consulter/users/' + deptId,
method: 'get'
})
}
+390 -9
View File
@@ -1,3 +1,5 @@
import { PageQuery } from '@/api/types'
export interface XljkConsultationScheduleVO {
/**
* 主键ID
@@ -14,6 +16,11 @@ export interface XljkConsultationScheduleVO {
*/
counselorName?: string;
/**
* 办公室
*/
office?: string;
/**
* 排班日期
*/
@@ -71,6 +78,11 @@ export interface XljkConsultationScheduleForm extends BaseEntity {
*/
counselorId?: number;
/**
* 办公室
*/
office?: string;
/**
* 排班日期
*/
@@ -103,6 +115,11 @@ export interface XljkConsultationScheduleQuery extends PageQuery {
*/
counselorId?: number;
/**
* 办公室
*/
office?: string;
/**
* 排班日期开始
*/
@@ -130,6 +147,10 @@ export interface XljkConsultationScheduleQuery extends PageQuery {
}
export interface XljkConsultationAppointmentVO extends BaseEntity {
/**
* 主键ID
*/
id?: string | number;
/**
* 学生ID
*/
@@ -163,13 +184,13 @@ export interface XljkConsultationAppointmentVO extends BaseEntity {
*/
reason?: string;
/**
* 状态(0待咨询 1已完成 2已取消 3未到访)
* 咨询主题
*/
status?: number;
consultTheme?: string;
/**
* 实际到访时间
* 状态(0待咨询 1已完成 2已取消 3未到访 4待审核 5已驳回)
*/
actualVisitTime?: any;
status?: number | string;
/**
* 沟通时长(分钟)
*/
@@ -186,6 +207,26 @@ export interface XljkConsultationAppointmentVO extends BaseEntity {
* 学生姓名
*/
studentName?: string;
/**
* 心理病史(0-是,1-否)
*/
psychologicalHistory?: number;
/**
* 心理病史详情
*/
psychologicalHistoryDetail?: string;
/**
* 基本情况
*/
basicSituation?: string;
/**
* 重大生活事件
*/
majorLifeEvents?: string;
/**
* 是否需要转介(0-是,1-否)
*/
needReferral?: number;
/**
* 班级
*/
@@ -194,6 +235,10 @@ export interface XljkConsultationAppointmentVO extends BaseEntity {
* 咨询师姓名
*/
counselorName?: string;
/**
* 办公室
*/
office?: string;
/**
* 院系
*/
@@ -202,6 +247,10 @@ export interface XljkConsultationAppointmentVO extends BaseEntity {
* 专业
*/
majorName?: string;
/**
* 本学期到访次数
*/
semesterVisitCount?: number;
}
export interface XljkConsultationAppointmentForm {
@@ -242,13 +291,13 @@ export interface XljkConsultationAppointmentForm {
*/
reason?: string;
/**
* 状态(0待咨询 1已完成 2已取消 3未到访)
* 咨询主题
*/
status?: number;
consultTheme?: string;
/**
* 实际到访时间
* 状态(0待咨询 1已完成 2已取消 3未到访 4待审核 5已驳回)
*/
actualVisitTime?: any;
status?: number | string;
/**
* 沟通时长(分钟)
*/
@@ -261,6 +310,30 @@ export interface XljkConsultationAppointmentForm {
* 咨询师建议
*/
counselorSuggestion?: string;
/**
* 学生姓名
*/
studentName?: string;
/**
* 心理病史(0-是,1-否)
*/
psychologicalHistory?: number;
/**
* 心理病史详情
*/
psychologicalHistoryDetail?: string;
/**
* 基本情况
*/
basicSituation?: string;
/**
* 重大生活事件
*/
majorLifeEvents?: string;
/**
* 是否需要转介(0-是,1-否)
*/
needReferral?: number;
}
export interface XljkConsultationAppointmentQuery extends PageQuery {
@@ -289,6 +362,11 @@ export interface XljkConsultationAppointmentQuery extends PageQuery {
*/
majorName?: string;
/**
* 班级
*/
className?: string;
/**
* 咨询师ID
*/
@@ -312,7 +390,11 @@ export interface XljkConsultationAppointmentQuery extends PageQuery {
/**
* 日期范围参数
*/
params?: any;
params?: {
beginAppointmentDate?: string;
endAppointmentDate?: string;
[key: string]: any;
};
}
export interface AvailableTimeSlot {
@@ -331,6 +413,11 @@ export interface AvailableTimeSlot {
*/
counselorName?: string;
/**
* 办公室
*/
office?: string;
/**
* 日期
*/
@@ -419,6 +506,11 @@ export interface CounselorOption {
* 咨询师姓名
*/
name: string;
/**
* 办公室
*/
office: string;
}
// 预约时段汇总视图对象
@@ -433,6 +525,7 @@ export interface XljkConsultationScheduleSummaryVO {
deleted: number;
counselorId: number;
counselorName: string;
office: string;
dateRange: string;
morningEnabled: boolean;
morningTime: string;
@@ -602,3 +695,291 @@ export interface NoShowVO {
*/
updateTime?: string;
}
/**
* 带预约状态的排班数据
*/
export interface ScheduleWithStatus {
/**
* 排班ID
*/
scheduleId: number;
/**
* 咨询师ID
*/
counselorId: number;
/**
* 咨询师姓名
*/
counselorName?: string;
/**
* 办公室
*/
office?: string;
/**
* 排班日期
*/
date: string;
/**
* 时段类型(0上午 1中午 2下午)
*/
timeSlot: string;
/**
* 开始时间(HH:mm)
*/
startTime: string;
/**
* 结束时间(HH:mm)
*/
endTime: string;
/**
* 时段显示文本(如:上午 08:30-12:00)
*/
timeDisplay: string;
/**
* 是否已预约满
*/
isFullyBooked: boolean;
/**
* 是否已过期
*/
isExpired: boolean;
/**
* 可预约数量
*/
availableSlots: number;
/**
* 总预约数量
*/
totalSlots: number;
/**
* 创建者
*/
createBy?: number;
/**
* 创建时间
*/
createTime?: string;
/**
* 更新者
*/
updateBy?: number;
/**
* 更新时间
*/
updateTime?: string;
}
export interface XljkConsulterVO {
/**
* 主键ID
*/
id: string | number;
/**
* 用户ID
*/
userId: number;
/**
* 用户账号
*/
userName: string;
/**
* 用户昵称
*/
nickName: string;
/**
* 部门ID
*/
deptId: number;
/**
* 部门名称
*/
deptName: string;
/**
* 办公室
*/
office?: string;
/**
* 联系电话
*/
phoneNumber: string;
/**
* 邮箱地址
*/
email: string;
/**
* 状态(1正常 0停用)
*/
status: string;
/**
* 备注说明
*/
remark: string;
/**
* 配置人ID
*/
configBy: number;
/**
* 配置人姓名
*/
configByName: string;
/**
* 创建者
*/
createBy: number;
/**
* 创建时间
*/
createTime: string;
/**
* 更新者
*/
updateBy: number;
/**
* 更新时间
*/
updateTime: string;
}
export interface XljkConsulterForm {
/**
* 主键ID
*/
id?: string | number;
/**
* 用户ID
*/
userId?: number;
/**
* 用户账号
*/
userName?: string;
/**
* 用户昵称
*/
nickName?: string;
/**
* 部门ID
*/
deptId?: number;
/**
* 部门名称
*/
deptName?: string;
/**
* 办公室
*/
office?: string;
/**
* 联系电话
*/
phoneNumber?: string;
/**
* 邮箱地址
*/
email?: string;
/**
* 状态(1正常 0停用)
*/
status?: string;
/**
* 备注说明
*/
remark?: string;
/**
* 配置人ID
*/
configBy?: number;
/**
* 配置人姓名
*/
configByName?: string;
}
export interface XljkConsulterQuery extends PageQuery {
/**
* 用户ID
*/
userId?: number;
/**
* 用户账号
*/
userName?: string;
/**
* 用户昵称
*/
nickName?: string;
/**
* 部门ID
*/
deptId?: number;
/**
* 状态(1正常 0停用)
*/
status?: string;
/**
* 配置人ID
*/
configBy?: number;
/**
* 办公室
*/
office?: string;
/**
* 日期范围参数
*/
'params[beginCreateTime]'?: string;
'params[endCreateTime]'?: string;
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,350 @@
<template>
<el-dialog
title="年审审批"
v-model="visible"
width="600px"
append-to-body
:before-close="handleClose"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="社团名称:">
<span>{{ auditInfo?.clubName || '加载中...' }}</span>
</el-form-item>
<el-form-item label="年审年度:">
<span>{{ auditInfo?.reviewYear || '加载中...' }}年</span>
</el-form-item>
<el-form-item label="审批节点:">
<span>{{ auditRecord?.nodeName || '加载中...' }}</span>
</el-form-item>
<el-form-item label="审批结果:" prop="approveResult">
<el-radio-group v-model="form.approveResult" @change="handleApproveResultChange">
<el-radio value="通过">通过</el-radio>
<el-radio value="不通过">不通过</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="审批意见:" prop="approveOpinion">
<el-input
v-model="form.approveOpinion"
type="textarea"
:rows="4"
placeholder="请填写审批意见"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="loading">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 审批流程选择对话框 -->
<ApprovalProcessDialog
v-model:visible="showApprovalProcessDialog"
:next-node-name="getNextNodeName()"
@confirm="handleApprovalProcessConfirm"
/>
</template>
<script setup lang="ts">
import { approveAnnualReview, getStglAnnualReviewAudit, getAnnualReviewAuditsByReviewId } from '@/api/stgl/club/annualReviewAudit';
import { getStglClubAnnualReview } from '@/api/stgl/club/annualReview';
import ApprovalProcessDialog from './auditorSelector/ApprovalProcessDialog.vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const emit = defineEmits<{
success: [];
}>();
const visible = ref(false);
const loading = ref(false);
const formRef = ref<ElFormInstance>();
const form = ref({
auditId: '',
approveResult: '',
approveOpinion: ''
});
const auditInfo = ref<any>(null);
const auditRecord = ref<any>(null);
const allAuditRecords = ref<any[]>([]);
const showApprovalProcessDialog = ref(false);
const rules = reactive<ElFormRules>({
approveResult: [
{ required: true, message: '请选择审批结果', trigger: 'change' }
],
approveOpinion: [
{ required: true, message: '请填写审批意见', trigger: 'blur' },
{ max: 500, message: '审批意见不能超过500个字符', trigger: 'blur' }
]
});
/** 打开弹窗 */
const open = async (auditId: string | number, annualReviewId: string | number) => {
visible.value = true;
form.value.auditId = auditId.toString();
// 获取年审记录信息用于显示
try {
const res = await getStglClubAnnualReview(annualReviewId);
auditInfo.value = res.data;
} catch (error) {
console.error('获取年审记录信息失败:', error);
}
// 获取审批记录信息用于显示节点名称
try {
const auditRes = await getStglAnnualReviewAudit(auditId);
auditRecord.value = auditRes.data;
} catch (error) {
console.error('获取审批记录信息失败:', error);
}
// 获取当前年审的所有审批记录,用于判断是否是本环节最后一个节点
try {
const allAuditsRes = await getAnnualReviewAuditsByReviewId(annualReviewId);
allAuditRecords.value = allAuditsRes.data || [];
} catch (error) {
console.error('获取所有审批记录失败:', error);
}
// 重置表单
resetForm();
};
/** 重置表单 */
const resetForm = () => {
form.value.approveResult = '';
form.value.approveOpinion = '';
formRef.value?.clearValidate();
};
/** 审批结果变化处理 */
const handleApproveResultChange = (value: string) => {
// 审批结果变化时的处理逻辑(如需要)
};
/** 关闭弹窗 */
const handleClose = () => {
visible.value = false;
resetForm();
auditInfo.value = null;
auditRecord.value = null;
allAuditRecords.value = [];
};
/** 提交审批 */
const handleSubmit = async () => {
if (!formRef.value) return;
const valid = await formRef.value.validate();
if (!valid) return;
// 如果是校团委审批的最后一个节点,直接结束流程,不弹出审批流程选择对话框
if (form.value.approveResult === '通过' && isSchoolCommitteeLastNode()) {
console.log('校团委最后节点,直接结束流程');
await submitApproval();
return;
}
// 如果审批通过,且当前节点是本环节的最后一个节点,弹出审批流程选择对话框
if (form.value.approveResult === '通过' && isLastNodeInCurrentPhase()) {
showApprovalProcessDialog.value = true;
return;
}
// 直接提交审批(审批不通过或不是本环节最后一个节点的情况)
await submitApproval();
};
/** 判断当前节点是否是本环节的最后一个节点 */
const isLastNodeInCurrentPhase = () => {
if (!auditRecord.value || !allAuditRecords.value.length) {
console.log('判断是否是最后节点: 数据不足', {
hasAuditRecord: !!auditRecord.value,
allAuditRecordsLength: allAuditRecords.value.length
});
return false;
}
const currentNodeCode = auditRecord.value.nodeCode;
const currentNodeOrder = Number(auditRecord.value.nodeOrder);
console.log('当前审批记录详情:', {
currentNodeCode,
currentNodeOrder,
currentNodeOrderType: typeof auditRecord.value.nodeOrder,
currentNodeOrderRaw: auditRecord.value.nodeOrder,
auditRecordKeys: Object.keys(auditRecord.value)
});
// 获取同一环节的所有节点
const samePhaseNodes = allAuditRecords.value.filter(record =>
record.nodeCode === currentNodeCode
);
console.log('所有审批记录:', allAuditRecords.value.map(record => ({
id: record.id,
nodeCode: record.nodeCode,
nodeName: record.nodeName,
nodeOrder: record.nodeOrder,
nodeOrderType: typeof record.nodeOrder,
approverName: record.approverName,
nodeStatus: record.nodeStatus
})));
console.log('同环节节点:', samePhaseNodes.map(n => ({
nodeOrder: n.nodeOrder,
nodeOrderType: typeof n.nodeOrder,
approverName: n.approverName,
nodeStatus: n.nodeStatus
})));
if (samePhaseNodes.length === 0) {
console.log('警告: 没有找到同环节的节点');
return false;
}
// 找出同一环节中最大的nodeOrder,使用Number转换确保数值比较
const nodeOrders = samePhaseNodes.map(node => Number(node.nodeOrder));
const maxNodeOrder = Math.max(...nodeOrders);
console.log('判断是否是最后节点:', {
currentNodeCode,
currentNodeOrder,
maxNodeOrder,
nodeOrders,
isLast: currentNodeOrder === maxNodeOrder,
comparison: `${currentNodeOrder} === ${maxNodeOrder}`
});
// 当前节点的nodeOrder等于同环节最大nodeOrder时,表示是最后一个节点
return currentNodeOrder === maxNodeOrder;
};
/** 判断当前是否是校团委审批的最后一个节点 */
const isSchoolCommitteeLastNode = () => {
if (!auditRecord.value) {
return false;
}
const currentNodeName = auditRecord.value.nodeName;
const currentNodeOrder = Number(auditRecord.value.nodeOrder);
// 检查是否是校团委审批节点且node_order是3.X格式
const isSchoolCommitteeNode = currentNodeName === '校团委审批';
const isNodeOrder3X = currentNodeOrder >= 3 && currentNodeOrder < 4;
console.log('校团委最后节点判断:', {
currentNodeName,
currentNodeOrder,
isSchoolCommitteeNode,
isNodeOrder3X,
isLastInPhase: isLastNodeInCurrentPhase()
});
// 校团委审批 + node_order=3.X + 是本环节最后一个节点
return isSchoolCommitteeNode && isNodeOrder3X && isLastNodeInCurrentPhase();
};
/** 获取下一步节点名称 */
const getNextNodeName = () => {
// 根据当前节点确定下一步节点
const currentNodeName = auditRecord.value?.nodeName;
if (currentNodeName === '指导老师审批') {
return '二级学院审批';
} else if (currentNodeName === '二级学院审批') {
return '校团委审批';
} else if (currentNodeName === '校团委审批') {
return '完成审批';
}
// 默认返回二级学院审批
return '二级学院审批';
};
/** 实际提交审批 */
const submitApproval = async (approvalProcessData?: any) => {
loading.value = true;
try {
// 判断是否是校团委最后节点
const isSchoolCommitteeFinalNode = isSchoolCommitteeLastNode();
// 准备审批数据
const approveData = {
auditId: form.value.auditId,
approveResult: form.value.approveResult,
approveOpinion: form.value.approveOpinion,
// 校团委最后节点时不设置下一步审核人,表示流程结束
// 其他情况:只有审批通过且是本环节最后一个节点时,才包含下一步审核人信息
nextAuditors: isSchoolCommitteeFinalNode ? [] :
(form.value.approveResult === '通过' && isLastNodeInCurrentPhase()) ?
(approvalProcessData?.auditors || []).map((auditor: any) => ({
userId: auditor.userId,
userName: auditor.userName,
nickName: auditor.nickName
})) : []
};
console.log('提交审批数据:', {
审批结果: approveData.approveResult,
是否最后节点: isLastNodeInCurrentPhase(),
是否校团委最后节点: isSchoolCommitteeFinalNode,
下一步审核人数量: approveData.nextAuditors.length,
流程状态: isSchoolCommitteeFinalNode ? '流程结束' : '继续流转'
});
await approveAnnualReview(
approveData.auditId,
approveData.approveResult,
approveData.approveOpinion,
approveData.nextAuditors
);
if (isSchoolCommitteeFinalNode) {
proxy?.$modal.msgSuccess('审批成功,年审流程已完成!');
} else {
proxy?.$modal.msgSuccess('审批成功');
}
handleClose();
emit('success');
} catch (error) {
console.error('审批失败:', error);
} finally {
loading.value = false;
}
};
/** 审批流程确认处理 */
const handleApprovalProcessConfirm = async (approvalProcessData: any) => {
console.log('审批流程确认,数据:', approvalProcessData);
await submitApproval(approvalProcessData);
};
defineExpose({
open
});
</script>
<style scoped>
.dialog-footer {
text-align: right;
}
</style>
@@ -0,0 +1,384 @@
<template>
<el-dialog
title="选择审批流程"
v-model="visible"
width="70%"
append-to-body
destroy-on-close
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<div class="approval-dialog-content">
<el-alert
:title="`请选择${props.nextNodeName}审核人`"
description="社团年审审批提交后将按照您选择的审批人员顺序进行审批,请仔细选择审核人员。"
type="info"
:closable="false"
show-icon
style="margin-bottom: 20px"
/>
<!-- 下一步审批节点提示 -->
<div class="next-node-info">
<div class="node-label">
<el-icon><Document /></el-icon>
下一步审批节点:{{ props.nextNodeName }}
</div>
</div>
<!-- 当前已选择的审核人显示 -->
<div class="current-auditors" v-if="currentAuditors.length > 0">
<div class="section-title">
<el-icon><User /></el-icon>
当前已选择的审核人 ({{ currentAuditors.length }} 人)
</div>
<div class="auditor-tags">
<el-tag
v-for="auditor in currentAuditors"
:key="auditor.userId"
type="info"
style="margin: 2px 5px 2px 0;"
>
{{ auditor.nickName }}({{ auditor.userName }})
</el-tag>
</div>
</div>
<!-- 审核人选择区域 -->
<div class="auditor-selection-section">
<div class="custom-process-config">
<el-form-item label="选择审核人" required>
<div class="auditor-selector">
<div class="selector-header">
<el-button type="primary" icon="Plus" @click="openAuditorSelector">添加审核人</el-button>
<el-text v-if="selectedAuditors.length > 0" type="info" size="small">
已选择 {{ selectedAuditors.length }} 个审核人,将按添加顺序进行审批
</el-text>
</div>
<!-- 已选择的审核人列表 -->
<div v-if="selectedAuditors.length > 0" class="selected-auditors">
<div class="auditor-flow">
<div v-for="(auditor, index) in selectedAuditors" :key="auditor.userId" class="auditor-node">
<div class="node-content">
<span class="node-index">{{ index + 1 }}</span>
<div class="auditor-info">
<div class="auditor-name">{{ auditor.nickName || auditor.userName }}</div>
<div class="auditor-dept">{{ auditor.deptName || '未知部门' }}</div>
</div>
<el-button type="danger" icon="Delete" size="small" circle @click="removeAuditor(auditor)" />
</div>
<div v-if="index < selectedAuditors.length - 1" class="flow-arrow">
<el-icon><ArrowRight /></el-icon>
</div>
</div>
</div>
<!-- 流程预览说明 -->
<div class="flow-description">
<el-alert type="info" :closable="false" show-icon>
<template #title>
<span>审批流程预览:{{ props.nextNodeName }}将按上述顺序进行逐级审批,每个节点审批通过后自动流转到下一节点</span>
</template>
</el-alert>
</div>
</div>
<!-- 空状态提示 -->
<div v-else class="empty-state">
<el-empty description="暂未选择审核人">
<el-button type="primary" @click="openAuditorSelector">选择审核人</el-button>
</el-empty>
</div>
</div>
</el-form-item>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCancel">取消</el-button>
<el-button
type="primary"
@click="handleConfirm"
:loading="submitting"
:disabled="selectedAuditors.length === 0"
>
<el-icon><Check /></el-icon>
确认提交 ({{ selectedAuditors.length }} 个审核人)
</el-button>
</div>
</template>
</el-dialog>
<!-- 审核人选择器 -->
<AuditorSelector
ref="auditorSelectorRef"
@confirm="handleAuditorsSelected"
/>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { Check, User, UserFilled, Document, Plus, ArrowRight, Delete } from '@element-plus/icons-vue';
import AuditorSelector from './index.vue';
interface Props {
visible: boolean;
currentAuditors?: any[]; // 当前已选择的审核人
nextNodeName?: string; // 下一步审批节点名称
}
interface Emits {
(e: 'update:visible', value: boolean): void;
(e: 'confirm', approvalData: any): void;
}
const props = withDefaults(defineProps<Props>(), {
currentAuditors: () => [],
nextNodeName: '二级学院审批'
});
const emit = defineEmits<Emits>();
const visible = ref(props.visible);
const submitting = ref(false);
const selectedAuditors = ref<any[]>([]);
const currentAuditors = ref<any[]>([]);
const auditorSelectorRef = ref<InstanceType<typeof AuditorSelector>>();
// 监听visible变化
watch(() => props.visible, (newVal) => {
visible.value = newVal;
if (newVal) {
// 初始化当前审核人和选择的审核人
currentAuditors.value = [...(props.currentAuditors || [])];
selectedAuditors.value = [...(props.currentAuditors || [])];
}
});
// 监听内部visible变化
watch(visible, (newVal) => {
emit('update:visible', newVal);
});
// 打开审核人选择器
const openAuditorSelector = () => {
auditorSelectorRef.value?.open(selectedAuditors.value);
};
// 审核人选择确认
const handleAuditorsSelected = (auditors: any[]) => {
selectedAuditors.value = [...auditors];
};
// 移除审核人
const removeAuditor = (auditor: any) => {
const index = selectedAuditors.value.findIndex(item => item.userId === auditor.userId);
if (index > -1) {
selectedAuditors.value.splice(index, 1);
}
};
// 取消操作
const handleCancel = () => {
visible.value = false;
ElMessage.info('已取消提交');
};
// 确认提交
const handleConfirm = async () => {
try {
if (selectedAuditors.value.length === 0) {
ElMessage.warning('请至少选择一个审核人');
return;
}
submitting.value = true;
// 构建审批流程数据
const approvalData = {
auditorIds: selectedAuditors.value.map(auditor => auditor.userId),
processName: '年审审批流程',
auditors: selectedAuditors.value
};
console.log('确认审批流程,数据:', approvalData);
// 触发确认事件
emit('confirm', approvalData);
visible.value = false;
ElMessage.success('审批流程配置成功');
} catch (error) {
console.error('确认审批流程失败:', error);
ElMessage.error('确认审批流程失败');
} finally {
submitting.value = false;
}
};
// 暴露方法供父组件调用
defineExpose({
open: (preSelectedAuditors?: any[]) => {
visible.value = true;
currentAuditors.value = [...(preSelectedAuditors || [])];
selectedAuditors.value = [...(preSelectedAuditors || [])];
}
});
</script>
<style scoped>
.approval-dialog-content {
max-height: 500px;
}
.dialog-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.next-node-info {
margin-bottom: 20px;
padding: 12px 16px;
background-color: #f0f9ff;
border: 1px solid #d1ecf1;
border-radius: 6px;
}
.node-label {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
color: #0c63e4;
font-size: 14px;
}
.current-auditors {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #e4e7ed;
border-radius: 6px;
background-color: #fafafa;
}
.section-title {
display: flex;
align-items: center;
margin-bottom: 10px;
font-weight: 500;
color: #303133;
gap: 8px;
}
.auditor-tags {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.auditor-selection-section {
margin-bottom: 20px;
}
.custom-process-config {
padding: 24px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background-color: #fafbfc;
}
.auditor-selector {
width: 100%;
}
.selector-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.selected-auditors {
margin-top: 20px;
}
.auditor-flow {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16px;
margin-bottom: 16px;
}
.auditor-node {
display: flex;
align-items: center;
gap: 12px;
}
.node-content {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: white;
border: 1px solid #e4e7ed;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.node-index {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: #409eff;
color: white;
border-radius: 50%;
font-size: 12px;
font-weight: 600;
}
.auditor-info {
flex: 1;
}
.auditor-name {
font-size: 14px;
font-weight: 600;
color: #2c3e50;
margin-bottom: 2px;
}
.auditor-dept {
font-size: 12px;
color: #909399;
}
.flow-arrow {
display: flex;
align-items: center;
color: #409eff;
font-size: 16px;
}
.flow-description {
margin-top: 16px;
}
.empty-state {
text-align: center;
padding: 40px 20px;
}
</style>
@@ -0,0 +1,396 @@
<template>
<div class="auditor-selector">
<el-dialog
v-model="visible"
title="选择审核人"
width="900px"
append-to-body
:before-close="handleClose"
>
<div class="auditor-select-content">
<!-- 搜索区域 -->
<div class="search-section" style="margin-bottom: 20px !important;">
<el-input v-model="searchKeyword" placeholder="搜索审核人姓名或部门" clearable @input="handleSearch" style="width: 300px">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
<!-- 审核人表格 -->
<el-table
ref="auditorTableRef"
:data="tableData"
@selection-change="handleSelectionChange"
max-height="400"
empty-text="暂无可用审核人"
style="margin-top: 20px !important;"
>
<el-table-column type="selection" width="55" :selectable="isSelectable" header-align="center" align="center" />
<el-table-column label="序号" type="index" width="60" header-align="center" align="center" />
<el-table-column label="姓名" prop="nickName" width="130" header-align="center" align="center" />
<el-table-column label="部门" prop="deptName" header-align="center" align="center" />
<el-table-column label="联系电话" prop="phoneNumber" width="130" header-align="center" />
<el-table-column label="邮箱" prop="email" show-overflow-tooltip header-align="center" />
<el-table-column label="状态" prop="status" width="80" header-align="center" align="center">
<template #default="scope">
<el-tag :type="scope.row.status === '1' ? 'success' : 'danger'" size="small">
{{ scope.row.status === '1' ? '正常' : '停用' }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<div
class="dialog-footer"
style="display: flex !important; justify-content: space-between !important; align-items: center !important; width: 100% !important; margin: 0 !important;"
>
<span class="selected-count-text" style="color: #909399; font-size: 12px;"> 已选择 {{ selectedUsers.length }} 个审核人 </span>
<div class="footer-buttons" style="display: flex !important; gap: 12px !important;">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleConfirm" :disabled="selectedUsers.length === 0"> 确定添加 </el-button>
</div>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted, nextTick, computed, getCurrentInstance } from 'vue';
import type { ComponentInternalInstance } from 'vue';
import { ElMessage, ElMessageBox, ElTable } from 'element-plus';
import { Search } from '@element-plus/icons-vue';
import { listStglAuditor } from '@/api/stgl/auditor';
import { listDept } from '@/api/system/dept';
import { StglAuditorVO, StglAuditorQuery } from '@/api/stgl/auditor/types';
import { DeptVO } from '@/api/system/dept/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_normal_disable } = proxy?.useDict('sys_normal_disable') || {};
const auditorTableRef = ref();
const emit = defineEmits<{
confirm: [users: StglAuditorVO[]];
}>();
// 响应式数据
const visible = ref(false);
const loading = ref(false);
const userList = ref<StglAuditorVO[]>([]);
const filteredUserList = ref<StglAuditorVO[]>([]);
const selectedUsers = ref<StglAuditorVO[]>([]);
const total = ref(0);
const deptOptions = ref<any[]>([]);
const searchKeyword = ref('');
// 计算表格数据
const tableData = computed(() => {
// 确保返回的数组总是有效的
if (searchKeyword.value && filteredUserList.value) {
return filteredUserList.value;
}
return userList.value || [];
});
// 查询参数
const queryParams = ref<StglAuditorQuery>({
pageNum: 1,
pageSize: 500, // 增加页面大小以显示所有审核人
nickName: '',
userName: '',
deptId: undefined,
status: '1' // 只查询正常状态的审核人
});
// 初始查询参数的备份,用于重置
const defaultQuery: StglAuditorQuery = {
pageNum: 1,
pageSize: 500, // 增加页面大小以显示所有审核人
nickName: '',
userName: '',
deptId: undefined,
status: '1'
};
/** 打开选择器 */
const open = (preSelected: StglAuditorVO[] = []) => {
visible.value = true;
selectedUsers.value = [...preSelected];
// 重置搜索状态
searchKeyword.value = '';
filteredUserList.value = [];
getDeptTree();
getList();
};
/** 获取部门树 */
const getDeptTree = async () => {
try {
const response = await listDept();
deptOptions.value = handleTree(response.data, 'id', 'parentId');
} catch (error) {
console.error('获取部门树失败:', error);
}
};
/** 获取审核人列表 */
const getList = async () => {
loading.value = true;
try {
const response = await listStglAuditor(queryParams.value);
userList.value = response.rows;
filteredUserList.value = response.rows;
total.value = response.total;
// 如果有搜索关键词,执行搜索
if (searchKeyword.value) {
handleSearch();
}
// 设置表格选中状态
nextTick(() => {
setTableSelection();
});
} catch (error) {
console.error('获取审核人列表失败:', error);
userList.value = [];
filteredUserList.value = [];
total.value = 0;
} finally {
loading.value = false;
}
};
/** 设置表格选中状态 */
const setTableSelection = () => {
if (!auditorTableRef.value) return;
tableData.value.forEach(user => {
const isSelected = selectedUsers.value.some(selected =>
selected.userId === user.userId
);
auditorTableRef.value.toggleRowSelection(user, isSelected);
});
};
/** 判断行是否可选 */
const isSelectable = (row: StglAuditorVO) => {
return row.status === '1'; // 只有正常状态的审核人可选
};
/** 表格选择变化 */
const handleSelectionChange = (selection: StglAuditorVO[]) => {
// 处理当前显示数据的选择
const currentPageUserIds = tableData.value.map(user => user.userId);
// 移除当前页原有的选择
selectedUsers.value = selectedUsers.value.filter(user =>
!currentPageUserIds.includes(user.userId)
);
// 添加当前页新的选择
selectedUsers.value.push(...selection);
};
/** 移除选中审核人 */
const removeUser = (user: StglAuditorVO) => {
selectedUsers.value = selectedUsers.value.filter(selected =>
selected.userId !== user.userId
);
// 如果当前页包含该用户,需要取消表格选中
if (auditorTableRef.value) {
const currentUser = tableData.value.find(u => u.userId === user.userId);
if (currentUser) {
auditorTableRef.value.toggleRowSelection(currentUser, false);
}
}
};
/** 清空所有选择 */
const clearSelected = () => {
selectedUsers.value = [];
if (auditorTableRef.value) {
auditorTableRef.value.clearSelection();
}
};
/** 搜索处理 */
const handleSearch = () => {
if (!searchKeyword.value) {
filteredUserList.value = userList.value;
return;
}
const keyword = searchKeyword.value.toLowerCase();
filteredUserList.value = userList.value.filter(
(user) =>
(user.nickName && user.nickName.toLowerCase().includes(keyword)) ||
(user.userName && user.userName.toLowerCase().includes(keyword)) ||
(user.deptName && user.deptName.toLowerCase().includes(keyword))
);
};
/** 搜索 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置搜索 */
const resetQuery = () => {
Object.assign(queryParams.value, defaultQuery);
handleQuery();
};
/** 关闭弹窗 */
const handleClose = () => {
visible.value = false;
selectedUsers.value = [];
userList.value = [];
filteredUserList.value = [];
searchKeyword.value = '';
Object.assign(queryParams.value, defaultQuery);
};
/** 确认选择 */
const handleConfirm = () => {
emit('confirm', selectedUsers.value);
visible.value = false;
};
/** 处理树形数据 */
const handleTree = (data: any[], id: string, parentId: string, children = 'children') => {
const config = { id, parentId, children };
const childrenListMap: any = {};
const nodeIds: any = {};
const tree = [];
for (const d of data) {
const parentIdValue = d[config.parentId];
if (childrenListMap[parentIdValue] == null) {
childrenListMap[parentIdValue] = [];
}
nodeIds[d[config.id]] = d;
childrenListMap[parentIdValue].push(d);
}
for (const d of data) {
const parentIdValue = d[config.parentId];
if (nodeIds[parentIdValue] == null) {
tree.push(d);
}
}
for (const t of tree) {
adaptToChildrenArray(t);
}
function adaptToChildrenArray(o: any) {
if (childrenListMap[o[config.id]] !== null) {
o[config.children] = childrenListMap[o[config.id]];
}
if (o[config.children]) {
for (const c of o[config.children]) {
adaptToChildrenArray(c);
}
}
}
return tree.map(item => ({
id: item.id,
label: item.deptName,
children: item.children ? item.children.map((child: any) => convertToTreeNode(child)) : []
}));
};
const convertToTreeNode = (item: any): any => ({
id: item.id,
label: item.deptName,
children: item.children ? item.children.map((child: any) => convertToTreeNode(child)) : []
});
defineExpose({
open
});
</script>
<style scoped>
.auditor-selector {
.auditor-select-content {
max-height: 500px;
}
.search-section {
margin-top: 20px !important;
margin-bottom: 20px !important;
}
:deep(.el-table) {
margin-top: 20px !important;
}
.dialog-footer {
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
width: 100%;
}
.footer-buttons {
display: flex;
gap: 12px;
}
}
/* Element Plus Dialog Footer 样式覆盖 - 使用最高优先级的选择器 */
.auditor-selector :deep(.el-dialog__footer) {
padding: 15px 20px !important;
text-align: left !important;
display: block !important;
}
.auditor-selector :deep(.el-dialog__footer .dialog-footer) {
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
width: 100% !important;
margin: 0 !important;
padding: 0 !important;
box-sizing: border-box !important;
}
/* 确保按钮容器样式正确 */
.auditor-selector .dialog-footer .footer-buttons {
display: flex !important;
gap: 12px !important;
margin-left: auto !important;
flex-shrink: 0 !important;
}
/* 确保文本不会撑开容器 */
.auditor-selector .dialog-footer .selected-count-text {
flex-shrink: 0 !important;
white-space: nowrap !important;
}
/* 确保搜索框和表格间距的全局样式 */
.auditor-selector .auditor-select-content .search-section {
margin-bottom: 20px !important;
}
.auditor-selector .auditor-select-content .el-table {
margin-top: 20px !important;
}
</style>
@@ -124,8 +124,27 @@
<!-- 指导老师意见 -->
<div class="opinion-section">
<div class="opinion-title">指导老师意见</div>
<div class="opinion-content">
{{ annualReview.advisorOpinion || '暂无意见' }}
<div v-if="advisorAudits.length > 0" class="audit-records">
<div v-for="audit in advisorAudits" :key="audit.id" class="audit-record">
<div class="audit-header">
<span class="approver-name">{{ audit.approverName }}</span>
<span class="audit-status" :class="getAuditStatusClass(audit.nodeStatus)">
{{ getAuditStatusText(audit.nodeStatus) }}
</span>
<span v-if="audit.approveTime" class="audit-time">
{{ parseTime(audit.approveTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
</span>
</div>
<div v-if="audit.approveOpinion" class="audit-opinion">
{{ audit.approveOpinion }}
</div>
<div v-else class="audit-opinion pending">
待审批
</div>
</div>
</div>
<div v-else class="opinion-content">
暂无审批记录
</div>
<div class="opinion-signature">签名(公章)</div>
</div>
@@ -133,8 +152,27 @@
<!-- 二级学院意见 -->
<div class="opinion-section">
<div class="opinion-title">二级学院意见</div>
<div class="opinion-content">
{{ annualReview.collegeOpinion || '暂无意见' }}
<div v-if="collegeAudits.length > 0" class="audit-records">
<div v-for="audit in collegeAudits" :key="audit.id" class="audit-record">
<div class="audit-header">
<span class="approver-name">{{ audit.approverName }}</span>
<span class="audit-status" :class="getAuditStatusClass(audit.nodeStatus)">
{{ getAuditStatusText(audit.nodeStatus) }}
</span>
<span v-if="audit.approveTime" class="audit-time">
{{ parseTime(audit.approveTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
</span>
</div>
<div v-if="audit.approveOpinion" class="audit-opinion">
{{ audit.approveOpinion }}
</div>
<div v-else class="audit-opinion pending">
待审批
</div>
</div>
</div>
<div v-else class="opinion-content">
暂无审批记录
</div>
<div class="opinion-signature">签名(公章)</div>
</div>
@@ -142,8 +180,27 @@
<!-- 校团委意见 -->
<div class="opinion-section">
<div class="opinion-title">校团委意见</div>
<div class="opinion-content">
{{ annualReview.youthLeagueOpinion || '暂无意见' }}
<div v-if="youthLeagueAudits.length > 0" class="audit-records">
<div v-for="audit in youthLeagueAudits" :key="audit.id" class="audit-record">
<div class="audit-header">
<span class="approver-name">{{ audit.approverName }}</span>
<span class="audit-status" :class="getAuditStatusClass(audit.nodeStatus)">
{{ getAuditStatusText(audit.nodeStatus) }}
</span>
<span v-if="audit.approveTime" class="audit-time">
{{ parseTime(audit.approveTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
</span>
</div>
<div v-if="audit.approveOpinion" class="audit-opinion">
{{ audit.approveOpinion }}
</div>
<div v-else class="audit-opinion pending">
待审批
</div>
</div>
</div>
<div v-else class="opinion-content">
暂无审批记录
</div>
<div class="opinion-signature">签名(公章)</div>
</div>
@@ -158,7 +215,9 @@
<script setup lang="ts">
import { getStglClubAnnualReview, getAnnualReviewActivities } from '@/api/stgl/club/annualReview';
import { getAnnualReviewAuditsByReviewId } from '@/api/stgl/club/annualReviewAudit';
import { StglClubAnnualReviewVO } from '@/api/stgl/club/annualReview/type';
import { StglAnnualReviewAuditVO } from '@/api/stgl/club/annualReviewAudit/type';
// 扩展的年审VO接口(包含JSON字段)
interface ExtendedAnnualReviewVO extends StglClubAnnualReviewVO {
@@ -180,6 +239,8 @@ const annualReview = ref<ExtendedAnnualReviewVO>();
const memberList = ref<any[]>([]);
// 活动列表
const activityList = ref<any[]>([]);
// 审批记录列表
const auditList = ref<StglAnnualReviewAuditVO[]>([]);
// 年份范围计算
const yearRange = computed(() => {
@@ -208,6 +269,41 @@ const memberCountText = computed(() => {
return `${annualReview.value?.memberCount || 0}人`;
});
// 指导老师审批记录
const advisorAudits = computed(() => {
return auditList.value.filter(audit => audit.nodeCode === 'advisor_approve');
});
// 二级学院审批记录
const collegeAudits = computed(() => {
return auditList.value.filter(audit => audit.nodeCode === 'college_approve');
});
// 校团委审批记录
const youthLeagueAudits = computed(() => {
return auditList.value.filter(audit => audit.nodeCode === 'youth_league_approve');
});
/** 获取审批状态样式类 */
const getAuditStatusClass = (status: string) => {
const statusClasses: { [key: string]: string } = {
'pending': 'status-pending',
'waiting': 'status-waiting',
'approved': 'status-approved'
};
return statusClasses[status] || '';
};
/** 获取审批状态文本 */
const getAuditStatusText = (status: string) => {
const statusTexts: { [key: string]: string } = {
'pending': '待审批',
'waiting': '等待中',
'approved': '已审批'
};
return statusTexts[status] || status;
};
/** 格式化活动时间显示 */
const formatActivityTime = (startTime: string, endTime?: string): string => {
if (!startTime) return '';
@@ -302,6 +398,19 @@ const getDetail = async () => {
memberList.value = [];
}
// 获取审批记录
try {
const auditRes = await getAnnualReviewAuditsByReviewId(props.annualReviewId);
if (auditRes && auditRes.data && Array.isArray(auditRes.data)) {
auditList.value = auditRes.data;
} else {
auditList.value = [];
}
} catch (error) {
console.warn('获取审批记录失败:', error);
auditList.value = [];
}
// 从stgl_club_annual_review_activity表中获取年审记录对应的活动数据
try {
const activitiesRes = await getAnnualReviewActivities(props.annualReviewId);
@@ -524,4 +633,79 @@ watch(
line-height: 1.4;
font-size: 13px;
}
/* 审批记录样式 */
.audit-records {
margin-bottom: 10px;
}
.audit-record {
margin-bottom: 15px;
padding: 12px;
border: 1px solid #e4e7ed;
border-radius: 4px;
background: #fafafa;
}
.audit-record:last-child {
margin-bottom: 0;
}
.audit-header {
display: flex;
align-items: center;
margin-bottom: 8px;
font-size: 14px;
}
.approver-name {
font-weight: bold;
color: #303133;
margin-right: 15px;
}
.audit-status {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
margin-right: 15px;
}
.status-pending {
background: #fdf6ec;
color: #e6a23c;
border: 1px solid #f5dab1;
}
.status-waiting {
background: #f4f4f5;
color: #909399;
border: 1px solid #d3d4d6;
}
.status-approved {
background: #f0f9ff;
color: #67c23a;
border: 1px solid #b3d8ff;
}
.audit-time {
color: #909399;
font-size: 12px;
margin-left: auto;
}
.audit-opinion {
line-height: 1.6;
color: #606266;
background: #fff;
padding: 8px;
border-radius: 4px;
border: 1px solid #e4e7ed;
}
.audit-opinion.pending {
color: #909399;
font-style: italic;
}
</style>
@@ -459,9 +459,24 @@ const loadClubInfo = async (clubId: string | number) => {
try {
const advisors = JSON.parse(clubData.advisorsJson);
if (Array.isArray(advisors) && advisors.length > 0) {
const firstAdvisor = advisors[0];
advisorName = firstAdvisor.name || firstAdvisor.teacher?.nickName || '';
advisorDept = firstAdvisor.dept || firstAdvisor.teacher?.deptName || '';
// 处理多个指导老师的情况
const advisorNames = [];
const advisorDepts = [];
advisors.forEach(advisor => {
const name = advisor.name || advisor.teacher?.nickName || '';
const dept = advisor.dept || advisor.teacher?.deptName || '';
if (name) {
advisorNames.push(name);
}
if (dept && !advisorDepts.includes(dept)) {
advisorDepts.push(dept);
}
});
advisorName = advisorNames.join('、');
advisorDept = advisorDepts.join('、');
}
} catch (error) {
console.warn('解析指导老师JSON数据失败:', error);
@@ -57,7 +57,7 @@
</el-row>
</template>
<el-table v-loading="loading" border :data="annualReviewList" @selection-change="handleSelectionChange" @row-dblclick="handleView">
<el-table v-loading="loading" border :data="annualReviewList" :key="tableKey" @selection-change="handleSelectionChange" @row-dblclick="handleView">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="社团名称" align="center" prop="clubName" min-width="150" />
<el-table-column label="成立日期" align="center" prop="establishDate" width="120">
@@ -80,7 +80,7 @@
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="100">
<el-table-column label="状态" align="center" prop="status" width="120">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.status) as any">{{ getStatusText(scope.row.status) }}</el-tag>
</template>
@@ -104,7 +104,10 @@
<el-tooltip content="撤回" placement="top" v-if="scope.row.status === '1'">
<el-button link type="warning" icon="RefreshLeft" @click="handleWithdraw(scope.row)" v-hasPermi="['stgl:clubAnnualReview:withdraw']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top" v-if="scope.row.status === '0'">
<el-tooltip content="审批" placement="top" v-if="(scope.row.status === '1' || scope.row.status === '2') && canApprove(scope.row)">
<el-button link type="success" icon="Check" @click="handleApprove(scope.row)" v-hasPermi="['stgl:annualReviewAudit:approve']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top" v-if="scope.row.status === '0' || scope.row.status === '4'">
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['stgl:clubAnnualReview:remove']"></el-button>
</el-tooltip>
</template>
@@ -129,10 +132,15 @@
<el-dialog title="年审记录详情" v-model="detailDialog.visible" width="1000px" append-to-body>
<annual-review-detail :annual-review-id="detailDialog.annualReviewId" />
</el-dialog>
<!-- 审批对话框 -->
<approve-dialog ref="approveDialogRef" @success="handleApproveSuccess" />
</div>
</template>
<script setup name="StglClubAnnualReview" lang="ts">
import { ref, reactive, toRefs, nextTick, onMounted, getCurrentInstance } from 'vue';
import type { ComponentInternalInstance } from 'vue';
import { useRoute } from 'vue-router';
import {
listStglClubAnnualReview,
@@ -143,11 +151,16 @@ import {
exportStglClubAnnualReview
} from '@/api/stgl/club/annualReview';
import { StglClubAnnualReviewVO, StglClubAnnualReviewQuery, ANNUAL_REVIEW_STATUS_OPTIONS } from '@/api/stgl/club/annualReview/type';
import { getPendingAuditsByApproverId } from '@/api/stgl/club/annualReviewAudit';
import { StglAnnualReviewAuditVO } from '@/api/stgl/club/annualReviewAudit/type';
import { useUserStore } from '@/store/modules/user';
import AnnualReviewForm from './form.vue';
import AnnualReviewDetail from './detail.vue';
import ApproveDialog from './approveDialog.vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const route = useRoute();
const userStore = useUserStore();
const { stgl_club_type } = toRefs(proxy?.useDict('stgl_club_type') as any);
const annualReviewList = ref<StglClubAnnualReviewVO[]>([]);
@@ -157,9 +170,16 @@ const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const tableKey = ref(0); // 用于强制重新渲染表格
// 审批权限相关状态
const pendingAudits = ref<StglAnnualReviewAuditVO[]>([]);
const pendingAuditsLoading = ref(false);
const pendingAuditsCache = ref<Map<number, boolean>>(new Map());
const queryFormRef = ref<ElFormInstance>();
const annualReviewFormRef = ref<InstanceType<typeof AnnualReviewForm>>();
const approveDialogRef = ref<InstanceType<typeof ApproveDialog>>();
// 状态选项
const statusOptions = ref(ANNUAL_REVIEW_STATUS_OPTIONS);
@@ -214,10 +234,36 @@ const { queryParams, dialog, detailDialog } = toRefs(data);
/** 查询社团年审记录列表 */
const getList = async () => {
loading.value = true;
const res = await listStglClubAnnualReview(queryParams.value);
annualReviewList.value = res.rows;
total.value = res.total;
try {
// 并行加载年审记录和待审批记录
const [annualReviewRes] = await Promise.all([
listStglClubAnnualReview(queryParams.value),
loadPendingAudits()
]);
// 确保数据更新
if (annualReviewRes && annualReviewRes.rows) {
annualReviewList.value = annualReviewRes.rows;
total.value = annualReviewRes.total;
} else {
console.warn('获取的年审记录数据格式异常:', annualReviewRes);
annualReviewList.value = [];
total.value = 0;
}
// 强制触发 Vue 的响应式更新
nextTick(() => {
console.log('年审记录列表已更新,记录数:', annualReviewList.value.length);
});
} catch (error) {
console.error('加载数据失败:', error);
proxy?.$modal.msgError('加载数据失败');
// 重置数据
annualReviewList.value = [];
total.value = 0;
} finally {
loading.value = false;
}
};
/** 搜索按钮操作 */
@@ -279,20 +325,51 @@ const handleView = (row: StglClubAnnualReviewVO) => {
const handleSubmit = async (row: StglClubAnnualReviewVO) => {
try {
await proxy?.$modal.confirm(`是否确认提交年审记录"${row.clubName}"?`);
// 调用提交接口
await submitStglClubAnnualReview(row.id);
await getList();
// API调用成功后,立即更新本地状态,避免被getList覆盖
await updateRowStatus(row.id, '1');
proxy?.$modal.msgSuccess('提交成功');
} catch {}
// 延迟重新获取数据,确保状态已经在界面上显示
setTimeout(async () => {
await getList();
}, 100);
} catch (error) {
console.error('提交失败:', error);
// 提交失败时才需要恢复状态,因为我们现在是先调用API再更新状态
}
};
/** 撤回按钮操作 */
const handleWithdraw = async (row: StglClubAnnualReviewVO) => {
try {
await proxy?.$modal.confirm(`是否确认撤回年审记录"${row.clubName}"?`);
// 调用撤回接口
await withdrawStglClubAnnualReview(row.id);
await getList();
// API调用成功后,立即更新本地状态,避免被getList覆盖
await updateRowStatus(row.id, '0');
proxy?.$modal.msgSuccess('撤回成功');
} catch {}
// 延迟重新获取数据,确保状态已经在界面上显示
setTimeout(async () => {
await Promise.all([
getList(),
loadPendingAudits()
]);
}, 100);
} catch (error) {
console.error('撤回失败:', error);
// 撤回失败时才需要恢复状态,因为我们现在是先调用API再更新状态
}
};
/** 删除按钮操作 */
@@ -322,6 +399,30 @@ const handleFormSuccess = () => {
getList();
};
/** 更新行状态的工具函数 */
const updateRowStatus = async (rowId: string | number, newStatus: string) => {
const rowIndex = annualReviewList.value.findIndex(item => item.id === rowId);
if (rowIndex !== -1) {
// 创建新对象确保Vue能检测到变化
const updatedRow = {
...annualReviewList.value[rowIndex],
status: newStatus
};
// 使用splice替换整个对象,触发响应式更新
annualReviewList.value.splice(rowIndex, 1, updatedRow);
// 强制触发DOM更新和表格重新渲染
tableKey.value++;
await nextTick();
console.log(`状态已同步更新: ${updatedRow.status}`);
console.log('当前数组状态:', annualReviewList.value.map(item => ({ id: item.id, status: item.status, clubName: item.clubName })));
return updatedRow;
}
return null;
};
/** 获取状态标签类型 */
const getStatusTagType = (status: string) => {
const tagTypes: { [key: string]: string } = {
@@ -336,8 +437,114 @@ const getStatusTagType = (status: string) => {
/** 获取状态文本 */
const getStatusText = (status: string) => {
// 本地状态映射,确保显示正确
const statusMap: { [key: string]: string } = {
'0': '草稿',
'1': '已提交',
'2': '审核中',
'3': '审核通过',
'4': '审核不通过'
};
// 优先使用本地映射
if (statusMap[status]) {
const result = statusMap[status];
console.log(`getStatusText: 状态=${status}, 返回文本="${result}"`);
return result;
}
// fallback到原来的逻辑
const option = statusOptions.value.find(item => item.value === status);
return option?.label || status;
const result = option?.label || status;
console.log(`getStatusText: 状态=${status}, fallback返回="${result}"`);
return result;
};
/** 获取当前用户的待审批记录 */
const loadPendingAudits = async () => {
if (!userStore.userId || pendingAuditsLoading.value) {
return;
}
try {
pendingAuditsLoading.value = true;
const response = await getPendingAuditsByApproverId(userStore.userId);
// 处理响应数据,确保 data 是数组
// 后端返回的是 R<List<StglAnnualReviewAudit>>,R.data 可能为 null 或 []
let data = [];
if (response && response.data !== undefined) {
// 如果response.data存在但为null,使用空数组
data = response.data || [];
} else if (response && response.rows !== undefined) {
// 有些API返回格式可能是分页格式
data = response.rows || [];
}
pendingAudits.value = data;
// 更新缓存
pendingAuditsCache.value.clear();
if (Array.isArray(data)) {
data.forEach((audit: StglAnnualReviewAuditVO) => {
if (audit.annualReviewId) {
pendingAuditsCache.value.set(Number(audit.annualReviewId), true);
}
});
}
} catch (error) {
console.error('获取待审批记录失败:', error);
// 重置数据
pendingAudits.value = [];
pendingAuditsCache.value.clear();
// 不显示错误提示,避免影响页面正常使用
} finally {
pendingAuditsLoading.value = false;
}
};
/** 判断当前用户是否可以审批该年审记录 */
const canApprove = (row: StglClubAnnualReviewVO) => {
// 检查当前用户是否登录
if (!userStore.userId) {
console.log('canApprove: 用户未登录');
return false;
}
// 检查年审记录状态,支持"已提交"和"审核中"状态
if (row.status !== '1' && row.status !== '2') {
console.log(`canApprove: 状态不符合 ${row.status}`);
return false;
}
// 检查缓存中是否有该年审记录的审批权限
const hasPermission = pendingAuditsCache.value.has(Number(row.id));
console.log(`canApprove: 年审ID=${row.id}, 当前用户=${userStore.userId}, 缓存权限=${hasPermission}, 缓存内容=`, Array.from(pendingAuditsCache.value.keys()));
return hasPermission;
};
/** 审批按钮操作 */
const handleApprove = async (row: StglClubAnnualReviewVO) => {
// 从待审批记录中找到对应的审批ID
const audit = pendingAudits.value.find(audit =>
Number(audit.annualReviewId) === Number(row.id)
);
if (!audit) {
proxy?.$modal.msgError('未找到对应的审批记录');
return;
}
approveDialogRef.value?.open(audit.id, row.id);
};
/** 审批成功回调 */
const handleApproveSuccess = async () => {
// 审批完成后,需要同时刷新年审记录和待审批记录
await Promise.all([
getList(),
loadPendingAudits()
]);
};
onMounted(() => {
+3 -3
View File
@@ -1278,9 +1278,9 @@ watch(
() => form.value.advisors,
(advisors: AdvisorVO[]) => {
// 防止递归更新
if (syncingAdvisors.value) return;
if (syncLock.value) return;
syncingAdvisors.value = true;
syncLock.value = true;
nextTick(() => {
try {
// 为了兼容旧版本,同步第一个指导老师的信息到旧字段
@@ -1297,7 +1297,7 @@ watch(
form.value.advisorEmail = undefined;
}
} finally {
syncingAdvisors.value = false;
syncLock.value = false;
}
});
},
@@ -33,21 +33,21 @@
<el-col :span="6">
<el-form-item label="院系" prop="collegeName">
<el-select v-model="queryParams.collegeName" placeholder="请选择院系" clearable style="width: 100%" @change="handleCollegeChange">
<el-option v-for="college in collegeOptions" :key="college.name" :label="college.name" :value="college.name" />
<el-option v-for="college in collegeOptions" :key="college.value" :label="college.label" :value="college.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="专业" prop="majorName">
<el-select v-model="queryParams.majorName" placeholder="请选择专业" clearable style="width: 100%" @change="handleMajorChange">
<el-option v-for="major in majorOptions" :key="major.name" :label="major.name" :value="major.name" />
<el-option v-for="major in majorOptions" :key="major.value" :label="major.label" :value="major.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="班级" prop="className">
<el-select v-model="queryParams.className" placeholder="请选择班级" clearable style="width: 100%">
<el-option v-for="classItem in classOptions" :key="classItem.name" :label="classItem.name" :value="classItem.name" />
<el-option v-for="classItem in classOptions" :key="classItem.value" :label="classItem.label" :value="classItem.value" />
</el-select>
</el-form-item>
</el-col>
@@ -106,10 +106,28 @@
<el-tag v-else-if="scope.row.status === 1" type="success">已到访</el-tag>
<el-tag v-else-if="scope.row.status === 2" type="danger">已取消</el-tag>
<el-tag v-else-if="scope.row.status === 3" type="warning">未到访</el-tag>
<el-tag v-else-if="scope.row.status === 4" type="info">待审核</el-tag>
<el-tag v-else-if="scope.row.status === 5" type="danger">已驳回</el-tag>
<el-tag v-else type="info">未知状态</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="是否需要转介" align="center" prop="needReferral" width="120">
<template #default="scope">
<div v-if="scope.row.status === 1">
<el-tag v-if="scope.row.needReferral === 2" type="warning">是</el-tag>
<el-tag v-else-if="scope.row.needReferral === 1" type="success">否</el-tag>
<span v-else>-</span>
</div>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="本学期到访次数" align="center" prop="semesterVisitCount" width="120">
<template #default="scope">
<el-tag v-if="scope.row.semesterVisitCount > 0" type="info">{{ scope.row.semesterVisitCount }}次</el-tag>
<span v-else>0次</span>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="150">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
@@ -129,6 +147,31 @@
></el-button>
</el-tooltip>
<!-- 待审核状态可审核通过或拒绝 -->
<el-tooltip content="审核通过" placement="top" v-if="scope.row.status === 4">
<el-button
link
type="success"
icon="Check"
size="default"
style="font-size: 16px; margin-right: 8px"
@click="handleApprove(scope.row)"
v-hasPermi="['xljk:consultationappointment:edit']"
></el-button>
</el-tooltip>
<el-tooltip content="审核拒绝" placement="top" v-if="scope.row.status === 4">
<el-button
link
type="danger"
icon="Close"
size="default"
style="font-size: 16px; margin-right: 8px"
@click="handleReject(scope.row)"
v-hasPermi="['xljk:consultationappointment:edit']"
></el-button>
</el-tooltip>
<!-- 待咨询状态可记录咨询结果 -->
<el-tooltip content="记录结果" placement="top" v-if="scope.row.status === 0">
<el-button
@@ -161,29 +204,45 @@
<el-descriptions-item label="咨询师">{{ form.counselorName }}</el-descriptions-item>
<el-descriptions-item label="预约日期">{{ parseTime(form.appointmentDate, '{y}-{m}-{d}') }}</el-descriptions-item>
<el-descriptions-item label="预约时段">{{ form.appointmentDetailTime }}</el-descriptions-item>
<el-descriptions-item label="咨询原因" :span="2">{{ form.reason }}</el-descriptions-item>
<el-descriptions-item label="咨询主题">{{ form.consultTheme || '未选择' }}</el-descriptions-item>
<el-descriptions-item label="咨询原因">{{ form.reason }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag v-if="form.status === 0" type="primary">待咨询</el-tag>
<el-tag v-else-if="form.status === 1" type="success">已到访</el-tag>
<el-tag v-else-if="form.status === 2" type="danger">已取消</el-tag>
<el-tag v-else-if="form.status === 3" type="warning">未到访</el-tag>
<el-tag v-else-if="form.status === 4" type="info">待审核</el-tag>
<el-tag v-else-if="form.status === 5" type="danger">已驳回</el-tag>
<el-tag v-else type="info">未知状态({{ form.status }})</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ parseTime(form.createTime) }}</el-descriptions-item>
<!-- 只有在已到访状态时才显示咨询结果相关字段 -->
<template v-if="form.status == 1">
<el-descriptions-item label="实际到访时间">
{{ form.actualVisitTime ? parseTime(form.actualVisitTime, '{y}-{m}-{d} {h}:{i}:{s}') : '未记录' }}
</el-descriptions-item>
<el-descriptions-item label="沟通时长">
{{ form.communicationDuration ? form.communicationDuration + ' 分钟' : '未记录' }}
</el-descriptions-item>
<el-descriptions-item label="咨询内容" :span="2">
<el-descriptions-item label="心理病史">
<el-tag v-if="form.psychologicalHistory === 2" type="warning">是</el-tag>
<el-tag v-else-if="form.psychologicalHistory === 1" type="success">否</el-tag>
<span v-else>未记录</span>
</el-descriptions-item>
<el-descriptions-item label="心理病史详情" :span="2" v-if="form.psychologicalHistory === 2">
{{ form.psychologicalHistoryDetail || '未记录' }}
</el-descriptions-item>
<el-descriptions-item label="基本情况" :span="2">
{{ form.basicSituation || '未记录' }}
</el-descriptions-item>
<el-descriptions-item label="重大生活事件" :span="2">
{{ form.majorLifeEvents || '未记录' }}
</el-descriptions-item>
<el-descriptions-item label="本次咨询过程" :span="2">
{{ form.consultationContent || '未记录' }}
</el-descriptions-item>
<el-descriptions-item label="咨询师建议" :span="2">
{{ form.counselorSuggestion || '未记录' }}
<el-descriptions-item label="是否需要转介">
<el-tag v-if="form.needReferral === 2" type="warning">是</el-tag>
<el-tag v-else-if="form.needReferral === 1" type="success">否</el-tag>
<span v-else>未记录</span>
</el-descriptions-item>
</template>
</el-descriptions>
@@ -196,8 +255,8 @@
</el-dialog>
<!-- 记录咨询结果对话框 -->
<el-dialog title="记录咨询结果" v-model="recordDialogVisible" width="600px" append-to-body>
<el-form ref="recordFormRef" :model="recordForm" :rules="recordRules" label-width="100px">
<el-dialog title="记录咨询结果" v-model="recordDialogVisible" width="800px" append-to-body>
<el-form ref="recordFormRef" :model="recordForm" :rules="recordRules" label-width="120px">
<el-form-item label="学生姓名">
<el-input v-model="recordForm.studentName" disabled />
</el-form-item>
@@ -215,27 +274,35 @@
</el-form-item>
<!-- 只有在已到访状态时才显示以下字段 -->
<template v-if="recordForm.status === 1">
<el-form-item label="实际到访时间" prop="actualVisitTime" class="no-wrap-label">
<el-date-picker
v-model="recordForm.actualVisitTime"
type="datetime"
placeholder="选择日期时间"
style="width: 100%"
value-format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item label="沟通时长" prop="communicationDuration">
<div class="flex-row align-center">
<el-input-number v-model="recordForm.communicationDuration" :min="1" :max="180" />
<span class="ml-2">分钟</span>
</div>
</el-form-item>
<el-form-item label="咨询内容" prop="consultationContent">
<el-input v-model="recordForm.consultationContent" type="textarea" placeholder="请输入咨询内容" :rows="4" />
<el-form-item label="心理病史" prop="psychologicalHistory">
<el-radio-group v-model="recordForm.psychologicalHistory" @change="handlePsychologicalHistoryChange">
<el-radio :value="2">是</el-radio>
<el-radio :value="1">否</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="咨询师建议" prop="counselorSuggestion">
<el-input v-model="recordForm.counselorSuggestion" type="textarea" placeholder="请输入咨询师建议" :rows="4" />
<el-form-item label="心理病史详情" prop="psychologicalHistoryDetail" v-if="recordForm.psychologicalHistory === 2">
<el-input v-model="recordForm.psychologicalHistoryDetail" type="textarea" placeholder="请详细描述心理病史" :rows="3" />
</el-form-item>
<el-form-item label="基本情况" prop="basicSituation">
<el-input v-model="recordForm.basicSituation" type="textarea" placeholder="请输入基本情况" :rows="4" />
</el-form-item>
<el-form-item label="重大生活事件" prop="majorLifeEvents">
<el-input v-model="recordForm.majorLifeEvents" type="textarea" placeholder="请输入重大生活事件(可选)" :rows="3" />
</el-form-item>
<el-form-item label="本次咨询过程" prop="consultationContent">
<el-input v-model="recordForm.consultationContent" type="textarea" placeholder="请输入本次咨询过程" :rows="4" />
</el-form-item>
<el-form-item label="是否需要转介" prop="needReferral">
<el-radio-group v-model="recordForm.needReferral" @change="handleReferralChange">
<el-radio :value="2">是</el-radio>
<el-radio :value="1">否</el-radio>
</el-radio-group>
</el-form-item>
</template>
</el-form>
@@ -268,7 +335,8 @@ import {
CounselorOption,
CollegeOption,
MajorOption,
ClassOption
ClassOption,
XljkConsultationAppointmentQuery
} from '@/api/xljk/consultation/types';
import { ElMessage, ElMessageBox } from 'element-plus';
import { parseTime } from '@/utils/ruoyi';
@@ -281,7 +349,9 @@ const appointment_status = ref([
{ label: '待咨询', value: 0 },
{ label: '已到访', value: 1 },
{ label: '已取消', value: 2 },
{ label: '未到访', value: 3 }
{ label: '未到访', value: 3 },
{ label: '待审核', value: 4 },
{ label: '已驳回', value: 5 }
]);
// 遮罩层
@@ -304,19 +374,17 @@ const open = ref(false);
const dateRange = ref<string[]>([]);
// 咨询师选项
const counselorOptions = ref<CounselorOption[]>([]);
// 院系选项
const collegeOptions = ref<CollegeOption[]>([]);
// 专业选项
const majorOptions = ref<MajorOption[]>([]);
// 班级选项
const classOptions = ref<ClassOption[]>([]);
// 修复院系、专业、班级选项的类型定义
const collegeOptions = ref<Array<{ value: string; label: string }>>([]);
const majorOptions = ref<Array<{ value: string; label: string }>>([]);
const classOptions = ref<Array<{ value: string; label: string }>>([]);
// 是否显示记录对话框
const recordDialogVisible = ref(false);
// 提交按钮加载状态
const submitLoading = ref(false);
// 查询参数
const queryParams = reactive({
// 查询参数的类型定义和字段修复
const queryParams = reactive<XljkConsultationAppointmentQuery>({
pageNum: 1,
pageSize: 10,
studentName: undefined,
@@ -339,20 +407,28 @@ const recordForm = reactive<XljkConsultationAppointmentForm>({
scheduleId: undefined,
appointmentDate: '',
appointmentTime: '',
appointmentDetailTime: '',
reason: '',
status: 1, // 默认已到访,使用数值类型
actualVisitTime: undefined,
communicationDuration: 30, // 默认30分钟
consultationContent: '',
counselorSuggestion: ''
counselorSuggestion: '',
studentName: '',
psychologicalHistory: 1, // 默认否
psychologicalHistoryDetail: '',
basicSituation: '',
majorLifeEvents: '',
needReferral: 1 // 默认不需要转介
});
// 记录表单校验
const recordRules = ref({
// 修复表单验证规则的类型定义
const recordRules = ref<any>({
status: [{ required: true, message: '请选择咨询结果', trigger: 'change' }],
actualVisitTime: [{ required: true, message: '请选择实际到访时间', trigger: 'change' }],
communicationDuration: [{ required: true, message: '请输入沟通时长', trigger: 'blur' }],
consultationContent: [{ required: true, message: '请输入咨询内容', trigger: 'blur' }]
psychologicalHistory: [{ required: true, message: '请选择心理病史', trigger: 'change' }],
basicSituation: [{ required: true, message: '请输入基本情况', trigger: 'blur' }],
consultationContent: [{ required: true, message: '请输入本次咨询过程', trigger: 'blur' }],
needReferral: [{ required: true, message: '请选择是否需要转介', trigger: 'change' }]
});
// 监听状态变化,动态调整表单验证规则
@@ -368,9 +444,11 @@ watch(
// 已到访
recordRules.value = {
status: [{ required: true, message: '请选择咨询结果', trigger: 'change' }],
actualVisitTime: [{ required: true, message: '请选择实际到访时间', trigger: 'change' }],
communicationDuration: [{ required: true, message: '请输入沟通时长', trigger: 'blur' }],
consultationContent: [{ required: true, message: '请输入咨询内容', trigger: 'blur' }]
psychologicalHistory: [{ required: true, message: '请选择心理病史', trigger: 'change' }],
basicSituation: [{ required: true, message: '请输入基本情况', trigger: 'blur' }],
consultationContent: [{ required: true, message: '请输入本次咨询过程', trigger: 'blur' }],
needReferral: [{ required: true, message: '请选择是否需要转介', trigger: 'change' }]
};
}
}
@@ -384,7 +462,9 @@ const getList = async () => {
loading.value = true;
try {
// 处理日期范围
if (!queryParams.params) {
queryParams.params = {};
}
if (dateRange.value && dateRange.value.length === 2) {
queryParams.params.beginAppointmentDate = dateRange.value[0];
queryParams.params.endAppointmentDate = dateRange.value[1];
@@ -503,24 +583,13 @@ const handleView = async (row: XljkConsultationAppointmentVO) => {
console.log('转换后的status:', form.status);
}
// 检查实际到访时间
console.log('实际到访时间:', form.actualVisitTime);
if (form.status === 1 && !form.actualVisitTime) {
console.warn('已到访状态但实际到访时间为空,尝试使用行数据');
if (row.actualVisitTime) {
form.actualVisitTime = row.actualVisitTime;
console.log('使用行数据的实际到访时间:', form.actualVisitTime);
} else {
console.warn('行数据也没有实际到访时间');
}
}
console.log('复制后的表单数据:', form);
// 如果是已到访状态,确保咨询结果相关字段有值
if (form.status === 1) {
console.log('已到访状态,咨询结果相关字段:', {
actualVisitTime: form.actualVisitTime,
communicationDuration: form.communicationDuration,
consultationContent: form.consultationContent,
counselorSuggestion: form.counselorSuggestion
@@ -558,6 +627,58 @@ const handleView = async (row: XljkConsultationAppointmentVO) => {
}
};
/** 审核通过操作 */
const handleApprove = async (row: XljkConsultationAppointmentVO) => {
try {
await ElMessageBox.confirm('确认审核通过该预约申请?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
// 构建更新数据
const updateData = {
id: row.id,
status: "0" // 审核通过,状态改为待咨询
};
await updateXljkAppointment(updateData);
ElMessage.success('审核通过成功');
getList();
} catch (error) {
if (error !== 'cancel') {
console.error('审核通过失败', error);
ElMessage.error('审核通过失败');
}
}
};
/** 审核拒绝操作 */
const handleReject = async (row: XljkConsultationAppointmentVO) => {
try {
await ElMessageBox.confirm('确认拒绝该预约申请?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
// 构建更新数据
const updateData = {
id: row.id,
status: "5" // 拒绝,状态改为已驳回
};
await updateXljkAppointment(updateData);
ElMessage.success('审核拒绝成功');
getList();
} catch (error) {
if (error !== 'cancel') {
console.error('审核拒绝失败', error);
ElMessage.error('审核拒绝失败');
}
}
};
/** 记录咨询结果操作 */
const handleRecord = async (row: XljkConsultationAppointmentVO) => {
try {
@@ -575,10 +696,15 @@ const handleRecord = async (row: XljkConsultationAppointmentVO) => {
appointmentDetailTime: '',
reason: '',
status: 1, // 默认已完成
actualVisitTime: new Date(),
communicationDuration: 30,
consultationContent: '',
counselorSuggestion: ''
counselorSuggestion: '',
studentName: '',
psychologicalHistory: 1, // 默认否(1=否,2=是)
psychologicalHistoryDetail: '',
basicSituation: '',
majorLifeEvents: '',
needReferral: 1 // 默认不需要转介(1=不需要,2=需要)
};
Object.assign(recordForm, defaultValues);
@@ -594,6 +720,16 @@ const handleRecord = async (row: XljkConsultationAppointmentVO) => {
recordForm.appointmentDetailTime = res.data.appointmentDetailTime;
recordForm.reason = res.data.reason;
recordForm.studentName = res.data.studentName;
// 如果API返回了已有的咨询记录数据,保持原有值
if (res.data.needReferral !== undefined && res.data.needReferral !== null) {
recordForm.needReferral = res.data.needReferral;
console.log('从API获取的needReferral值:', res.data.needReferral, res.data.needReferral === 2 ? '是(需要转介)' : res.data.needReferral === 1 ? '否(不需要转介)' : '未知状态');
}
if (res.data.psychologicalHistory !== undefined && res.data.psychologicalHistory !== null) {
recordForm.psychologicalHistory = res.data.psychologicalHistory;
console.log('从API获取的psychologicalHistory值:', res.data.psychologicalHistory, res.data.psychologicalHistory === 2 ? '是(有病史)' : res.data.psychologicalHistory === 1 ? '否(无病史)' : '未知状态');
}
} else {
console.warn('API响应格式不正确或无数据:', res);
// 使用行数据填充
@@ -608,6 +744,7 @@ const handleRecord = async (row: XljkConsultationAppointmentVO) => {
}
console.log('填充后的记录表单:', recordForm);
console.log('最终needReferral值:', recordForm.needReferral, recordForm.needReferral === 2 ? '是(需要转介)' : recordForm.needReferral === 1 ? '否(不需要转介)' : '未知状态');
recordDialogVisible.value = true;
} catch (error) {
console.error('获取预约详情失败', error);
@@ -625,6 +762,8 @@ const submitRecordForm = async () => {
try {
submitLoading.value = true;
console.log('提交前的recordForm数据:', recordForm);
// 构建提交数据
const submitData: any = {
id: recordForm.id,
@@ -633,39 +772,59 @@ const submitRecordForm = async () => {
// 如果是已到访状态,添加其他字段
if (recordForm.status === 1) {
// 确保日期格式正确
if (recordForm.actualVisitTime) {
// 如果是Date对象,转换为ISO字符串格式
if (recordForm.actualVisitTime instanceof Date) {
const year = recordForm.actualVisitTime.getFullYear();
const month = String(recordForm.actualVisitTime.getMonth() + 1).padStart(2, '0');
const day = String(recordForm.actualVisitTime.getDate()).padStart(2, '0');
const hours = String(recordForm.actualVisitTime.getHours()).padStart(2, '0');
const minutes = String(recordForm.actualVisitTime.getMinutes()).padStart(2, '0');
const seconds = String(recordForm.actualVisitTime.getSeconds()).padStart(2, '0');
submitData.actualVisitTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
} else {
submitData.actualVisitTime = recordForm.actualVisitTime;
}
console.log('提交的实际到访时间:', submitData.actualVisitTime);
}
submitData.communicationDuration = recordForm.communicationDuration;
submitData.consultationContent = recordForm.consultationContent;
submitData.counselorSuggestion = recordForm.counselorSuggestion;
submitData.counselorSuggestion = recordForm.counselorSuggestion || '';
submitData.psychologicalHistory = recordForm.psychologicalHistory;
submitData.psychologicalHistoryDetail = recordForm.psychologicalHistoryDetail || '';
submitData.basicSituation = recordForm.basicSituation;
submitData.majorLifeEvents = recordForm.majorLifeEvents || '';
submitData.needReferral = recordForm.needReferral;
console.log('已到访状态,提交的数据字段:');
console.log('- psychologicalHistoryDetail:', submitData.psychologicalHistoryDetail, '类型:', typeof submitData.psychologicalHistoryDetail, '长度:', submitData.psychologicalHistoryDetail?.length);
console.log('- basicSituation:', submitData.basicSituation, '类型:', typeof submitData.basicSituation, '长度:', submitData.basicSituation?.length);
console.log('- majorLifeEvents:', submitData.majorLifeEvents, '类型:', typeof submitData.majorLifeEvents, '长度:', submitData.majorLifeEvents?.length);
console.log('- needReferral:', submitData.needReferral, '类型:', typeof submitData.needReferral);
console.log('- psychologicalHistory:', submitData.psychologicalHistory, '类型:', typeof submitData.psychologicalHistory);
} else {
// 未到访状态,设置默认值
submitData.actualVisitTime = null;
submitData.communicationDuration = 0;
submitData.consultationContent = '';
submitData.counselorSuggestion = '';
submitData.psychologicalHistory = null;
submitData.psychologicalHistoryDetail = '';
submitData.basicSituation = '';
submitData.majorLifeEvents = '';
submitData.needReferral = null;
}
await recordXljkConsultation(recordForm.id as number, submitData);
console.log('最终提交的数据:', submitData);
const response = await recordXljkConsultation(recordForm.id as number, submitData);
console.log('API响应:', response);
ElMessage.success('记录成功');
recordDialogVisible.value = false;
getList();
// 重新查询列表,并验证数据是否保存成功
await getList();
// 验证数据保存情况
try {
const verifyRes = await getXljkAppointment(recordForm.id as number);
console.log('验证保存结果,获取的数据:', verifyRes.data);
if (verifyRes.code === 200 && verifyRes.data) {
console.log('验证字段保存情况:');
console.log('- psychologicalHistoryDetail 保存值:', verifyRes.data.psychologicalHistoryDetail);
console.log('- basicSituation 保存值:', verifyRes.data.basicSituation);
console.log('- majorLifeEvents 保存值:', verifyRes.data.majorLifeEvents);
console.log('- needReferral 保存值:', verifyRes.data.needReferral);
console.log('- psychologicalHistory 保存值:', verifyRes.data.psychologicalHistory);
}
} catch (error) {
console.error('验证保存结果失败:', error);
}
} catch (error) {
console.error('提交记录失败', error);
ElMessage.error('提交记录失败');
@@ -710,8 +869,8 @@ const handleDelete = async (row?: XljkConsultationAppointmentVO) => {
/** 导出按钮操作 */
const handleExport = async () => {
// 构建查询参数,处理日期范围和params对象
const exportParams = { ...queryParams };
// 构建查询参数,处理日期范围
const exportParams: any = { ...queryParams };
delete exportParams.params; // 移除params对象
// 将日期范围添加到参数中
@@ -771,7 +930,7 @@ const handleExport = async () => {
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
} catch (error: any) {
console.error('导出失败:', error);
ElMessage.error(`导出失败: ${error.message}`);
}
@@ -788,7 +947,7 @@ const handleCollegeChange = async (collegeName: string) => {
if (collegeName) {
// 获取该院系下的专业
try {
const res = await getMajorOptions(collegeName);
const res = await getXljkConsultationMajorOptions(collegeName);
majorOptions.value = res.data || [];
} catch (error) {
console.error('获取专业选项失败', error);
@@ -808,7 +967,7 @@ const handleMajorChange = async (majorName: string) => {
if (majorName && queryParams.collegeName) {
// 获取专业下的班级
try {
const res = await getClassOptions(queryParams.collegeName, majorName);
const res = await getXljkConsultationClassOptions(queryParams.collegeName, majorName);
classOptions.value = res.data || [];
} catch (error) {
console.error('获取班级选项失败', error);
@@ -816,7 +975,7 @@ const handleMajorChange = async (majorName: string) => {
} else if (queryParams.collegeName) {
// 如果清空专业但有院系,则获取院系下所有班级
try {
const res = await getClassOptions(queryParams.collegeName);
const res = await getXljkConsultationClassOptions(queryParams.collegeName);
classOptions.value = res.data || [];
} catch (error) {
console.error('获取班级选项失败', error);
@@ -827,6 +986,18 @@ const handleMajorChange = async (majorName: string) => {
}
};
/** 转介选择变化时的调试日志 */
const handleReferralChange = (value: number) => {
console.log('转介字段值变化:', value, value === 2 ? '是(需要转介)' : value === 1 ? '否(不需要转介)' : '未知状态');
console.log('当前recordForm.needReferral:', recordForm.needReferral);
};
/** 心理病史选择变化时的调试日志 */
const handlePsychologicalHistoryChange = (value: number) => {
console.log('心理病史字段值变化:', value, value === 2 ? '是(有心理病史)' : value === 1 ? '否(无心理病史)' : '未知状态');
console.log('当前recordForm.psychologicalHistory:', recordForm.psychologicalHistory);
};
// 初始化
onMounted(() => {
console.log('初始化appointment_status字典数据:', appointment_status.value);
@@ -33,23 +33,41 @@
</div>
</div>
<!-- 表格内容:显示全天和时间段 -->
<!-- 表格内容:显示时间段 -->
<div class="week-body">
<div class="time-row">
<div class="time-label-cell">
<span>全天</span>
</div>
<div v-for="day in weekDays" :key="day.date" class="day-cell">
<!-- 这里可以放全天事件,目前为空 -->
</div>
</div>
<!-- 时间段行 -->
<div class="time-slots-container">
<!-- 上午时段 -->
<!-- 动态显示所有排班时段 -->
<template v-for="timeSlotType in getUniqueTimeSlots()" :key="timeSlotType">
<div class="time-row">
<div class="time-label-cell">
<span>上午</span>
<span>{{ getTimeSlotLabel(timeSlotType) }}</span>
</div>
<div v-for="day in weekDays" :key="day.date" class="day-cell">
<div
v-for="schedule in getSchedulesForDateAndTimeSlot(day.date, timeSlotType)"
:key="`${schedule.scheduleId}-${schedule.startTime}`"
class="time-slot-card"
:class="{
'morning': schedule.timeSlot === '0',
'noon': schedule.timeSlot === '1',
'afternoon': schedule.timeSlot === '2',
'fully-booked': schedule.isFullyBooked || schedule.isExpired
}"
@click="handleScheduleClickEvent(schedule)"
>
{{ schedule.startTime }}-{{ schedule.endTime }}
</div>
</div>
</div>
</template>
<!-- 兼容性:如果没有新排班数据,继续显示原有的中午/下午时段 -->
<template v-if="getUniqueTimeSlots().length === 0">
<!-- 中午时段 -->
<div class="time-row">
<div class="time-label-cell">
<span>中午</span>
</div>
<div v-for="day in weekDays" :key="day.date" class="day-cell">
<div
@@ -83,6 +101,7 @@
</div>
</div>
</div>
</template>
</div>
</div>
</div>
@@ -111,14 +130,32 @@
>
<div class="month-day-number">{{ day.dayNumber }}</div>
<!-- 上午时段 -->
<!-- 显示该日期的所有排班 -->
<div
v-for="schedule in getSchedulesForDate(day.date)"
:key="`${schedule.scheduleId}-${schedule.startTime}`"
class="month-slot"
:class="{
'morning': schedule.timeSlot === '0',
'noon': schedule.timeSlot === '1',
'afternoon': schedule.timeSlot === '2',
'fully-booked': schedule.isFullyBooked || schedule.isExpired
}"
@click.stop="handleScheduleClickEvent(schedule)"
>
{{ schedule.timeDisplay }}
</div>
<!-- 兼容性:如果没有新排班数据,继续显示原有的中午/下午时段 -->
<template v-if="getSchedulesForDate(day.date).length === 0">
<!-- 中午时段 -->
<div
v-if="getMorningSlot(day.date)"
class="month-slot morning"
:class="{ 'fully-booked': getMorningSlot(day.date).allBooked || getMorningSlot(day.date).isExpired }"
@click.stop="handleSlotClick(getMorningSlot(day.date))"
>
上午 {{ getMorningSlot(day.date).startTime }}-{{ getMorningSlot(day.date).endTime }}
中午 {{ getMorningSlot(day.date).startTime }}-{{ getMorningSlot(day.date).endTime }}
</div>
<!-- 下午时段 -->
@@ -130,6 +167,7 @@
>
下午 {{ getAfternoonSlot(day.date).startTime }}-{{ getAfternoonSlot(day.date).endTime }}
</div>
</template>
</div>
</div>
</div>
@@ -142,7 +180,7 @@ import { ref, computed, onMounted, watch } from 'vue';
import { parseTime } from '@/utils/ruoyi';
import { ElMessage } from 'element-plus';
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue';
import { AvailableTimeSlot } from '@/api/xljk/consultation/types';
import { AvailableTimeSlot, ScheduleWithStatus } from '@/api/xljk/consultation/types';
// 定义组件的Props
const props = defineProps({
@@ -150,6 +188,10 @@ const props = defineProps({
type: Array as () => AvailableTimeSlot[],
default: () => []
},
calendarSchedules: {
type: Array as () => ScheduleWithStatus[],
default: () => []
},
defaultView: {
type: String,
default: 'week',
@@ -158,7 +200,7 @@ const props = defineProps({
});
// 定义组件的事件
const emit = defineEmits(['dateRangeChange', 'slotClick', 'viewChange']);
const emit = defineEmits(['dateRangeChange', 'slotClick', 'scheduleClick', 'viewChange']);
// 当前日期和周期的开始日期
const today = ref(new Date());
@@ -382,7 +424,7 @@ const monthDays = computed(() => {
// 如果是最后一行,添加额外日志
if (week === 5) {
console.log(`日期 ${formattedDate} (${date.toLocaleDateString()}) 数据状态:`, {
上午时段: morningSlot ? '有数据' : '无数据',
中午时段: morningSlot ? '有数据' : '无数据',
下午时段: afternoonSlot ? '有数据' : '无数据',
isOtherMonth
});
@@ -412,7 +454,7 @@ function formatDate(date: Date) {
return parseTime(date, '{y}-{m}-{d}');
}
// 获取指定日期的上午时段
// 获取指定日期的中午时段
function getMorningSlot(dateStr: string) {
return props.availableSlots.find((slot) => slot.date === dateStr && slot.timeSlot === '0');
}
@@ -422,6 +464,76 @@ function getAfternoonSlot(dateStr: string) {
return props.availableSlots.find((slot) => slot.date === dateStr && slot.timeSlot === '1');
}
// 获取指定日期的所有排班
function getSchedulesForDate(dateStr: string) {
return props.calendarSchedules.filter(schedule => schedule.date === dateStr)
.sort((a, b) => {
// 先按时段类型排序
const timeSlotCompare = a.timeSlot.localeCompare(b.timeSlot);
if (timeSlotCompare !== 0) return timeSlotCompare;
// 再按开始时间排序
return a.startTime.localeCompare(b.startTime);
});
}
// 处理排班点击
function handleScheduleClickEvent(schedule: ScheduleWithStatus) {
if (!schedule) return;
// 如果该时段已过期,则不允许预约
if (schedule.isExpired) {
ElMessage.warning('该时段已过期,请选择其他时段');
return;
}
// 如果该时段全部约满,则不允许预约
if (schedule.isFullyBooked) {
ElMessage.warning('该时段已全部约满,请选择其他时段');
return;
}
emit('scheduleClick', schedule);
}
// 获取所有唯一的时段类型
function getUniqueTimeSlots() {
const timeSlots = new Set<string>();
props.calendarSchedules.forEach(schedule => {
timeSlots.add(schedule.timeSlot);
});
return Array.from(timeSlots).sort();
}
// 获取时段类型标签
function getTimeSlotLabel(timeSlot: string) {
switch (timeSlot) {
case '0': return '中午';
case '1': return '下午';
case '2': return '下午';
default: return '时段' + timeSlot;
}
}
// 获取指定日期和时段类型的排班
function getSchedulesForDateAndTimeSlot(dateStr: string, timeSlot: string) {
const result = props.calendarSchedules
.filter(schedule => schedule.date === dateStr && schedule.timeSlot === timeSlot)
.sort((a, b) => a.startTime.localeCompare(b.startTime));
// 特别调试9月18日time_slot=0的情况
if (dateStr === '2025-09-18' && timeSlot === '0') {
console.log('🔍 WeekView调试 - getSchedulesForDateAndTimeSlot:', {
dateStr,
timeSlot,
totalSchedules: props.calendarSchedules.length,
filteredResult: result,
resultCount: result.length
});
}
return result;
}
// 监听视图变化
watch(
() => props.defaultView,
@@ -551,12 +663,18 @@ watch(
background-color: #67c23a;
}
.time-slot-card.noon {
background-color: #e6a23c;
}
.time-slot-card.fully-booked {
background-color: #c0c0c0;
cursor: not-allowed;
opacity: 0.7;
}
/* 月视图样式 */
.month-grid {
flex: 1;
@@ -645,9 +763,15 @@ watch(
background-color: #67c23a;
}
.month-slot.noon {
background-color: #e6a23c;
}
.month-slot.fully-booked {
background-color: #c0c0c0;
cursor: not-allowed;
opacity: 0.7;
}
</style>
@@ -19,9 +19,11 @@
<!-- 使用WeekView组件,支持月视图和周视图切换 -->
<WeekView
:available-slots="availableSlots"
:calendar-schedules="calendarSchedules"
:default-view="calendarView"
@date-range-change="handleDateRangeChange"
@slot-click="handleSlotClick"
@schedule-click="handleScheduleClick"
@view-change="handleViewChange"
/>
</div>
@@ -35,27 +37,31 @@
<el-form-item label="咨询师" prop="counselorId">
<el-input v-model="counselorDisplayName" disabled />
</el-form-item>
<el-form-item label="办公室" prop="office">
<el-input v-model="selectedOffice" disabled />
</el-form-item>
<el-form-item label="预约日期" prop="appointmentDate">
<el-input v-model="formattedDate" disabled />
</el-form-item>
<el-form-item label="姓名" prop="studentName">
<el-input v-model="studentName" disabled />
</el-form-item>
<el-form-item label="预约时段" prop="appointmentDetailTime">
<el-select v-model="appointmentForm.appointmentDetailTime" placeholder="请选择具体预约时段" style="width: 100%">
<el-input v-model="appointmentForm.appointmentDetailTime" disabled />
</el-form-item>
<el-form-item label="咨询主题" prop="consultTheme">
<el-select v-model="appointmentForm.consultTheme" placeholder="请选择咨询主题*" style="width: 100%">
<el-option
v-for="slot in availableDetailSlots"
:key="slot.timeSlot"
:label="slot.timeSlot"
:value="slot.timeSlot"
:disabled="slot.isBooked || slot.isExpired"
:style="getTimeSlotStyle(slot)"
v-for="theme in xljk_consult_theme"
:key="theme.value"
:label="theme.label"
:value="theme.value"
/>
</el-select>
</el-form-item>
<el-form-item label="学号" prop="studentNo" v-if="!hasStudentInfo">
<el-input v-model="studentNo" placeholder="请输入学号" @blur="handleStudentNoBlur" />
</el-form-item>
<el-form-item label="姓名" prop="studentName">
<el-input v-model="studentName" disabled />
</el-form-item>
<el-form-item label="咨询原因" prop="reason">
<el-input v-model="appointmentForm.reason" type="textarea" placeholder="请简要描述咨询原因" :rows="4" />
</el-form-item>
@@ -69,13 +75,14 @@
</el-dialog>
<!-- 我的预约列表弹窗 -->
<el-dialog v-model="myAppointmentsVisible" title="我的预约记录" width="800px" append-to-body destroy-on-close>
<el-table :data="myAppointmentsList" border style="width: 100%">
<el-table-column label="预约日期" prop="appointmentDate" width="120" />
<el-table-column label="预约时段" prop="appointmentDetailTime" width="120" />
<el-table-column label="咨询师" prop="counselorName" width="100" />
<el-table-column label="咨询原因" prop="reason" show-overflow-tooltip />
<el-table-column label="状态" prop="status" width="100">
<el-dialog v-model="myAppointmentsVisible" title="我的预约记录" width="1200px" append-to-body destroy-on-close>
<el-table :data="myAppointmentsList" border style="width: 100%" header-align="center">
<el-table-column label="预约日期" prop="appointmentDate" width="120" align="center" />
<el-table-column label="预约时段" prop="appointmentDetailTime" width="140" align="center" />
<el-table-column label="咨询师" prop="counselorName" width="100" align="center" />
<el-table-column label="办公室" prop="office" width="200" show-overflow-tooltip header-align="center" />
<el-table-column label="咨询原因" prop="reason" min-width="200" show-overflow-tooltip header-align="center" />
<el-table-column label="状态" prop="status" width="100" align="center">
<template #default="scope">
<!-- 添加调试信息 -->
<div style="display: none">状态值: {{ scope.row.status }} 类型: {{ typeof scope.row.status }} 字典数据: {{ appointment_status }}</div>
@@ -85,6 +92,8 @@
<el-tag v-else-if="scope.row.status === 1 || scope.row.status === '1'" type="success">已到访</el-tag>
<el-tag v-else-if="scope.row.status === 2 || scope.row.status === '2'" type="danger">已取消</el-tag>
<el-tag v-else-if="scope.row.status === 3 || scope.row.status === '3'" type="warning">未到访</el-tag>
<el-tag v-else-if="scope.row.status === 4 || scope.row.status === '4'" type="info">待审核</el-tag>
<el-tag v-else-if="scope.row.status === 5 || scope.row.status === '5'" type="danger">已驳回</el-tag>
<el-tag v-else type="info">未知({{ scope.row.status }})</el-tag>
</template>
</el-table-column>
@@ -96,7 +105,7 @@
</div>
<!-- 使用更直接的方式显示取消按钮 -->
<template v-if="scope.row.status === 0 || scope.row.status === '0'">
<template v-if="scope.row.status === 0 || scope.row.status === '0' || scope.row.status === 4 || scope.row.status === '4'">
<el-button link type="danger" size="small" @click="cancelAppointment(scope.row)"> 取消预约 </el-button>
</template>
<span v-else>--</span>
@@ -117,6 +126,7 @@
import { ref, reactive, onMounted, computed, watch } from 'vue';
import {
getXljkAvailableTimeSlots,
getXljkCalendarSchedules,
submitXljkAppointment,
getXljkMyAppointments,
updateXljkAppointment,
@@ -124,7 +134,7 @@ import {
getXljkCounselorOptions,
getXljkAvailableDetailSlots
} from '@/api/xljk/consultation';
import { AvailableTimeSlot, XljkConsultationAppointmentForm, CounselorOption, TimeSlotDetail } from '@/api/xljk/consultation/types';
import { AvailableTimeSlot, XljkConsultationAppointmentForm, CounselorOption, TimeSlotDetail, ScheduleWithStatus } from '@/api/xljk/consultation/types';
import { ElMessage, ElMessageBox } from 'element-plus';
import { parseTime } from '@/utils/ruoyi';
import { useUserStore } from '@/store/modules/user';
@@ -134,12 +144,17 @@ const { proxy } = getCurrentInstance() as any;
// 不再使用后端字典数据
// const { appointment_status } = toRefs(proxy?.useDict('appointment_status'));
// 获取咨询主题字典数据
const { xljk_consult_theme } = toRefs<any>(proxy?.useDict('xljk_consult_theme'));
// 本地定义状态字典(与预约记录管理页面保持一致)
const appointment_status = ref([
{ label: '待咨询', value: 0, listClass: 'primary' },
{ label: '已到访', value: 1, listClass: 'success' },
{ label: '已取消', value: 2, listClass: 'danger' },
{ label: '未到访', value: 3, listClass: 'warning' }
{ label: '未到访', value: 3, listClass: 'warning' },
{ label: '待审核', value: 4, listClass: 'info' },
{ label: '已驳回', value: 5, listClass: 'danger' }
]);
// 用户信息
@@ -150,6 +165,7 @@ const hasStudentInfo = computed(() => Boolean(studentId.value));
// 日历相关
const availableSlots = ref<AvailableTimeSlot[]>([]);
const calendarSchedules = ref<ScheduleWithStatus[]>([]);
const calendarView = ref('month'); // 默认使用月视图
// 预约表单相关
@@ -161,6 +177,7 @@ const studentNo = ref('');
const studentName = ref('');
const studentId = ref<string | number | null>(null);
const availableDetailSlots = ref<TimeSlotDetail[]>([]);
const selectedOffice = ref(''); // 添加办公室字段
const appointmentForm = reactive<XljkConsultationAppointmentForm>({
counselorId: undefined,
@@ -168,6 +185,7 @@ const appointmentForm = reactive<XljkConsultationAppointmentForm>({
appointmentDate: '',
appointmentTime: '',
appointmentDetailTime: '',
consultTheme: '', // 咨询主题
reason: '',
status: 0 // 默认待咨询状态,使用数值类型
});
@@ -175,7 +193,7 @@ const appointmentForm = reactive<XljkConsultationAppointmentForm>({
const appointmentRules = {
reason: [{ required: true, message: '请输入咨询原因', trigger: 'blur' }],
studentNo: [{ required: true, message: '请输入学号', trigger: 'blur' }],
appointmentDetailTime: [{ required: true, message: '请选择具体预约时段', trigger: 'change' }]
consultTheme: [{ required: true, message: '请选择咨询主题', trigger: 'change' }]
};
// 我的预约相关
@@ -196,7 +214,13 @@ const formattedTimeSlot = computed(() => {
// 计算属性:咨询师名称
const counselorDisplayName = computed(() => {
return selectedSlot.value?.counselorName || '正在加载...';
if (selectedSlot.value?.counselorName) {
return selectedSlot.value.counselorName;
}
if (selectedSlot.value?.counselorId) {
return '正在加载...';
}
return '请选择咨询师';
});
// 处理日期范围变化
@@ -230,21 +254,45 @@ function handleViewChange(view: string) {
// 加载可用预约时段
async function loadAvailableSlots(startDate: string, endDate: string) {
try {
// 加载新的排班数据
const scheduleRes = await getXljkCalendarSchedules(startDate, endDate);
if (scheduleRes.code === 200) {
calendarSchedules.value = scheduleRes.data || [];
}
// 为了兼容现有逻辑,同时保留原有的时段数据加载
const res = await getXljkAvailableTimeSlots(startDate, endDate);
if (res.code === 200) {
availableSlots.value = res.data || [];
// 如果有可用时段但没有咨询师姓名,尝试加载咨询师信息
if (availableSlots.value.length > 0 && !availableSlots.value[0].counselorName) {
// 如果有可用时段,检查并补充咨询师姓名
if (availableSlots.value.length > 0) {
// 检查是否有时段缺少咨询师姓名或办公室信息
const hasMissingCounselorInfo = availableSlots.value.some(slot =>
(!slot.counselorName || !slot.office) && slot.counselorId
);
if (hasMissingCounselorInfo) {
try {
console.log('检测到缺少咨询师信息,开始获取咨询师详细信息...');
const counselorRes = await getXljkCounselorOptions();
if (counselorRes.code === 200 && counselorRes.data) {
const counselors = counselorRes.data;
// 为每个时段添加咨询师姓名
console.log('获取到咨询师列表:', counselors);
// 为每个时段添加咨询师姓名和办公室信息
availableSlots.value = availableSlots.value.map((slot) => {
if ((!slot.counselorName || !slot.office) && slot.counselorId) {
const counselor = counselors.find((c: CounselorOption) => c.id === slot.counselorId);
if (counselor) {
return { ...slot, counselorName: counselor.name };
const updatedSlot = {
...slot,
counselorName: slot.counselorName || counselor.name,
office: slot.office || counselor.office
};
console.log('更新时段信息:', updatedSlot);
return updatedSlot;
}
}
return slot;
});
@@ -254,25 +302,46 @@ async function loadAvailableSlots(startDate: string, endDate: string) {
}
}
}
}
} catch (error) {
console.error('获取可预约时段失败', error);
ElMessage.error('获取可预约时段失败');
}
}
// 处理时段点击
async function handleSlotClick(slot: AvailableTimeSlot) {
selectedSlot.value = slot;
// 处理新的排班点击
async function handleScheduleClick(schedule: ScheduleWithStatus) {
console.log('点击排班:', schedule);
// 如果没有咨询师姓名,尝试获取咨询师信息
if (!slot.counselorName && slot.counselorId) {
// 如果该时段已过期,则不允许预约
if (schedule.isExpired) {
ElMessage.warning('该时段已过期,请选择其他时段');
return;
}
// 如果该时段全部约满,则不允许预约
if (schedule.isFullyBooked) {
ElMessage.warning('该时段已全部约满,请选择其他时段');
return;
}
// 获取咨询师名称和办公室信息
let counselorName = schedule.counselorName || '';
let office = schedule.office || '';
// 如果缺少咨询师姓名或办公室信息,则去获取完整的咨询师信息
if ((!counselorName || !office) && schedule.counselorId) {
try {
console.log('获取咨询师详细信息,counselorId:', schedule.counselorId);
const res = await getXljkCounselorOptions();
if (res.code === 200 && res.data) {
const counselor = res.data.find((item: CounselorOption) => item.id === slot.counselorId);
const counselor = res.data.find((item: CounselorOption) => item.id === schedule.counselorId);
if (counselor) {
slot.counselorName = counselor.name;
selectedSlot.value = { ...slot };
counselorName = counselorName || counselor.name;
office = office || counselor.office || '';
console.log('获取到咨询师信息:', { name: counselor.name, office: counselor.office });
} else {
console.log('未找到对应的咨询师信息, counselorId:', schedule.counselorId);
}
}
} catch (error) {
@@ -280,12 +349,80 @@ async function handleSlotClick(slot: AvailableTimeSlot) {
}
}
console.log('最终的办公室信息:', office);
// 填充预约表单 - 直接使用排班数据,不需要再选择详细时段
appointmentForm.counselorId = schedule.counselorId;
appointmentForm.scheduleId = schedule.scheduleId;
appointmentForm.appointmentDate = schedule.date;
appointmentForm.appointmentTime = `${schedule.startTime}-${schedule.endTime}`;
appointmentForm.appointmentDetailTime = `${schedule.startTime}-${schedule.endTime}`; // 直接使用完整时段
appointmentForm.consultTheme = ''; // 清空咨询主题,等待用户选择
appointmentForm.reason = ''; // 清空咨询原因,避免带出上次的内容
// 设置选中的时段信息以便显示
selectedSlot.value = {
scheduleId: schedule.scheduleId,
counselorId: schedule.counselorId,
counselorName: counselorName,
office: office,
date: schedule.date,
timeSlot: schedule.timeSlot,
startTime: schedule.startTime,
endTime: schedule.endTime,
allBooked: schedule.isFullyBooked,
isExpired: schedule.isExpired
} as AvailableTimeSlot;
// 设置办公室信息
selectedOffice.value = office;
// 如果已登录且有用户ID,初始化学生信息
if (userId.value) {
// 初始设置为当前用户ID
studentId.value = userId.value;
studentName.value = nickname.value || '';
studentNo.value = nickname.value || '';
}
appointmentFormVisible.value = true;
}
// 处理时段点击
async function handleSlotClick(slot: AvailableTimeSlot) {
selectedSlot.value = slot;
// 如果没有咨询师姓名或办公室信息,尝试获取咨询师信息
if ((!slot.counselorName || !slot.office) && slot.counselorId) {
try {
console.log('获取咨询师详细信息,counselorId:', slot.counselorId);
const res = await getXljkCounselorOptions();
if (res.code === 200 && res.data) {
const counselor = res.data.find((item: CounselorOption) => item.id === slot.counselorId);
if (counselor) {
slot.counselorName = slot.counselorName || counselor.name;
slot.office = slot.office || counselor.office || '';
selectedSlot.value = { ...slot };
console.log('获取到咨询师信息:', { name: counselor.name, office: counselor.office });
} else {
console.log('未找到对应的咨询师信息, counselorId:', slot.counselorId);
}
}
} catch (error) {
console.error('获取咨询师信息失败', error);
}
}
// 设置办公室信息
selectedOffice.value = slot.office || '';
// 填充预约表单
appointmentForm.counselorId = slot.counselorId;
appointmentForm.scheduleId = slot.scheduleId;
appointmentForm.appointmentDate = slot.date;
appointmentForm.appointmentTime = `${slot.startTime}-${slot.endTime}`;
appointmentForm.appointmentDetailTime = ''; // 清空详细时段,等待用户选择
appointmentForm.consultTheme = ''; // 清空咨询主题,等待用户选择
appointmentForm.reason = ''; // 清空咨询原因,避免带出上次的内容
// 加载详细时段
@@ -387,7 +524,12 @@ function submitAppointmentForm() {
}
if (!appointmentForm.appointmentDetailTime) {
ElMessage.warning('请选择具体预约时段');
ElMessage.warning('预约时段信息缺失,请重新点击时段');
return;
}
if (!appointmentForm.consultTheme) {
ElMessage.warning('请选择咨询主题');
return;
}
@@ -452,13 +594,37 @@ async function viewMyAppointments() {
let res = await getXljkMyAppointments(id, true); // 强制刷新数据
console.log('查询结果:', res);
// 获取咨询师信息用于补充办公室字段
let counselors: CounselorOption[] = [];
try {
const counselorRes = await getXljkCounselorOptions();
if (counselorRes.code === 200 && counselorRes.data) {
counselors = counselorRes.data;
console.log('获取到咨询师列表:', counselors);
}
} catch (error) {
console.error('获取咨询师信息失败', error);
}
if (res.code === 200 && res.data && res.data.length > 0) {
// 处理数据,确保状态字段是数值类型
// 处理数据,确保状态字段是数值类型,并补充办公室信息
myAppointmentsList.value = res.data.map((item: any) => {
console.log('原始记录项:', item);
// 注意:移除了本地状态优先的逻辑,始终使用后端最新状态
// 补充办公室信息
if (item.counselorId && counselors.length > 0) {
const counselor = counselors.find((c: CounselorOption) => c.id === item.counselorId);
if (counselor) {
item.office = counselor.office || '';
item.counselorName = item.counselorName || counselor.name || '';
console.log('补充咨询师信息:', { counselorId: item.counselorId, name: counselor.name, office: counselor.office });
} else {
console.log('未找到对应的咨询师信息, counselorId:', item.counselorId);
}
}
// 如果status是undefined或null,设置默认值为0
if (item.status === undefined || item.status === null) {
item.status = 0; // 默认为待咨询
@@ -473,7 +639,7 @@ async function viewMyAppointments() {
}
}
console.log('处理后的预约记录项:', item.id, '状态:', item.status, '类型:', typeof item.status);
console.log('处理后的预约记录项:', item.id, '状态:', item.status, '办公室:', item.office, '类型:', typeof item.status);
return item;
});
@@ -491,12 +657,24 @@ async function viewMyAppointments() {
console.log('数字类型查询结果:', res);
if (res.code === 200) {
// 处理数据,确保状态字段是数值类型
// 处理数据,确保状态字段是数值类型,并补充办公室信息
myAppointmentsList.value = (res.data || []).map((item: any) => {
console.log('原始记录项:', item);
// 注意:移除了本地状态优先的逻辑,始终使用后端最新状态
// 补充办公室信息
if (item.counselorId && counselors.length > 0) {
const counselor = counselors.find((c: CounselorOption) => c.id === item.counselorId);
if (counselor) {
item.office = counselor.office || '';
item.counselorName = item.counselorName || counselor.name || '';
console.log('补充咨询师信息:', { counselorId: item.counselorId, name: counselor.name, office: counselor.office });
} else {
console.log('未找到对应的咨询师信息, counselorId:', item.counselorId);
}
}
// 如果status是undefined或null,设置默认值为0
if (item.status === undefined || item.status === null) {
item.status = 0; // 默认为待咨询
@@ -511,7 +689,7 @@ async function viewMyAppointments() {
}
}
console.log('处理后的预约记录项:', item.id, '状态:', item.status, '类型:', typeof item.status);
console.log('处理后的预约记录项:', item.id, '状态:', item.status, '办公室:', item.office, '类型:', typeof item.status);
return item;
});
@@ -594,7 +772,7 @@ async function initCounselorInfo() {
availableSlots.value = availableSlots.value.map((slot) => {
const counselor = counselors.find((c: CounselorOption) => c.id === slot.counselorId);
if (counselor) {
return { ...slot, counselorName: counselor.name };
return { ...slot, counselorName: counselor.name, office: counselor.office };
}
return slot;
});
@@ -616,9 +794,34 @@ async function refreshMyAppointments() {
try {
// 强制刷新数据
const res = await getXljkMyAppointments(studentId.value, true);
// 获取咨询师信息用于补充办公室字段
let counselors: CounselorOption[] = [];
try {
const counselorRes = await getXljkCounselorOptions();
if (counselorRes.code === 200 && counselorRes.data) {
counselors = counselorRes.data;
console.log('刷新时获取到咨询师列表:', counselors);
}
} catch (error) {
console.error('获取咨询师信息失败', error);
}
if (res.code === 200) {
// 处理数据,确保状态字段是数值类型
// 处理数据,确保状态字段是数值类型,并补充办公室信息
myAppointmentsList.value = (res.data || []).map((item: any) => {
// 补充办公室信息
if (item.counselorId && counselors.length > 0) {
const counselor = counselors.find((c: CounselorOption) => c.id === item.counselorId);
if (counselor) {
item.office = counselor.office || '';
item.counselorName = item.counselorName || counselor.name || '';
console.log('刷新时补充咨询师信息:', { counselorId: item.counselorId, name: counselor.name, office: counselor.office });
} else {
console.log('刷新时未找到对应的咨询师信息, counselorId:', item.counselorId);
}
}
// 如果status是undefined或null,设置默认值为0
if (item.status === undefined || item.status === null) {
item.status = 0; // 默认为待咨询
@@ -631,6 +834,7 @@ async function refreshMyAppointments() {
}
}
console.log('刷新后的预约记录项:', item.id, '状态:', item.status, '办公室:', item.office);
return item;
});
ElMessage.success('刷新成功');
+432 -103
View File
@@ -12,6 +12,11 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="办公室" prop="office">
<el-input v-model="queryParams.office" placeholder="请输入办公室" clearable style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="排班日期" prop="dateRange">
<el-date-picker
@@ -28,7 +33,7 @@
<el-col :span="6">
<el-form-item label="时段类型" prop="timeSlot">
<el-select v-model="queryParams.timeSlot" placeholder="请选择时段类型" clearable style="width: 100%">
<el-option label="上午" value="0" />
<el-option label="中午" value="0" />
<el-option label="下午" value="1" />
</el-select>
</el-form-item>
@@ -75,8 +80,9 @@
<el-table v-loading="loading" border :data="scheduleList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="咨询师" align="center" prop="counselorName" min-width="100" show-overflow-tooltip />
<el-table-column label="办公室" align="center" prop="office" min-width="120" show-overflow-tooltip />
<el-table-column label="日期范围" align="center" prop="dateRange" min-width="180" />
<el-table-column label="上午时段" align="center" min-width="120">
<el-table-column label="中午时段" align="center" min-width="120">
<template #default="scope">
<span v-if="scope.row.morningEnabled">{{ scope.row.morningTime }}</span>
<span v-else>-</span>
@@ -129,13 +135,16 @@
</el-card>
<!-- 添加或修改预约时段对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-dialog :title="title" v-model="open" width="800px" append-to-body>
<el-form ref="scheduleFormRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="咨询师" prop="counselorId">
<el-select v-model="form.counselorId" placeholder="请选择咨询师" style="width: 100%">
<el-select v-model="form.counselorId" placeholder="请选择咨询师" style="width: 100%" @change="handleCounselorChange">
<el-option v-for="item in counselorOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="办公室" prop="office">
<el-input v-model="form.office" placeholder="请输入办公室" style="width: 100%" />
</el-form-item>
<el-form-item label="排班日期" prop="dateRange" v-if="!form.id">
<el-date-picker
v-model="form.dateRange"
@@ -151,60 +160,92 @@
<el-date-picker v-model="form.scheduleDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 100%" />
</el-form-item>
<!-- 上午时段设置 -->
<el-divider content-position="left">上午时段</el-divider>
<el-form-item label="启用上午" prop="morningEnabled">
<el-switch v-model="form.morningEnabled" />
<!-- 中午时段设置 -->
<el-divider content-position="left">中午时段</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="启用中午" prop="noonEnabled">
<el-switch v-model="form.noonEnabled" />
</el-form-item>
<template v-if="form.morningEnabled">
<el-form-item label="开始时间" prop="morningStartTime">
</el-col>
<el-col :span="12" class="text-right">
<el-button v-if="form.noonEnabled" type="primary" link icon="Plus" class="add-time-slot-btn" @click="addNoonTimeSlot">增加时段</el-button>
</el-col>
</el-row>
<template v-if="form.noonEnabled">
<div v-for="(slot, index) in form.noonTimeSlots" :key="`noon-${index}`" class="time-slot-row">
<el-row :gutter="20">
<el-col :span="10">
<el-form-item :label="index === 0 ? '开始时间' : ''" :prop="`noonTimeSlots.${index}.startTime`">
<el-time-picker
v-model="morningStartTimeTemp"
v-model="slot.startTimeTemp"
format="HH:mm"
placeholder="选择时间"
style="width: 100%"
:picker-options="morningTimePickerOptions"
@change="handleMorningStartTimeChange"
@change="(time) => handleNoonStartTimeChange(time, index)"
/>
</el-form-item>
<el-form-item label="结束时间" prop="morningEndTime">
</el-col>
<el-col :span="10">
<el-form-item :label="index === 0 ? '结束时间' : ''" :prop="`noonTimeSlots.${index}.endTime`">
<el-time-picker
v-model="morningEndTimeTemp"
v-model="slot.endTimeTemp"
format="HH:mm"
placeholder="选择时间"
style="width: 100%"
:picker-options="morningTimePickerOptions"
@change="handleMorningEndTimeChange"
@change="(time) => handleNoonEndTimeChange(time, index)"
/>
</el-form-item>
</el-col>
<el-col :span="4" class="time-slot-actions">
<el-button v-if="index > 0" type="danger" link icon="Delete" class="delete-time-slot-btn" @click="removeNoonTimeSlot(index)"></el-button>
</el-col>
</el-row>
</div>
</template>
<!-- 下午时段设置 -->
<el-divider content-position="left">下午时段</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="启用下午" prop="afternoonEnabled">
<el-switch v-model="form.afternoonEnabled" />
</el-form-item>
</el-col>
<el-col :span="12" class="text-right">
<el-button v-if="form.afternoonEnabled" type="primary" link icon="Plus" class="add-time-slot-btn" @click="addAfternoonTimeSlot">增加时段</el-button>
</el-col>
</el-row>
<template v-if="form.afternoonEnabled">
<el-form-item label="开始时间" prop="afternoonStartTime">
<div v-for="(slot, index) in form.afternoonTimeSlots" :key="`afternoon-${index}`" class="time-slot-row">
<el-row :gutter="20">
<el-col :span="10">
<el-form-item :label="index === 0 ? '开始时间' : ''" :prop="`afternoonTimeSlots.${index}.startTime`">
<el-time-picker
v-model="afternoonStartTimeTemp"
v-model="slot.startTimeTemp"
format="HH:mm"
placeholder="选择时间"
style="width: 100%"
:picker-options="afternoonTimePickerOptions"
@change="handleAfternoonStartTimeChange"
@change="(time) => handleAfternoonStartTimeChange(time, index)"
/>
</el-form-item>
<el-form-item label="结束时间" prop="afternoonEndTime">
</el-col>
<el-col :span="10">
<el-form-item :label="index === 0 ? '结束时间' : ''" :prop="`afternoonTimeSlots.${index}.endTime`">
<el-time-picker
v-model="afternoonEndTimeTemp"
v-model="slot.endTimeTemp"
format="HH:mm"
placeholder="选择时间"
style="width: 100%"
:picker-options="afternoonTimePickerOptions"
@change="handleAfternoonEndTimeChange"
@change="(time) => handleAfternoonEndTimeChange(time, index)"
/>
</el-form-item>
</el-col>
<el-col :span="4" class="time-slot-actions">
<el-button v-if="index > 0" type="danger" link icon="Delete" class="delete-time-slot-btn" @click="removeAfternoonTimeSlot(index)"></el-button>
</el-col>
</el-row>
</div>
</template>
<el-form-item label="状态" prop="status">
@@ -268,34 +309,13 @@ const open = ref(false);
const dateRange = ref<string[]>([]);
// 咨询师选项
const counselorOptions = ref<CounselorOption[]>([]);
// 时间选择器临时变量
const startTimeTemp = ref<Date | null>(null);
const endTimeTemp = ref<Date | null>(null);
const morningStartTimeTemp = ref<Date | null>(null);
const morningEndTimeTemp = ref<Date | null>(null);
const afternoonStartTimeTemp = ref<Date | null>(null);
const afternoonEndTimeTemp = ref<Date | null>(null);
// 时间选择器配置
const morningTimePickerOptions = {
start: '08:00',
step: '00:30',
end: '12:00',
format: 'HH:mm'
};
const afternoonTimePickerOptions = {
start: '14:00',
step: '00:30',
end: '17:00',
format: 'HH:mm'
};
// 查询参数
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
counselorId: undefined,
office: undefined,
timeSlot: undefined,
status: undefined,
params: {} as any
@@ -305,23 +325,50 @@ const queryParams = reactive({
const form = reactive<any>({
id: undefined,
counselorId: undefined,
office: '',
scheduleDate: '',
dateRange: [],
morningEnabled: true,
morningStartTime: '08:30',
morningEndTime: '12:00',
noonEnabled: true,
noonTimeSlots: [
{
startTime: '12:00',
endTime: '12:40',
startTimeTemp: (() => {
const time = new Date();
time.setHours(12, 0, 0, 0);
return time;
})(),
endTimeTemp: (() => {
const time = new Date();
time.setHours(12, 40, 0, 0);
return time;
})()
}
],
afternoonEnabled: true,
afternoonStartTime: '14:30',
afternoonEndTime: '17:00',
timeSlot: '0',
startTime: '',
endTime: '',
afternoonTimeSlots: [
{
startTime: '17:00',
endTime: '17:40',
startTimeTemp: (() => {
const time = new Date();
time.setHours(17, 0, 0, 0);
return time;
})(),
endTimeTemp: (() => {
const time = new Date();
time.setHours(17, 40, 0, 0);
return time;
})()
}
],
status: '0'
});
// 表单校验
const rules = {
counselorId: [{ required: true, message: '咨询师不能为空', trigger: 'change' }],
office: [{ required: true, message: '办公室不能为空', trigger: 'blur' }],
scheduleDate: [{ required: false, message: '排班日期不能为空', trigger: 'change' }],
dateRange: [{ required: true, message: '排班日期范围不能为空', trigger: 'change' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
@@ -377,38 +424,42 @@ const cancel = () => {
const reset = () => {
form.id = undefined;
form.counselorId = undefined;
form.office = '';
form.scheduleDate = '';
form.dateRange = [];
form.morningEnabled = true;
form.noonEnabled = true;
form.afternoonEnabled = true;
form.morningStartTime = '08:30';
form.morningEndTime = '12:00';
form.afternoonStartTime = '14:30';
form.afternoonEndTime = '17:00';
form.timeSlot = '0';
form.startTime = '';
form.endTime = '';
form.status = '0';
startTimeTemp.value = null;
endTimeTemp.value = null;
// 设置中午时段默认值
const noonStartTime = new Date();
noonStartTime.setHours(12, 0, 0, 0);
const noonEndTime = new Date();
noonEndTime.setHours(12, 40, 0, 0);
// 设置默认时间
const morningStart = new Date();
morningStart.setHours(8, 30, 0, 0);
morningStartTimeTemp.value = morningStart;
form.noonTimeSlots = [
{
startTime: '12:00',
endTime: '12:40',
startTimeTemp: noonStartTime,
endTimeTemp: noonEndTime
}
];
const morningEnd = new Date();
morningEnd.setHours(12, 0, 0, 0);
morningEndTimeTemp.value = morningEnd;
// 设置下午时段默认值
const afternoonStartTime = new Date();
afternoonStartTime.setHours(17, 0, 0, 0);
const afternoonEndTime = new Date();
afternoonEndTime.setHours(17, 40, 0, 0);
const afternoonStart = new Date();
afternoonStart.setHours(14, 30, 0, 0);
afternoonStartTimeTemp.value = afternoonStart;
const afternoonEnd = new Date();
afternoonEnd.setHours(17, 0, 0, 0);
afternoonEndTimeTemp.value = afternoonEnd;
form.afternoonTimeSlots = [
{
startTime: '17:00',
endTime: '17:40',
startTimeTemp: afternoonStartTime,
endTimeTemp: afternoonEndTime
}
];
scheduleFormRef.value?.resetFields();
};
@@ -441,7 +492,7 @@ const handleAdd = () => {
};
/** 查看按钮操作 */
const handleView = async (row: XljkConsultationScheduleVO) => {
const handleView = async (row: XljkConsultationScheduleSummaryVO) => {
try {
const res = await getXljkSchedule(row.id);
if (res.code === 200 && res.data) {
@@ -455,7 +506,7 @@ const handleView = async (row: XljkConsultationScheduleVO) => {
};
/** 修改按钮操作 */
const handleUpdate = async (row?: XljkConsultationScheduleVO) => {
const handleUpdate = async (row?: XljkConsultationScheduleSummaryVO) => {
if (row && row.id) {
try {
// 修复:将字符串状态正确转换为数字进行比较
@@ -491,6 +542,7 @@ const submitForm = async () => {
await updateXljkSchedule({
id: form.id,
counselorId: form.counselorId,
office: form.office,
scheduleDate: form.scheduleDate,
timeSlot: form.timeSlot,
startTime: form.startTime,
@@ -514,26 +566,67 @@ const submitForm = async () => {
}
// 检查是否至少启用了一个时段
if (!form.morningEnabled && !form.afternoonEnabled) {
ElMessage.warning('请至少启用一个时段(上午或下午)');
if (!form.noonEnabled && !form.afternoonEnabled) {
ElMessage.warning('请至少启用一个时段(中午或下午)');
return;
}
// 确保时间数据同步 - 从时间选择器同步到字符串字段
if (form.noonEnabled) {
form.noonTimeSlots.forEach((slot, index) => {
if (slot.startTimeTemp && !slot.startTime) {
const hours = slot.startTimeTemp.getHours().toString().padStart(2, '0');
const minutes = slot.startTimeTemp.getMinutes().toString().padStart(2, '0');
slot.startTime = `${hours}:${minutes}`;
}
if (slot.endTimeTemp && !slot.endTime) {
const hours = slot.endTimeTemp.getHours().toString().padStart(2, '0');
const minutes = slot.endTimeTemp.getMinutes().toString().padStart(2, '0');
slot.endTime = `${hours}:${minutes}`;
}
});
}
if (form.afternoonEnabled) {
form.afternoonTimeSlots.forEach((slot, index) => {
if (slot.startTimeTemp && !slot.startTime) {
const hours = slot.startTimeTemp.getHours().toString().padStart(2, '0');
const minutes = slot.startTimeTemp.getMinutes().toString().padStart(2, '0');
slot.startTime = `${hours}:${minutes}`;
}
if (slot.endTimeTemp && !slot.endTime) {
const hours = slot.endTimeTemp.getHours().toString().padStart(2, '0');
const minutes = slot.endTimeTemp.getMinutes().toString().padStart(2, '0');
slot.endTime = `${hours}:${minutes}`;
}
});
}
// 批量创建预约时段的请求数据
const batchCreateData = {
counselorId: form.counselorId,
office: form.office,
startDate: form.dateRange[0],
endDate: form.dateRange[1],
morningEnabled: form.morningEnabled,
morningStartTime: form.morningStartTime,
morningEndTime: form.morningEndTime,
noonEnabled: form.noonEnabled,
noonTimeSlots: form.noonTimeSlots.map(slot => ({
startTime: slot.startTime,
endTime: slot.endTime
})),
afternoonEnabled: form.afternoonEnabled,
afternoonStartTime: form.afternoonStartTime,
afternoonEndTime: form.afternoonEndTime,
afternoonTimeSlots: form.afternoonTimeSlots.map(slot => ({
startTime: slot.startTime,
endTime: slot.endTime
})),
status: form.status,
skipWeekends: true // 自动跳过周末
};
// 调试日志 - 确认时间数据
console.log('提交的批量创建数据:', batchCreateData);
console.log('中午时段数据:', form.noonTimeSlots);
console.log('下午时段数据:', form.afternoonTimeSlots);
// 调用批量创建API
const res = await addXljkSchedule(batchCreateData);
if (res.code === 200) {
@@ -551,7 +644,7 @@ const submitForm = async () => {
};
/** 删除按钮操作 */
const handleDelete = async (row?: XljkConsultationScheduleVO) => {
const handleDelete = async (row?: XljkConsultationScheduleSummaryVO) => {
const _ids = row?.id || ids.value;
const confirmMessage = row
? `是否确认删除该预约时段?${row.scheduleCount > 1 ? `(包含${row.scheduleCount}条记录)` : ''}`
@@ -594,47 +687,147 @@ const handleEndTimeChange = (time: Date) => {
}
};
/** 处理上午开始时间变化 */
const handleMorningStartTimeChange = (time: Date) => {
/** 处理中午开始时间变化 */
const handleNoonStartTimeChange = (time: Date, index: number) => {
if (time) {
const hours = time.getHours().toString().padStart(2, '0');
const minutes = time.getMinutes().toString().padStart(2, '0');
form.morningStartTime = `${hours}:${minutes}`;
const timeString = `${hours}:${minutes}`;
form.noonTimeSlots[index].startTime = timeString;
form.noonTimeSlots[index].startTimeTemp = time;
} else {
form.morningStartTime = '';
form.noonTimeSlots[index].startTime = '';
form.noonTimeSlots[index].startTimeTemp = null;
}
};
/** 处理上午结束时间变化 */
const handleMorningEndTimeChange = (time: Date) => {
/** 处理中午结束时间变化 */
const handleNoonEndTimeChange = (time: Date, index: number) => {
if (time) {
const hours = time.getHours().toString().padStart(2, '0');
const minutes = time.getMinutes().toString().padStart(2, '0');
form.morningEndTime = `${hours}:${minutes}`;
const timeString = `${hours}:${minutes}`;
form.noonTimeSlots[index].endTime = timeString;
form.noonTimeSlots[index].endTimeTemp = time;
} else {
form.morningEndTime = '';
form.noonTimeSlots[index].endTime = '';
form.noonTimeSlots[index].endTimeTemp = null;
}
};
/** 新增中午时段 */
const addNoonTimeSlot = () => {
const lastSlot = form.noonTimeSlots[form.noonTimeSlots.length - 1];
let startTime = '12:00';
let endTime = '12:40';
if (lastSlot && lastSlot.endTime) {
startTime = lastSlot.endTime;
// 计算结束时间(开始时间+40分钟)
const [hours, minutes] = startTime.split(':').map(Number);
const startDate = new Date();
startDate.setHours(hours, minutes, 0, 0);
const endDate = new Date(startDate.getTime() + 40 * 60 * 1000); // 加40分钟
endTime = `${endDate.getHours().toString().padStart(2, '0')}:${endDate.getMinutes().toString().padStart(2, '0')}`;
}
// 创建对应的Date对象用于时间选择器
const startTimeDate = new Date();
const [startHours, startMinutes] = startTime.split(':').map(Number);
startTimeDate.setHours(startHours, startMinutes, 0, 0);
const endTimeDate = new Date();
const [endHours, endMinutes] = endTime.split(':').map(Number);
endTimeDate.setHours(endHours, endMinutes, 0, 0);
form.noonTimeSlots.push({
startTime,
endTime,
startTimeTemp: startTimeDate,
endTimeTemp: endTimeDate
});
};
/** 移除中午时段 */
const removeNoonTimeSlot = (index: number) => {
form.noonTimeSlots.splice(index, 1);
};
/** 处理下午开始时间变化 */
const handleAfternoonStartTimeChange = (time: Date) => {
const handleAfternoonStartTimeChange = (time: Date, index: number) => {
if (time) {
const hours = time.getHours().toString().padStart(2, '0');
const minutes = time.getMinutes().toString().padStart(2, '0');
form.afternoonStartTime = `${hours}:${minutes}`;
const timeString = `${hours}:${minutes}`;
form.afternoonTimeSlots[index].startTime = timeString;
form.afternoonTimeSlots[index].startTimeTemp = time;
} else {
form.afternoonStartTime = '';
form.afternoonTimeSlots[index].startTime = '';
form.afternoonTimeSlots[index].startTimeTemp = null;
}
};
/** 处理下午结束时间变化 */
const handleAfternoonEndTimeChange = (time: Date) => {
const handleAfternoonEndTimeChange = (time: Date, index: number) => {
if (time) {
const hours = time.getHours().toString().padStart(2, '0');
const minutes = time.getMinutes().toString().padStart(2, '0');
form.afternoonEndTime = `${hours}:${minutes}`;
const timeString = `${hours}:${minutes}`;
form.afternoonTimeSlots[index].endTime = timeString;
form.afternoonTimeSlots[index].endTimeTemp = time;
} else {
form.afternoonEndTime = '';
form.afternoonTimeSlots[index].endTime = '';
form.afternoonTimeSlots[index].endTimeTemp = null;
}
};
/** 新增下午时段 */
const addAfternoonTimeSlot = () => {
const lastSlot = form.afternoonTimeSlots[form.afternoonTimeSlots.length - 1];
let startTime = '17:00';
let endTime = '17:40';
if (lastSlot && lastSlot.endTime) {
startTime = lastSlot.endTime;
// 计算结束时间(开始时间+40分钟)
const [hours, minutes] = startTime.split(':').map(Number);
const startDate = new Date();
startDate.setHours(hours, minutes, 0, 0);
const endDate = new Date(startDate.getTime() + 40 * 60 * 1000); // 加40分钟
endTime = `${endDate.getHours().toString().padStart(2, '0')}:${endDate.getMinutes().toString().padStart(2, '0')}`;
}
// 创建对应的Date对象用于时间选择器
const startTimeDate = new Date();
const [startHours, startMinutes] = startTime.split(':').map(Number);
startTimeDate.setHours(startHours, startMinutes, 0, 0);
const endTimeDate = new Date();
const [endHours, endMinutes] = endTime.split(':').map(Number);
endTimeDate.setHours(endHours, endMinutes, 0, 0);
form.afternoonTimeSlots.push({
startTime,
endTime,
startTimeTemp: startTimeDate,
endTimeTemp: endTimeDate
});
};
/** 移除下午时段 */
const removeAfternoonTimeSlot = (index: number) => {
form.afternoonTimeSlots.splice(index, 1);
};
/** 处理咨询师变化 */
const handleCounselorChange = (counselorId: number) => {
if (counselorId) {
const selectedCounselor = counselorOptions.value.find(counselor => counselor.id === counselorId);
if (selectedCounselor && selectedCounselor.office) {
form.office = selectedCounselor.office;
}
} else {
form.office = '';
}
};
@@ -684,4 +877,140 @@ onMounted(() => {
margin-bottom: 0 !important;
}
}
/* 时间槽行样式 */
.time-slot-row {
margin-bottom: 10px;
padding: 10px;
background-color: #f5f7fa;
border-radius: 4px;
}
/* 时间槽操作按钮样式 */
.time-slot-actions {
display: flex;
justify-content: flex-end;
align-items: center;
padding-top: 8px;
}
/* 右对齐样式 */
.text-right {
text-align: right;
}
/* 新增时段按钮样式 */
.add-time-slot-btn {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
color: #409eff !important;
padding: 8px 12px;
font-size: 14px;
font-weight: normal;
transition: color 0.3s ease;
}
.add-time-slot-btn:hover {
color: #66b1ff !important;
background-color: transparent !important;
}
.add-time-slot-btn:focus {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
outline: none !important;
}
.add-time-slot-btn:active {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
}
/* 更强的选择器覆盖 Element Plus 默认样式 */
.el-button.add-time-slot-btn.el-button--primary.is-link {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
color: #409eff !important;
}
.el-button.add-time-slot-btn.el-button--primary.is-link:hover {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
color: #66b1ff !important;
}
.el-button.add-time-slot-btn.el-button--primary.is-link:focus {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
outline: none !important;
}
.el-button.add-time-slot-btn.el-button--primary.is-link:active {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
}
/* 删除时段按钮样式 */
.delete-time-slot-btn {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
color: #f56c6c !important;
padding: 8px;
font-size: 16px;
transition: color 0.3s ease;
}
.delete-time-slot-btn:hover {
color: #f78989 !important;
background-color: transparent !important;
}
.delete-time-slot-btn:focus {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
outline: none !important;
}
.delete-time-slot-btn:active {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
}
/* 更强的选择器覆盖 Element Plus 默认样式 */
.el-button.delete-time-slot-btn.el-button--danger.is-link {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
color: #f56c6c !important;
}
.el-button.delete-time-slot-btn.el-button--danger.is-link:hover {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
color: #f78989 !important;
}
.el-button.delete-time-slot-btn.el-button--danger.is-link:focus {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
outline: none !important;
}
.el-button.delete-time-slot-btn.el-button--danger.is-link:active {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
}
</style>
File diff suppressed because it is too large. Load diff