“出国申请审批权限修改”

This commit is contained in:
weizhong committed 2025-10-28 14:11:39 +08:00
commit f907b9cd40
97 files changed
+16889 -1631

No files matched your search

@@ -0,0 +1,48 @@
server:
port: 9090
spring:
application:
name: ruoyi-monitor-admin
profiles:
active: @profiles.active@
logging:
config: classpath:logback-plus.xml
--- # 监控中心服务端配置
spring:
security:
user:
name: @monitor.username@
password: @monitor.password@
boot:
admin:
ui:
title: RuoYi-Vue-Plus服务监控中心
context-path: /admin
--- # Actuator 监控端点的配置项
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: ALWAYS
logfile:
external-file: ./logs/ruoyi-monitor-admin.log
--- # 监控配置
spring.boot.admin.client:
# 增加客户端开关
enabled: true
# 设置 Spring Boot Admin Server 地址
url: http://172.16.0.67:9090/admin
instance:
service-host-type: IP
metadata:
username: ${spring.boot.admin.client.username}
userpassword: ${spring.boot.admin.client.password}
username: @monitor.username@
password: @monitor.password@
@@ -38,7 +38,7 @@ spring.boot.admin.client:
# 增加客户端开关
enabled: true
# 设置 Spring Boot Admin Server 地址
url: http://172.16.0.67:9090/admin
url: http://127.0.0.1:9090/admin
instance:
service-host-type: IP
metadata:
@@ -1,6 +1,5 @@
package org.dromara.workflow.domain.bo;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.dromara.common.core.validate.AddGroup;
@@ -12,7 +11,6 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* 驳回参数请求
*
@@ -50,6 +48,11 @@ public class BackProcessBo implements Serializable {
*/
private String message;
/**
* 手写签名(Base64编码的图片数据)
*/
private String signature;
/**
* 通知
*/
@@ -48,6 +48,11 @@ public class CompleteTaskBo implements Serializable {
*/
private String message;
/**
* 手写签名(Base64编码的图片数据)
*/
private String signature;
/**
* 消息通知
*/
@@ -151,6 +151,11 @@ public class FlowHisTaskVo implements Serializable {
*/
private String message;
/**
* 手写签名(Base64编码的图片数据)
*/
private String signature;
/**
* 业务详情 存业务类的json
*/
@@ -0,0 +1,48 @@
package org.dromara.workflow.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import org.dromara.workflow.domain.vo.FlowHisTaskVo;
import java.util.List;
/**
* 历史任务扩展Mapper
* 用于处理Warm-Flow框架不支持的自定义字段
*
* @author System
*/
@Mapper
public interface FlwHisTaskExtMapper {
/**
* 更新历史任务的签名字段
*
* @param taskId 任务ID
* @param signature 签名数据(Base64)
* @return 影响行数
*/
@Update("UPDATE flow_his_task SET signature = #{signature} WHERE task_id = #{taskId}")
int updateSignatureByTaskId(@Param("taskId") Long taskId, @Param("signature") String signature);
/**
* 根据实例ID更新签名字段(批量更新场景)
*
* @param instanceId 流程实例ID
* @param signature 签名数据(Base64)
* @return 影响行数
*/
@Update("UPDATE flow_his_task SET signature = #{signature} WHERE instance_id = #{instanceId}")
int updateSignatureByInstanceId(@Param("instanceId") Long instanceId, @Param("signature") String signature);
/**
* 根据实例ID查询历史任务列表(包含签名字段)
*
* @param instanceId 流程实例ID
* @param nodeType 节点类型
* @return 历史任务列表
*/
List<FlowHisTaskVo> selectHisTaskListWithSignature(@Param("instanceId") Long instanceId,
@Param("nodeType") Integer nodeType);
}
@@ -42,6 +42,7 @@ import org.dromara.workflow.domain.vo.FlowHisTaskVo;
import org.dromara.workflow.domain.vo.FlowInstanceVo;
import org.dromara.workflow.handler.FlowProcessEventHandler;
import org.dromara.workflow.mapper.FlwCategoryMapper;
import org.dromara.workflow.mapper.FlwHisTaskExtMapper;
import org.dromara.workflow.mapper.FlwInstanceMapper;
import org.dromara.workflow.service.IFlwInstanceService;
import org.dromara.workflow.service.IFlwTaskService;
@@ -71,6 +72,7 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
private final IFlwTaskService flwTaskService;
private final FlwInstanceMapper flwInstanceMapper;
private final FlwCategoryMapper flwCategoryMapper;
private final FlwHisTaskExtMapper flwHisTaskExtMapper;
/**
* 分页查询正在运行的流程实例
@@ -128,15 +130,21 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
*/
private QueryWrapper<FlowInstanceBo> buildQueryWrapper(FlowInstanceBo flowInstanceBo) {
QueryWrapper<FlowInstanceBo> queryWrapper = Wrappers.query();
queryWrapper.like(StringUtils.isNotBlank(flowInstanceBo.getNodeName()), "fi.node_name", flowInstanceBo.getNodeName());
queryWrapper.like(StringUtils.isNotBlank(flowInstanceBo.getFlowName()), "fd.flow_name", flowInstanceBo.getFlowName());
queryWrapper.like(StringUtils.isNotBlank(flowInstanceBo.getFlowCode()), "fd.flow_code", flowInstanceBo.getFlowCode());
queryWrapper.like(StringUtils.isNotBlank(flowInstanceBo.getNodeName()), "fi.node_name",
flowInstanceBo.getNodeName());
queryWrapper.like(StringUtils.isNotBlank(flowInstanceBo.getFlowName()), "fd.flow_name",
flowInstanceBo.getFlowName());
queryWrapper.like(StringUtils.isNotBlank(flowInstanceBo.getFlowCode()), "fd.flow_code",
flowInstanceBo.getFlowCode());
if (StringUtils.isNotBlank(flowInstanceBo.getCategory())) {
List<Long> categoryIds = flwCategoryMapper.selectCategoryIdsByParentId(Convert.toLong(flowInstanceBo.getCategory()));
List<Long> categoryIds = flwCategoryMapper
.selectCategoryIdsByParentId(Convert.toLong(flowInstanceBo.getCategory()));
queryWrapper.in("fd.category", StreamUtils.toList(categoryIds, Convert::toStr));
}
queryWrapper.eq(StringUtils.isNotBlank(flowInstanceBo.getBusinessId()), "fi.business_id", flowInstanceBo.getBusinessId());
queryWrapper.in(CollUtil.isNotEmpty(flowInstanceBo.getCreateByIds()), "fi.create_by", flowInstanceBo.getCreateByIds());
queryWrapper.eq(StringUtils.isNotBlank(flowInstanceBo.getBusinessId()), "fi.business_id",
flowInstanceBo.getBusinessId());
queryWrapper.in(CollUtil.isNotEmpty(flowInstanceBo.getCreateByIds()), "fi.create_by",
flowInstanceBo.getCreateByIds());
queryWrapper.eq("fi.del_flag", "0");
queryWrapper.orderByDesc("fi.create_time");
return queryWrapper;
@@ -149,7 +157,8 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
*/
@Override
public FlowInstance selectInstByBusinessId(String businessId) {
return flowInstanceMapper.selectOne(new LambdaQueryWrapper<FlowInstance>().eq(FlowInstance::getBusinessId, businessId));
return flowInstanceMapper
.selectOne(new LambdaQueryWrapper<FlowInstance>().eq(FlowInstance::getBusinessId, businessId));
}
/**
@@ -180,7 +189,8 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteByBusinessIds(List<Long> businessIds) {
List<FlowInstance> flowInstances = flowInstanceMapper.selectList(new LambdaQueryWrapper<FlowInstance>().in(FlowInstance::getBusinessId, StreamUtils.toList(businessIds, Convert::toStr)));
List<FlowInstance> flowInstances = flowInstanceMapper.selectList(new LambdaQueryWrapper<FlowInstance>()
.in(FlowInstance::getBusinessId, StreamUtils.toList(businessIds, Convert::toStr)));
if (CollUtil.isEmpty(flowInstances)) {
log.warn("未找到对应的流程实例信息,无法执行删除操作。");
return false;
@@ -204,8 +214,8 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
}
// 获取定义信息
Map<Long, Definition> definitionMap = defService.getByIds(
StreamUtils.toList(instances, Instance::getDefinitionId)
).stream().collect(Collectors.toMap(Definition::getId, definition -> definition));
StreamUtils.toList(instances, Instance::getDefinitionId)).stream()
.collect(Collectors.toMap(Definition::getId, definition -> definition));
// 逐一触发删除事件
instances.forEach(instance -> {
@@ -302,14 +312,11 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
}
list.addAll(flowHisTaskVos);
}
//历史任务
LambdaQueryWrapper<FlowHisTask> wrapper = Wrappers.lambdaQuery();
wrapper.eq(FlowHisTask::getInstanceId, instanceId);
wrapper.eq(FlowHisTask::getNodeType, NodeType.BETWEEN.getKey());
wrapper.orderByDesc(FlowHisTask::getCreateTime).orderByDesc(FlowHisTask::getUpdateTime);
List<FlowHisTask> flowHisTasks = flowHisTaskMapper.selectList(wrapper);
if (CollUtil.isNotEmpty(flowHisTasks)) {
list.addAll(BeanUtil.copyToList(flowHisTasks, FlowHisTaskVo.class));
// 历史任务(使用自定义Mapper查询,包含签名字段)
List<FlowHisTaskVo> flowHisTaskVos = flwHisTaskExtMapper.selectHisTaskListWithSignature(instanceId,
NodeType.BETWEEN.getKey());
if (CollUtil.isNotEmpty(flowHisTaskVos)) {
list.addAll(flowHisTaskVos);
}
return Map.of("list", list, "instanceId", instanceId);
}
@@ -336,7 +343,8 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
@Override
public Map<String, Object> instanceVariable(Long instanceId) {
FlowInstance flowInstance = flowInstanceMapper.selectById(instanceId);
Map<String, Object> variableMap = Optional.ofNullable(flowInstance.getVariableMap()).orElse(Collections.emptyMap());
Map<String, Object> variableMap = Optional.ofNullable(flowInstance.getVariableMap())
.orElse(Collections.emptyMap());
List<Map<String, Object>> variableList = variableMap.entrySet().stream()
.map(entry -> Map.of("key", entry.getKey(), "value", entry.getValue()))
.toList();
@@ -38,6 +38,7 @@ import org.dromara.warm.flow.orm.mapper.FlowInstanceMapper;
import org.dromara.warm.flow.orm.mapper.FlowNodeMapper;
import org.dromara.warm.flow.orm.mapper.FlowTaskMapper;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.mapper.FlwHisTaskExtMapper;
import org.dromara.workflow.common.constant.FlowConstant;
import org.dromara.workflow.common.enums.TaskAssigneeType;
import org.dromara.workflow.common.enums.TaskStatusEnum;
@@ -86,6 +87,7 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
private final IFlwTaskAssigneeService flwTaskAssigneeService;
private final IFlwCommonService flwCommonService;
private final IFlwNodeExtService flwNodeExtService;
private final FlwHisTaskExtMapper flwHisTaskExtMapper;
/**
* 启动任务
@@ -185,6 +187,12 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
.hisTaskExt(completeTaskBo.getFileId());
// 执行任务跳转,并根据返回的处理人设置下一步处理人
taskService.skip(taskId, flowParams);
// 保存手写签名到历史任务表
if (StringUtils.isNotBlank(completeTaskBo.getSignature())) {
flwHisTaskExtMapper.updateSignatureByTaskId(taskId, completeTaskBo.getSignature());
}
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
@@ -409,6 +417,12 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
.hisStatus(TaskStatusEnum.BACK.getStatus())
.hisTaskExt(bo.getFileId());
taskService.skip(task.getId(), flowParams);
// 保存手写签名到历史任务表(驳回场景)
if (StringUtils.isNotBlank(bo.getSignature())) {
flwHisTaskExtMapper.updateSignatureByTaskId(taskId, bo.getSignature());
}
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
@@ -0,0 +1,66 @@
<?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.workflow.mapper.FlwHisTaskExtMapper">
<!-- FlowHisTaskVo结果映射 -->
<resultMap id="FlowHisTaskVoResult" type="org.dromara.workflow.domain.vo.FlowHisTaskVo">
<id property="id" column="id"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
<result property="tenantId" column="tenant_id"/>
<result property="delFlag" column="del_flag"/>
<result property="definitionId" column="definition_id"/>
<result property="instanceId" column="instance_id"/>
<result property="taskId" column="task_id"/>
<result property="cooperateType" column="cooperate_type"/>
<result property="nodeCode" column="node_code"/>
<result property="nodeName" column="node_name"/>
<result property="nodeType" column="node_type"/>
<result property="targetNodeCode" column="target_node_code"/>
<result property="targetNodeName" column="target_node_name"/>
<result property="approver" column="approver"/>
<result property="collaborator" column="collaborator"/>
<result property="skipType" column="skip_type"/>
<result property="flowStatus" column="flow_status"/>
<result property="message" column="message"/>
<result property="signature" column="signature"/>
<result property="ext" column="ext"/>
<result property="formCustom" column="form_custom"/>
<result property="formPath" column="form_path"/>
</resultMap>
<!-- 根据实例ID查询历史任务列表(包含签名字段) -->
<select id="selectHisTaskListWithSignature" resultMap="FlowHisTaskVoResult">
SELECT
id,
create_time,
update_time,
tenant_id,
del_flag,
definition_id,
instance_id,
task_id,
cooperate_type,
node_code,
node_name,
node_type,
target_node_code,
target_node_name,
approver,
collaborator,
skip_type,
flow_status,
message,
signature,
ext,
form_custom,
form_path
FROM flow_his_task
WHERE instance_id = #{instanceId}
AND node_type = #{nodeType}
AND del_flag = '0'
ORDER BY create_time DESC, update_time DESC
</select>
</mapper>
@@ -59,6 +59,7 @@
a.instance_id,
a.flow_status flow_task_status,
a.message,
a.signature,
a.ext,
a.create_time,
a.update_time,
@@ -0,0 +1,123 @@
package org.dromara.xg.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.xg.domain.vo.XgCollegeLeaderVo;
import org.dromara.xg.domain.bo.XgCollegeLeaderBo;
import org.dromara.xg.service.IXgCollegeLeaderService;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
/**
* 二级学院分管领导配置
*
* @author Cursor AI
* @date 2025-01-24
*/
@Tag(name = "二级学院分管领导配置")
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/xg/ejxyfgld")
public class XgCollegeLeaderController extends BaseController {
private final IXgCollegeLeaderService xgCollegeLeaderService;
/**
* 查询二级学院分管领导配置列表
*/
@Operation(summary = "查询二级学院分管领导配置列表")
@SaCheckPermission("xg:ejxyfgld:list")
@GetMapping("/list")
public TableDataInfo<XgCollegeLeaderVo> list(XgCollegeLeaderBo bo, PageQuery pageQuery) {
return xgCollegeLeaderService.queryPageList(bo, pageQuery);
}
/**
* 根据学院ID查询分管领导列表
*/
@Operation(summary = "根据学院ID查询分管领导列表")
@SaCheckPermission("xg:ejxyfgld:list")
@GetMapping("/listByCollege/{collegeId}")
public R<List<XgCollegeLeaderVo>> listByCollege(@PathVariable Long collegeId) {
List<XgCollegeLeaderVo> list = xgCollegeLeaderService.queryByCollegeId(collegeId);
return R.ok(list);
}
/**
* 导出二级学院分管领导配置列表
*/
@Operation(summary = "导出二级学院分管领导配置列表")
@SaCheckPermission("xg:ejxyfgld:export")
@Log(title = "二级学院分管领导配置", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(XgCollegeLeaderBo bo, HttpServletResponse response) {
List<XgCollegeLeaderVo> list = xgCollegeLeaderService.queryList(bo);
ExcelUtil.exportExcel(list, "二级学院分管领导配置", XgCollegeLeaderVo.class, response);
}
/**
* 获取二级学院分管领导配置详细信息
*
* @param id 主键
*/
@Operation(summary = "获取二级学院分管领导配置详细信息")
@SaCheckPermission("xg:ejxyfgld:query")
@GetMapping("/{id}")
public R<XgCollegeLeaderVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
return R.ok(xgCollegeLeaderService.queryById(id));
}
/**
* 新增二级学院分管领导配置
*/
@Operation(summary = "新增二级学院分管领导配置")
@SaCheckPermission("xg:ejxyfgld:add")
@Log(title = "二级学院分管领导配置", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody XgCollegeLeaderBo bo) {
return toAjax(xgCollegeLeaderService.insertByBo(bo));
}
/**
* 修改二级学院分管领导配置
*/
@Operation(summary = "修改二级学院分管领导配置")
@SaCheckPermission("xg:ejxyfgld:edit")
@Log(title = "二级学院分管领导配置", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody XgCollegeLeaderBo bo) {
return toAjax(xgCollegeLeaderService.updateByBo(bo));
}
/**
* 删除二级学院分管领导配置
*
* @param ids 主键串
*/
@Operation(summary = "删除二级学院分管领导配置")
@SaCheckPermission("xg:ejxyfgld:remove")
@Log(title = "二级学院分管领导配置", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
return toAjax(xgCollegeLeaderService.deleteWithValidByIds(List.of(ids), true));
}
}
@@ -0,0 +1,87 @@
package org.dromara.xg.controller;
import lombok.RequiredArgsConstructor;
import jakarta.validation.constraints.NotNull;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
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.mybatis.core.page.TableDataInfo;
import org.dromara.xg.domain.vo.XgDormApplicationVo;
import org.dromara.xg.domain.vo.XgHolidayStayVo;
import org.dromara.xg.domain.vo.XgStudentLeaveVo;
import org.dromara.xg.domain.bo.XgDormApplicationBo;
import org.dromara.xg.domain.bo.XgHolidayStayBo;
import org.dromara.xg.domain.bo.XgStudentLeaveBo;
import org.dromara.xg.service.IXgStudentDynamicsService;
/**
* 学生动向信息管理(宿管员)
*
* @author zlwu
* @date 2025-01-15
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/studentDynamics")
public class XgStudentDynamicsController extends BaseController {
private final IXgStudentDynamicsService xgStudentDynamicsService;
/**
* 查询宿管员权限内的学生调宿退宿申请列表
*/
@SaCheckPermission("studentDynamics:dormApplication:list")
@GetMapping("/dormApplicationList")
public TableDataInfo<XgDormApplicationVo> getDormApplicationList(XgDormApplicationBo bo, PageQuery pageQuery) {
return xgStudentDynamicsService.queryDormApplicationPageList(bo, pageQuery);
}
/**
* 查询宿管员权限内的学生假期留宿信息列表
*/
@SaCheckPermission("studentDynamics:holidayStay:list")
@GetMapping("/holidayStayList")
public TableDataInfo<XgHolidayStayVo> getHolidayStayList(XgHolidayStayBo bo, PageQuery pageQuery) {
return xgStudentDynamicsService.queryHolidayStayPageList(bo, pageQuery);
}
/**
* 查询宿管员权限内的学生请假信息列表
*/
@SaCheckPermission("studentDynamics:studentLeave:list")
@GetMapping("/studentLeaveList")
public TableDataInfo<XgStudentLeaveVo> getStudentLeaveList(XgStudentLeaveBo bo, PageQuery pageQuery) {
return xgStudentDynamicsService.queryStudentLeavePageList(bo, pageQuery);
}
/**
* 获取调宿退宿申请详细信息
*/
@SaCheckPermission("studentDynamics:dormApplication:query")
@GetMapping("/dormApplication/{id}")
public R<XgDormApplicationVo> getDormApplicationInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
return R.ok(xgStudentDynamicsService.queryDormApplicationById(id));
}
/**
* 获取假期留宿详细信息
*/
@SaCheckPermission("studentDynamics:holidayStay:query")
@GetMapping("/holidayStay/{id}")
public R<XgHolidayStayVo> getHolidayStayInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
return R.ok(xgStudentDynamicsService.queryHolidayStayById(id));
}
/**
* 获取学生请假详细信息
*/
@SaCheckPermission("studentDynamics:studentLeave:query")
@GetMapping("/studentLeave/{id}")
public R<XgStudentLeaveVo> getStudentLeaveInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
return R.ok(xgStudentDynamicsService.queryStudentLeaveById(id));
}
}
@@ -823,6 +823,23 @@ public class XgStudentMsgController extends BaseController {
return R.ok(approverUserIds);
}
/**
* 根据学生ID获取二级学院分管领导ID列表
*
* @param studentId 学生ID
* @return 分管领导ID列表
*/
@SaCheckPermission("studentMsg:studentMsg:query")
@GetMapping("/getCollegeLeaders/{studentId}")
public R<List<Long>> getCollegeLeaders(@NotNull(message = "学生ID不能为空") @PathVariable Long studentId) {
log.info("根据学生ID获取二级学院分管领导ID列表 - 学生ID: {}", studentId);
List<Long> leaderIds = xgStudentMsgService.getCollegeLeadersByStudentId(studentId);
if (leaderIds.isEmpty()) {
return R.fail("未找到该学生所在学院的分管领导");
}
return R.ok(leaderIds);
}
/**
* 导入数据
*
@@ -28,7 +28,7 @@ public class XgSysUserController extends BaseController {
private final IXgSysUserService xgSysUserService;
/**
* 查询有效用户列表
* 查询有效用户列表(教职工account_type=1)
*/
@SaCheckPermission("xg:sysUser:list")
@GetMapping("/activeList")
@@ -0,0 +1,61 @@
package org.dromara.xg.domain;
import org.dromara.common.tenant.core.TenantEntity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
/**
* 二级学院分管领导配置对象 xg_college_leader
*
* @author Cursor AI
* @date 2025-01-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("xg_college_leader")
public class XgCollegeLeader extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID(雪花ID)
*/
@TableId(value = "id")
private Long id;
/**
* 删除标志(0代表存在 1代表删除)
*/
@TableLogic(value = "0", delval = "1")
private Long deleted;
/**
* 二级学院ID,关联sys_dept.dept_id
*/
private Long collegeId;
/**
* 二级学院名称
*/
private String collegeName;
/**
* 分管领导ID,关联sys_user.user_id
*/
private Long leaderId;
/**
* 分管领导姓名
*/
private String leaderName;
/**
* 备注信息
*/
private String remark;
}
@@ -27,6 +27,11 @@ public class XgSysUser extends TenantEntity {
@TableId(value = "user_id")
private Long userId;
/**
* 用户账号
*/
private String userName;
/**
* 用户昵称
*/
@@ -0,0 +1,61 @@
package org.dromara.xg.domain.bo;
import org.dromara.xg.domain.XgCollegeLeader;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.*;
/**
* 二级学院分管领导配置业务对象 xg_college_leader
*
* @author Cursor AI
* @date 2025-01-24
*/
@Data
@EqualsAndHashCode(callSuper = true)
@AutoMapper(target = XgCollegeLeader.class, reverseConvertGenerate = false)
public class XgCollegeLeaderBo extends BaseEntity {
/**
* 主键ID(雪花ID)
*/
@NotNull(message = "主键ID不能为空", groups = { EditGroup.class })
private Long id;
/**
* 删除标志(0代表存在 1代表删除)
*/
private Long deleted;
/**
* 二级学院ID,关联sys_dept.dept_id
*/
@NotNull(message = "二级学院不能为空", groups = { AddGroup.class, EditGroup.class })
private Long collegeId;
/**
* 二级学院名称
*/
private String collegeName;
/**
* 分管领导ID,关联sys_user.user_id
*/
@NotNull(message = "分管领导不能为空", groups = { AddGroup.class, EditGroup.class })
private Long leaderId;
/**
* 分管领导姓名
*/
private String leaderName;
/**
* 备注信息
*/
private String remark;
}
@@ -0,0 +1,88 @@
package org.dromara.xg.domain.vo;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.dromara.xg.domain.XgCollegeLeader;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.excel.convert.ExcelDictConvert;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 二级学院分管领导配置视图对象 xg_college_leader
*
* @author Cursor AI
* @date 2025-01-24
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = XgCollegeLeader.class)
public class XgCollegeLeaderVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID(雪花ID)
*/
@ExcelProperty(value = "主键ID")
private Long id;
/**
* 二级学院ID,关联sys_dept.dept_id
*/
@ExcelProperty(value = "二级学院ID")
private Long collegeId;
/**
* 二级学院名称
*/
@ExcelProperty(value = "二级学院名称")
private String collegeName;
/**
* 分管领导ID,关联sys_user.user_id
*/
@ExcelProperty(value = "分管领导ID")
private Long leaderId;
/**
* 分管领导姓名
*/
@ExcelProperty(value = "分管领导姓名")
private String leaderName;
/**
* 备注信息
*/
@ExcelProperty(value = "备注")
private String remark;
/**
* 创建者
*/
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;
}
@@ -0,0 +1,15 @@
package org.dromara.xg.mapper;
import org.dromara.xg.domain.XgCollegeLeader;
import org.dromara.xg.domain.vo.XgCollegeLeaderVo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
/**
* 二级学院分管领导配置Mapper接口
*
* @author Cursor AI
* @date 2025-01-24
*/
public interface XgCollegeLeaderMapper extends BaseMapperPlus<XgCollegeLeader, XgCollegeLeaderVo> {
}
@@ -0,0 +1,85 @@
package org.dromara.xg.mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 学生动向信息管理(宿管员)Mapper接口
*
* @author zlwu
* @date 2025-01-15
*/
public interface XgStudentDynamicsMapper {
/**
* 检查用户是否有宿舍管理权限
*
* @param userId 用户ID
* @return 管理员类型列表 (1全校管理员 2校区管理员 3宿舍区管理员 4宿舍楼管理员)
*/
List<String> checkUserDormManagerType(@Param("userId") Long userId);
/**
* 获取全校管理员权限的所有楼栋ID列表
*
* @param userId 用户ID
* @return 楼栋ID列表
*/
List<Long> getBuildingIdsBySchoolAdmin(@Param("userId") Long userId);
/**
* 获取校区管理员权限的楼栋ID列表
*
* @param userId 用户ID
* @return 楼栋ID列表
*/
List<Long> getBuildingIdsByCampusAdmin(@Param("userId") Long userId);
/**
* 获取宿舍区管理员权限的楼栋ID列表
*
* @param userId 用户ID
* @return 楼栋ID列表
*/
List<Long> getBuildingIdsByAreaAdmin(@Param("userId") Long userId);
/**
* 获取宿舍楼管理员权限的楼栋ID列表
*
* @param userId 用户ID
* @return 楼栋ID列表
*/
List<Long> getBuildingIdsByBuildingAdmin(@Param("userId") Long userId);
/**
* 获取全校管理员权限的所有学生ID列表
*
* @param userId 用户ID
* @return 学生ID列表
*/
List<Long> getStudentIdsBySchoolAdmin(@Param("userId") Long userId);
/**
* 获取校区管理员权限的学生ID列表
*
* @param userId 用户ID
* @return 学生ID列表
*/
List<Long> getStudentIdsByCampusAdmin(@Param("userId") Long userId);
/**
* 获取宿舍区管理员权限的学生ID列表
*
* @param userId 用户ID
* @return 学生ID列表
*/
List<Long> getStudentIdsByAreaAdmin(@Param("userId") Long userId);
/**
* 获取宿舍楼管理员权限的学生ID列表
*
* @param userId 用户ID
* @return 学生ID列表
*/
List<Long> getStudentIdsByBuildingAdmin(@Param("userId") Long userId);
}
@@ -78,6 +78,14 @@ public interface XgStudentMsgMapper extends BaseMapperPlus<XgStudentMsg, XgStude
List<Long> selectUserIdsByCollegeIdAndRoleKey(@Param("collegeId") Long collegeId,
@Param("roleKey") String roleKey);
/**
* 根据学院ID查询二级学院分管领导ID列表
*
* @param collegeId 学院ID
* @return 分管领导ID列表
*/
List<Long> selectCollegeLeaderIdsByCollegeId(@Param("collegeId") Long collegeId);
/**
* 查询二级学院下的学院、专业、班级学生统计信息
*
@@ -0,0 +1,76 @@
package org.dromara.xg.service;
import org.dromara.xg.domain.vo.XgCollegeLeaderVo;
import org.dromara.xg.domain.bo.XgCollegeLeaderBo;
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 Cursor AI
* @date 2025-01-24
*/
public interface IXgCollegeLeaderService {
/**
* 查询二级学院分管领导配置
*
* @param id 主键
* @return 二级学院分管领导配置
*/
XgCollegeLeaderVo queryById(Long id);
/**
* 分页查询二级学院分管领导配置列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 二级学院分管领导配置分页列表
*/
TableDataInfo<XgCollegeLeaderVo> queryPageList(XgCollegeLeaderBo bo, PageQuery pageQuery);
/**
* 查询符合条件的二级学院分管领导配置列表
*
* @param bo 查询条件
* @return 二级学院分管领导配置列表
*/
List<XgCollegeLeaderVo> queryList(XgCollegeLeaderBo bo);
/**
* 新增二级学院分管领导配置
*
* @param bo 二级学院分管领导配置
* @return 是否新增成功
*/
Boolean insertByBo(XgCollegeLeaderBo bo);
/**
* 修改二级学院分管领导配置
*
* @param bo 二级学院分管领导配置
* @return 是否修改成功
*/
Boolean updateByBo(XgCollegeLeaderBo bo);
/**
* 校验并批量删除二级学院分管领导配置信息
*
* @param ids 待删除的主键集合
* @param isValid 是否进行有效性校验
* @return 是否删除成功
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 根据学院ID查询分管领导列表
*
* @param collegeId 学院ID
* @return 分管领导列表
*/
List<XgCollegeLeaderVo> queryByCollegeId(Long collegeId);
}
@@ -0,0 +1,49 @@
package org.dromara.xg.service;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.xg.domain.vo.XgDormApplicationVo;
import org.dromara.xg.domain.vo.XgHolidayStayVo;
import org.dromara.xg.domain.vo.XgStudentLeaveVo;
import org.dromara.xg.domain.bo.XgDormApplicationBo;
import org.dromara.xg.domain.bo.XgHolidayStayBo;
import org.dromara.xg.domain.bo.XgStudentLeaveBo;
/**
* 学生动向信息管理(宿管员)Service接口
*
* @author zlwu
* @date 2025-01-15
*/
public interface IXgStudentDynamicsService {
/**
* 查询宿管员权限内的学生调宿退宿申请列表
*/
TableDataInfo<XgDormApplicationVo> queryDormApplicationPageList(XgDormApplicationBo bo, PageQuery pageQuery);
/**
* 查询宿管员权限内的学生假期留宿信息列表
*/
TableDataInfo<XgHolidayStayVo> queryHolidayStayPageList(XgHolidayStayBo bo, PageQuery pageQuery);
/**
* 查询宿管员权限内的学生请假信息列表
*/
TableDataInfo<XgStudentLeaveVo> queryStudentLeavePageList(XgStudentLeaveBo bo, PageQuery pageQuery);
/**
* 根据主键查询调宿退宿申请详情
*/
XgDormApplicationVo queryDormApplicationById(Long id);
/**
* 根据主键查询假期留宿详情
*/
XgHolidayStayVo queryHolidayStayById(Long id);
/**
* 根据主键查询学生请假详情
*/
XgStudentLeaveVo queryStudentLeaveById(Long id);
}
@@ -140,6 +140,14 @@ public interface IXgStudentMsgService {
*/
List<Long> getCollegeLeaveApproversByStudentId(Long studentId);
/**
* 根据学生ID获取二级学院分管领导ID列表
*
* @param studentId 学生ID
* @return 分管领导ID列表
*/
List<Long> getCollegeLeadersByStudentId(Long studentId);
/**
* 批量申请床位
*
@@ -0,0 +1,163 @@
package org.dromara.xg.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 lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.dromara.xg.domain.bo.XgCollegeLeaderBo;
import org.dromara.xg.domain.vo.XgCollegeLeaderVo;
import org.dromara.xg.domain.XgCollegeLeader;
import org.dromara.xg.mapper.XgCollegeLeaderMapper;
import org.dromara.xg.service.IXgCollegeLeaderService;
import java.util.List;
import java.util.Map;
import java.util.Collection;
/**
* 二级学院分管领导配置Service业务层处理
*
* @author Cursor AI
* @date 2025-01-24
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class XgCollegeLeaderServiceImpl implements IXgCollegeLeaderService {
private final XgCollegeLeaderMapper baseMapper;
/**
* 查询二级学院分管领导配置
*
* @param id 主键
* @return 二级学院分管领导配置
*/
@Override
public XgCollegeLeaderVo queryById(Long id) {
return baseMapper.selectVoById(id);
}
/**
* 分页查询二级学院分管领导配置列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 二级学院分管领导配置分页列表
*/
@Override
public TableDataInfo<XgCollegeLeaderVo> queryPageList(XgCollegeLeaderBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<XgCollegeLeader> lqw = buildQueryWrapper(bo);
Page<XgCollegeLeaderVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询符合条件的二级学院分管领导配置列表
*
* @param bo 查询条件
* @return 二级学院分管领导配置列表
*/
@Override
public List<XgCollegeLeaderVo> queryList(XgCollegeLeaderBo bo) {
LambdaQueryWrapper<XgCollegeLeader> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
/**
* 根据学院ID查询分管领导列表
*
* @param collegeId 学院ID
* @return 分管领导列表
*/
@Override
public List<XgCollegeLeaderVo> queryByCollegeId(Long collegeId) {
LambdaQueryWrapper<XgCollegeLeader> lqw = Wrappers.lambdaQuery();
lqw.eq(XgCollegeLeader::getCollegeId, collegeId);
lqw.orderByDesc(XgCollegeLeader::getCreateTime);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<XgCollegeLeader> buildQueryWrapper(XgCollegeLeaderBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<XgCollegeLeader> lqw = Wrappers.lambdaQuery();
lqw.eq(bo.getCollegeId() != null, XgCollegeLeader::getCollegeId, bo.getCollegeId());
lqw.like(StringUtils.isNotBlank(bo.getCollegeName()), XgCollegeLeader::getCollegeName, bo.getCollegeName());
lqw.eq(bo.getLeaderId() != null, XgCollegeLeader::getLeaderId, bo.getLeaderId());
lqw.like(StringUtils.isNotBlank(bo.getLeaderName()), XgCollegeLeader::getLeaderName, bo.getLeaderName());
lqw.orderByDesc(XgCollegeLeader::getCreateTime);
return lqw;
}
/**
* 新增二级学院分管领导配置
*
* @param bo 二级学院分管领导配置
* @return 是否新增成功
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean insertByBo(XgCollegeLeaderBo bo) {
XgCollegeLeader add = MapstructUtils.convert(bo, XgCollegeLeader.class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setId(add.getId());
}
return flag;
}
/**
* 修改二级学院分管领导配置
*
* @param bo 二级学院分管领导配置
* @return 是否修改成功
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean updateByBo(XgCollegeLeaderBo bo) {
XgCollegeLeader update = MapstructUtils.convert(bo, XgCollegeLeader.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(XgCollegeLeader entity) {
// 校验是否已存在相同的配置
LambdaQueryWrapper<XgCollegeLeader> lqw = Wrappers.lambdaQuery();
lqw.eq(XgCollegeLeader::getCollegeId, entity.getCollegeId());
lqw.eq(XgCollegeLeader::getLeaderId, entity.getLeaderId());
if (entity.getId() != null) {
lqw.ne(XgCollegeLeader::getId, entity.getId());
}
long count = baseMapper.selectCount(lqw);
if (count > 0) {
throw new RuntimeException("该学院已配置此分管领导,不能重复配置!");
}
}
/**
* 校验并批量删除二级学院分管领导配置信息
*
* @param ids 待删除的主键集合
* @param isValid 是否进行有效性校验
* @return 是否删除成功
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
// 可以添加业务校验逻辑
}
return baseMapper.deleteByIds(ids) > 0;
}
}
@@ -0,0 +1,353 @@
package org.dromara.xg.service.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.xg.domain.vo.XgDormApplicationVo;
import org.dromara.xg.domain.vo.XgHolidayStayVo;
import org.dromara.xg.domain.vo.XgStudentLeaveVo;
import org.dromara.xg.domain.bo.XgDormApplicationBo;
import org.dromara.xg.domain.bo.XgHolidayStayBo;
import org.dromara.xg.domain.bo.XgStudentLeaveBo;
import org.dromara.xg.domain.XgDormApplication;
import org.dromara.xg.domain.XgHolidayStay;
import org.dromara.xg.domain.XgStudentLeave;
import org.dromara.xg.mapper.XgDormApplicationMapper;
import org.dromara.xg.mapper.XgHolidayStayMapper;
import org.dromara.xg.mapper.XgStudentLeaveMapper;
import org.dromara.xg.mapper.XgStudentDynamicsMapper;
import org.dromara.xg.service.IXgStudentDynamicsService;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 学生动向信息管理(宿管员)Service实现
*
* @author zlwu
* @date 2025-01-15
*/
@RequiredArgsConstructor
@Service
public class XgStudentDynamicsServiceImpl implements IXgStudentDynamicsService {
private final XgDormApplicationMapper dormApplicationMapper;
private final XgHolidayStayMapper holidayStayMapper;
private final XgStudentLeaveMapper studentLeaveMapper;
private final XgStudentDynamicsMapper studentDynamicsMapper;
/**
* 查询宿管员权限内的学生调宿退宿申请列表
*/
@Override
public TableDataInfo<XgDormApplicationVo> queryDormApplicationPageList(XgDormApplicationBo bo,
PageQuery pageQuery) {
// 获取当前登录用户ID
Long currentUserId = LoginHelper.getUserId();
// 构建查询条件
LambdaQueryWrapper<XgDormApplication> lqw = buildDormApplicationQueryWrapper(bo, currentUserId);
// 分页查询
Page<XgDormApplicationVo> result = dormApplicationMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询宿管员权限内的学生假期留宿信息列表
*/
@Override
public TableDataInfo<XgHolidayStayVo> queryHolidayStayPageList(XgHolidayStayBo bo, PageQuery pageQuery) {
// 获取当前登录用户ID
Long currentUserId = LoginHelper.getUserId();
// 构建查询条件
LambdaQueryWrapper<XgHolidayStay> lqw = buildHolidayStayQueryWrapper(bo, currentUserId);
// 分页查询
Page<XgHolidayStayVo> result = holidayStayMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询宿管员权限内的学生请假信息列表
*/
@Override
public TableDataInfo<XgStudentLeaveVo> queryStudentLeavePageList(XgStudentLeaveBo bo, PageQuery pageQuery) {
// 获取当前登录用户ID
Long currentUserId = LoginHelper.getUserId();
// 构建查询条件
LambdaQueryWrapper<XgStudentLeave> lqw = buildStudentLeaveQueryWrapper(bo, currentUserId);
// 分页查询
Page<XgStudentLeaveVo> result = studentLeaveMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 根据主键查询调宿退宿申请详情
*/
@Override
public XgDormApplicationVo queryDormApplicationById(Long id) {
return dormApplicationMapper.selectVoById(id);
}
/**
* 根据主键查询假期留宿详情
*/
@Override
public XgHolidayStayVo queryHolidayStayById(Long id) {
return holidayStayMapper.selectVoById(id);
}
/**
* 根据主键查询学生请假详情
*/
@Override
public XgStudentLeaveVo queryStudentLeaveById(Long id) {
return studentLeaveMapper.selectVoById(id);
}
/**
* 构建调宿退宿申请查询条件(带宿管员权限)
*/
private LambdaQueryWrapper<XgDormApplication> buildDormApplicationQueryWrapper(XgDormApplicationBo bo,
Long currentUserId) {
LambdaQueryWrapper<XgDormApplication> lqw = new LambdaQueryWrapper<>();
// 获取宿管员管理的楼栋ID列表
List<Long> managedBuildingIds = getManagedBuildingIds(currentUserId);
if (managedBuildingIds.isEmpty()) {
// 如果没有管理权限,返回空结果
lqw.eq(XgDormApplication::getId, -1);
return lqw;
}
// 只查询宿管员权限内楼栋的学生申请
lqw.in(XgDormApplication::getCurrentBuildingId, managedBuildingIds);
// 添加其他查询条件
// 姓名或学号检索(OR关系)
if (bo.getStudentName() != null && bo.getStudentNo() != null) {
lqw.and(wrapper -> wrapper
.like(XgDormApplication::getStudentName, bo.getStudentName())
.or()
.like(XgDormApplication::getStudentNo, bo.getStudentNo()));
} else {
lqw.like(bo.getStudentName() != null, XgDormApplication::getStudentName, bo.getStudentName());
lqw.like(bo.getStudentNo() != null, XgDormApplication::getStudentNo, bo.getStudentNo());
}
lqw.like(bo.getCollegeName() != null, XgDormApplication::getCollegeName, bo.getCollegeName());
lqw.like(bo.getMajorName() != null, XgDormApplication::getMajorName, bo.getMajorName());
lqw.like(bo.getClassName() != null, XgDormApplication::getClassName, bo.getClassName());
lqw.eq(bo.getApplicationType() != null, XgDormApplication::getApplicationType, bo.getApplicationType());
lqw.eq(bo.getApplicationStatus() != null, XgDormApplication::getApplicationStatus, bo.getApplicationStatus());
lqw.ge(bo.getApplyTime() != null, XgDormApplication::getApplyTime, bo.getApplyTime());
// 默认按创建时间倒序
lqw.orderByDesc(XgDormApplication::getCreateTime);
return lqw;
}
/**
* 构建假期留宿查询条件(带宿管员权限)
*/
private LambdaQueryWrapper<XgHolidayStay> buildHolidayStayQueryWrapper(XgHolidayStayBo bo, Long currentUserId) {
LambdaQueryWrapper<XgHolidayStay> lqw = new LambdaQueryWrapper<>();
// 获取宿管员管理的楼栋ID列表
List<Long> managedBuildingIds = getManagedBuildingIds(currentUserId);
if (managedBuildingIds.isEmpty()) {
// 如果没有管理权限,返回空结果
lqw.eq(XgHolidayStay::getId, -1);
return lqw;
}
// 只查询宿管员权限内楼栋的学生留宿
lqw.in(XgHolidayStay::getBuildingId, managedBuildingIds);
// 添加其他查询条件
// 姓名或学号检索(OR关系)
if (bo.getStudentName() != null && bo.getStudentNo() != null) {
lqw.and(wrapper -> wrapper
.like(XgHolidayStay::getStudentName, bo.getStudentName())
.or()
.like(XgHolidayStay::getStudentNo, bo.getStudentNo()));
} else {
lqw.like(bo.getStudentName() != null, XgHolidayStay::getStudentName, bo.getStudentName());
lqw.like(bo.getStudentNo() != null, XgHolidayStay::getStudentNo, bo.getStudentNo());
}
lqw.like(bo.getCollegeName() != null, XgHolidayStay::getCollegeName, bo.getCollegeName());
lqw.like(bo.getMajorName() != null, XgHolidayStay::getMajorName, bo.getMajorName());
lqw.like(bo.getClassName() != null, XgHolidayStay::getClassName, bo.getClassName());
lqw.eq(bo.getHolidayType() != null, XgHolidayStay::getHolidayType, bo.getHolidayType());
lqw.ge(bo.getApplyStartDate() != null, XgHolidayStay::getApplyStartDate, bo.getApplyStartDate());
lqw.le(bo.getApplyEndDate() != null, XgHolidayStay::getApplyEndDate, bo.getApplyEndDate());
// 默认按创建时间倒序
lqw.orderByDesc(XgHolidayStay::getCreateTime);
return lqw;
}
/**
* 构建学生请假查询条件(带宿管员权限)
*/
private LambdaQueryWrapper<XgStudentLeave> buildStudentLeaveQueryWrapper(XgStudentLeaveBo bo, Long currentUserId) {
LambdaQueryWrapper<XgStudentLeave> lqw = new LambdaQueryWrapper<>();
// 获取宿管员权限内的学生ID列表
List<Long> managedStudentIds = getManagedStudentIds(currentUserId);
if (managedStudentIds.isEmpty()) {
// 如果没有管理权限,返回空结果
lqw.eq(XgStudentLeave::getId, -1);
return lqw;
}
// 只查询宿管员权限内的学生请假
lqw.in(XgStudentLeave::getStudentId, managedStudentIds);
// 添加其他查询条件
// 姓名或学号检索(OR关系)
if (bo.getStudentName() != null && bo.getStudentNo() != null) {
lqw.and(wrapper -> wrapper
.like(XgStudentLeave::getStudentName, bo.getStudentName())
.or()
.like(XgStudentLeave::getStudentNo, bo.getStudentNo()));
} else {
lqw.like(bo.getStudentName() != null, XgStudentLeave::getStudentName, bo.getStudentName());
lqw.like(bo.getStudentNo() != null, XgStudentLeave::getStudentNo, bo.getStudentNo());
}
lqw.like(bo.getCollegeName() != null, XgStudentLeave::getCollegeName, bo.getCollegeName());
lqw.like(bo.getMajorName() != null, XgStudentLeave::getMajorName, bo.getMajorName());
lqw.like(bo.getClassName() != null, XgStudentLeave::getClassName, bo.getClassName());
lqw.like(bo.getLeaveTypeName() != null, XgStudentLeave::getLeaveTypeName, bo.getLeaveTypeName());
lqw.eq(bo.getApprovalStatus() != null, XgStudentLeave::getApprovalStatus, bo.getApprovalStatus());
lqw.ge(bo.getStartTime() != null, XgStudentLeave::getStartTime, bo.getStartTime());
lqw.le(bo.getEndTime() != null, XgStudentLeave::getEndTime, bo.getEndTime());
// 默认按创建时间倒序
lqw.orderByDesc(XgStudentLeave::getCreateTime);
return lqw;
}
/**
* 获取宿管员管理的楼栋ID列表(支持多级权限合并)
*/
private List<Long> getManagedBuildingIds(Long userId) {
// 查询用户管理权限类型
List<String> managerTypes = studentDynamicsMapper.checkUserDormManagerType(userId);
if (managerTypes == null || managerTypes.isEmpty()) {
// 没有宿舍管理权限,返回空列表
return new ArrayList<>();
}
// 存储所有权限的楼栋ID,使用Set去重
Set<Long> buildingIdSet = new HashSet<>();
// 按照权限分别查询并合并结果(支持用户拥有多种角色的情况)
if (managerTypes.contains("1")) {
// 全校管理员,获取所有楼栋
List<Long> allBuildings = studentDynamicsMapper.getBuildingIdsBySchoolAdmin(userId);
if (allBuildings != null) {
buildingIdSet.addAll(allBuildings);
}
// 全校管理员可以查看所有楼栋,直接返回
return new ArrayList<>(buildingIdSet);
}
// 对于非全校管理员,合并多种权限的结果
if (managerTypes.contains("2")) {
// 校区管理员,获取管理的校区下的楼栋
List<Long> campusBuildings = studentDynamicsMapper.getBuildingIdsByCampusAdmin(userId);
if (campusBuildings != null) {
buildingIdSet.addAll(campusBuildings);
}
}
if (managerTypes.contains("3")) {
// 宿舍区管理员,获取管理的宿舍区下的楼栋
List<Long> areaBuildings = studentDynamicsMapper.getBuildingIdsByAreaAdmin(userId);
if (areaBuildings != null) {
buildingIdSet.addAll(areaBuildings);
}
}
if (managerTypes.contains("4")) {
// 宿舍楼管理员,获取管理的楼栋
List<Long> directBuildings = studentDynamicsMapper.getBuildingIdsByBuildingAdmin(userId);
if (directBuildings != null) {
buildingIdSet.addAll(directBuildings);
}
}
return new ArrayList<>(buildingIdSet);
}
/**
* 获取宿管员权限内的学生ID列表(支持多级权限合并)
*/
private List<Long> getManagedStudentIds(Long userId) {
// 查询用户管理权限类型
List<String> managerTypes = studentDynamicsMapper.checkUserDormManagerType(userId);
if (managerTypes == null || managerTypes.isEmpty()) {
// 没有宿舍管理权限,返回空列表
return new ArrayList<>();
}
// 存储所有权限的学生ID,使用Set去重
Set<Long> studentIdSet = new HashSet<>();
// 按照权限分别查询并合并结果(支持用户拥有多种角色的情况)
if (managerTypes.contains("1")) {
// 全校管理员,获取所有学生
List<Long> allStudents = studentDynamicsMapper.getStudentIdsBySchoolAdmin(userId);
if (allStudents != null) {
studentIdSet.addAll(allStudents);
}
// 全校管理员可以查看所有学生,直接返回
return new ArrayList<>(studentIdSet);
}
// 对于非全校管理员,合并多种权限的结果
if (managerTypes.contains("2")) {
// 校区管理员,获取管理的校区下的学生
List<Long> campusStudents = studentDynamicsMapper.getStudentIdsByCampusAdmin(userId);
if (campusStudents != null) {
studentIdSet.addAll(campusStudents);
}
}
if (managerTypes.contains("3")) {
// 宿舍区管理员,获取管理的宿舍区下的学生
List<Long> areaStudents = studentDynamicsMapper.getStudentIdsByAreaAdmin(userId);
if (areaStudents != null) {
studentIdSet.addAll(areaStudents);
}
}
if (managerTypes.contains("4")) {
// 宿舍楼管理员,获取管理的楼栋下的学生
List<Long> buildingStudents = studentDynamicsMapper.getStudentIdsByBuildingAdmin(userId);
if (buildingStudents != null) {
studentIdSet.addAll(buildingStudents);
}
}
return new ArrayList<>(studentIdSet);
}
}
@@ -1095,6 +1095,32 @@ public class XgStudentMsgServiceImpl implements IXgStudentMsgService {
return approverUserIds;
}
/**
* 根据学生ID获取二级学院分管领导ID列表
*
* @param studentId 学生ID
* @return 分管领导ID列表
*/
@Override
public List<Long> getCollegeLeadersByStudentId(Long studentId) {
log.info("根据学生ID获取二级学院分管领导ID列表 - 学生ID: {}", studentId);
// 1. 根据学生ID查询学院ID
Long collegeId = baseMapper.selectCollegeIdByStudentId(studentId);
if (collegeId == null) {
log.warn("未找到学生对应的学院信息 - 学生ID: {}", studentId);
return new ArrayList<>();
}
log.info("学生所在学院ID: {}", collegeId);
// 2. 根据学院ID查询二级学院分管领导ID列表
List<Long> leaderIds = baseMapper.selectCollegeLeaderIdsByCollegeId(collegeId);
log.info("找到的二级学院分管领导数量: {}", leaderIds.size());
return leaderIds;
}
/**
* 批量申请床位
*
@@ -0,0 +1,8 @@
<?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.xg.mapper.XgCollegeLeaderMapper">
</mapper>
@@ -171,6 +171,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
AND (d.dept_id = #{collegeId} OR FIND_IN_SET(#{collegeId}, d.ancestors))
</select>
<!-- 根据学院ID查询二级学院分管领导ID列表 -->
<select id="selectCollegeLeaderIdsByCollegeId" resultType="java.lang.Long">
SELECT leader_id
FROM xg_college_leader
WHERE college_id = #{collegeId}
AND deleted = 0
</select>
<!-- 查询二级学院下的学院、专业、班级学生统计信息 -->
<select id="getCollegeStudentStats" resultType="java.util.Map">
SELECT
@@ -0,0 +1,213 @@
<?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.xg.mapper.XgStudentDynamicsMapper">
<!-- 检查用户是否有宿舍管理权限 -->
<select id="checkUserDormManagerType" resultType="String">
SELECT
DISTINCT manager_type
FROM
xg_dorm_manager
WHERE
user_id = #{userId}
AND deleted = '0'
AND status = '0'
</select>
<!-- 获取全校管理员权限的所有楼栋ID列表 -->
<select id="getBuildingIdsBySchoolAdmin" resultType="Long">
SELECT
id
FROM
xg_dorm_building
WHERE
deleted = '0'
AND EXISTS (
SELECT 1 FROM xg_dorm_manager m
WHERE m.user_id = #{userId}
AND m.manager_type = '1'
AND m.deleted = '0'
AND m.status = '0'
)
</select>
<!-- 获取校区管理员权限的楼栋ID列表 -->
<select id="getBuildingIdsByCampusAdmin" resultType="Long">
SELECT
b.id
FROM
xg_dorm_building b
WHERE
b.deleted = '0'
AND b.campus_id IN (
SELECT
s.auth_source
FROM
xg_dorm_manager m
JOIN
xg_dorm_manager_source s ON m.id = s.manager_id
WHERE
m.user_id = #{userId}
AND m.manager_type = '2'
AND m.deleted = '0'
AND m.status = '0'
AND s.deleted = '0'
AND s.status = '0'
)
</select>
<!-- 获取宿舍区管理员权限的楼栋ID列表 -->
<select id="getBuildingIdsByAreaAdmin" resultType="Long">
SELECT
b.id
FROM
xg_dorm_building b
WHERE
b.deleted = '0'
AND b.area_id IN (
SELECT
s.auth_source
FROM
xg_dorm_manager m
JOIN
xg_dorm_manager_source s ON m.id = s.manager_id
WHERE
m.user_id = #{userId}
AND m.manager_type = '3'
AND m.deleted = '0'
AND m.status = '0'
AND s.deleted = '0'
AND s.status = '0'
)
</select>
<!-- 获取宿舍楼管理员权限的楼栋ID列表 -->
<select id="getBuildingIdsByBuildingAdmin" resultType="Long">
SELECT
b.id
FROM
xg_dorm_building b
WHERE
b.deleted = '0'
AND b.id IN (
SELECT
s.auth_source
FROM
xg_dorm_manager m
JOIN
xg_dorm_manager_source s ON m.id = s.manager_id
WHERE
m.user_id = #{userId}
AND m.manager_type = '4'
AND m.deleted = '0'
AND m.status = '0'
AND s.deleted = '0'
AND s.status = '0'
)
</select>
<!-- 获取全校管理员权限的所有学生ID列表 -->
<select id="getStudentIdsBySchoolAdmin" resultType="Long">
SELECT
sm.id
FROM
xg_student_msg sm
WHERE
sm.deleted = 0
AND EXISTS (
SELECT 1 FROM xg_dorm_manager m
WHERE m.user_id = #{userId}
AND m.manager_type = '1'
AND m.deleted = '0'
AND m.status = '0'
)
</select>
<!-- 获取校区管理员权限的学生ID列表 -->
<select id="getStudentIdsByCampusAdmin" resultType="Long">
SELECT
sm.id
FROM
xg_student_msg sm
INNER JOIN
xg_dorm_building db ON sm.dormitory_building = db.id
WHERE
sm.deleted = 0
AND db.deleted = '0'
AND db.campus_id IN (
SELECT
s.auth_source
FROM
xg_dorm_manager m
JOIN
xg_dorm_manager_source s ON m.id = s.manager_id
WHERE
m.user_id = #{userId}
AND m.manager_type = '2'
AND m.deleted = '0'
AND m.status = '0'
AND s.deleted = '0'
AND s.status = '0'
)
</select>
<!-- 获取宿舍区管理员权限的学生ID列表 -->
<select id="getStudentIdsByAreaAdmin" resultType="Long">
SELECT
sm.id
FROM
xg_student_msg sm
INNER JOIN
xg_dorm_building db ON sm.dormitory_building = db.id
WHERE
sm.deleted = 0
AND db.deleted = '0'
AND db.area_id IN (
SELECT
s.auth_source
FROM
xg_dorm_manager m
JOIN
xg_dorm_manager_source s ON m.id = s.manager_id
WHERE
m.user_id = #{userId}
AND m.manager_type = '3'
AND m.deleted = '0'
AND m.status = '0'
AND s.deleted = '0'
AND s.status = '0'
)
</select>
<!-- 获取宿舍楼管理员权限的学生ID列表 -->
<select id="getStudentIdsByBuildingAdmin" resultType="Long">
SELECT
sm.id
FROM
xg_student_msg sm
INNER JOIN
xg_dorm_building db ON sm.dormitory_building = db.id
WHERE
sm.deleted = 0
AND db.deleted = '0'
AND db.id IN (
SELECT
s.auth_source
FROM
xg_dorm_manager m
JOIN
xg_dorm_manager_source s ON m.id = s.manager_id
WHERE
m.user_id = #{userId}
AND m.manager_type = '4'
AND m.deleted = '0'
AND m.status = '0'
AND s.deleted = '0'
AND s.status = '0'
)
</select>
</mapper>
@@ -6,10 +6,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<resultMap type="org.dromara.xg.domain.XgSysUser" id="XgSysUserResult">
<id property="userId" column="user_id"/>
<result property="userName" column="user_name"/>
<result property="nickName" column="nick_name"/>
<result property="phonenumber" column="phonenumber"/>
<result property="status" column="status"/>
<result property="delFlag" column="del_flag"/>
<result property="accountType" column="account_type"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
@@ -18,7 +20,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<!-- 查询有效用户列表 -->
<select id="selectActiveUserList" resultMap="XgSysUserResult">
select user_id, nick_name, phonenumber
select user_id, user_name, nick_name, phonenumber, account_type
from sys_user
where status = '0' and del_flag = '0'
order by create_time desc
@@ -26,7 +28,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<!-- 根据账户类型查询有效用户列表 -->
<select id="selectActiveUserListByAccountType" parameterType="String" resultMap="XgSysUserResult">
select user_id, nick_name, phonenumber
select user_id, user_name, nick_name, phonenumber, account_type
from sys_user
where status = '0' and del_flag = '0' and account_type = #{accountType}
order by create_time desc
@@ -112,6 +112,18 @@ public class XskhDisciplineRecordController extends BaseController {
return R.ok(xskhDisciplineRecordService.getStudentOptions(keyword));
}
/**
* 分页查询学生选项列表(用于移动端)
*/
@SaCheckPermission("disciplineRecord:disciplineRecord:query")
@GetMapping("/studentOptionsPage")
public R<List<XskhStudentOptionVo>> getStudentOptionsPage(
@RequestParam(required = false) String keyword,
@RequestParam(required = false, defaultValue = "1") Integer pageNum,
@RequestParam(required = false, defaultValue = "20") Integer pageSize) {
return R.ok(xskhDisciplineRecordService.getStudentOptionsPage(keyword, pageNum, pageSize));
}
/**
* 查询当前登录学生的违纪处分记录列表
*/
@@ -166,4 +178,14 @@ public class XskhDisciplineRecordController extends BaseController {
@RequestParam String endTime) {
return R.ok(xskhDisciplineRecordService.getMaxScoreByTypeAndPeriod(typeId, startTime, endTime));
}
/**
* 查询当前用户创建的违纪记录列表(支持按学生姓名或学号搜索)
*/
@SaCheckPermission("disciplineRecord:disciplineRecord:query")
@GetMapping("/myCreated")
public TableDataInfo<XskhDisciplineRecordVo> getMyCreatedDisciplineRecords(XskhDisciplineRecordBo bo,
PageQuery pageQuery) {
return xskhDisciplineRecordService.queryMyCreatedPageList(bo, pageQuery);
}
}
@@ -23,9 +23,13 @@ public interface XskhDisciplineRecordMapper extends BaseMapperPlus<XskhDisciplin
* 查询学生选项列表(用于下拉框)
*
* @param keyword 搜索关键词(学号或姓名)
* @param pageNum 页码
* @param pageSize 每页数量
* @return 学生选项列表
*/
List<XskhStudentOptionVo> selectStudentOptions(String keyword);
List<XskhStudentOptionVo> selectStudentOptions(@Param("keyword") String keyword,
@Param("pageNum") Integer pageNum,
@Param("pageSize") Integer pageSize);
/**
* 查询班主任班级的违纪处分记录列表(班主任专用)
@@ -50,9 +50,22 @@ public interface IXskhDisciplineRecordService {
/**
* 查询学生选项列表(用于下拉框)
*
* @param keyword 搜索关键词
* @return 学生选项列表
*/
List<XskhStudentOptionVo> getStudentOptions(String keyword);
/**
* 分页查询学生选项列表(用于移动端)
*
* @param keyword 搜索关键词
* @param pageNum 页码
* @param pageSize 每页数量
* @return 学生选项列表
*/
List<XskhStudentOptionVo> getStudentOptionsPage(String keyword, Integer pageNum, Integer pageSize);
/**
* 查询当前登录学生的违纪处分记录列表
*/
@@ -101,4 +114,13 @@ public interface IXskhDisciplineRecordService {
* @return 最高总扣分
*/
java.math.BigDecimal getMaxScoreByTypeAndPeriod(Long typeId, String startTime, String endTime);
/**
* 查询当前用户创建的违纪记录列表(支持按学生姓名或学号搜索)
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 违纪记录分页列表
*/
TableDataInfo<XskhDisciplineRecordVo> queryMyCreatedPageList(XskhDisciplineRecordBo bo, PageQuery pageQuery);
}
@@ -165,7 +165,15 @@ public class XskhDisciplineRecordServiceImpl implements IXskhDisciplineRecordSer
*/
@Override
public List<XskhStudentOptionVo> getStudentOptions(String keyword) {
return baseMapper.selectStudentOptions(keyword);
return baseMapper.selectStudentOptions(keyword, null, null);
}
/**
* 分页查询学生选项列表(用于移动端)
*/
@Override
public List<XskhStudentOptionVo> getStudentOptionsPage(String keyword, Integer pageNum, Integer pageSize) {
return baseMapper.selectStudentOptions(keyword, pageNum, pageSize);
}
/**
@@ -286,4 +294,46 @@ public class XskhDisciplineRecordServiceImpl implements IXskhDisciplineRecordSer
log.info("违纪类型 {} 的最高总扣分为: {}", typeId, maxScore);
return maxScore;
}
/**
* 查询当前用户创建的违纪记录列表(支持按学生姓名或学号搜索)
*/
@Override
public TableDataInfo<XskhDisciplineRecordVo> queryMyCreatedPageList(XskhDisciplineRecordBo bo,
PageQuery pageQuery) {
// 获取当前登录用户ID
Long currentUserId = LoginHelper.getUserId();
log.info("查询用户 {} 创建的违纪记录列表", currentUserId);
// 构建查询条件,强制只查询当前用户创建的记录
LambdaQueryWrapper<XskhDisciplineRecord> lqw = Wrappers.lambdaQuery();
// 核心条件:只查询当前用户创建的记录
lqw.eq(XskhDisciplineRecord::getCreateBy, currentUserId);
// 支持按学生姓名或学号模糊搜索(使用OR连接)
if (StringUtils.isNotBlank(bo.getStudentName()) || StringUtils.isNotBlank(bo.getStudentNo())) {
String keyword = StringUtils.isNotBlank(bo.getStudentName()) ? bo.getStudentName() : bo.getStudentNo();
lqw.and(wrapper -> wrapper
.like(XskhDisciplineRecord::getStudentName, keyword)
.or()
.like(XskhDisciplineRecord::getStudentNo, keyword));
}
// 其他可选查询条件
lqw.eq(bo.getStudentId() != null, XskhDisciplineRecord::getStudentId, bo.getStudentId());
lqw.eq(bo.getCollegeId() != null, XskhDisciplineRecord::getCollegeId, bo.getCollegeId());
lqw.eq(bo.getMajorId() != null, XskhDisciplineRecord::getMajorId, bo.getMajorId());
lqw.eq(bo.getClassId() != null, XskhDisciplineRecord::getClassId, bo.getClassId());
lqw.eq(bo.getDisciplineTypeId() != null, XskhDisciplineRecord::getDisciplineTypeId, bo.getDisciplineTypeId());
lqw.eq(StringUtils.isNotBlank(bo.getStatus()), XskhDisciplineRecord::getStatus, bo.getStatus());
// 按创建时间倒序排列
lqw.orderByDesc(XskhDisciplineRecord::getCreateTime);
// 执行分页查询
Page<XskhDisciplineRecordVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
log.info("查询到 {} 条记录", result.getTotal());
return TableDataInfo.build(result);
}
}
@@ -49,7 +49,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
select id, student_id, student_no, student_name, college_id, college_name, major_id, major_name, class_id, class_name, discipline_type_id, discipline_type_name, discipline_date, discipline_reason, discipline_content, score_deduction, status, violation_source, attachment_oss_ids, create_by, create_time, create_dept, update_by, update_time, deleted, tenant_id from xskh_discipline_record
</sql>
<select id="selectStudentOptions" parameterType="String" resultMap="StudentOptionResult">
<select id="selectStudentOptions" resultMap="StudentOptionResult">
select
id, student_no, name, college_id, college_name,
major_id, major_name, class_id, class_name
@@ -61,7 +61,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
or name like concat('%', #{keyword}, '%'))
</if>
order by student_no
<choose>
<when test="pageNum != null and pageSize != null">
limit ${(pageNum - 1) * pageSize}, #{pageSize, jdbcType=INTEGER}
</when>
<otherwise>
limit 50
</otherwise>
</choose>
</select>
<select id="selectClassTeacherVoPage" resultType="XskhDisciplineRecordVo">
+100
View File
@@ -1,4 +1,104 @@
-- ----------------------------
-- 0、warm-flow-all.sql,地址:https://gitee.com/dromara/warm-flow/blob/master/sql/mysql/warm-flow-all.sql
-- ----------------------------
CREATE TABLE `flow_definition`
(
`id` bigint NOT NULL COMMENT '主键id',
`flow_code` varchar(40) NOT NULL COMMENT '流程编码',
`flow_name` varchar(100) NOT NULL COMMENT '流程名称',
`category` varchar(100) DEFAULT NULL COMMENT '流程类别',
`version` varchar(20) NOT NULL COMMENT '流程版本',
`is_publish` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否发布(0未发布 1已发布 9失效)',
`form_custom` char(1) DEFAULT 'N' COMMENT '审批表单是否自定义(Y是 N否)',
`form_path` varchar(100) DEFAULT NULL COMMENT '审批表单路径',
`activity_status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '流程激活状态(0挂起 1激活)',
`listener_type` varchar(100) DEFAULT NULL COMMENT '监听器类型',
`listener_path` varchar(400) DEFAULT NULL COMMENT '监听器路径',
`ext` varchar(500) DEFAULT NULL COMMENT '业务详情 存业务表对象json字符串',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志',
`tenant_id` varchar(40) DEFAULT NULL COMMENT '租户id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB COMMENT ='流程定义表';
CREATE TABLE `flow_node`
(
`id` bigint NOT NULL COMMENT '主键id',
`node_type` tinyint(1) NOT NULL COMMENT '节点类型(0开始节点 1中间节点 2结束节点 3互斥网关 4并行网关)',
`definition_id` bigint NOT NULL COMMENT '流程定义id',
`node_code` varchar(100) NOT NULL COMMENT '流程节点编码',
`node_name` varchar(100) DEFAULT NULL COMMENT '流程节点名称',
`permission_flag` varchar(200) DEFAULT NULL COMMENT '权限标识(权限类型:权限标识,可以多个,用逗号隔开)',
`node_ratio` decimal(6, 3) DEFAULT NULL COMMENT '流程签署比例值',
`coordinate` varchar(100) DEFAULT NULL COMMENT '坐标',
`any_node_skip` varchar(100) DEFAULT NULL COMMENT '任意结点跳转',
`listener_type` varchar(100) DEFAULT NULL COMMENT '监听器类型',
`listener_path` varchar(400) DEFAULT NULL COMMENT '监听器路径',
`handler_type` varchar(100) DEFAULT NULL COMMENT '处理器类型',
`handler_path` varchar(400) DEFAULT NULL COMMENT '处理器路径',
`form_custom` char(1) DEFAULT 'N' COMMENT '审批表单是否自定义(Y是 N否)',
`form_path` varchar(100) DEFAULT NULL COMMENT '审批表单路径',
`version` varchar(20) NOT NULL COMMENT '版本',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`ext` text COMMENT '扩展属性',
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志',
`tenant_id` varchar(40) DEFAULT NULL COMMENT '租户id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB COMMENT ='流程节点表';
CREATE TABLE `flow_skip`
(
`id` bigint NOT NULL COMMENT '主键id',
`definition_id` bigint NOT NULL COMMENT '流程定义id',
`now_node_code` varchar(100) NOT NULL COMMENT '当前流程节点的编码',
`now_node_type` tinyint(1) DEFAULT NULL COMMENT '当前节点类型(0开始节点 1中间节点 2结束节点 3互斥网关 4并行网关)',
`next_node_code` varchar(100) NOT NULL COMMENT '下一个流程节点的编码',
`next_node_type` tinyint(1) DEFAULT NULL COMMENT '下一个节点类型(0开始节点 1中间节点 2结束节点 3互斥网关 4并行网关)',
`skip_name` varchar(100) DEFAULT NULL COMMENT '跳转名称',
`skip_type` varchar(40) DEFAULT NULL COMMENT '跳转类型(PASS审批通过 REJECT退回)',
`skip_condition` varchar(200) DEFAULT NULL COMMENT '跳转条件',
`coordinate` varchar(100) DEFAULT NULL COMMENT '坐标',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志',
`tenant_id` varchar(40) DEFAULT NULL COMMENT '租户id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB COMMENT ='节点跳转关联表';
CREATE TABLE `flow_instance`
(
`id` bigint NOT NULL COMMENT '主键id',
`definition_id` bigint NOT NULL COMMENT '对应flow_definition表的id',
`business_id` varchar(40) NOT NULL COMMENT '业务id',
`node_type` tinyint(1) NOT NULL COMMENT '节点类型(0开始节点 1中间节点 2结束节点 3互斥网关 4并行网关)',
`node_code` varchar(40) NOT NULL COMMENT '流程节点编码',
`node_name` varchar(100) DEFAULT NULL COMMENT '流程节点名称',
`variable` text COMMENT '任务变量',
`flow_status` varchar(20) NOT NULL COMMENT '流程状态(0待提交 1审批中 2审批通过 4终止 5作废 6撤销 8已完成 9已退回 10失效 11拿回)',
`activity_status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '流程激活状态(0挂起 1激活)',
`def_json` text COMMENT '流程定义json',
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`ext` varchar(500) DEFAULT NULL COMMENT '扩展字段,预留给业务系统使用',
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志',
`tenant_id` varchar(40) DEFAULT NULL COMMENT '租户id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB COMMENT ='流程实例表';
CREATE TABLE `flow_task`
(
`id` bigint NOT NULL COMMENT '主键id',
`definition_id` bigint NOT NULL COMMENT '对应flow_definition表的id',
`instance_id` bigint NOT NULL COMMENT '对应flow_instance表的id',
`node_code` varchar(100) NOT NULL COMMENT '节点编码',
`node_name` varchar(100) DEFAULT NULL COMMENT '节点名称',
`node_type` tinyint(1) NOT NULL COMMENT '节点类型(0开始节点 1中间节点 2结束节点 3互斥网关 4并行网关)',
`flow_status` varchar(20) NOT NULL COMMENT '流程状态(0待提交 1审批中 2审批通过 4终止 5作废 6撤销 8已完成 9已退回 10失效 11拿回)',
`form_custom` char(1) DEFAULT 'N' COMMENT '审批表单是否自定义(Y是 N否)',
`form_path` varchar(100) DEFAULT NULL COMMENT '审批表单路径',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志',
@@ -1,75 +0,0 @@
# 代码调整记录 - 2025-01-08 - Cursor AI
## 📝 变更概述
**变更时间**: 2025-01-08 15:30:00
**变更人员**: Cursor AI
**变更类型**: 数据库结构修复
**影响模块**: 社团管理 - 活动报名功能
## 🔴 **问题描述**
**问题现象**: 活动报名接口数据插入失败,报错缺少字段
**根本原因**: `stgl_activity_registration` 数据库表结构与实体类不匹配,缺失多个字段
**缺失字段清单**:
- `remark` - 申请理由(非必填)
- `gender` - 性别
- `phone` - 联系电话
- `email` - 邮箱
- `department` - 院系
- `major` - 专业
- `class_name` - 班级
## ✅ **解决方案**
### 1. 创建修复SQL脚本
**文件**: `docs/设计文档/database/社团管理/修复活动报名表字段.sql`
- 添加7个缺失字段到 `stgl_activity_registration` 表
- 新增性能优化索引 (`idx_gender`, `idx_phone`, `idx_department`)
- 包含验证和检查语句
### 2. 更新建表脚本
**文件**: `docs/设计文档/database/社团管理/社团管理建表脚本.sql`
- 在 `stgl_activity_registration` 表中补充完整字段定义
- 调整字段顺序,符合逻辑分组(基本信息、审核信息、成绩信息、学生个人信息)
- 新增对应索引
## 📊 **影响范围**
- ✅ **后端**: StglActivityRegistration 实体类字段完整匹配
- ✅ **数据库**: 表结构字段补全,支持完整业务数据
- ✅ **API**: 活动报名相关接口正常工作
- ✅ **前端**: 报名表单数据可正常提交和保存
## 🧪 **验证方法**
```sql
-- 1. 执行修复脚本
source docs/设计文档/database/社团管理/修复活动报名表字段.sql
-- 2. 验证表结构
DESCRIBE stgl_activity_registration;
-- 3. 测试插入数据
-- 使用活动报名API接口测试完整数据提交
```
## 📋 **待执行任务**
- [x] 分析问题原因
- [x] 创建修复SQL脚本
- [x] 更新建表脚本
- [x] 记录变更日志
- [ ] **需要执行**: 在数据库中运行修复脚本
- [ ] **需要测试**: 验证活动报名功能正常
## 🔗 **相关文件**
- `backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglActivityRegistration.java`
- `backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglActivityRegistrationMapper.xml`
- `docs/设计文档/database/社团管理/社团管理建表脚本.sql`
- `docs/设计文档/database/社团管理/修复活动报名表字段.sql` (新增)
## 💡 **经验总结**
1. **数据库与实体类一致性**: 开发时必须确保数据库表结构与实体类字段完全匹配
2. **渐进式开发问题**: 实体类后续添加字段时,必须同步更新数据库表结构
3. **文档同步重要性**: 建表脚本必须与实际代码保持同步,避免部署时出现问题
4. **字段分组设计**: 表字段应该按业务逻辑分组,提高可读性和维护性
---
*本记录严格按照项目规范要求,代码变更与文档同步更新*
@@ -1,241 +0,0 @@
# 代码调整记录 - 2025-01-14 - cursor
## 📝 变更概述
**变更日期**: 2025-01-14 15:30:00
**处理人员**: cursor
**变更类型**: 前端功能修复 + UI优化
## 🎯 变更内容
### 1. 修复预约时段状态切换功能 (XljkConsultationSchedule)
**问题描述**:
- 心理健康咨询预约时段管理页面,点击编辑按钮后状态切换异常
- 无论当前状态如何,都会变成停用状态
- 显示双重状态标签,界面异常
**根本原因分析**:
1. **状态值类型不一致**: 前端接收的`status`是字符串类型(`"0"`, `"1"`),但切换逻辑使用数字比较(`=== 1`)
2. **字典数据缺失**: `schedule_status`字典未正确配置,导致显示异常
3. **状态更新接口数据类型错误**: 发送数字状态但接收端可能期望字符串
**修复方案**:
#### 1.1 修复状态切换逻辑
**文件**: `web/src/views/xljk/consultation/schedule/index.vue`
**修改前**:
```typescript
const handleUpdate = async (row?: XljkConsultationScheduleVO) => {
if (row && row.id) {
try {
await updateXljkSchedule({
id: row.id,
status: row.status === 1 ? 0 : 1 // 错误:字符串与数字比较
} as XljkConsultationScheduleForm);
ElMessage.success('状态更新成功');
getList();
} catch (error) {
console.error('更新时段状态失败', error);
ElMessage.error('状态更新失败');
}
}
};
```
**修改后**:
```typescript
const handleUpdate = async (row?: XljkConsultationScheduleVO) => {
if (row && row.id) {
try {
// 修复:将字符串状态正确转换为数字进行比较
const currentStatus = parseInt(row.status as string);
const newStatus = currentStatus === 1 ? 0 : 1;
console.log(`状态切换调试 - 当前状态: ${row.status} (${typeof row.status}), 转换后: ${currentStatus}, 新状态: ${newStatus}`);
await updateXljkSchedule({
id: row.id,
status: newStatus.toString()
} as XljkConsultationScheduleForm);
ElMessage.success(`状态已${newStatus === 0 ? '启用' : '停用'}`);
getList();
} catch (error) {
console.error('更新时段状态失败', error);
ElMessage.error('状态更新失败');
}
}
};
```
**修复要点**:
- ✅ 使用 `parseInt()` 将字符串状态转换为数字进行比较
- ✅ 添加调试日志输出,便于问题排查
- ✅ 改进用户提示消息,明确显示状态变更结果
- ✅ 确保发送的状态值类型一致性
#### 1.2 创建字典数据配置
**文件**: `backend/script/sql/xljk-dict-data.sql`
**新增内容**:
```sql
-- 预约时段状态字典
INSERT INTO sys_dict_type VALUES (102, '000000', '预约时段状态', 'schedule_status', 103, 1, sysdate(), NULL, NULL, '预约时段状态列表');
-- 预约时段状态字典数据
INSERT INTO sys_dict_data VALUES (300, '000000', 1, '正常', '0', 'schedule_status', '', 'success', 'Y', 103, 1, sysdate(), NULL, NULL, '正常状态');
INSERT INTO sys_dict_data VALUES (301, '000000', 2, '停用', '1', 'schedule_status', '', 'danger', 'N', 103, 1, sysdate(), NULL, NULL, '停用状态');
```
**配置要点**:
- ✅ 创建 `schedule_status` 字典类型
- ✅ 配置状态值:`0-正常`(success样式)、`1-停用`(danger样式)
- ✅ 同时创建相关的预约状态和时段类型字典
### 2. 优化预约时段管理列表列宽 (2025-01-14 15:45:00)
**调整目标**: 缩小上午时段、下午时段、创建时间3个列的列宽,优化页面布局
**列宽调整对比**:
| 字段名称 | 调整前 | 调整后 | 变化 |
|----------|--------|--------|------|
| **上午时段** | `min-width="150"` | `min-width="120"` | -30px |
| **下午时段** | `min-width="150"` | `min-width="120"` | -30px |
| **创建时间** | `width="150"` | `width="120"` | -30px |
**修改位置**: `web/src/views/xljk/consultation/schedule/index.vue` (第84-102行)
**修改内容**:
```vue
<!-- 上午时段列 -->
<el-table-column label="上午时段" align="center" min-width="120">
<!-- 下午时段列 -->
<el-table-column label="下午时段" align="center" min-width="120">
<!-- 创建时间列 -->
<el-table-column label="创建时间" align="center" prop="createTime" width="120">
```
**优化效果**:
- ✅ 总体节省空间:90px (30px × 3列)
- ✅ 上午/下午时段列保持响应式 (`min-width`)
- ✅ 创建时间列固定宽度更紧凑
- ✅ 为其他列提供更多显示空间
- ✅ 提升页面整体布局效率
**表格布局更新**:
- **原总最小宽度**: 1115px
- **新总最小宽度**: 1025px
- **空间优化**: 节省90px宽度
### 3. 移除预约限制管理页面创建时间字段 (2025-01-14 16:00:00)
**调整目标**: 移除预约限制管理(未到访记录)列表界面的创建时间字段显示
**页面位置**: `web/src/views/xljk/consultation/noshow/index.vue`
**移除字段**:
```vue
<!-- 已移除的创建时间列 -->
<el-table-column label="创建时间" align="center" prop="createTime" width="150">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
```
**修改位置**: 第92-96行
**优化效果**:
- ✅ **简化界面**: 移除不必要的时间信息显示
- ✅ **节省空间**: 释放150px列宽空间
- ✅ **聚焦核心**: 突出学生信息、限制状态等关键信息
- ✅ **提升体验**: 减少信息冗余,提高页面简洁度
**当前保留字段**:
- 学生姓名、学号、院系、专业、班级
- 未到访日期、限制到期日、状态
- 操作(查看、解除限制)
**表格布局优化**:
- **移除列宽**: 150px
- **界面更简洁**: 专注于业务核心信息
- **操作便捷**: 保留查看详情和解除限制功能
## 🔧 技术细节
### 问题原因深度分析
1. **JavaScript类型转换问题**:
```javascript
// 问题代码
"1" === 1 // false (字符串与数字比较)
// 修复代码
parseInt("1") === 1 // true (转换后比较)
```
2. **数据流类型不一致**:
```
后端返回 → status: "0" (字符串)
前端比较 → status === 1 (数字)
结果 → 永远返回false,总是执行 : 1 分支
```
3. **字典配置缺失**:
```vue
<dict-tag :options="schedule_status" :value="scope.row.status" />
```
当 `schedule_status` 字典为空时,`dict-tag` 组件无法正确渲染,可能显示原始值或双重标签。
### 测试验证
**验证步骤**:
1. ✅ 启动前端开发服务器
2. ✅ 导航至心理健康 → 咨询管理 → 预约时段
3. ✅ 检查状态列显示是否正常(应显示"正常"或"停用")
4. ✅ 点击编辑按钮测试状态切换
5. ✅ 观察控制台调试日志
6. ✅ 验证状态在正常/停用之间正确切换
**预期结果**:
- 状态列正确显示"正常"/"停用"标签
- 点击编辑按钮后状态正确切换
- 用户提示消息明确显示变更结果
- 控制台输出调试信息供问题排查
## 📊 影响范围
**直接影响**:
- ✅ 心理健康咨询预约时段管理功能恢复正常
- ✅ 状态切换功能可正确启用/停用时段
**间接影响**:
- ✅ 改善用户体验,操作反馈更清晰
- ✅ 为类似的状态切换功能提供标准修复模式
- ✅ 完善了心理健康模块的字典数据配置
## 🚀 后续优化建议
1. **统一类型处理**: 考虑在前端建立统一的状态值处理工具函数
2. **完善错误处理**: 添加更详细的错误处理和用户提示
3. **字典数据管理**: 建立字典数据的统一管理和维护机制
4. **测试用例**: 为状态切换功能添加单元测试和集成测试
## 📋 变更文件清单
| 文件路径 | 变更类型 | 说明 |
|----------|----------|------|
| `web/src/views/xljk/consultation/schedule/index.vue` | 修改 | 修复状态切换逻辑 + 优化列宽布局 |
| `web/src/views/xljk/consultation/noshow/index.vue` | 修改 | 移除创建时间字段显示 |
| `backend/script/sql/xljk-dict-data.sql` | 新增 | 创建心理健康模块字典数据配置 |
| `docs/变更管理/code-adjust-record-2025-01-14-cursor.md` | 新增 | 本变更记录文档 |
---
**验证完成**: ✅ 功能测试通过
**文档更新**: ✅ 已同步更新
**代码审查**: ✅ 自检完成
@@ -1,66 +0,0 @@
# 代码调整记录 - 2025-01-15 - cursor
## 🔧 数据库字段修复
### 📅 变更时间
2025-01-15 14:30:00
### 🎯 变更目标
修复 `stgl_activity` 表缺失的 `remark` 字段
### ⚠️ 问题描述
在社团活动管理功能中出现以下错误:
```
Unknown column 'sa.remark' in 'field list'
```
**错误原因分析:**
1. 实体类 `StglActivity.java` 中定义了 `remark` 字段
2. MyBatis映射文件 `StglActivityMapper.xml` 中的SQL查询包含了 `sa.remark` 字段
3. 但数据库表 `stgl_activity` 中实际缺少 `remark` 字段
### 🔍 修复过程
#### 1. 问题定位
- 检查实体类:`StglActivity.java` 第216-217行定义了remark字段
- 检查映射文件:`StglActivityMapper.xml` 第59行和第40行引用了remark字段
- 检查数据库表:`stgl_activity` 表确实缺少remark字段
#### 2. 修复方案
为 `stgl_activity` 表添加缺失的 `remark` 字段
#### 3. 执行的SQL语句
```sql
ALTER TABLE `stgl_activity`
ADD COLUMN `remark` varchar(500) DEFAULT NULL COMMENT '备注' AFTER `import_source`;
```
#### 4. 修复结果验证
- ✅ remark字段成功添加到stgl_activity表
- ✅ 字段类型:varchar(500)
- ✅ 字段注释:备注
- ✅ 字段位置:import_source字段之后
### 📁 相关文件
- **实体类**: `backend/ruoyi-modules/ruoyi-stgl/src/main/java/org/dromara/stgl/domain/StglActivity.java`
- **映射文件**: `backend/ruoyi-modules/ruoyi-stgl/src/main/resources/mapper/stgl/StglActivityMapper.xml`
- **修复脚本**: `backend/script/sql/fix-stgl-activity-remark-field.sql`
- **建表脚本**: `docs/设计文档/database/社团管理/社团管理建表脚本.sql`
### 🎯 影响范围
- **功能模块**: 社团活动管理
- **影响接口**: 所有涉及stgl_activity表查询的接口
- **数据完整性**: 修复后代码与数据库结构保持一致
### ✅ 验证结果
修复完成后,社团活动管理相关功能应该能正常运行,不再出现字段缺失错误。
### 📝 经验教训
1. **数据库同步**: 确保实体类、映射文件与数据库表结构完全一致
2. **建表脚本**: 需要检查建表脚本是否包含所有必要字段
3. **字段映射**: MyBatis映射文件中的字段必须在数据库表中实际存在
---
**修复人员**: Cursor AI
**修复时间**: 2025-01-15 14:30:00
**状态**: ✅ 已完成
@@ -1,137 +0,0 @@
# 代码调整记录 - 2025-01-21 - Cursor AI
## 📋 变更概述
**时间**: 2025-01-21 10:30:00
**操作人**: Cursor AI
**变更类型**: 🐛 Bug修复
**影响范围**: 志愿者成员管理模块
## 🔍 问题描述
**问题现象**: 当前登录用户`2025LY23117`是志愿者马拉松工作组的队长,该队伍总共有3个人,但在志愿者成员管理页面只能看到一个成员。
**根本原因**: 志愿者角色类型(roleType)在前后端定义不一致,导致权限过滤逻辑失效。
### 问题详情
| 组件 | 队长值 | 队员值 | 状态 |
|------|--------|--------|------|
| 前端字典(修复前) | 1 | 2 | ❌ 错误 |
| 后端权限过滤 | 0 | 1 | ✅ 正确 |
| 数据库SQL映射 | 0 | 1 | ✅ 正确 |
| 字典表(修复前) | 1 | 2 | ❌ 错误 |
**影响**:
- 权限过滤方法`getCaptainTeamIds`查找`roleType=0`时找不到队长记录
- 查询条件被设置为`relationId=-1L`,导致返回空结果
- 用户只能看到第一个队伍的数据
## 🛠️ 解决方案
**采用方案A**: 统一使用数据库SQL的定义(队长=0,队员=1)
### 修复内容
#### 1. 修改字典表数据
**文件**: `backend/script/sql/stgl_volunteer_role_dict.sql`
- 队长的`dict_value`从'1'改为'0'
- 队员的`dict_value`从'2'改为'1'
#### 2. 修改前端字典定义
**文件**: `web/src/views/stgl/volunteer/member/index.vue`
- 第358-361行:更新手动定义的字典
- 第432行:修改默认角色值为'1'(队员)
- 第242行:移除parseInt转换,保持字符串类型
- 第632行:修改类型转换为String()
#### 3. 创建数据库更新脚本
**文件**: `script/update-volunteer-role-dict.sql`
- 创建线上数据库更新脚本
- 包含验证查询和完成提示
## 📝 修改细节
### 代码变更
```javascript
// 修复前
const stgl_volunteer_role = ref([
{ value: '1', label: '队长' },
{ value: '2', label: '队员' }
]);
// 修复后
const stgl_volunteer_role = ref([
{ value: '0', label: '队长' },
{ value: '1', label: '队员' }
]);
```
```sql
-- 修复前字典数据
VALUES (..., '队长', '1', ...)
VALUES (..., '队员', '2', ...)
-- 修复后字典数据
VALUES (..., '队长', '0', ...)
VALUES (..., '队员', '1', ...)
```
### 权限过滤逻辑验证
后端权限过滤逻辑保持不变(已正确):
```java
captainBo.setRoleType(0); // 队长角色 = 0
```
数据库SQL映射保持不变(已正确):
```xml
WHEN 0 THEN '队长' <!-- 队长 = 0 -->
WHEN 1 THEN '队员' <!-- 队员 = 1 -->
```
## 🎯 预期效果
修复完成后:
1. ✅ 志愿者队长能够正确查看所属队伍的所有成员
2. ✅ 权限过滤逻辑正常工作
3. ✅ 前后端角色类型定义统一
4. ✅ 字典数据显示正确
## 📋 部署步骤
1. **执行数据库更新脚本**:
```bash
mysql -u username -p database < script/update-volunteer-role-dict.sql
```
2. **重新构建前端**:
```bash
cd web
npm run build
```
3. **重启后端服务**(如有缓存)
4. **验证功能**:
- 使用队长账号登录
- 访问志愿者成员管理页面
- 确认能看到所有队伍成员
## 🔍 验证要点
- [ ] 队长能查看所属队伍的所有成员
- [ ] 角色显示正确(队长/队员)
- [ ] 新增成员时角色选择正常
- [ ] 权限过滤按预期工作
## 📚 相关文档
- **问题分析**: 本文档"问题描述"部分
- **技术规范**: `.cursor/cursorrules-backend.md` - 权限管理规范
- **API文档**: `docs/开发文档/API/志愿者管理API.md`
## 🏷️ 标签
`#志愿者管理` `#权限过滤` `#角色类型` `#前后端统一` `#字典数据` `#Bug修复`
@@ -0,0 +1,300 @@
# 后端文件上传数据丢失问题
**发现日期**: 2025-01-24
**问题模块**: 文件上传/OSS服务
**影响范围**: 所有使用附件上传的业务模块
**严重程度**: ⚠️ 中等(影响用户体验,但有前端兜底)
---
## 问题描述
后端在保存文件时丢失了真实的文件名和扩展名信息,导致前端回显时无法正确识别文件类型。
### 问题表现
从 `/resource/oss/listByIds/{ossIds}` API 返回的数据中:
```javascript
{
ossId: "1981738218223079425",
originalName: "12038a7c-48bd-4978-bcbb-1119a201a6e9", // ❌ UUID,不是用户上传的真实文件名
fileSuffix: "9", // ❌ 数字字符,不是真实扩展名(应该是 jpg/png 等)
fileName: "2025/10/24/aa991fe4d6114995a0562d783cea56139", // ❌ 没有扩展名
url: "/resource/oss/download/1981738218223079425"
}
```
### 期望的数据格式
```javascript
{
ossId: "1981738218223079425",
originalName: "违纪现场照片.jpg", // ✅ 用户上传时的真实文件名
fileSuffix: "jpg", // ✅ 真实的文件扩展名
fileName: "2025/10/24/aa991fe4d6114995a0562d783cea56139.jpg", // ✅ 包含扩展名
url: "/resource/oss/download/1981738218223079425"
}
```
---
## 问题分析
### 可能的原因
1. **文件上传时未保存原始文件名**
- 上传接口 `/resource/oss/upload` 可能未正确提取文件名
- `originalName` 字段被UUID覆盖
2. **扩展名提取错误**
- `fileSuffix` 字段提取逻辑有误
- 从文件路径末尾提取了一个字符(如 `9`、`e`、`d`)而不是真实扩展名
3. **数据库保存逻辑问题**
- `sys_oss` 表中的字段未正确保存
- 可能是某个字段映射错误
### 影响的业务场景
- ✅ **新增上传**:正常工作(文件可以上传)
- ❌ **回显展示**:文件类型识别错误,图片无法正常显示
- ❌ **下载功能**:下载的文件名显示为UUID,用户体验差
- ❌ **文件管理**:管理员无法识别文件内容
---
## 复现步骤
1. 在移动端进入"宿舍卫生检查"模块
2. 点击"录入违纪",上传一张图片(如 `photo.jpg`)
3. 保存违纪记录
4. 刷新页面或重新进入
5. 点击"编辑"查看违纪记录
6. **问题**:附件无法显示为图片,文件名显示为UUID
---
## 调试信息
### 前端上传请求
```javascript
// 调用上传接口
POST /resource/oss/upload
Content-Type: multipart/form-data
// FormData:
file: [Blob] photo.jpg
bizType: "dorm_violation_attachment"
```
### 后端返回(上传成功)
```javascript
{
code: 200,
data: {
ossId: "1981738218223079425",
url: "/resource/oss/download/1981738218223079425",
fileName: "2025/10/24/aa991fe4d6114995a0562d783cea56139" // ⚠️ 缺少扩展名
}
}
```
### 后端返回(回显查询)
```javascript
// GET /resource/oss/listByIds/1981738218223079425
{
code: 200,
data: [{
ossId: "1981738218223079425",
originalName: "12038a7c-48bd-4978-bcbb-1119a201a6e9", // ❌ UUID
fileSuffix: "9", // ❌ 错误
fileName: "2025/10/24/aa991fe4d6114995a0562d783cea56139",
url: "/resource/oss/download/1981738218223079425"
}]
}
```
---
## 建议的修复方案
### 方案1:修复上传接口(推荐)⭐
**修改文件**:`backend/src/main/java/com/ruoyi/resource/controller/SysOssController.java`
**修复点**:
1. 上传时正确提取并保存 `originalName`(用户上传的文件名)
2. 正确提取文件扩展名保存到 `fileSuffix`
3. 保存到OSS的文件名应包含扩展名
```java
@PostMapping("/upload")
public R<SysOss> upload(@RequestParam("file") MultipartFile file,
@RequestParam(required = false) String bizType) {
// 获取原始文件名
String originalFilename = file.getOriginalFilename();
// 提取真实扩展名
String extension = "";
if (originalFilename != null && originalFilename.contains(".")) {
extension = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
}
// 生成OSS文件名(包含扩展名)
String ossFileName = generateOssFileName() + "." + extension;
// 保存到数据库
SysOss oss = new SysOss();
oss.setOriginalName(originalFilename); // ✅ 保存真实文件名
oss.setFileSuffix(extension); // ✅ 保存真实扩展名
oss.setFileName(ossFileName); // ✅ 包含扩展名的OSS文件名
// ... 其他逻辑
}
```
### 方案2:修复查询接口
**修改文件**:`backend/src/main/java/com/ruoyi/resource/service/impl/SysOssServiceImpl.java`
**修复点**:
1. 从数据库正确读取 `originalName` 和 `fileSuffix`
2. 确保返回的VO类包含正确的字段值
```java
public List<SysOssVo> listByIds(String ossIds) {
// ... 查询逻辑 ...
List<SysOssVo> voList = new ArrayList<>();
for (SysOss oss : ossList) {
SysOssVo vo = new SysOssVo();
vo.setOssId(oss.getOssId());
vo.setOriginalName(oss.getOriginalName()); // ✅ 确保读取正确
vo.setFileSuffix(oss.getFileSuffix()); // ✅ 确保读取正确
vo.setFileName(oss.getFileName());
vo.setUrl(oss.getUrl());
voList.add(vo);
}
return voList;
}
```
### 方案3:数据库表结构检查
**检查表**:`sys_oss`
**需要确认的字段**:
```sql
-- 检查字段定义
SHOW COLUMNS FROM sys_oss LIKE '%name%';
SHOW COLUMNS FROM sys_oss LIKE '%suffix%';
-- 检查数据示例
SELECT oss_id, original_name, file_suffix, file_name
FROM sys_oss
WHERE oss_id = '1981738218223079425';
```
**期望的字段定义**:
- `original_name` VARCHAR(255) - 用户上传的原始文件名
- `file_suffix` VARCHAR(10) - 文件扩展名(jpg/png/pdf等)
- `file_name` VARCHAR(500) - OSS中的文件路径(应包含扩展名)
---
## 临时解决方案(前端兜底)
**已实施**:在 `ViolationRecordDialog.vue` 中添加了兜底逻辑
```javascript
// 检测 fileSuffix 是否为有效扩展名
if (item.fileSuffix && item.fileSuffix.length <= 5 && /^[a-zA-Z]+$/.test(item.fileSuffix)) {
// 使用 fileSuffix
realExtension = item.fileSuffix.toLowerCase();
fileType = this.getFileTypeBySuffix(realExtension);
} else {
// 默认按图片处理(违纪证明材料大概率是图片)
realExtension = 'jpg';
fullFileName = `违纪证明_${timestamp + index}.jpg`;
fileType = 'image/jpeg';
}
```
**局限性**:
- ✅ 可以临时解决图片回显问题
- ❌ 无法恢复真实的文件名
- ❌ 非图片文件可能被错误识别
- ❌ 用户体验不佳
---
## 验证方法
### 1. 后端修复后的验证
**步骤**:
1. 上传一张图片 `test.jpg`
2. 调用 `/resource/oss/listByIds/{ossId}` 查询
3. 检查返回数据:
- `originalName` 应该是 `test.jpg`
- `fileSuffix` 应该是 `jpg`
- `fileName` 应该包含 `.jpg` 扩展名
**期望日志**:
```
违纪记录:原始数据 - originalName=test.jpg, fileSuffix=jpg, fileName=2025/10/24/xxx.jpg
违纪记录:最终文件名=test.jpg, 扩展名=jpg, 文件类型=image/jpeg
```
### 2. 前端验证
**步骤**:
1. 上传图片后编辑违纪记录
2. 检查附件回显是否正常
3. 检查文件名是否正确显示
**期望结果**:
- ✅ 图片正常显示(不是灰白背景)
- ✅ 文件名显示为真实文件名(不是UUID)
- ✅ 下载文件名正确
---
## 相关文件
**后端**:
- `backend/src/main/java/com/ruoyi/resource/controller/SysOssController.java`
- `backend/src/main/java/com/ruoyi/resource/service/impl/SysOssServiceImpl.java`
- `backend/src/main/java/com/ruoyi/resource/domain/vo/SysOssVo.java`
- `backend/src/main/java/com/ruoyi/resource/domain/SysOss.java`
**前端**:
- `mobile/src/pages/xgss/wsjc/components/ViolationRecordDialog.vue`(已添加兜底逻辑)
**数据库**:
- `sys_oss` 表
---
## 优先级和影响
**优先级**: P2(中等)
**影响评估**:
- **用户体验**: ⬇️ 下降(文件名显示为UUID,无法识别内容)
- **功能可用性**: ✅ 正常(有前端兜底,基本功能可用)
- **数据完整性**: ⚠️ 受影响(历史数据无法恢复真实文件名)
**建议处理时间**: 2周内修复
---
**创建时间**: 2025-01-24
**创建人**: Cursor AI
**状态**: 🔴 待修复
+1
View File
@@ -73,6 +73,7 @@
"@vue/shared": "^3.0.0",
"core-js": "^3.8.3",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.18",
"flyio": "^0.6.2",
"jsencrypt": "^3.3.2",
"uview-ui": "2.0.38",
+35
View File
@@ -150,6 +150,41 @@ page {
border-color: #007dfb !important;
}
/* Tabs组件主题色覆盖 - 全局统一底部方块颜色 */
.u-tabs__wrapper__nav__line {
background-color: #0051d9 !important;
}
.u-tabs .u-tabs__wrapper__nav__line {
background: #0051d9 !important;
}
/* Navbar导航栏主题色覆盖 - 勤工俭学模块统一背景色 */
.u-navbar {
background-color: #0051d9 !important;
}
.u-navbar__content {
background-color: #0051d9 !important;
}
.u-navbar__content__title {
color: #fff !important;
}
/* uni-app 原生导航栏样式覆盖 */
uni-page-head {
background-color: #0051d9 !important;
}
.uni-page-head {
background-color: #0051d9 !important;
}
.uni-page-head__title {
color: #fff !important;
}
/* ========== 全局温暖商务风格样式 ========== */
/* 全局页面背景 */
+35
View File
@@ -0,0 +1,35 @@
// 字典数据API - 重新导出common/dict.js中的方法以保持兼容性
import {
getDicts,
getDict,
useDict,
getDictLabel,
getDictValue,
hasDictValue,
refreshDict,
getDictTagType,
} from '@/common/dict.js';
// 重新导出所有字典相关方法
export {
getDicts,
getDict,
useDict,
getDictLabel,
getDictValue,
hasDictValue,
refreshDict,
getDictTagType,
};
// 默认导出
export default {
getDicts,
getDict,
useDict,
getDictLabel,
getDictValue,
hasDictValue,
refreshDict,
getDictTagType,
};
-200
View File
@@ -1,200 +0,0 @@
# 移动端字典工具使用指南
## 📋 简介
为移动端(Vue 2.6.14 + uni-app 2.x)提供完整的字典数据获取和管理功能,与web端保持一致的API设计。
## 🚀 快速开始
### 1. 基础导入
```javascript
import { useDict, getDict, getDictLabel } from '@/common/dict.js'
```
### 2. 在页面中使用
```javascript
export default {
data() {
return {
dictionaries: {},
userInfo: {
gender: '0',
status: '1'
}
}
},
created() {
this.loadDictionaries()
},
methods: {
async loadDictionaries() {
// 批量加载字典
const dictManager = useDict('sys_user_sex', 'sys_normal_disable')
// Vue 2响应式更新(必须设置)
dictManager.setUpdateCallback((dictType, data) => {
this.$set(this.dictionaries, dictType, data)
})
},
// 获取字典标签显示
getDictText(dictType, value) {
const dictData = this.dictionaries[dictType] || []
return getDictLabel(dictData, value)
}
}
}
```
### 3. 在模板中显示
```vue
<template>
<view>
<!-- 显示用户性别 -->
<text>性别: {{ getDictText('sys_user_sex', userInfo.gender) }}</text>
<!-- 显示用户状态 -->
<text>状态: {{ getDictText('sys_normal_disable', userInfo.status) }}</text>
</view>
</template>
```
## 📚 主要功能
| 功能 | 方法 | 说明 |
|------|------|------|
| 批量字典管理 | `useDict(...types)` | 推荐使用,支持多字典并发加载 |
| 单个字典获取 | `getDict(type, cache?)` | 获取单个字典数据 |
| 字典标签获取 | `getDictLabel(data, value)` | 根据值获取标签 |
| 字典值获取 | `getDictValue(data, label)` | 根据标签获取值 |
| 值存在检查 | `hasDictValue(data, value)` | 检查值是否存在 |
| 缓存刷新 | `refreshDict(types)` | 刷新指定字典缓存 |
## 🎯 使用场景
### 场景1: uview组件集成
```javascript
// radio-group
<u-radio-group v-model="form.gender">
<u-radio
v-for="item in dictionaries.sys_user_sex"
:key="item.value"
:name="item.value"
>
{{ item.label }}
</u-radio>
</u-radio-group>
// select组件
computed: {
selectOptions() {
return (this.dictionaries.sys_user_sex || []).map(item => ({
value: item.value,
label: item.label
}))
}
}
```
### 场景2: 表格数据转换
```javascript
computed: {
processedData() {
return this.tableData.map(row => ({
...row,
genderText: getDictLabel(this.dictionaries.sys_user_sex || [], row.gender),
statusText: getDictLabel(this.dictionaries.sys_normal_disable || [], row.status)
}))
}
}
```
### 场景3: 单独获取字典
```javascript
async methods: {
async loadSingleDict() {
try {
const genderDict = await getDict('sys_user_sex')
console.log('性别字典:', genderDict)
} catch (error) {
console.error('加载失败:', error)
}
}
}
```
## ⚡ 性能特性
- **智能缓存**: 24小时有效期,自动管理
- **并发加载**: 多字典同时请求,提升加载速度
- **响应式更新**: 完美适配Vue 2响应式系统
- **错误处理**: 完善的错误处理和兜底机制
## 🔧 高级用法
### 缓存管理
```javascript
import { dictCache, refreshDict } from '@/common/dict.js'
// 清除指定缓存
dictCache.remove('sys_user_sex')
// 清除所有缓存
dictCache.clear()
// 刷新缓存
await refreshDict(['sys_user_sex', 'sys_normal_disable'])
```
### 错误处理
```javascript
// 安全的字典获取(带默认值)
safeDictLabel(dictType, value, defaultLabel = '-') {
try {
const dictData = this.dictionaries[dictType] || []
const label = getDictLabel(dictData, value)
return label || defaultLabel
} catch (error) {
console.error(`获取字典失败 ${dictType}:${value}`, error)
return defaultLabel
}
}
```
## 📖 相关文件
- **核心实现**: `/mobile/src/common/dict.js`
- **使用示例**: `/mobile/src/common/dict-usage-example.js`
- **变更记录**: `/docs/变更管理/code-adjust-record-2025-01-27-移动端字典工具实现.md`
## ⚠️ 重要提醒
### Vue 2必须使用回调机制
```javascript ❌
// 错误:直接赋值不会触发响应式更新
this.dictionaries = dictManager.data
// 正确:使用回调确保响应式更新 ✅
dictManager.setUpdateCallback((dictType, data) => {
this.$set(this.dictionaries, dictType, data)
})
```
### 兼容性
- ✅ Vue 2.6.14+
- ✅ uni-app 2.x
- ✅ H5 / 小程序 / App
## 🎯 最佳实践
1. **优先使用useDict**: 支持并发加载,性能更好
2. **合理使用缓存**: 频繁变动的字典设置`useCache=false`
3. **添加错误处理**: 关键业务场景要有异常处理
4. **使用callback机制**: Vue 2中必须使用setUpdateCallback
---
✨ **完成时间**: 2025-01-27 20:15:00
👨‍💻 **开发者**: wuzilong
📱 **适用版本**: Vue 2.6.14 + uni-app 2.x
+3 -1
View File
@@ -12,6 +12,7 @@ export const ENCRYPT_CONFIG = {
TENANT_ID: '000000',
// 加密请求头
ENCRYPT_HEADER: 'encrypt-key',
//TODO 开发环境需要修改为false
// 是否启用加密
ENCRYPT_ENABLED: true,
};
@@ -36,8 +37,9 @@ export const REQUEST_CONFIG = {
? 'http://localhost/login?redirect=/index'
: 'https://hzdyjs.camasort.com/login?redirect=/index', // 生产环境前端域名
//TODO 生产环境需要修改为true
// 是否启用加密
encryptEnabled: true,
encryptEnabled: false,
// 防重复提交间隔时间(ms)
repeatSubmitInterval: 500,
};
+175 -12
View File
@@ -73,18 +73,36 @@
<!-- 办理人信息 -->
<view class="approver-section">
<text class="section-label">办理人:</text>
<view class="approver-tags">
<template v-if="record.approveName">
<view class="approver-info-list">
<template
v-if="record.approverList && record.approverList.length > 0"
>
<view
v-for="(approver, approverIndex) in record.approverList"
:key="approverIndex"
class="approver-info-item"
>
<u-tag
v-for="(name, nameIndex) in record.approveName.split(
',',
)"
:key="nameIndex"
:text="name.trim()"
:text="approver.name"
:type="getApproverType()"
size="mini"
class="approver-tag"
/>
<view
v-if="approver.phone"
class="approver-phone-wrapper"
@click="handleCallPhone(approver.phone)"
>
<u-icon
name="phone-fill"
size="28"
color="#1989fa"
/>
<text class="approver-phone">
{{ approver.phone }}
</text>
</view>
</view>
</template>
<u-tag
v-else
@@ -253,10 +271,43 @@ export default {
console.log('实例ID:', this.insId);
console.log('记录数量:', this.historyList.length);
// 处理附件信息(与web端保持一致)
// 处理附件信息和审批人信息(与web端保持一致)
if (this.historyList.length > 0) {
console.log('开始处理附件信息...');
console.log('开始处理附件信息和审批人信息...');
// 收集所有审批人ID
const allApproverIds = new Set();
for (const item of this.historyList) {
if (item.approver) {
// approver字段可能包含多个ID,用逗号分隔
const ids = item.approver
.split(',')
.map((id) => id.trim())
.filter((id) => id);
ids.forEach((id) => allApproverIds.add(id));
}
}
// 批量查询审批人信息(包含手机号码)
let userInfoMap = {};
if (allApproverIds.size > 0) {
console.log(
'开始批量查询审批人信息,ID列表:',
Array.from(allApproverIds),
);
try {
userInfoMap = await this.getUserInfoByIds(
Array.from(allApproverIds),
);
console.log('审批人信息查询完成:', userInfoMap);
} catch (error) {
console.error('批量查询审批人信息失败:', error);
}
}
// 处理每条审批记录
for (const item of this.historyList) {
// 处理附件信息
if (item.ext) {
try {
const attachmentRes = await this.getAttachmentsByIds(item.ext);
@@ -268,8 +319,31 @@ export default {
} else {
item.attachmentList = [];
}
// 处理审批人信息(包含姓名和手机号)
if (item.approver && item.approveName) {
const approverIds = item.approver
.split(',')
.map((id) => id.trim())
.filter((id) => id);
const approverNames = item.approveName
.split(',')
.map((name) => name.trim())
.filter((name) => name);
item.approverList = approverIds.map((id, index) => {
const userInfo = userInfoMap[id];
return {
id: id,
name: approverNames[index] || '未知',
phone: userInfo ? userInfo.phonenumber : null,
};
});
} else {
item.approverList = [];
}
console.log('附件信息处理完成');
}
console.log('附件信息和审批人信息处理完成');
}
} else {
console.error('API响应异常:', response);
@@ -300,6 +374,67 @@ export default {
return response;
},
// 批量查询用户信息(包含手机号码)
async getUserInfoByIds(userIds) {
try {
const response = await request({
url: '/system/user/optionselect',
method: 'GET',
params: {
userIds: userIds.join(','),
},
});
if (response.code === 200 && response.data) {
// 将数组转换为以userId为key的Map,方便后续查找
const userMap = {};
response.data.forEach((user) => {
userMap[user.userId] = user;
});
return userMap;
} else {
console.error('查询用户信息失败:', response.msg);
return {};
}
} catch (error) {
console.error('批量查询用户信息异常:', error);
return {};
}
},
// 拨打电话
handleCallPhone(phoneNumber) {
if (!phoneNumber) {
uni.showToast({
title: '手机号码为空',
icon: 'none',
});
return;
}
uni.showModal({
title: '拨打电话',
content: `确定要拨打 ${phoneNumber} 吗?`,
success: (res) => {
if (res.confirm) {
uni.makePhoneCall({
phoneNumber: phoneNumber,
success: () => {
console.log('拨打电话成功');
},
fail: (err) => {
console.error('拨打电话失败:', err);
uni.showToast({
title: '拨打电话失败',
icon: 'none',
});
},
});
}
},
});
},
// 获取状态样式类(使用字典方式)
getStatusClass(flowStatus) {
console.log('flowStatus:', flowStatus);
@@ -670,16 +805,44 @@ export default {
display: block;
}
.approver-tags {
.approver-info-list {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.approver-info-item {
display: flex;
align-items: center;
gap: 12rpx;
flex-wrap: wrap;
gap: 8rpx;
}
.approver-tag {
margin: 0;
}
.approver-phone-wrapper {
display: flex;
align-items: center;
gap: 6rpx;
padding: 4rpx 8rpx;
border-radius: 8rpx;
transition: all 0.3s;
cursor: pointer;
}
.approver-phone-wrapper:active {
background-color: #e6f7ff;
transform: scale(0.98);
}
.approver-phone {
font-size: 26rpx;
color: #1989fa;
font-weight: 500;
}
.comment-text {
font-size: 26rpx;
color: #333;
+35 -4
View File
@@ -200,6 +200,13 @@
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/xgss/wsjc/wsjcMainAdd",
"style": {
"navigationBarTitleText": "宿舍卫生检查",
"navigationStyle": "custom"
}
},
{
"path": "pages/xgss/xssswsjc/xssswsjc",
"style": {
@@ -239,6 +246,22 @@
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/xskh/wjjlEntry/index",
"style": {
"navigationBarTitleText": "违纪记录录入",
"navigationBarBackgroundColor": "#0051d9",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/xskh/wjjlEntry/disciplineRecordForm",
"style": {
"navigationBarTitleText": "违纪记录",
"navigationBarBackgroundColor": "#0051d9",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/xskh/khsq/khsq",
"style": {
@@ -267,7 +290,7 @@
"path": "pages/workStudy/index",
"style": {
"navigationBarTitleText": "勤工俭学",
"navigationBarBackgroundColor": "#1890ff",
"navigationBarBackgroundColor": "#0051d9",
"navigationBarTextStyle": "white"
}
},
@@ -275,7 +298,7 @@
"path": "pages/workStudy/detail",
"style": {
"navigationBarTitleText": "岗位详情",
"navigationBarBackgroundColor": "#1890ff",
"navigationBarBackgroundColor": "#0051d9",
"navigationBarTextStyle": "white"
}
},
@@ -283,7 +306,7 @@
"path": "pages/workStudy/apply",
"style": {
"navigationBarTitleText": "申请岗位",
"navigationBarBackgroundColor": "#1890ff",
"navigationBarBackgroundColor": "#0051d9",
"navigationBarTextStyle": "white"
}
},
@@ -291,7 +314,7 @@
"path": "pages/workStudy/applicationDetail",
"style": {
"navigationBarTitleText": "申请详情",
"navigationBarBackgroundColor": "#1890ff",
"navigationBarBackgroundColor": "#0051d9",
"navigationBarTextStyle": "white"
}
},
@@ -326,6 +349,14 @@
"navigationBarBackgroundColor": "#0051d9",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/xg/studentDynamics/index",
"style": {
"navigationBarTitleText": "学生动向信息",
"navigationBarBackgroundColor": "#0051d9",
"navigationBarTextStyle": "white"
}
}
],
"globalStyle": {
+47 -3
View File
@@ -181,7 +181,7 @@
/>
</template>
</ServiceItem>
<ServiceItem
<!-- <ServiceItem
v-if="hasPermi(['student:dormviolation:listapp'])"
title="宿舍违纪"
@click="gotoDormViolation"
@@ -193,7 +193,7 @@
style="width: 24px; height: 24px"
/>
</template>
</ServiceItem>
</ServiceItem> -->
<ServiceItem
v-if="hasPermi(['student:dormhygiene:listapp'])"
title="宿舍卫生检查"
@@ -221,6 +221,20 @@
</template>
</ServiceItem>
<ServiceItem
v-if="hasPermi(['student:dynamics:list'])"
title="学生动向信息(宿管)"
@click="gotoStudentDynamics"
>
<template #icon>
<image
src="/static/0011.png"
mode="aspectFit"
style="width: 24px; height: 24px"
/>
</template>
</ServiceItem>
<ServiceItem
v-if="hasPermi(['student:workstudy:list'])"
title="勤工俭学"
@@ -249,6 +263,20 @@
</template>
</ServiceItem>
<ServiceItem
v-if="hasPermi(['student:violation:entry'])"
title="违纪记录录入"
@click="gotoViolationEntry"
>
<template #icon>
<image
src="/static/00125.png"
mode="aspectFit"
style="width: 24px; height: 24px"
/>
</template>
</ServiceItem>
<ServiceItem
v-if="hasPermi(['student:assessment:applyapp'])"
title="考核申请"
@@ -719,11 +747,13 @@ export default {
this.hasPermi(['student:counsel:list']) ||
this.hasPermi(['student:info:editapp']) ||
this.hasPermi(['student:internship:list']) ||
this.hasPermi(['student:dormviolation:listapp']) ||
// this.hasPermi(['student:dormviolation:listapp']) ||
this.hasPermi(['student:visit:list']) ||
this.hasPermi(['student:dormhygiene:listapp']) ||
this.hasPermi(['student:dynamics:list']) ||
this.hasPermi(['student:workstudy:list']) ||
this.hasPermi(['student:violation:listapp']) ||
this.hasPermi(['student:violation:entry']) ||
this.hasPermi(['student:assessment:applyapp']) ||
this.hasPermi(['student:counsel:reservation'])
);
@@ -974,6 +1004,13 @@ export default {
});
},
// 跳转到违纪记录录入页面
gotoViolationEntry() {
uni.navigateTo({
url: '/pages/xskh/wjjlEntry/index',
});
},
// 跳转到考核申请页面
gotoAssessmentApply() {
uni.navigateTo({
@@ -988,6 +1025,13 @@ export default {
});
},
// 跳转到学生动向信息(宿管)页面
gotoStudentDynamics() {
uni.navigateTo({
url: '/pages/xg/studentDynamics/index',
});
},
// 跳转到修改密码页面
gotoResetPassword() {
uni.navigateTo({
+187 -231
View File
@@ -1,5 +1,12 @@
<template>
<view class="internship-page">
<!-- 顶部导航 -->
<u-navbar title="实习打卡" :border-bottom="false" bg-color="#3c9cff" title-color="#fff">
<view slot="left" @click="goBack">
<u-icon name="arrow-left" color="#fff" size="22"></u-icon>
</view>
</u-navbar>
<!-- 顶部用户信息卡片 -->
<view class="user-card">
<view class="user-info">
@@ -23,9 +30,11 @@
<!-- 实习信息卡片 -->
<view class="internship-card" v-if="internshipInfo">
<view class="card-header">
<u-icon name="bookmark" size="18" color="#409eff"></u-icon>
<view class="header-left">
<u-icon name="file-text" size="18" color="#409eff"></u-icon>
<text class="card-title">实习信息</text>
</view>
</view>
<view class="internship-info">
<view class="info-row">
<text class="label">实习单位:</text>
@@ -59,9 +68,11 @@
<!-- 今日签到卡片 -->
<view class="checkin-card">
<view class="card-header">
<u-icon name="clock" size="18" color="#67c23a"></u-icon>
<view class="card-header card-header-between">
<view class="header-left">
<u-icon name="checkmark-circle" size="18" color="#67c23a"></u-icon>
<text class="card-title">今日签到</text>
</view>
<text class="date-text">{{ currentDate }}</text>
</view>
@@ -85,14 +96,20 @@
<view class="checkin-actions" v-else>
<u-button
type="primary"
size="large"
shape="round"
:loading="checkinLoading"
:disabled="checkinLoading || !canCheckin"
@click="handleCheckin"
:custom-style="checkinButtonStyle"
class="checkin-btn"
:custom-style="{ height: '88rpx' }"
>
<u-icon name="map" size="20" color="#fff" style="margin-right: 8px"></u-icon>
{{ checkinLoading ? '签到中...' : '立即签到' }}
<u-icon
name="checkmark-circle"
size="20"
color="#fff"
style="margin-right: 8rpx"
></u-icon>
<text style="color: #fff">{{ checkinLoading ? '签到中...' : '立即签到' }}</text>
</u-button>
<view class="checkin-tips" v-if="!canCheckin">
@@ -106,6 +123,7 @@
<view class="location-card">
<view class="card-header">
<view class="header-left">
<u-icon name="map" size="18" color="#f56c6c"></u-icon>
<text class="card-title">签到位置</text>
</view>
</view>
@@ -114,14 +132,14 @@
<view class="location-row">
<u-button
type="primary"
size="default"
plain
shape="round"
@click="handleGetLocation"
:loading="locationLoading"
class="location-btn"
:custom-style="{ height: '88rpx' }"
>
<u-icon name="map-fill" size="20" color="#2979ff" />
<text style="margin-left: 8rpx">
<u-icon name="map-fill" size="20" color="#fff" />
<text style="margin-left: 8rpx; color: #fff">
{{ checkIsWechatBrowser() ? '微信定位' : '定位' }}
</text>
</u-button>
@@ -129,13 +147,23 @@
<!-- 位置信息显示 -->
<view v-if="currentLocation" class="location-info-section">
<!-- 坐标信息 -->
<view class="location-coords-container">
<!-- 坐标信息和查看位置按钮 -->
<view class="location-coords-row">
<text class="location-coords">
经纬度:{{ currentLocation.latitude.toFixed(6) }},{{
currentLocation.longitude.toFixed(6)
}}
</text>
<u-button
size="small"
type="primary"
shape="round"
@click="openLocationInMap"
class="view-map-btn"
>
<u-icon name="eye" size="14" color="#fff" />
<text style="margin-left: 4rpx; color: #fff">查看位置</text>
</u-button>
</view>
<!-- 精度信息 -->
@@ -158,20 +186,6 @@
></u-tag>
</view>
</view>
<!-- 查看位置按钮 -->
<view class="location-action-container">
<u-button
size="default"
type="primary"
plain
@click="openLocationInMap"
class="view-map-btn"
>
<u-icon name="eye" size="16" color="#2979ff" />
<text>查看位置</text>
</u-button>
</view>
</view>
</view>
@@ -179,7 +193,7 @@
<view class="location-error-card" v-if="locationError && !currentLocation">
<view class="card-header">
<view class="header-left">
<u-icon name="map-pin" size="18" color="#f56c6c"></u-icon>
<u-icon name="map" size="18" color="#f56c6c"></u-icon>
<text class="card-title">位置信息</text>
</view>
<u-button
@@ -243,9 +257,11 @@
<!-- 考勤统计 -->
<view class="statistics-card">
<view class="card-header">
<u-icon size="18" color="#e6a23c"></u-icon>
<view class="header-left">
<u-icon name="calendar" size="18" color="#e6a23c"></u-icon>
<text class="card-title">本月考勤</text>
</view>
</view>
<view class="stats-grid">
<view class="stat-item">
<text class="stat-number">{{ monthStats.totalDays || 0 }}</text>
@@ -354,21 +370,15 @@ export default {
// 当前日期
currentDate: '',
// 样式
checkinButtonStyle: {
width: '100%',
height: '50px',
borderRadius: '25px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none',
fontSize: '16px',
fontWeight: 'bold',
},
};
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack();
},
// === 微信SDK相关方法 ===
// 检查微信环境并自动初始化SDK
@@ -1435,18 +1445,17 @@ export default {
<style scoped>
.internship-page {
min-height: 100vh;
background: #f5f7fa;
padding: 16px;
background-color: #f5f6fa;
padding-bottom: 40rpx;
}
/* 用户信息卡片 */
.user-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 20px;
margin-bottom: 16px;
color: white;
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
background-color: #ffffff;
border-radius: 16rpx;
padding: 30rpx;
margin: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.user-info {
@@ -1455,7 +1464,7 @@ export default {
}
.avatar-section {
margin-right: 16px;
margin-right: 24rpx;
}
.info-section {
@@ -1463,15 +1472,16 @@ export default {
}
.user-name {
font-size: 18px;
font-size: 32rpx;
font-weight: bold;
margin-bottom: 4px;
color: #303133;
margin-bottom: 8rpx;
}
.user-detail {
font-size: 14px;
opacity: 0.9;
margin-bottom: 2px;
font-size: 26rpx;
color: #606266;
margin-bottom: 4rpx;
}
/* 通用卡片样式 */
@@ -1480,216 +1490,222 @@ export default {
.location-card,
.statistics-card,
.records-card {
background: white;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
background-color: #ffffff;
border-radius: 16rpx;
padding: 24rpx;
margin: 0 20rpx 20rpx 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
padding-bottom: 12px;
margin-bottom: 24rpx;
padding-bottom: 20rpx;
border-bottom: 1px solid #f0f0f0;
min-height: 32px; /* 确保最小高度 */
gap: 8px; /* 确保元素之间有间距 */
min-height: 48rpx;
}
.card-header-between {
justify-content: space-between;
}
.header-left {
display: flex;
align-items: center;
flex: 1;
min-width: 0; /* 允许flex子项收缩 */
margin-right: 12px; /* 与右侧按钮保持距离 */
gap: 8rpx;
}
.card-title {
font-size: 16px;
font-size: 30rpx;
font-weight: bold;
margin-left: 8px;
margin-left: 0;
color: #303133;
white-space: nowrap; /* 防止文字换行 */
overflow: hidden; /* 隐藏溢出文字 */
text-overflow: ellipsis; /* 显示省略号 */
flex-shrink: 1; /* 允许收缩 */
white-space: nowrap;
}
.refresh-btn {
flex-shrink: 0;
min-width: 70px; /* 确保按钮最小宽度 */
min-width: 100rpx;
}
.date-text {
font-size: 12px;
font-size: 24rpx;
color: #909399;
}
/* 实习信息 */
.internship-info {
padding-top: 8px;
padding-top: 12rpx;
}
.info-row {
display: flex;
align-items: center;
margin-bottom: 8px;
margin-bottom: 16rpx;
}
.label {
font-size: 14px;
font-size: 26rpx;
color: #606266;
width: 80px;
min-width: 140rpx;
flex-shrink: 0;
}
.value {
font-size: 14px;
font-size: 26rpx;
color: #303133;
flex: 1;
}
/* 签到状态 */
.checkin-status {
padding: 16px;
padding: 24rpx;
background: #f0f9ff;
border-radius: 8px;
border-left: 4px solid #67c23a;
border-radius: 12rpx;
border-left: 6rpx solid #67c23a;
}
.status-item {
display: flex;
align-items: center;
margin-bottom: 8px;
margin-bottom: 12rpx;
}
.status-text {
font-size: 16px;
font-size: 30rpx;
font-weight: bold;
color: #67c23a;
margin-left: 8px;
margin-left: 12rpx;
}
.time-info,
.location-info {
display: flex;
align-items: center;
margin-bottom: 4px;
margin-bottom: 8rpx;
}
.time-label,
.location-label {
font-size: 14px;
font-size: 26rpx;
color: #606266;
margin-right: 8px;
margin-right: 12rpx;
}
.time-value,
.location-value {
font-size: 14px;
font-size: 26rpx;
color: #303133;
}
/* 签到操作 */
.checkin-actions {
padding: 16px 0;
padding: 24rpx 0;
}
.checkin-btn {
width: 100%;
}
.checkin-tips {
display: flex;
align-items: center;
justify-content: center;
margin-top: 12px;
margin-top: 16rpx;
}
.tips-text {
font-size: 12px;
font-size: 24rpx;
color: #909399;
margin-left: 4px;
margin-left: 8rpx;
}
/* 位置信息(参考请假页面样式) */
/* 位置信息 */
.location-row {
margin-bottom: 16px;
margin-bottom: 24rpx;
}
.location-btn {
width: 100%;
height: 44px;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
border: 1px solid #2979ff;
background: #fff;
color: #2979ff;
border-radius: 12rpx;
}
.location-info-section {
margin-top: 16px;
padding: 16px;
margin-top: 24rpx;
padding: 24rpx;
background: #f8f9fa;
border-radius: 8px;
border-radius: 12rpx;
border: 1px solid #e4e7ed;
}
.location-coords-container {
margin-bottom: 12px;
margin-bottom: 20rpx;
}
.location-coords-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
gap: 16rpx;
}
.location-coords {
font-size: 14px;
font-size: 26rpx;
color: #303133;
font-family: monospace;
line-height: 1.5;
word-break: break-all;
flex: 1;
}
.location-action-container {
display: flex;
justify-content: center;
margin-top: 12px;
margin-top: 20rpx;
}
.view-map-btn {
min-width: 120px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
height: 56rpx !important;
min-width: auto !important;
width: auto !important;
flex-shrink: 0;
padding: 0 20rpx !important;
}
/* 精度信息 */
.accuracy-section {
margin-bottom: 16px;
margin-bottom: 24rpx;
}
.accuracy-info {
display: flex;
align-items: center;
padding: 10px 12px;
padding: 16rpx 20rpx;
background: #f0f9ff;
border-radius: 8px;
border-radius: 12rpx;
border: 1px solid #b3e5fc;
}
.accuracy-icon {
margin-right: 8px;
margin-right: 12rpx;
flex-shrink: 0;
}
.accuracy-text {
font-size: 13px;
font-size: 26rpx;
color: #67c23a;
font-weight: 500;
flex: 1;
}
.accuracy-tag {
margin-left: 8px;
margin-left: 12rpx;
flex-shrink: 0;
}
@@ -1697,41 +1713,37 @@ export default {
.location-actions {
display: flex;
justify-content: center;
margin-top: 4px; /* 与上方内容保持间距 */
margin-top: 12rpx;
}
.map-btn {
min-width: 140px;
width: 100%; /* 在小屏幕上占满宽度 */
max-width: 200px; /* 最大宽度限制 */
min-width: 200rpx;
width: 100%;
max-width: 300rpx;
}
.btn-text {
margin-left: 6px;
margin-left: 8rpx;
}
/* 小屏幕适配 */
@media screen and (max-width: 375px) {
.card-header {
flex-wrap: nowrap; /* 保持单行,避免换行 */
gap: 4px;
min-height: 36px; /* 稍微增加高度 */
flex-wrap: nowrap;
min-height: 36px;
}
.header-left {
min-width: 0; /* 允许完全收缩 */
flex: 1;
margin-right: 6px;
gap: 6rpx;
}
.card-title {
font-size: 15px; /* 稍微减小字体 */
max-width: 100px; /* 限制最大宽度 */
font-size: 28rpx;
}
.refresh-btn {
min-width: 58px; /* 小屏幕下减小按钮宽度 */
font-size: 12px; /* 减小按钮字体 */
min-width: 58px;
font-size: 12px;
}
.map-btn {
@@ -1742,17 +1754,12 @@ export default {
/* 极小屏幕适配(iPhone SE等) */
@media screen and (max-width: 320px) {
.card-header {
gap: 2px;
}
.header-left {
margin-right: 4px;
gap: 4rpx;
}
.card-title {
font-size: 14px;
max-width: 80px;
font-size: 26rpx;
}
.refresh-btn {
@@ -1783,55 +1790,55 @@ export default {
.location-error-card {
background: #fffbe6;
border: 1px solid #ffe58f;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border-radius: 16rpx;
padding: 24rpx;
margin: 0 20rpx 20rpx 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.error-content {
padding-top: 8px;
padding-top: 12rpx;
}
.error-header {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
margin-bottom: 24rpx;
}
.error-icon {
margin-right: 8px;
margin-right: 12rpx;
}
.error-title {
font-size: 16px;
font-size: 30rpx;
font-weight: bold;
color: #e6a23c;
}
.error-message {
display: block;
font-size: 14px;
font-size: 26rpx;
color: #faad14;
text-align: center;
margin-bottom: 20px;
margin-bottom: 30rpx;
line-height: 1.5;
}
.error-tips-section {
background: #fff7e6;
border-radius: 8px;
padding: 16px;
border-radius: 12rpx;
padding: 24rpx;
border: 1px solid #ffd591;
}
.tips-title {
display: block;
font-size: 14px;
font-size: 26rpx;
font-weight: bold;
color: #d48806;
margin-bottom: 12px;
margin-bottom: 20rpx;
}
.tips-list {
@@ -1841,7 +1848,7 @@ export default {
.tip-item {
display: flex;
align-items: center;
margin-bottom: 8px;
margin-bottom: 12rpx;
}
.tip-item:last-child {
@@ -1849,12 +1856,12 @@ export default {
}
.tip-icon {
margin-right: 8px;
margin-right: 12rpx;
flex-shrink: 0;
}
.tip-text {
font-size: 13px;
font-size: 24rpx;
color: #d48806;
line-height: 1.4;
}
@@ -1863,40 +1870,40 @@ export default {
.stats-grid {
display: flex;
justify-content: space-between;
padding-top: 8px;
padding-top: 12rpx;
}
.stat-item {
flex: 1;
text-align: center;
padding: 12px 8px;
padding: 20rpx 12rpx;
background: #f8f9fa;
border-radius: 8px;
margin: 0 4px;
border-radius: 12rpx;
margin: 0 8rpx;
}
.stat-number {
display: block;
font-size: 20px;
font-size: 36rpx;
font-weight: bold;
color: #409eff;
margin-bottom: 4px;
color: #2979ff;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 12px;
font-size: 24rpx;
color: #606266;
}
/* 签到记录 */
.records-list {
padding-top: 8px;
padding-top: 12rpx;
}
.record-item {
display: flex;
align-items: center;
padding: 12px 0;
padding: 20rpx 0;
border-bottom: 1px solid #f0f0f0;
}
@@ -1905,17 +1912,17 @@ export default {
}
.record-date {
font-size: 14px;
font-size: 26rpx;
color: #303133;
width: 100px;
width: 160rpx;
flex-shrink: 0;
}
.record-time {
font-size: 14px;
font-size: 26rpx;
color: #606266;
flex: 1;
margin-left: 12px;
margin-left: 20rpx;
}
.record-status {
@@ -1927,72 +1934,21 @@ export default {
display: flex;
flex-direction: column;
align-items: center;
padding: 32px 0;
padding: 60rpx 0;
}
.empty-text {
font-size: 14px;
font-size: 26rpx;
color: #909399;
margin-top: 8px;
margin-top: 12rpx;
}
/* 响应式设计 */
@media screen and (max-width: 375px) {
.coord-row {
padding: 10px;
/* uview组件样式调整 */
::v-deep .u-tag {
margin: 0;
}
.coord-divider {
margin: 0 12px;
}
.coord-value {
font-size: 13px;
}
.map-btn {
min-width: 120px;
font-size: 14px;
}
.error-tips-section {
padding: 12px;
}
}
@media screen and (min-width: 414px) {
.location-address {
padding: 16px;
}
.coord-row {
padding: 16px;
}
.accuracy-info {
padding: 12px 16px;
}
.coord-value {
font-size: 15px;
}
}
/* 深色模式适配 */
@media (prefers-color-scheme: dark) {
.location-address,
.coord-row {
background: rgba(255, 255, 255, 0.05);
}
.accuracy-info {
background: rgba(103, 194, 58, 0.1);
border-color: rgba(103, 194, 58, 0.3);
}
.error-tips-section {
background: rgba(255, 247, 230, 0.1);
border-color: rgba(255, 213, 145, 0.3);
}
::v-deep .u-button {
border-radius: 12rpx;
}
</style>
+46 -28
View File
@@ -1,6 +1,14 @@
<template>
<view class="visit-add-container">
<!-- 顶部导航 -->
<u-navbar title="新增走访" :border-bottom="false" bg-color="#3c9cff" title-color="#fff">
<view slot="left" @click="goBack">
<u-icon name="arrow-left" color="#fff" size="22"></u-icon>
</view>
</u-navbar>
<!-- 表单内容 -->
<view class="content-wrapper">
<u-form
ref="formRef"
:model="form"
@@ -204,15 +212,18 @@
</u-form-item>
</view>
</u-form>
</view>
<!-- 底部操作按钮 -->
<view class="bottom-actions">
<u-button
type="primary"
shape="round"
@click="submitForm"
:loading="submitLoading"
style="width: 100%;"
style="width: 100%"
>
<u-icon name="checkmark-circle" size="16" style="margin-right: 4rpx"></u-icon>
提交走访记录
</u-button>
</view>
@@ -229,11 +240,7 @@
<view class="student-selector">
<view class="selector-header">
<text class="selector-title">选择学生</text>
<u-icon
name="close"
size="20"
@click="showStudentSelector = false"
/>
<u-icon name="close" size="20" @click="showStudentSelector = false" />
</view>
<view class="search-section">
@@ -260,7 +267,9 @@
</view>
<view class="student-detail">
<text class="detail-text">{{ student.className || '-' }}</text>
<text class="detail-text">{{ student.companyName || '暂无实习信息' }}</text>
<text class="detail-text">
{{ student.companyName || '暂无实习信息' }}
</text>
</view>
<u-icon
name="checkmark"
@@ -281,7 +290,7 @@
<view class="student-loading" v-if="studentLoading">
<u-loading-icon mode="circle" />
<text style="margin-left: 10rpx; color: #909399;">加载中...</text>
<text style="margin-left: 10rpx; color: #909399">加载中...</text>
</view>
</view>
</view>
@@ -325,23 +334,21 @@ export default {
visitSummary: '',
visitImages: '',
remark: '',
status: 'planned' // 默认状态为计划中
status: 'planned', // 默认状态为计划中
},
// 表单验证规则
rules: {
studentId: [
{ required: true, message: '请选择学生', trigger: ['blur', 'change'] }
],
studentId: [{ required: true, message: '请选择学生', trigger: ['blur', 'change'] }],
visitDate: [
{ required: true, message: '请选择走访日期', trigger: ['blur', 'change'] }
{ required: true, message: '请选择走访日期', trigger: ['blur', 'change'] },
],
visitorName: [
{ required: true, message: '请输入走访人员姓名', trigger: ['blur', 'change'] }
{ required: true, message: '请输入走访人员姓名', trigger: ['blur', 'change'] },
],
visitPurpose: [
{ required: true, message: '请输入走访目的', trigger: ['blur', 'change'] }
]
{ required: true, message: '请输入走访目的', trigger: ['blur', 'change'] },
],
},
// 学生选择
@@ -363,7 +370,7 @@ export default {
maxImageCount: 10,
// 加载状态
submitLoading: false
submitLoading: false,
};
},
@@ -372,6 +379,11 @@ export default {
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack();
},
// 初始化表单
async initForm() {
// 获取当前登录用户信息,自动填入走访人
@@ -412,7 +424,7 @@ export default {
const params = {
classTeacherId,
pageNum: 1,
pageSize: 100 // 获取较多数据供选择
pageSize: 100, // 获取较多数据供选择
};
const response = await getStudentInternshipList(params);
@@ -553,7 +565,6 @@ export default {
console.log('最终form.visitDate:', this.form.visitDate);
console.log('最终visitDateValue:', this.visitDateValue);
console.log('日期设置成功');
} catch (error) {
console.error('日期处理异常:', error.message);
console.error('异常详情 - value:', value, 'type:', typeof value);
@@ -602,7 +613,7 @@ export default {
sourceType: ['album', 'camera'],
success: (res) => {
this.handleImageUpload(res.tempFilePaths);
}
},
});
},
@@ -616,7 +627,7 @@ export default {
// 暂时使用本地路径作为示例
this.imageList.push({
url: filePath,
path: filePath
path: filePath,
});
}
@@ -632,10 +643,10 @@ export default {
// 预览图片
previewImage(index) {
const urls = this.imageList.map(item => item.url);
const urls = this.imageList.map((item) => item.url);
uni.previewImage({
urls,
current: index
current: index,
});
},
@@ -645,8 +656,6 @@ export default {
this.form.visitImages = JSON.stringify(this.imageList);
},
// 提交表单
async submitForm() {
// 表单验证
@@ -661,7 +670,7 @@ export default {
try {
const formData = {
...this.form,
status: '1' // 已提交状态 (0-草稿 1-已提交 2-已审核)
status: '1', // 已提交状态 (0-草稿 1-已提交 2-已审核)
};
const response = await addVisit(formData);
@@ -680,8 +689,8 @@ export default {
} finally {
this.submitLoading = false;
}
}
}
},
},
};
</script>
@@ -689,6 +698,9 @@ export default {
.visit-add-container {
min-height: 100vh;
background-color: #f8f8f8;
}
.content-wrapper {
padding-bottom: 120rpx; // 为底部按钮留出空间
}
@@ -696,6 +708,12 @@ export default {
background: #ffffff;
margin-bottom: 20rpx;
padding: 0 20rpx;
border-radius: 12rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
&:first-child {
margin-top: 20rpx;
}
.section-title {
padding: 24rpx 0 16rpx;
+69 -27
View File
@@ -1,5 +1,17 @@
<template>
<view class="visit-detail-container">
<!-- 顶部导航 -->
<u-navbar title="走访详情" :border-bottom="false" bg-color="#3c9cff" title-color="#fff">
<view slot="left" @click="goBack">
<u-icon name="arrow-left" color="#fff" size="22"></u-icon>
</view>
</u-navbar>
<!-- 加载状态 -->
<u-loading-page :loading="pageLoading" loading-text="加载中..."></u-loading-page>
<!-- 内容区域 -->
<view v-if="!pageLoading" class="content-wrapper">
<!-- 学生信息 -->
<view class="info-section">
<view class="section-title">学生信息</view>
@@ -68,7 +80,9 @@
</view>
<view class="info-item info-item-full" v-if="visitInfo.visitPurpose">
<text class="info-label">走访目的:</text>
<text class="info-value info-value-multiline">{{ visitInfo.visitPurpose }}</text>
<text class="info-value info-value-multiline">
{{ visitInfo.visitPurpose }}
</text>
</view>
</view>
</view>
@@ -79,23 +93,33 @@
<view class="section-content">
<view class="info-item info-item-full" v-if="visitInfo.companySituation">
<text class="info-label">企业情况:</text>
<text class="info-value info-value-multiline">{{ visitInfo.companySituation }}</text>
<text class="info-value info-value-multiline">
{{ visitInfo.companySituation }}
</text>
</view>
<view class="info-item info-item-full" v-if="visitInfo.studentPerformance">
<text class="info-label">学生表现:</text>
<text class="info-value info-value-multiline">{{ visitInfo.studentPerformance }}</text>
<text class="info-value info-value-multiline">
{{ visitInfo.studentPerformance }}
</text>
</view>
<view class="info-item info-item-full" v-if="visitInfo.problemsFound">
<text class="info-label">发现问题:</text>
<text class="info-value info-value-multiline">{{ visitInfo.problemsFound }}</text>
<text class="info-value info-value-multiline">
{{ visitInfo.problemsFound }}
</text>
</view>
<view class="info-item info-item-full" v-if="visitInfo.suggestions">
<text class="info-label">改进建议:</text>
<text class="info-value info-value-multiline">{{ visitInfo.suggestions }}</text>
<text class="info-value info-value-multiline">
{{ visitInfo.suggestions }}
</text>
</view>
<view class="info-item info-item-full" v-if="visitInfo.visitSummary">
<text class="info-label">走访总结:</text>
<text class="info-value info-value-multiline">{{ visitInfo.visitSummary }}</text>
<text class="info-value info-value-multiline">
{{ visitInfo.visitSummary }}
</text>
</view>
</view>
</view>
@@ -146,28 +170,27 @@
<view class="action-section" v-if="canEdit">
<u-button
type="primary"
shape="round"
@click="handleEdit"
:loading="loading"
style="margin-right: 20rpx;"
style="flex: 1; margin-right: 20rpx"
>
<u-icon name="edit-pen" size="16" style="margin-right: 4rpx"></u-icon>
编辑走访记录
</u-button>
<u-button
type="error"
shape="round"
@click="handleDelete"
:loading="loading"
plain
style="flex: 1"
>
<u-icon name="trash" size="16" style="margin-right: 4rpx"></u-icon>
删除记录
</u-button>
</view>
<!-- 加载遮罩 -->
<u-loading-page
:loading="pageLoading"
loading-text="加载中..."
loading-mode="circle"
/>
</view>
</view>
</template>
@@ -189,7 +212,7 @@ export default {
visitInfo: {},
// 图片数据
imageList: []
imageList: [],
};
},
@@ -197,10 +220,12 @@ export default {
// 是否可以编辑
canEdit() {
// 只有计划中或草稿状态的记录可以编辑
return this.visitInfo.status === 'planned' ||
return (
this.visitInfo.status === 'planned' ||
this.visitInfo.status === '0' ||
this.visitInfo.status === 'draft';
}
this.visitInfo.status === 'draft'
);
},
},
onLoad(options) {
@@ -214,6 +239,11 @@ export default {
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack();
},
// 加载走访记录详情
async loadVisitDetail() {
this.pageLoading = true;
@@ -232,7 +262,6 @@ export default {
this.imageList = [];
}
}
} else {
uni.showToast({ title: response.msg || '获取数据失败', icon: 'none' });
uni.navigateBack();
@@ -282,17 +311,17 @@ export default {
// 预览图片
previewImage(index) {
const urls = this.imageList.map(item => item.url);
const urls = this.imageList.map((item) => item.url);
uni.previewImage({
urls,
current: index
current: index,
});
},
// 编辑走访记录
handleEdit() {
uni.navigateTo({
url: `/pages/visit/edit?id=${this.visitId}`
url: `/pages/visit/edit?id=${this.visitId}`,
});
},
@@ -305,7 +334,7 @@ export default {
if (res.confirm) {
await this.deleteVisit();
}
}
},
});
},
@@ -329,8 +358,8 @@ export default {
} finally {
this.loading = false;
}
}
}
},
},
};
</script>
@@ -338,6 +367,9 @@ export default {
.visit-detail-container {
min-height: 100vh;
background-color: #f8f8f8;
}
.content-wrapper {
padding: 20rpx;
padding-bottom: 40rpx;
}
@@ -347,6 +379,7 @@ export default {
border-radius: 12rpx;
margin-bottom: 20rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
.section-title {
padding: 24rpx 20rpx 16rpx;
@@ -354,7 +387,7 @@ export default {
font-weight: 600;
color: #303133;
border-bottom: 1rpx solid #ebeef5;
background: #f8f9fa;
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
}
.section-content {
@@ -390,8 +423,9 @@ export default {
flex: 1;
&.info-value-multiline {
line-height: 1.6;
line-height: 1.8;
word-break: break-all;
color: #606266;
}
}
}
@@ -406,6 +440,12 @@ export default {
height: 200rpx;
border-radius: 8rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
}
image {
width: 100%;
@@ -416,9 +456,11 @@ export default {
.action-section {
display: flex;
gap: 20rpx;
padding: 20rpx;
background: #ffffff;
border-radius: 12rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
</style>
+50 -25
View File
@@ -1,6 +1,17 @@
<template>
<view class="visit-edit-container">
<!-- 顶部导航 -->
<u-navbar title="编辑走访" :border-bottom="false" bg-color="#3c9cff" title-color="#fff">
<view slot="left" @click="goBack">
<u-icon name="arrow-left" color="#fff" size="22"></u-icon>
</view>
</u-navbar>
<!-- 加载状态 -->
<u-loading-page :loading="pageLoading" loading-text="加载中..."></u-loading-page>
<!-- 表单内容 -->
<view v-if="!pageLoading" class="content-wrapper">
<u-form
ref="formRef"
:model="form"
@@ -194,23 +205,28 @@
</u-form-item>
</view>
</u-form>
</view>
<!-- 底部操作按钮 -->
<view class="bottom-actions">
<view v-if="!pageLoading" class="bottom-actions">
<u-button
type="default"
type="info"
shape="round"
@click="saveDraft"
:loading="draftLoading"
style="margin-right: 20rpx;"
style="margin-right: 20rpx"
>
<u-icon name="folder" size="16" style="margin-right: 4rpx"></u-icon>
保存草稿
</u-button>
<u-button
type="primary"
shape="round"
@click="submitForm"
:loading="submitLoading"
style="flex: 1;"
style="flex: 1"
>
<u-icon name="checkmark-circle" size="16" style="margin-right: 4rpx"></u-icon>
更新走访记录
</u-button>
</view>
@@ -226,11 +242,7 @@
/>
<!-- 加载遮罩 -->
<u-loading-page
:loading="pageLoading"
loading-text="加载中..."
loading-mode="circle"
/>
<u-loading-page :loading="pageLoading" loading-text="加载中..." loading-mode="circle" />
</view>
</template>
@@ -268,20 +280,20 @@ export default {
visitSummary: '',
visitImages: '',
remark: '',
status: ''
status: '',
},
// 表单验证规则
rules: {
visitDate: [
{ required: true, message: '请选择走访日期', trigger: ['blur', 'change'] }
{ required: true, message: '请选择走访日期', trigger: ['blur', 'change'] },
],
visitorName: [
{ required: true, message: '请输入走访人员姓名', trigger: ['blur', 'change'] }
{ required: true, message: '请输入走访人员姓名', trigger: ['blur', 'change'] },
],
visitPurpose: [
{ required: true, message: '请输入走访目的', trigger: ['blur', 'change'] }
]
{ required: true, message: '请输入走访目的', trigger: ['blur', 'change'] },
],
},
// 日期选择
@@ -295,7 +307,7 @@ export default {
// 加载状态
submitLoading: false,
draftLoading: false
draftLoading: false,
};
},
@@ -310,6 +322,11 @@ export default {
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack();
},
// 加载走访记录详情
async loadVisitDetail() {
this.pageLoading = true;
@@ -337,7 +354,7 @@ export default {
visitSummary: response.data.visitSummary,
visitImages: response.data.visitImages,
remark: response.data.remark,
status: response.data.status
status: response.data.status,
};
// 处理图片数据
@@ -354,7 +371,6 @@ export default {
if (response.data.visitDate) {
this.visitDateValue = new Date(response.data.visitDate).getTime();
}
} else {
uni.showToast({ title: response.msg || '获取数据失败', icon: 'none' });
uni.navigateBack();
@@ -392,7 +408,7 @@ export default {
sourceType: ['album', 'camera'],
success: (res) => {
this.handleImageUpload(res.tempFilePaths);
}
},
});
},
@@ -406,7 +422,7 @@ export default {
// 暂时使用本地路径作为示例
this.imageList.push({
url: filePath,
path: filePath
path: filePath,
});
}
@@ -422,10 +438,10 @@ export default {
// 预览图片
previewImage(index) {
const urls = this.imageList.map(item => item.url);
const urls = this.imageList.map((item) => item.url);
uni.previewImage({
urls,
current: index
current: index,
});
},
@@ -441,7 +457,7 @@ export default {
try {
const formData = {
...this.form,
status: 'planned' // 草稿状态
status: 'planned', // 草稿状态
};
const response = await updateVisit(formData);
@@ -476,7 +492,7 @@ export default {
try {
const formData = {
...this.form,
status: 'completed' // 已完成状态
status: 'completed', // 已完成状态
};
const response = await updateVisit(formData);
@@ -495,8 +511,8 @@ export default {
} finally {
this.submitLoading = false;
}
}
}
},
},
};
</script>
@@ -504,6 +520,9 @@ export default {
.visit-edit-container {
min-height: 100vh;
background-color: #f8f8f8;
}
.content-wrapper {
padding-bottom: 120rpx; // 为底部按钮留出空间
}
@@ -511,6 +530,12 @@ export default {
background: #ffffff;
margin-bottom: 20rpx;
padding: 0 20rpx;
border-radius: 12rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
&:first-child {
margin-top: 20rpx;
}
.section-title {
padding: 24rpx 0 16rpx;
+138 -68
View File
@@ -1,5 +1,12 @@
<template>
<view class="visit-container">
<!-- 顶部导航 -->
<u-navbar title="班主任走访" :border-bottom="false" bg-color="#3c9cff" title-color="#fff">
<view slot="left" @click="goBack">
<u-icon name="arrow-left" color="#fff" size="22"></u-icon>
</view>
</u-navbar>
<!-- 搜索栏 -->
<view class="search-bar">
<u-search
@@ -9,7 +16,6 @@
@search="handleSearch"
@custom="handleSearch"
bg-color="#f6f6f6"
height="72"
shape="round"
/>
</view>
@@ -49,6 +55,7 @@
<view class="filter-actions">
<u-button
type="primary"
shape="round"
size="small"
@click="handleFilter"
:loading="loading"
@@ -56,10 +63,11 @@
筛选
</u-button>
<u-button
type="default"
type="info"
shape="round"
size="small"
@click="resetFilter"
style="margin-left: 10rpx;"
style="margin-left: 10rpx"
>
重置
</u-button>
@@ -69,26 +77,32 @@
<!-- 筛选按钮 -->
<view class="action-bar">
<u-button
type="default"
type="info"
shape="round"
size="small"
:icon="showFilter ? 'arrow-up' : 'arrow-down'"
@click="showFilter = !showFilter"
>
{{ showFilter ? '收起筛选' : '展开筛选' }}
</u-button>
<u-button
type="primary"
size="small"
icon="plus"
@click="handleAdd"
>
<u-button type="primary" shape="round" size="small" icon="plus" @click="handleAdd">
新增走访
</u-button>
</view>
<!-- 加载状态 -->
<u-loading-page
:loading="loading && visitList.length === 0"
loading-text="加载中..."
></u-loading-page>
<!-- 走访记录列表 -->
<view class="list-container">
<view class="list-content" v-if="visitList.length > 0">
<view v-if="!loading || visitList.length > 0" class="content-wrapper">
<view v-if="visitList.length === 0" class="empty-container">
<u-empty text="暂无走访记录" mode="data"></u-empty>
</view>
<view class="list-content" v-else>
<view
class="visit-card"
v-for="item in visitList"
@@ -103,7 +117,7 @@
</view>
<view class="status-badge">
<u-tag
:text="item.statusName || item.status"
:text="getStatusLabel(item.status)"
:type="getStatusType(item.status)"
size="mini"
/>
@@ -120,58 +134,73 @@
<view class="card-row">
<u-icon name="calendar" size="16" color="#909399" />
<text class="info-text">{{ item.visitDate || '-' }}</text>
<u-icon name="account" size="16" color="#909399" style="margin-left: 20rpx;" />
<u-icon
name="account"
size="16"
color="#909399"
style="margin-left: 20rpx"
/>
<text class="info-text">{{ item.visitorName || '-' }}</text>
</view>
<!-- 班级专业信息 -->
<view class="card-row">
<u-icon name="bookmark" size="16" color="#909399" />
<text class="info-text">{{ item.className || '-' }} / {{ item.majorName || '-' }}</text>
<text class="info-text">
{{ item.className || '-' }} / {{ item.majorName || '-' }}
</text>
</view>
<!-- 操作按钮 -->
<view class="card-actions">
<u-button
type="default"
size="mini"
type="primary"
shape="round"
size="small"
@click.stop="handleEdit(item)"
v-if="item.status === 'planned' || item.status === '0'"
>
<u-icon
name="edit-pen"
size="14"
color="#fff"
style="margin-right: 4rpx"
></u-icon>
编辑
</u-button>
<u-button
type="error"
size="mini"
shape="round"
size="small"
@click.stop="handleDelete(item)"
style="margin-left: 10rpx;"
style="margin-left: 10rpx"
v-if="item.status === 'planned' || item.status === '0'"
>
<u-icon
name="trash"
size="14"
color="#fff"
style="margin-right: 4rpx"
></u-icon>
删除
</u-button>
<u-button
type="info"
shape="round"
size="small"
plain
@click.stop="handleView(item)"
v-else
>
<u-icon name="eye" size="14" style="margin-right: 4rpx"></u-icon>
查看
</u-button>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-else-if="!loading">
<u-empty
mode="data"
text="暂无走访记录"
textColor="#c8c9cc"
iconColor="#c8c9cc"
/>
</view>
<!-- 加载状态 -->
<view class="loading-state" v-if="loading">
<u-loading-icon mode="circle" />
<text style="margin-left: 10rpx; color: #909399;">加载中...</text>
</view>
</view>
<!-- 上拉加载更多 -->
<view class="load-more" v-if="hasMore && visitList.length > 0">
<view class="load-more" v-if="hasMore">
<u-loadmore
:status="loadMoreStatus"
@loadmore="loadMore"
@@ -180,6 +209,7 @@
no-more-text="没有更多了"
/>
</view>
</view>
<!-- 状态选择器 -->
<u-picker
@@ -218,7 +248,7 @@ export default {
visitorName: '',
status: '',
beginVisitDate: '',
endVisitDate: ''
endVisitDate: '',
},
// 列表数据
@@ -238,7 +268,7 @@ export default {
// 日期选择
showDatePicker: false,
dateRangeText: '',
selectedDateRange: []
selectedDateRange: [],
};
},
@@ -257,6 +287,11 @@ export default {
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack();
},
// 初始化页面
async initPage() {
await this.loadStatusDict();
@@ -272,22 +307,24 @@ export default {
this.statusOptions = [
[
{ text: '全部状态', value: '' },
...response.data.map(item => ({
...response.data.map((item) => ({
text: item.dictLabel,
value: item.dictValue
}))
]
value: item.dictValue,
})),
],
];
}
} catch (error) {
console.error('加载状态字典失败:', error);
// 设置默认状态选项
this.statusOptions = [[
this.statusOptions = [
[
{ text: '全部状态', value: '' },
{ text: '计划中', value: 'planned' },
{ text: '已完成', value: 'completed' },
{ text: '已取消', value: 'cancelled' }
]];
{ text: '已取消', value: 'cancelled' },
],
];
}
},
@@ -310,7 +347,7 @@ export default {
const { rows, total } = response;
if (isLoadMore) {
this.visitList = [...this.visitList, ...rows || []];
this.visitList = [...this.visitList, ...(rows || [])];
} else {
this.visitList = rows || [];
}
@@ -372,7 +409,7 @@ export default {
visitorName: '',
status: '',
beginVisitDate: '',
endVisitDate: ''
endVisitDate: '',
};
this.statusText = '';
this.dateRangeText = '';
@@ -405,6 +442,32 @@ export default {
this.showDatePicker = false;
},
// 获取状态标签文本
getStatusLabel(status) {
// 如果statusDict字典已加载,从字典中查找
if (this.statusDict && this.statusDict.length > 0) {
const dictItem = this.statusDict.find((item) => item.dictValue === String(status));
if (dictItem) {
return dictItem.dictLabel;
}
}
// 如果字典未加载或找不到,使用默认映射
switch (String(status)) {
case 'planned':
case '0':
return '计划中';
case 'completed':
case '1':
return '已完成';
case 'cancelled':
case '2':
return '已取消';
default:
return status || '未知';
}
},
// 获取状态类型
getStatusType(status) {
switch (status) {
@@ -425,21 +488,21 @@ export default {
// 查看详情
handleView(item) {
uni.navigateTo({
url: `/pages/visit/detail?id=${item.visitId}`
url: `/pages/visit/detail?id=${item.visitId}`,
});
},
// 新增走访
handleAdd() {
uni.navigateTo({
url: '/pages/visit/add'
url: '/pages/visit/add',
});
},
// 编辑走访
handleEdit(item) {
uni.navigateTo({
url: `/pages/visit/edit?id=${item.visitId}`
url: `/pages/visit/edit?id=${item.visitId}`,
});
},
@@ -452,7 +515,7 @@ export default {
if (res.confirm) {
await this.deleteVisit(item.visitId);
}
}
},
});
},
@@ -474,8 +537,8 @@ export default {
} finally {
uni.hideLoading();
}
}
}
},
},
};
</script>
@@ -515,24 +578,38 @@ export default {
border-bottom: 1rpx solid #ebeef5;
}
.list-container {
.content-wrapper {
padding: 20rpx;
}
.empty-container {
padding: 80rpx 20rpx;
text-align: center;
}
.list-content {
.visit-card {
background: #ffffff;
border-radius: 12rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
&:active {
transform: scale(0.99);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
align-items: flex-start;
margin-bottom: 16rpx;
.student-info {
flex: 1;
.student-name {
font-size: 32rpx;
font-weight: 600;
@@ -545,6 +622,10 @@ export default {
color: #909399;
}
}
.status-badge {
flex-shrink: 0;
}
}
.card-row {
@@ -571,20 +652,9 @@ export default {
border-top: 1rpx solid #ebeef5;
}
}
.empty-state {
padding: 80rpx 20rpx;
text-align: center;
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
}
.load-more {
padding: 20rpx;
padding: 20rpx 0;
}
</style>
@@ -1,5 +1,12 @@
<template>
<view class="detail-container">
<!-- 顶部导航 -->
<u-navbar title="申请详情" :border-bottom="false" bg-color="#0051d9" title-color="#fff">
<view slot="left" @click="goBack">
<u-icon name="arrow-left" color="#fff" size="22"></u-icon>
</view>
</u-navbar>
<!-- 申请状态卡片 -->
<view class="status-card" v-if="applicationData">
<view class="card-title">申请状态</view>
@@ -17,7 +24,10 @@
<text class="status-label">申请时间:</text>
<text class="status-value">{{ applicationData.createTime }}</text>
</view>
<view class="status-item" v-if="applicationData.updateTime !== applicationData.createTime">
<view
class="status-item"
v-if="applicationData.updateTime !== applicationData.createTime"
>
<text class="status-label">更新时间:</text>
<text class="status-value">{{ applicationData.updateTime }}</text>
</view>
@@ -32,11 +42,11 @@
<view class="position-name">{{ applicationData.positionName }}</view>
<view class="position-detail">
<view class="detail-item">
<u-icon name="list" size="28" color="#666" />
<u-icon name="list" size="18" color="#666" />
<text>岗位类型:{{ applicationData.positionType }}</text>
</view>
<view class="detail-item">
<u-icon name="map" size="28" color="#666" />
<u-icon name="map" size="18" color="#666" />
<text>工作地点:{{ applicationData.workLocation }}</text>
</view>
</view>
@@ -61,7 +71,9 @@
</view>
<view class="info-item">
<text class="info-label">联系电话:</text>
<text class="info-value contact-phone" @click="callPhone">{{ applicationData.contactPhone }}</text>
<text class="info-value contact-phone" @click="callPhone">
{{ applicationData.contactPhone }}
</text>
</view>
<view class="info-item">
<text class="info-label">学院:</text>
@@ -187,7 +199,7 @@
size="large"
@click="editApplication"
v-if="canEdit"
style="margin-right: 20rpx;"
style="margin-right: 20rpx"
>
编辑申请
</u-button>
@@ -208,10 +220,7 @@
</template>
<script>
import {
getApplicationDetail,
cancelApplication
} from '@/api/workStudy.js';
import { getApplicationDetail, cancelApplication } from '@/api/workStudy.js';
export default {
name: 'ApplicationDetail',
@@ -220,15 +229,17 @@ export default {
applicationId: '',
applicationData: null,
loading: false,
canceling: false
canceling: false,
};
},
computed: {
// 是否有审核记录
hasReviewRecord() {
return this.applicationData &&
(this.applicationData.collegeReviewStatus || this.applicationData.finalReviewStatus);
return (
this.applicationData &&
(this.applicationData.collegeReviewStatus || this.applicationData.finalReviewStatus)
);
},
// 是否显示操作按钮
@@ -238,15 +249,19 @@ export default {
// 是否可以编辑
canEdit() {
return this.applicationData &&
['DRAFT', 'COLLEGE_REJECTED'].includes(this.applicationData.status);
return (
this.applicationData &&
['DRAFT', 'COLLEGE_REJECTED'].includes(this.applicationData.status)
);
},
// 是否可以撤销
canCancel() {
return this.applicationData &&
['DRAFT', 'SUBMITTED', 'COLLEGE_REVIEWING'].includes(this.applicationData.status);
}
return (
this.applicationData &&
['DRAFT', 'SUBMITTED', 'COLLEGE_REVIEWING'].includes(this.applicationData.status)
);
},
},
onLoad(options) {
@@ -256,7 +271,7 @@ export default {
} else {
uni.showToast({
title: '申请ID不能为空',
icon: 'none'
icon: 'none',
});
setTimeout(() => {
uni.navigateBack();
@@ -265,6 +280,11 @@ export default {
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack();
},
// 加载申请详情
async loadApplicationDetail() {
this.loading = true;
@@ -281,7 +301,7 @@ export default {
console.error('加载申请详情失败:', error);
uni.showToast({
title: error.message || '加载失败',
icon: 'none'
icon: 'none',
});
setTimeout(() => {
@@ -297,7 +317,7 @@ export default {
if (!this.applicationData) return;
uni.navigateTo({
url: `/pages/workStudy/apply?positionId=${this.applicationData.positionId}&applicationId=${this.applicationData.id}&mode=edit`
url: `/pages/workStudy/apply?positionId=${this.applicationData.positionId}&applicationId=${this.applicationData.id}&mode=edit`,
});
},
@@ -307,7 +327,7 @@ export default {
const { confirm } = await uni.showModal({
title: '确认撤销',
content: '确定要撤销此申请吗?',
showCancel: true
showCancel: true,
});
if (!confirm) return; // 用户取消
@@ -318,7 +338,7 @@ export default {
if (response.code === 200) {
uni.showToast({
title: '撤销成功',
icon: 'success'
icon: 'success',
});
// 重新加载数据
@@ -332,7 +352,7 @@ export default {
console.error('撤销申请失败:', error);
uni.showToast({
title: error.message || '撤销失败',
icon: 'none'
icon: 'none',
});
} finally {
this.canceling = false;
@@ -348,9 +368,9 @@ export default {
console.error('拨打电话失败:', error);
uni.showToast({
title: '拨打电话失败',
icon: 'none'
icon: 'none',
});
}
},
});
}
},
@@ -364,15 +384,15 @@ export default {
// 获取申请状态文字
getApplicationStatusText(status) {
const statusMap = {
'DRAFT': '草稿',
'SUBMITTED': '已提交',
'COLLEGE_REVIEWING': '学院审核中',
'COLLEGE_APPROVED': '学院审核通过',
'COLLEGE_REJECTED': '学院审核未通过',
'FINAL_REVIEWING': '学生处审核中',
'FINAL_APPROVED': '审核通过',
'FINAL_REJECTED': '审核未通过',
'CANCELLED': '已撤销'
DRAFT: '草稿',
SUBMITTED: '已提交',
COLLEGE_REVIEWING: '学院审核中',
COLLEGE_APPROVED: '学院审核通过',
COLLEGE_REJECTED: '学院审核未通过',
FINAL_REVIEWING: '学生处审核中',
FINAL_APPROVED: '审核通过',
FINAL_REJECTED: '审核未通过',
CANCELLED: '已撤销',
};
return statusMap[status] || '未知状态';
},
@@ -380,15 +400,15 @@ export default {
// 获取申请状态类型
getApplicationStatusType(status) {
const typeMap = {
'DRAFT': 'info',
'SUBMITTED': 'warning',
'COLLEGE_REVIEWING': 'warning',
'COLLEGE_APPROVED': 'success',
'COLLEGE_REJECTED': 'error',
'FINAL_REVIEWING': 'warning',
'FINAL_APPROVED': 'success',
'FINAL_REJECTED': 'error',
'CANCELLED': 'info'
DRAFT: 'info',
SUBMITTED: 'warning',
COLLEGE_REVIEWING: 'warning',
COLLEGE_APPROVED: 'success',
COLLEGE_REJECTED: 'error',
FINAL_REVIEWING: 'warning',
FINAL_APPROVED: 'success',
FINAL_REJECTED: 'error',
CANCELLED: 'info',
};
return typeMap[status] || 'default';
},
@@ -396,9 +416,9 @@ export default {
// 获取审核状态文字
getReviewStatusText(status) {
const statusMap = {
'PENDING': '待审核',
'APPROVED': '通过',
'REJECTED': '未通过'
PENDING: '待审核',
APPROVED: '通过',
REJECTED: '未通过',
};
return statusMap[status] || '未知';
},
@@ -406,38 +426,41 @@ export default {
// 获取审核状态类型
getReviewStatusType(status) {
const typeMap = {
'PENDING': 'warning',
'APPROVED': 'success',
'REJECTED': 'error'
PENDING: 'warning',
APPROVED: 'success',
REJECTED: 'error',
};
return typeMap[status] || 'default';
}
}
},
},
};
</script>
<style scoped>
.detail-container {
min-height: 100vh;
background-color: #f5f5f5;
background-color: #f5f6fa;
padding-bottom: 120rpx; /* 为底部按钮留空间 */
}
.status-card, .info-card, .content-card, .review-card {
.status-card,
.info-card,
.content-card,
.review-card {
background-color: #fff;
margin: 20rpx;
border-radius: 12rpx;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.card-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
color: #303133;
margin-bottom: 24rpx;
padding-bottom: 16rpx;
border-bottom: 1rpx solid #f0f0f0;
border-bottom: 1px solid #f0f0f0;
}
/* 状态卡片 */
@@ -463,12 +486,12 @@ export default {
.status-label {
font-size: 28rpx;
color: #666;
color: #606266;
}
.status-value {
font-size: 28rpx;
color: #333;
color: #303133;
}
/* 岗位信息 */
@@ -516,14 +539,14 @@ export default {
.info-label {
font-size: 28rpx;
color: #666;
color: #606266;
min-width: 160rpx;
flex-shrink: 0;
}
.info-value {
font-size: 28rpx;
color: #333;
color: #303133;
flex: 1;
line-height: 1.5;
}
@@ -536,7 +559,7 @@ export default {
/* 内容卡片 */
.content-text {
font-size: 28rpx;
color: #666;
color: #606266;
line-height: 1.8;
white-space: pre-wrap;
}
@@ -563,7 +586,7 @@ export default {
.review-title {
font-size: 30rpx;
font-weight: 500;
color: #333;
color: #303133;
}
.review-info {
+157 -130
View File
@@ -1,5 +1,12 @@
<template>
<view class="apply-container">
<!-- 顶部导航 -->
<u-navbar title="申请岗位" :border-bottom="false" bg-color="#0051d9" title-color="#fff">
<view slot="left" @click="goBack">
<u-icon name="arrow-left" color="#fff" size="22"></u-icon>
</view>
</u-navbar>
<!-- 岗位信息卡片 -->
<view class="position-card" v-if="positionData">
<view class="card-title">申请岗位</view>
@@ -14,21 +21,12 @@
<!-- 申请表单 -->
<view class="form-container">
<u-form
:model="formData"
ref="form"
label-width="140"
label-position="left"
>
<u-form :model="formData" ref="form" label-width="140" label-position="left">
<!-- 基本信息 -->
<view class="form-section">
<view class="section-title">基本信息</view>
<u-form-item
label="学生姓名"
prop="studentName"
:required="true"
>
<u-form-item label="学生姓名" prop="studentName" :required="true">
<u-input
v-model="formData.studentName"
placeholder="请输入学生姓名"
@@ -36,11 +34,7 @@
/>
</u-form-item>
<u-form-item
label="学号"
prop="studentNumber"
:required="true"
>
<u-form-item label="学号" prop="studentNumber" :required="true">
<u-input
v-model="formData.studentNumber"
placeholder="请输入学号"
@@ -48,11 +42,7 @@
/>
</u-form-item>
<u-form-item
label="身份证号"
prop="idCard"
:required="true"
>
<u-form-item label="身份证号" prop="idCard" :required="true">
<u-input
v-model="formData.idCard"
placeholder="请输入身份证号"
@@ -60,11 +50,7 @@
/>
</u-form-item>
<u-form-item
label="联系电话"
prop="contactPhone"
:required="true"
>
<u-form-item label="联系电话" prop="contactPhone" :required="true">
<u-input
v-model="formData.contactPhone"
placeholder="请输入联系电话"
@@ -77,11 +63,7 @@
<view class="form-section">
<view class="section-title">学籍信息</view>
<u-form-item
label="学院名称"
prop="collegeName"
:required="true"
>
<u-form-item label="学院名称" prop="collegeName" :required="true">
<u-input
v-model="formData.collegeName"
placeholder="学院名称"
@@ -89,11 +71,7 @@
/>
</u-form-item>
<u-form-item
label="专业名称"
prop="majorName"
:required="true"
>
<u-form-item label="专业名称" prop="majorName" :required="true">
<u-input
v-model="formData.majorName"
placeholder="专业名称"
@@ -101,57 +79,48 @@
/>
</u-form-item>
<u-form-item
label="班级名称"
prop="className"
:required="true"
>
<u-form-item label="班级名称" prop="className" :required="true">
<u-input
v-model="formData.className"
placeholder="班级名称"
:disabled="true"
/>
</u-form-item>
</view>
<!-- 申请信息 -->
<view class="form-section">
<view class="section-title">申请信息</view>
<u-form-item
label="家庭经济情况"
prop="familyFinancialStatus"
:required="true"
<u-form-item label="家庭经济情况" prop="familyFinancialStatus" :required="true">
<u-picker
:columns="familyStatusColumns"
v-model="familyStatusIndex"
:show="showFamilyStatusPicker"
@confirm="onFamilyStatusConfirm"
@close="showFamilyStatusPicker = false"
></u-picker>
<view class="picker-input" @click="showFamilyStatusPicker = true">
<text
class="picker-text"
:class="{ placeholder: !formData.familyFinancialStatus }"
>
<u-radio-group v-model="formData.familyFinancialStatus">
<u-radio
v-for="option in familyStatusOptions"
:key="option.value"
:name="option.value"
:label="option.label"
style="margin-right: 20rpx; margin-bottom: 15rpx; font-size: 28rpx;"
/>
</u-radio-group>
{{ formData.familyFinancialStatus || '请选择家庭经济情况' }}
</text>
<u-icon name="arrow-right" size="14" color="#c8c9cc"></u-icon>
</view>
</u-form-item>
<u-form-item
label="计划工作时长"
prop="plannedWorkHours"
>
<u-form-item label="计划工作时长" prop="plannedWorkHours">
<u-input
v-model="formData.plannedWorkHours"
placeholder="请输入计划工作时长(小时/周)"
type="number"
placeholder-style="font-size: 24rpx"
/>
</u-form-item>
<u-form-item
label="申请理由"
prop="applicationReason"
:required="true"
>
<u-form-item label="申请理由" prop="applicationReason" :required="true">
<u-textarea
v-model="formData.applicationReason"
placeholder="请详细说明申请此岗位的理由"
@@ -159,14 +128,11 @@
:show-word-limit="true"
height="200"
count
placeholder-style="font-size: 24rpx"
/>
</u-form-item>
<u-form-item
label="个人能力及经验"
prop="personalAbility"
:required="true"
>
<u-form-item label="个人能力及经验" prop="personalAbility" :required="true">
<u-textarea
v-model="formData.personalAbility"
placeholder="请描述与岗位相关的个人能力和经验"
@@ -174,14 +140,11 @@
:show-word-limit="true"
height="200"
count
placeholder-style="font-size: 24rpx"
/>
</u-form-item>
<u-form-item
label="时间安排"
prop="timeArrangement"
:required="true"
>
<u-form-item label="时间安排" prop="timeArrangement" :required="true">
<u-textarea
v-model="formData.timeArrangement"
placeholder="请说明您的时间安排,确保能够胜任此岗位工作"
@@ -189,13 +152,11 @@
:show-word-limit="true"
height="150"
count
placeholder-style="font-size: 24rpx"
/>
</u-form-item>
<u-form-item
label="备注"
prop="remark"
>
<u-form-item label="备注" prop="remark">
<u-textarea
v-model="formData.remark"
placeholder="其他需要说明的情况(选填)"
@@ -203,6 +164,7 @@
:show-word-limit="true"
height="120"
count
placeholder-style="font-size: 24rpx"
/>
</u-form-item>
</view>
@@ -212,28 +174,20 @@
<!-- 底部操作按钮 -->
<view class="bottom-actions">
<u-button
type="default"
plain
type="info"
size="large"
@click="saveDraft"
:loading="saving"
style="margin-right: 20rpx;"
:custom-style="draftButtonStyle"
>
保存草稿
</u-button>
<u-button
type="primary"
size="large"
@click="submitApplication"
:loading="submitting"
>
<u-button type="primary" size="large" @click="submitApplication" :loading="submitting">
提交申请
</u-button>
</view>
<!-- 加载状态 -->
<u-loading-page :loading="loading" loading-text="加载中..." />
</view>
@@ -246,7 +200,7 @@ import {
getApplicationOptions,
addApplication,
updateApplication,
getApplicationDetail
getApplicationDetail,
} from '@/api/workStudy.js';
export default {
@@ -274,7 +228,7 @@ export default {
applicationReason: '',
personalAbility: '',
timeArrangement: '',
remark: ''
remark: '',
},
// 家庭经济情况选项(与web端保持一致)
@@ -282,13 +236,25 @@ export default {
{ value: '特困', label: '特困' },
{ value: '贫困', label: '贫困' },
{ value: '一般', label: '一般' },
{ value: '良好', label: '良好' }
{ value: '良好', label: '良好' },
],
// 家庭经济情况选择器相关
showFamilyStatusPicker: false,
familyStatusIndex: [0],
// uview-ui 2.x 单列选择器:使用纯字符串数组(二维)最稳定
// 格式:[[字符串1, 字符串2, ...]] - 外层数组表示列,内层数组表示选项
familyStatusColumns: [['特困', '贫困', '一般', '良好']],
// 加载状态
loading: false,
saving: false,
submitting: false
submitting: false,
// 按钮样式
draftButtonStyle: {
marginRight: '20rpx',
},
};
},
@@ -302,7 +268,7 @@ export default {
} else {
uni.showToast({
title: '岗位ID不能为空',
icon: 'none'
icon: 'none',
});
setTimeout(() => {
uni.navigateBack();
@@ -311,16 +277,54 @@ export default {
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack();
},
// 家庭经济情况选择器确认
// uview-ui 2.x Picker 组件 - 纯字符串数组模式
// confirm 事件返回:{ value: [选中值1, 选中值2, ...], indexs: [索引1, 索引2, ...] }
// 单列选择器:value 是 ['贫困'],indexs 是 [1]
onFamilyStatusConfirm(e) {
console.log('=== 家庭经济情况选择器确认 ===');
console.log('完整事件参数:', e);
console.log('e.value:', e.value); // 例如:['贫困']
console.log('e.indexs:', e.indexs); // 例如:[1]
// 单列选择器,取第一列的值
if (e && e.value && Array.isArray(e.value) && e.value.length > 0) {
const selectedValue = e.value[0]; // 直接获取字符串,如 "贫困"
console.log('选中的值:', selectedValue);
// 直接赋值
this.formData.familyFinancialStatus = selectedValue;
// 更新索引用于回显
if (e.indexs && Array.isArray(e.indexs)) {
this.familyStatusIndex = e.indexs;
console.log('更新索引为:', this.familyStatusIndex);
}
console.log(
'✅ 更新成功,familyFinancialStatus =',
this.formData.familyFinancialStatus,
);
} else {
console.error('❌ 无效的选择器返回值:', e);
}
this.showFamilyStatusPicker = false;
console.log('=== 选择器关闭 ===\n');
},
// 初始化数据
async initData() {
this.loading = true;
try {
// 并行加载数据
const promises = [
this.loadPositionData(),
this.loadStudentInfo()
];
const promises = [this.loadPositionData(), this.loadStudentInfo()];
await Promise.all(promises);
@@ -331,12 +335,11 @@ export default {
// 填充学生基本信息
this.fillStudentInfo();
} catch (error) {
console.error('初始化数据失败:', error);
uni.showToast({
title: '加载数据失败',
icon: 'none'
icon: 'none',
});
setTimeout(() => {
uni.navigateBack();
@@ -373,13 +376,11 @@ export default {
const data = response.data;
// 填充表单数据
Object.keys(this.formData).forEach(key => {
Object.keys(this.formData).forEach((key) => {
if (data[key] !== undefined) {
this.formData[key] = data[key];
}
});
}
},
@@ -397,7 +398,6 @@ export default {
if (this.mode === 'create') {
this.formData.idCard = this.studentInfo.idCard || '';
this.formData.contactPhone = this.studentInfo.phone || ''; // 修正:phone
}
}
},
@@ -415,24 +415,29 @@ export default {
{ field: 'familyFinancialStatus', name: '家庭经济情况' },
{ field: 'applicationReason', name: '申请理由' },
{ field: 'personalAbility', name: '个人能力及经验' },
{ field: 'timeArrangement', name: '时间安排' }
{ field: 'timeArrangement', name: '时间安排' },
];
for (const item of requiredFields) {
if (!this.formData[item.field] || this.formData[item.field].trim() === '') {
uni.showToast({
title: `请填写${item.name}`,
icon: 'none'
icon: 'none',
});
return false;
}
}
// 验证身份证号格式
if (this.formData.idCard && !/^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/.test(this.formData.idCard)) {
if (
this.formData.idCard &&
!/^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/.test(
this.formData.idCard,
)
) {
uni.showToast({
title: '身份证号格式不正确',
icon: 'none'
icon: 'none',
});
return false;
}
@@ -441,7 +446,7 @@ export default {
if (this.formData.contactPhone && !/^1[3-9]\d{9}$/.test(this.formData.contactPhone)) {
uni.showToast({
title: '手机号格式不正确',
icon: 'none'
icon: 'none',
});
return false;
}
@@ -460,7 +465,7 @@ export default {
...this.formData,
positionId: this.positionId,
positionName: this.positionData.positionName,
positionType: this.positionData.positionType || ''
positionType: this.positionData.positionType || '',
};
let response;
@@ -474,7 +479,7 @@ export default {
if (response.code === 200) {
uni.showToast({
title: '保存成功',
icon: 'success'
icon: 'success',
});
if (autoNavigate) {
@@ -490,7 +495,7 @@ export default {
console.error('保存草稿失败:', error);
uni.showToast({
title: error.message || '保存失败',
icon: 'none'
icon: 'none',
});
return false;
} finally {
@@ -506,7 +511,7 @@ export default {
const { confirm } = await uni.showModal({
title: '确认提交',
content: '确定要提交申请吗?提交后将无法修改。',
showCancel: true
showCancel: true,
});
if (!confirm) return; // 用户取消
@@ -522,50 +527,49 @@ export default {
uni.showToast({
title: '提交成功',
icon: 'success'
icon: 'success',
});
setTimeout(() => {
uni.navigateBack();
}, 1000);
}
} catch (error) {
console.error('提交申请失败:', error);
uni.showToast({
title: error.message || '提交失败',
icon: 'none'
icon: 'none',
});
} finally {
this.submitting = false;
}
}
}
},
},
};
</script>
<style scoped>
.apply-container {
min-height: 100vh;
background-color: #f5f5f5;
background-color: #f5f6fa;
padding-bottom: 120rpx; /* 为底部按钮留空间 */
}
.position-card {
background-color: #fff;
margin: 20rpx;
border-radius: 12rpx;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.card-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
color: #303133;
margin-bottom: 20rpx;
padding-bottom: 16rpx;
border-bottom: 1rpx solid #f0f0f0;
border-bottom: 1px solid #f0f0f0;
}
.position-info {
@@ -593,9 +597,9 @@ export default {
.form-container {
background-color: #fff;
margin: 20rpx;
border-radius: 12rpx;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.form-section {
@@ -609,10 +613,10 @@ export default {
.section-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
color: #303133;
margin-bottom: 30rpx;
padding-bottom: 12rpx;
border-bottom: 2rpx solid #1890ff;
border-bottom: 2rpx solid #3c9cff;
position: relative;
}
@@ -623,7 +627,7 @@ export default {
left: 0;
width: 60rpx;
height: 2rpx;
background-color: #1890ff;
background-color: #3c9cff;
}
.bottom-actions {
@@ -661,4 +665,27 @@ export default {
font-size: 24rpx;
color: #999;
}
/* picker输入框样式 */
.picker-input {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 16rpx 24rpx;
background-color: #f8f9fa;
border-radius: 12rpx;
border: 1px solid #eee;
}
.picker-text {
font-size: 28rpx;
color: #303133;
flex: 1;
}
.picker-text.placeholder {
font-size: 24rpx; /* placeholder 字体更小 */
color: #c8c9cc;
}
</style>
+103 -71
View File
@@ -1,5 +1,12 @@
<template>
<view class="detail-container">
<!-- 顶部导航 -->
<u-navbar title="岗位详情" :border-bottom="false" bg-color="#0051d9" title-color="#fff">
<view slot="left" @click="goBack">
<u-icon name="arrow-left" color="#fff" size="22"></u-icon>
</view>
</u-navbar>
<!-- 岗位基本信息 -->
<view class="info-card" v-if="positionData">
<view class="card-header">
@@ -24,13 +31,17 @@
<view class="core-info">
<view class="info-item">
<view class="info-label">
<u-icon name="account" size="32" color="#1890ff" />
<u-icon name="account" size="18" color="#303133" />
<text>招录情况</text>
</view>
<view class="info-content">
<view class="recruitment-info">
<text class="recruitment-text">需求 {{ positionData.requiredCount }} 人</text>
<text class="recruitment-text current">已招录 {{ positionData.currentCount || 0 }} 人</text>
<text class="recruitment-text">
需求 {{ positionData.requiredCount }} 人
</text>
<text class="recruitment-text current">
已招录 {{ positionData.currentCount || 0 }} 人
</text>
</view>
<u-line-progress
:percent="getRecruitmentProgress()"
@@ -43,7 +54,7 @@
<view class="info-item">
<view class="info-label">
<u-icon name="clock" size="32" color="#1890ff" />
<u-icon name="clock" size="18" color="#303133" />
<text>工作时间</text>
</view>
<view class="info-content">
@@ -53,17 +64,19 @@
<view class="info-item">
<view class="info-label">
<u-icon name="calendar" size="32" color="#1890ff" />
<u-icon name="calendar" size="18" color="#303133" />
<text>有效期限</text>
</view>
<view class="info-content">
<text class="info-value">{{ positionData.startDate }} 至 {{ positionData.endDate }}</text>
<text class="info-value">
{{ positionData.startDate }} 至 {{ positionData.endDate }}
</text>
</view>
</view>
<view class="info-item">
<view class="info-label">
<u-icon name="account" size="32" color="#1890ff" />
<u-icon name="account" size="18" color="#303133" />
<text>联系老师</text>
</view>
<view class="info-content">
@@ -73,17 +86,19 @@
<view class="info-item">
<view class="info-label">
<u-icon name="phone" size="32" color="#1890ff" />
<u-icon name="phone" size="18" color="#303133" />
<text>联系电话</text>
</view>
<view class="info-content">
<text class="info-value contact-phone" @click="callPhone">{{ positionData.contactPhone }}</text>
<text class="info-value contact-phone" @click="callPhone">
{{ positionData.contactPhone }}
</text>
</view>
</view>
<view class="info-item" v-if="positionData.salaryInfo">
<view class="info-label">
<u-icon name="rmb-circle" size="32" color="#1890ff" />
<u-icon name="rmb-circle" size="18" color="#303133" />
<text>薪资情况</text>
</view>
<view class="info-content">
@@ -96,7 +111,7 @@
<!-- 岗位要求 -->
<view class="content-card" v-if="positionData && positionData.positionRequirements">
<view class="card-title">
<u-icon name="file-text" size="32" color="#1890ff" />
<u-icon name="file-text" size="18" color="#303133" />
<text>岗位要求</text>
</view>
<view class="content-text">{{ positionData.positionRequirements }}</view>
@@ -105,7 +120,7 @@
<!-- 工作内容 -->
<view class="content-card" v-if="positionData && positionData.workContent">
<view class="card-title">
<u-icon name="list" size="32" color="#1890ff" />
<u-icon name="list" size="18" color="#303133" />
<text>工作内容</text>
</view>
<view class="content-text">{{ positionData.workContent }}</view>
@@ -114,7 +129,7 @@
<!-- 申请状态 -->
<view class="status-card" v-if="applicationStatus">
<view class="card-title">
<u-icon name="info-circle" size="32" color="#1890ff" />
<u-icon name="info-circle" size="18" color="#303133" />
<text>我的申请状态</text>
</view>
<view class="status-content">
@@ -131,7 +146,10 @@
<text class="status-label">申请时间:</text>
<text class="status-value">{{ applicationStatus.lastApplicationTime }}</text>
</view>
<view class="status-actions" v-if="applicationStatus.canEdit || applicationStatus.canCancel">
<view
class="status-actions"
v-if="applicationStatus.canEdit || applicationStatus.canCancel"
>
<u-button
type="warning"
size="small"
@@ -145,7 +163,7 @@
size="small"
@click="cancelApplication"
v-if="applicationStatus.canCancel"
style="margin-left: 20rpx;"
style="margin-left: 20rpx"
>
撤销申请
</u-button>
@@ -156,7 +174,7 @@
<!-- 备注信息 -->
<view class="content-card" v-if="positionData && positionData.remark">
<view class="card-title">
<u-icon name="chat" size="32" color="#1890ff" />
<u-icon name="chat" size="18" color="#303133" />
<text>备注信息</text>
</view>
<view class="content-text">{{ positionData.remark }}</view>
@@ -181,10 +199,7 @@
</template>
<script>
import {
getPositionWithApplicationStatus,
cancelApplication
} from '@/api/workStudy.js';
import { getPositionWithApplicationStatus, cancelApplication } from '@/api/workStudy.js';
export default {
name: 'WorkStudyDetail',
@@ -194,7 +209,7 @@ export default {
positionData: null,
applicationStatus: null,
loading: false,
submitting: false
submitting: false,
};
},
@@ -205,7 +220,7 @@ export default {
} else {
uni.showToast({
title: '岗位ID不能为空',
icon: 'none'
icon: 'none',
});
setTimeout(() => {
uni.navigateBack();
@@ -214,6 +229,11 @@ export default {
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack();
},
// 加载岗位详情
async loadPositionDetail() {
this.loading = true;
@@ -231,7 +251,7 @@ export default {
console.error('加载岗位详情失败:', error);
uni.showToast({
title: error.message || '加载失败',
icon: 'none'
icon: 'none',
});
setTimeout(() => {
@@ -247,20 +267,24 @@ export default {
if (!this.canApply()) {
uni.showToast({
title: this.getApplyButtonText(),
icon: 'none'
icon: 'none',
});
return;
}
// 如果已有申请且可以编辑,则编辑申请
if (this.applicationStatus && this.applicationStatus.hasApplication && this.applicationStatus.canEdit) {
if (
this.applicationStatus &&
this.applicationStatus.hasApplication &&
this.applicationStatus.canEdit
) {
this.editApplication();
return;
}
// 否则新建申请
uni.navigateTo({
url: `/pages/workStudy/apply?positionId=${this.positionId}`
url: `/pages/workStudy/apply?positionId=${this.positionId}`,
});
},
@@ -268,7 +292,7 @@ export default {
editApplication() {
if (this.applicationStatus && this.applicationStatus.applicationId) {
uni.navigateTo({
url: `/pages/workStudy/apply?positionId=${this.positionId}&applicationId=${this.applicationStatus.applicationId}&mode=edit`
url: `/pages/workStudy/apply?positionId=${this.positionId}&applicationId=${this.applicationStatus.applicationId}&mode=edit`,
});
}
},
@@ -283,7 +307,7 @@ export default {
const { confirm } = await uni.showModal({
title: '确认撤销',
content: '确定要撤销此申请吗?',
showCancel: true
showCancel: true,
});
if (!confirm) return; // 用户取消
@@ -294,7 +318,7 @@ export default {
if (response.code === 200) {
uni.showToast({
title: '撤销成功',
icon: 'success'
icon: 'success',
});
// 重新加载数据
@@ -308,7 +332,7 @@ export default {
console.error('撤销申请失败:', error);
uni.showToast({
title: error.message || '撤销失败',
icon: 'none'
icon: 'none',
});
} finally {
this.submitting = false;
@@ -324,9 +348,9 @@ export default {
console.error('拨打电话失败:', error);
uni.showToast({
title: '拨打电话失败',
icon: 'none'
icon: 'none',
});
}
},
});
}
},
@@ -334,13 +358,13 @@ export default {
// 获取状态文字
getStatusText(status) {
const statusMap = {
'DRAFT': '草稿',
'PUBLISHED': '已发布',
'RECRUITING': '招录中',
'FULL': '已满员',
'EXPIRED': '已过期',
'SUSPENDED': '已暂停',
'CANCELLED': '已取消'
DRAFT: '草稿',
PUBLISHED: '已发布',
RECRUITING: '招录中',
FULL: '已满员',
EXPIRED: '已过期',
SUSPENDED: '已暂停',
CANCELLED: '已取消',
};
return statusMap[status] || '未知状态';
},
@@ -348,13 +372,13 @@ export default {
// 获取状态类型
getStatusType(status) {
const typeMap = {
'DRAFT': 'info',
'PUBLISHED': 'warning',
'RECRUITING': 'success',
'FULL': 'primary',
'EXPIRED': 'error',
'SUSPENDED': 'warning',
'CANCELLED': 'error'
DRAFT: 'info',
PUBLISHED: 'warning',
RECRUITING: 'success',
FULL: 'primary',
EXPIRED: 'error',
SUSPENDED: 'warning',
CANCELLED: 'error',
};
return typeMap[status] || 'default';
},
@@ -386,8 +410,10 @@ export default {
// 如果已有申请,判断是否可以重新申请
if (this.applicationStatus && this.applicationStatus.hasApplication) {
// 如果申请被拒绝,可以重新申请
return this.applicationStatus.applicationStatus === 'FINAL_REJECTED' ||
this.applicationStatus.applicationStatus === 'COLLEGE_REJECTED';
return (
this.applicationStatus.applicationStatus === 'FINAL_REJECTED' ||
this.applicationStatus.applicationStatus === 'COLLEGE_REJECTED'
);
}
// 检查是否已满员
@@ -417,7 +443,11 @@ export default {
if (status === 'DRAFT') {
return '完善申请信息';
} else if (status === 'SUBMITTED' || status === 'COLLEGE_REVIEWING' || status === 'FINAL_REVIEWING') {
} else if (
status === 'SUBMITTED' ||
status === 'COLLEGE_REVIEWING' ||
status === 'FINAL_REVIEWING'
) {
return '申请审核中';
} else if (status === 'FINAL_APPROVED') {
return '申请已通过';
@@ -436,35 +466,37 @@ export default {
// 获取申请状态类型
getApplicationStatusType(status) {
const typeMap = {
'DRAFT': 'info',
'SUBMITTED': 'warning',
'COLLEGE_REVIEWING': 'warning',
'COLLEGE_APPROVED': 'success',
'COLLEGE_REJECTED': 'error',
'FINAL_REVIEWING': 'warning',
'FINAL_APPROVED': 'success',
'FINAL_REJECTED': 'error',
'CANCELLED': 'info'
DRAFT: 'info',
SUBMITTED: 'warning',
COLLEGE_REVIEWING: 'warning',
COLLEGE_APPROVED: 'success',
COLLEGE_REJECTED: 'error',
FINAL_REVIEWING: 'warning',
FINAL_APPROVED: 'success',
FINAL_REJECTED: 'error',
CANCELLED: 'info',
};
return typeMap[status] || 'default';
}
}
},
},
};
</script>
<style scoped>
.detail-container {
min-height: 100vh;
background-color: #f5f5f5;
background-color: #f5f6fa;
padding-bottom: 120rpx; /* 为底部按钮留空间 */
}
.info-card, .content-card, .status-card {
.info-card,
.content-card,
.status-card {
background-color: #fff;
margin: 20rpx;
border-radius: 12rpx;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.card-header {
@@ -481,7 +513,7 @@ export default {
.position-name {
font-size: 36rpx;
font-weight: bold;
color: #333;
color: #303133;
flex: 1;
line-height: 1.3;
}
@@ -530,7 +562,7 @@ export default {
.info-label text {
font-size: 28rpx;
font-weight: 500;
color: #333;
color: #303133;
margin-left: 12rpx;
}
@@ -540,7 +572,7 @@ export default {
.info-value {
font-size: 28rpx;
color: #666;
color: #606266;
line-height: 1.5;
}
@@ -566,7 +598,7 @@ export default {
}
.recruitment-text.current {
color: #1890ff;
color: #0051d9;
font-weight: 500;
}
@@ -579,15 +611,15 @@ export default {
}
.card-title text {
font-size: 32rpx;
font-size: 30rpx;
font-weight: 500;
color: #333;
color: #303133;
margin-left: 12rpx;
}
.content-text {
font-size: 28rpx;
color: #666;
color: #606266;
line-height: 1.8;
white-space: pre-wrap;
}
+129 -105
View File
@@ -1,15 +1,21 @@
<template>
<view class="work-study-container">
<!-- 顶部导航 -->
<u-navbar title="勤工俭学" :border-bottom="false" bg-color="#0051d9" title-color="#fff">
<view slot="left" @click="goBack">
<u-icon name="arrow-left" color="#fff" size="22"></u-icon>
</view>
</u-navbar>
<!-- 搜索栏 -->
<view class="search-bar">
<view class="search-container">
<u-search
v-model="searchKeyword"
placeholder="搜索岗位名称或联系老师"
:show-action="false"
@search="handleSearch"
@custom="handleSearch"
bg-color="#f6f6f6"
height="72"
bg-color="#f5f6fa"
shape="round"
/>
</view>
@@ -55,19 +61,14 @@
</view>
</view>
<view class="filter-actions">
<u-button
type="primary"
size="small"
@click="handleFilter"
:loading="loading"
>
<u-button type="primary" size="small" @click="handleFilter" :loading="loading">
筛选
</u-button>
<u-button
type="default"
size="small"
@click="resetFilter"
style="margin-left: 10rpx;"
style="margin-left: 10rpx"
>
重置
</u-button>
@@ -107,7 +108,9 @@
<view class="card-header">
<view class="position-info">
<view class="position-name">{{ item.positionName }}</view>
<view class="position-type">{{ item.positionScopeName || '未设置' }}</view>
<view class="position-type">
{{ item.positionScopeName || '未设置' }}
</view>
</view>
<view class="status-badge">
<u-tag
@@ -122,30 +125,36 @@
<!-- 岗位信息 -->
<view class="card-content">
<view class="info-row">
<u-icon name="account" size="28" color="#666" />
<u-icon name="account" size="18" color="#666" />
<text class="info-text">需求人数:{{ item.requiredCount }}人</text>
<text class="info-text current-count">已招录:{{ item.currentCount || 0 }}人</text>
<text class="info-text current-count">
已招录:{{ item.currentCount || 0 }}人
</text>
</view>
<view class="info-row">
<u-icon name="clock" size="28" color="#666" />
<u-icon name="clock" size="18" color="#666" />
<text class="info-text">工作时间:{{ item.workTime }}</text>
</view>
<view class="info-row">
<u-icon name="map" size="28" color="#666" />
<u-icon name="map" size="18" color="#666" />
<text class="info-text">联系老师:{{ item.contactTeacher }}</text>
</view>
<view class="info-row">
<u-icon name="phone" size="28" color="#666" />
<u-icon name="phone" size="18" color="#666" />
<text class="info-text">联系电话:{{ item.contactPhone }}</text>
</view>
<view class="info-row">
<u-icon name="calendar" size="28" color="#666" />
<text class="info-text">有效期:{{ item.startDate }} 至 {{ item.endDate }}</text>
<u-icon name="calendar" size="18" color="#666" />
<text class="info-text">
有效期:{{ item.startDate }} 至 {{ item.endDate }}
</text>
</view>
<!-- 申请状态信息 -->
<view class="info-row" v-if="getPositionApplicationStatus(item)">
<u-icon name="checkmark-circle" size="28" color="#1890ff" />
<text class="info-text" style="color: #1890ff;">我的申请:{{ getPositionApplicationStatus(item) }}</text>
<u-icon name="checkmark-circle" size="18" color="#1890ff" />
<text class="info-text" style="color: #1890ff">
我的申请:{{ getPositionApplicationStatus(item) }}
</text>
</view>
</view>
@@ -163,7 +172,7 @@
size="small"
@click.stop="handleApply($event, item)"
:disabled="!canApply(item)"
style="margin-left: 20rpx;"
style="margin-left: 20rpx"
>
{{ getApplyButtonText(item) }}
</u-button>
@@ -173,20 +182,12 @@
<!-- 空状态 -->
<view class="empty-state" v-else-if="!loading">
<u-empty
text="暂无岗位信息"
icon="search"
:show="true"
/>
<u-empty text="暂无岗位信息" icon="search" :show="true" />
</view>
<!-- 加载更多 -->
<view class="load-more" v-if="hasMore && positionList.length > 0">
<u-loadmore
:status="loadMoreStatus"
:load-text="loadText"
@loadmore="loadMore"
/>
<u-loadmore :status="loadMoreStatus" :load-text="loadText" @loadmore="loadMore" />
</view>
</view>
@@ -218,16 +219,22 @@
<!-- 申请状态 -->
<view class="card-content">
<view class="info-row">
<u-icon name="file-text" size="28" color="#666" />
<text class="info-text">申请状态:{{ getApplicationStatusText(item) }}</text>
<u-icon name="file-text" size="18" color="#666" />
<text class="info-text">
申请状态:{{ getApplicationStatusText(item) }}
</text>
</view>
<view class="info-row" v-if="item.collegeReviewStatus">
<u-icon name="checkmark-circle" size="28" color="#666" />
<text class="info-text">学院审核:{{ getReviewStatusText(item.collegeReviewStatus) }}</text>
<u-icon name="checkmark-circle" size="18" color="#666" />
<text class="info-text">
学院审核:{{ getReviewStatusText(item.collegeReviewStatus) }}
</text>
</view>
<view class="info-row" v-if="item.finalReviewStatus">
<u-icon name="checkmark-circle" size="28" color="#666" />
<text class="info-text">学生处审核:{{ getReviewStatusText(item.finalReviewStatus) }}</text>
<u-icon name="checkmark-circle" size="18" color="#666" />
<text class="info-text">
学生处审核:{{ getReviewStatusText(item.finalReviewStatus) }}
</text>
</view>
</view>
@@ -245,7 +252,7 @@
size="small"
@click.stop="handleCancelApplication($event, item)"
v-if="canCancelApplication(item)"
style="margin-left: 20rpx;"
style="margin-left: 20rpx"
>
撤销申请
</u-button>
@@ -255,11 +262,7 @@
<!-- 空状态 -->
<view class="empty-state" v-else-if="!loading">
<u-empty
text="暂无申请记录"
icon="file-text"
:show="true"
/>
<u-empty text="暂无申请记录" icon="file-text" :show="true" />
</view>
</view>
@@ -292,7 +295,7 @@ import {
getMyApplications,
cancelApplication,
getCurrentStudentInfo,
getStudentApplicationStatusList
getStudentApplicationStatusList,
} from '@/api/workStudy.js';
export default {
@@ -304,10 +307,7 @@ export default {
// 标签页
currentTab: 0, // 确保是数字类型
tabsList: [
{ name: '可申请岗位' },
{ name: '我的申请' }
],
tabsList: [{ name: '可申请岗位' }, { name: '我的申请' }],
// 筛选条件
showFilter: false,
@@ -321,16 +321,16 @@ export default {
[
{ label: '全部', value: '' },
{ label: '本学院', value: 'COLLEGE' },
{ label: '全校', value: 'UNIVERSITY' }
]
{ label: '全校', value: 'UNIVERSITY' },
],
],
statusOptions: [
[
{ label: '全部', value: '' },
{ label: '已发布', value: 'PUBLISHED' },
{ label: '招录中', value: 'RECRUITING' },
{ label: '已暂停', value: 'SUSPENDED' }
]
{ label: '已暂停', value: 'SUSPENDED' },
],
],
// 查询参数
@@ -339,7 +339,7 @@ export default {
pageSize: 10,
positionName: '',
positionScope: '',
status: ''
status: '',
},
// 列表数据
@@ -352,7 +352,7 @@ export default {
loadText: {
loadmore: '点击或上拉加载更多',
loading: '正在加载...',
nomore: '没有更多了'
nomore: '没有更多了',
},
// 加载状态
@@ -363,7 +363,7 @@ export default {
studentInfo: null,
// 学生申请状态列表(用于判断申请状态)
studentApplications: []
studentApplications: [],
};
},
@@ -400,6 +400,11 @@ export default {
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack();
},
// 初始化数据
async initData() {
try {
@@ -413,7 +418,7 @@ export default {
console.error('初始化数据失败:', error);
uni.showToast({
title: '加载数据失败',
icon: 'none'
icon: 'none',
});
}
},
@@ -481,7 +486,7 @@ export default {
console.error('加载岗位列表失败:', error);
uni.showToast({
title: error.message || '加载失败',
icon: 'none'
icon: 'none',
});
this.loadMoreStatus = 'loadmore';
} finally {
@@ -508,7 +513,7 @@ export default {
console.error('加载申请列表失败:', error);
uni.showToast({
title: error.message || '加载失败',
icon: 'none'
icon: 'none',
});
} finally {
this.loading = false;
@@ -588,7 +593,7 @@ export default {
item = event;
}
uni.navigateTo({
url: `/pages/workStudy/detail?id=${item.id}`
url: `/pages/workStudy/detail?id=${item.id}`,
});
},
@@ -599,12 +604,17 @@ export default {
position = event;
}
const application = this.studentApplications.find(app => app.positionId === position.id);
const application = this.studentApplications.find(
(app) => app.positionId === position.id,
);
// 如果已有申请且状态为已提交或审核中,跳转到申请管理页面
if (application && ['submitted', 'college_reviewing', 'final_reviewing'].includes(application.status)) {
if (
application &&
['submitted', 'college_reviewing', 'final_reviewing'].includes(application.status)
) {
uni.switchTab({
url: '/pages/workStudy/index'
url: '/pages/workStudy/index',
});
// 切换到"我的申请"标签页
this.currentTab = 1;
@@ -616,7 +626,7 @@ export default {
if (application && application.status === 'approved') {
uni.showToast({
title: '您已被该岗位录用',
icon: 'none'
icon: 'none',
});
return;
}
@@ -624,13 +634,13 @@ export default {
if (!this.canApply(position)) {
uni.showToast({
title: '该岗位当前不可申请',
icon: 'none'
icon: 'none',
});
return;
}
uni.navigateTo({
url: `/pages/workStudy/apply?positionId=${position.id}`
url: `/pages/workStudy/apply?positionId=${position.id}`,
});
},
@@ -641,7 +651,7 @@ export default {
item = event;
}
uni.navigateTo({
url: `/pages/workStudy/applicationDetail?id=${item.id}`
url: `/pages/workStudy/applicationDetail?id=${item.id}`,
});
},
@@ -655,7 +665,7 @@ export default {
const { confirm } = await uni.showModal({
title: '确认撤销',
content: '确定要撤销此申请吗?',
showCancel: true
showCancel: true,
});
if (!confirm) return; // 用户取消
@@ -664,7 +674,7 @@ export default {
if (response.code === 200) {
uni.showToast({
title: '撤销成功',
icon: 'success'
icon: 'success',
});
this.loadMyApplications(true);
} else {
@@ -674,7 +684,7 @@ export default {
console.error('撤销申请失败:', error);
uni.showToast({
title: error.message || '撤销失败',
icon: 'none'
icon: 'none',
});
}
},
@@ -700,20 +710,22 @@ export default {
// 获取状态类型
getStatusType(status) {
const typeMap = {
'DRAFT': 'info',
'PUBLISHED': 'warning',
'RECRUITING': 'success',
'FULL': 'primary',
'EXPIRED': 'error',
'SUSPENDED': 'warning',
'CANCELLED': 'error'
DRAFT: 'info',
PUBLISHED: 'warning',
RECRUITING: 'success',
FULL: 'primary',
EXPIRED: 'error',
SUSPENDED: 'warning',
CANCELLED: 'error',
};
return typeMap[status] || 'default';
},
// 判断是否可以申请(完全参考web端实现)
canApply(position) {
const application = this.studentApplications.find(app => app.positionId === position.id);
const application = this.studentApplications.find(
(app) => app.positionId === position.id,
);
// 如果已录用,禁用按钮
if (application && application.status === 'approved') {
@@ -745,7 +757,9 @@ export default {
// 获取申请按钮文字(参考web端实现8种状态)
getApplyButtonText(position) {
const application = this.studentApplications.find(app => app.positionId === position.id);
const application = this.studentApplications.find(
(app) => app.positionId === position.id,
);
// 如果已录用
if (application && application.status === 'approved') {
@@ -753,7 +767,10 @@ export default {
}
// 如果有申请且状态为审核中
if (application && ['submitted', 'college_reviewing', 'final_reviewing'].includes(application.status)) {
if (
application &&
['submitted', 'college_reviewing', 'final_reviewing'].includes(application.status)
) {
return '申请审核中';
}
@@ -836,13 +853,13 @@ export default {
const status = item.status ? item.status.toLowerCase() : '';
const typeMap = {
'draft': 'info',
'submitted': 'warning',
'college_reviewing': 'warning',
'final_reviewing': 'warning',
'approved': 'success',
'rejected': 'error',
'cancelled': 'info'
draft: 'info',
submitted: 'warning',
college_reviewing: 'warning',
final_reviewing: 'warning',
approved: 'success',
rejected: 'error',
cancelled: 'info',
};
return typeMap[status] || 'default';
},
@@ -850,9 +867,9 @@ export default {
// 获取审核状态文字
getReviewStatusText(status) {
const statusMap = {
'PENDING': '待审核',
'APPROVED': '通过',
'REJECTED': '未通过'
PENDING: '待审核',
APPROVED: '通过',
REJECTED: '未通过',
};
return statusMap[status] || '未知';
},
@@ -863,31 +880,35 @@ export default {
// 支持两种格式:web端的小写格式和移动端的大写格式
const status = item.status ? item.status.toLowerCase() : '';
return ['draft', 'submitted', 'college_reviewing'].includes(status) ||
['DRAFT', 'SUBMITTED', 'COLLEGE_REVIEWING'].includes(item.status);
return (
['draft', 'submitted', 'college_reviewing'].includes(status) ||
['DRAFT', 'SUBMITTED', 'COLLEGE_REVIEWING'].includes(item.status)
);
},
// 获取岗位的申请状态(用于在岗位卡片中显示)
getPositionApplicationStatus(position) {
const application = this.studentApplications.find(app => app.positionId === position.id);
const application = this.studentApplications.find(
(app) => app.positionId === position.id,
);
if (!application) return null;
return this.getApplicationStatusText(application);
}
}
},
},
};
</script>
<style scoped>
.work-study-container {
min-height: 100vh;
background-color: #f5f5f5;
background-color: #f5f6fa;
}
.search-bar {
.search-container {
padding: 20rpx;
background-color: #fff;
border-bottom: 1rpx solid #eee;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.tabs-container {
@@ -927,12 +948,13 @@ export default {
padding: 20rpx;
}
.position-card, .application-card {
.position-card,
.application-card {
background-color: #fff;
border-radius: 12rpx;
border-radius: 16rpx;
margin-bottom: 20rpx;
padding: 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.card-header {
@@ -942,18 +964,20 @@ export default {
margin-bottom: 20rpx;
}
.position-info, .application-info {
.position-info,
.application-info {
flex: 1;
}
.position-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
color: #303133;
margin-bottom: 8rpx;
}
.position-type, .apply-time {
.position-type,
.apply-time {
font-size: 24rpx;
color: #999;
}
@@ -974,14 +998,14 @@ export default {
.info-text {
font-size: 28rpx;
color: #666;
color: #606266;
margin-left: 12rpx;
flex: 1;
}
.current-count {
margin-left: 20rpx;
color: #1890ff;
color: #0051d9;
}
.card-actions {
@@ -0,0 +1,432 @@
<template>
<view class="dorm-application-tab">
<!-- 搜索区域 -->
<view class="search-container">
<u-search
v-model="searchKeyword"
placeholder="请输入姓名或学号"
:show-action="true"
action-text="搜索"
:animation="true"
@search="handleQuery"
@custom="handleQuery"
@clear="handleClear"
></u-search>
</view>
<!-- 加载状态 -->
<u-loading-page
:loading="loading && dataList.length === 0"
loading-text="加载中..."
></u-loading-page>
<!-- 列表内容 -->
<view v-if="!loading || dataList.length > 0" class="content-wrapper">
<!-- 空状态 -->
<view v-if="dataList.length === 0" class="empty-container">
<u-empty text="暂无调宿/退宿申请记录" mode="data"></u-empty>
</view>
<!-- 列表 -->
<scroll-view
v-else
scroll-y
class="scroll-view"
:lower-threshold="50"
@scrolltolower="handleLoadMore"
>
<view class="list-container">
<view
v-for="(item, index) in dataList"
:key="item.id || index"
class="list-card"
>
<!-- 卡片头部 -->
<view class="card-header">
<view class="student-info">
<text class="student-name">{{ item.studentName || '-' }}</text>
<text class="student-no">{{ item.studentNo || '-' }}</text>
</view>
<u-tag
:text="getApplicationTypeText(item.applicationType)"
:type="getApplicationTypeTagType(item.applicationType)"
size="mini"
></u-tag>
</view>
<!-- 卡片内容 -->
<view class="card-content">
<view class="info-row">
<text class="label">学院:</text>
<text class="value">{{ item.collegeName || '-' }}</text>
</view>
<view class="info-row">
<text class="label">专业:</text>
<text class="value">{{ item.majorName || '-' }}</text>
</view>
<view class="info-row">
<text class="label">班级:</text>
<text class="value">{{ item.className || '-' }}</text>
</view>
<view class="info-row">
<text class="label">当前宿舍:</text>
<text class="value">
{{ item.currentBuildingName }}{{ item.currentRoomNo
}}{{ item.currentBedNo }}
</text>
</view>
<view v-if="item.applicationType === '1'" class="info-row">
<text class="label">目标宿舍:</text>
<text class="value">
{{ item.targetBuildingName }}{{ item.targetRoomNo
}}{{ item.targetBedNo }}
</text>
</view>
<view class="info-row">
<text class="label">审批状态:</text>
<u-tag
:text="getApprovalStatusText(item.applicationStatus)"
:type="getApprovalStatusTagType(item.applicationStatus)"
size="mini"
></u-tag>
</view>
<view class="info-row">
<text class="label">申请时间:</text>
<text class="value">{{ formatTime(item.applyTime) }}</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="card-actions">
<u-button
type="primary"
size="small"
plain
@click="handleViewApproval(item)"
>
<u-icon
name="list"
size="16"
color="#fff"
style="margin-right: 4px"
></u-icon>
审批记录
</u-button>
</view>
</view>
</view>
<!-- 加载更多提示 -->
<view class="load-more-wrapper">
<u-loadmore :status="loadMoreStatus" :load-text="loadMoreText" />
</view>
</scroll-view>
</view>
<!-- 审批记录组件 -->
<approval-record ref="approvalRecordRef"></approval-record>
</view>
</template>
<script>
import { request } from '@/common/request.js';
import { getDict, getDictLabel, getDictTagType } from '@/common/dict.js';
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
export default {
name: 'DormApplicationTab',
components: {
ApprovalRecord,
},
data() {
return {
loading: false,
dataList: [],
total: 0,
searchKeyword: '', // 搜索关键字(姓名或学号)
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
studentName: '',
studentNo: '',
},
// 加载更多状态
loadMoreStatus: 'loadmore',
loadMoreText: {
loadmore: '点击加载更多',
loading: '正在加载...',
nomore: '没有更多了',
},
// 字典数据
dictApplicationType: [],
dictApprovalStatus: [],
};
},
async created() {
await this.loadDictData();
await this.getList();
},
methods: {
// 加载字典数据
async loadDictData() {
try {
this.dictApplicationType = await getDict('dorm_application_type');
this.dictApprovalStatus = await getDict('student_leave_approval_status');
} catch (error) {
console.error('加载字典数据失败:', error);
}
},
// 获取列表数据
async getList() {
this.loading = true;
try {
// 根据搜索关键字设置查询参数
const params = {
...this.queryParams,
studentName: this.searchKeyword,
studentNo: this.searchKeyword,
};
const res = await request({
url: '/studentDynamics/dormApplicationList',
method: 'GET',
params: params,
});
if (res && res.code === 200) {
if (this.queryParams.pageNum === 1) {
this.dataList = res.rows || [];
} else {
this.dataList = [...this.dataList, ...(res.rows || [])];
}
this.total = res.total || 0;
// 更新加载更多状态
if (this.dataList.length >= this.total) {
this.loadMoreStatus = 'nomore';
} else {
this.loadMoreStatus = 'loadmore';
}
} else {
uni.showToast({
title: res?.msg || '获取数据失败',
icon: 'none',
});
}
} catch (error) {
console.error('获取调宿退宿申请列表失败:', error);
uni.showToast({
title: '获取数据失败',
icon: 'none',
});
} finally {
this.loading = false;
}
},
// 加载更多
handleLoadMore() {
if (this.loadMoreStatus === 'nomore' || this.loadMoreStatus === 'loading') {
return;
}
this.loadMoreStatus = 'loading';
this.queryParams.pageNum++;
this.getList();
},
// 搜索操作
handleQuery() {
this.queryParams.pageNum = 1;
this.dataList = [];
this.getList();
},
// 清空搜索
handleClear() {
this.searchKeyword = '';
this.handleQuery();
},
// 格式化时间
formatTime(timeStr) {
if (!timeStr) return '-';
try {
const date = new Date(timeStr);
if (isNaN(date.getTime())) return timeStr;
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
} catch (error) {
console.error('时间格式化失败:', error);
return timeStr;
}
},
// 获取申请类型文本
getApplicationTypeText(value) {
return getDictLabel(this.dictApplicationType, value) || '-';
},
// 获取申请类型标签类型
getApplicationTypeTagType(value) {
const fallbackMap = {
1: 'primary', // 调宿
2: 'warning', // 退宿
};
return getDictTagType(this.dictApplicationType, value, fallbackMap);
},
// 获取审批状态文本
getApprovalStatusText(value) {
return getDictLabel(this.dictApprovalStatus, value) || '-';
},
// 获取审批状态标签类型
getApprovalStatusTagType(value) {
const fallbackMap = {
0: 'warning', // 待审批
1: 'success', // 已通过
2: 'error', // 已驳回
};
return getDictTagType(this.dictApprovalStatus, value, fallbackMap);
},
// 查看审批记录
handleViewApproval(item) {
console.log('查看审批记录:', item);
if (!item.id) {
uni.showToast({
title: '数据异常,无法查看审批记录',
icon: 'none',
});
return;
}
// 调用审批记录组件,传入业务ID
this.$refs.approvalRecordRef.init(item.id);
},
},
};
</script>
<style lang="scss" scoped>
.dorm-application-tab {
background-color: #f5f6fa;
min-height: calc(100vh - 176rpx);
}
.search-container {
padding: 20rpx;
background-color: #fff;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.content-wrapper {
padding: 20rpx;
}
.empty-container {
padding: 100rpx 20rpx;
}
.scroll-view {
height: calc(100vh - 176rpx - 88rpx); // 减去搜索栏高度
}
.list-container {
display: flex;
flex-direction: column;
gap: 20rpx;
padding-bottom: 20rpx;
}
.list-card {
background-color: #fff;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
border-bottom: 1px solid #f0f0f0;
}
.student-info {
display: flex;
align-items: center;
gap: 16rpx;
}
.student-name {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.student-no {
font-size: 26rpx;
color: #909399;
}
.card-content {
padding: 24rpx;
}
.info-row {
display: flex;
align-items: center;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
}
.label {
font-size: 28rpx;
color: #606266;
min-width: 140rpx;
margin-right: 16rpx;
}
.value {
font-size: 28rpx;
color: #303133;
flex: 1;
word-break: break-all;
}
.card-actions {
display: flex;
justify-content: flex-end;
align-items: center;
padding: 20rpx 24rpx;
border-top: 1px solid #f0f0f0;
}
.load-more-wrapper {
padding: 20rpx 0;
}
// uview组件样式调整
::v-deep .u-search {
background-color: #f5f6fa;
}
</style>
@@ -0,0 +1,392 @@
<template>
<view class="holiday-stay-tab">
<!-- 搜索区域 -->
<view class="search-container">
<u-search
v-model="searchKeyword"
placeholder="请输入姓名或学号"
:show-action="true"
action-text="搜索"
:animation="true"
@search="handleQuery"
@custom="handleQuery"
@clear="handleClear"
></u-search>
</view>
<!-- 加载状态 -->
<u-loading-page
:loading="loading && dataList.length === 0"
loading-text="加载中..."
></u-loading-page>
<!-- 列表内容 -->
<view v-if="!loading || dataList.length > 0" class="content-wrapper">
<!-- 空状态 -->
<view v-if="dataList.length === 0" class="empty-container">
<u-empty text="暂无假期留宿记录" mode="data"></u-empty>
</view>
<!-- 列表 -->
<scroll-view
v-else
scroll-y
class="scroll-view"
:lower-threshold="50"
@scrolltolower="handleLoadMore"
>
<view class="list-container">
<view
v-for="(item, index) in dataList"
:key="item.id || index"
class="list-card"
>
<!-- 卡片头部 -->
<view class="card-header">
<view class="student-info">
<text class="student-name">{{ item.studentName || '-' }}</text>
<text class="student-no">{{ item.studentNo || '-' }}</text>
</view>
<u-tag
:text="getHolidayTypeText(item.holidayType)"
:type="getHolidayTypeTagType(item.holidayType)"
size="mini"
></u-tag>
</view>
<!-- 卡片内容 -->
<view class="card-content">
<view class="info-row">
<text class="label">学院:</text>
<text class="value">{{ item.collegeName || '-' }}</text>
</view>
<view class="info-row">
<text class="label">专业:</text>
<text class="value">{{ item.majorName || '-' }}</text>
</view>
<view class="info-row">
<text class="label">班级:</text>
<text class="value">{{ item.className || '-' }}</text>
</view>
<view class="info-row">
<text class="label">宿舍信息:</text>
<text class="value">
{{ item.buildingName }}{{ item.roomName }}
</text>
</view>
<view class="info-row">
<text class="label">留宿时间:</text>
<text class="value">
{{ item.applyStartDate }} 至 {{ item.applyEndDate }}
</text>
</view>
<view class="info-row">
<text class="label">留宿原因:</text>
<u-tag
:text="getStayReasonTypeText(item.stayReasonType)"
:type="getStayReasonTypeTagType(item.stayReasonType)"
size="mini"
></u-tag>
</view>
<view class="info-row">
<text class="label">联系电话:</text>
<text class="value">{{ item.studentPhone || '-' }}</text>
</view>
<view class="info-row">
<text class="label">紧急联系人:</text>
<text class="value">{{ item.emergencyContact || '-' }}</text>
</view>
</view>
<!-- 操作按钮 - 暂不开发
<view class="card-actions">
<u-button
type="primary"
size="small"
@click="handleView(item)"
>查看</u-button>
</view>
-->
</view>
</view>
<!-- 加载更多提示 -->
<view class="load-more-wrapper">
<u-loadmore :status="loadMoreStatus" :load-text="loadMoreText" />
</view>
</scroll-view>
</view>
</view>
</template>
<script>
import { request } from '@/common/request.js';
import { getDict, getDictLabel, getDictTagType } from '@/common/dict.js';
export default {
name: 'HolidayStayTab',
data() {
return {
loading: false,
dataList: [],
total: 0,
searchKeyword: '', // 搜索关键字(姓名或学号)
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
studentName: '',
studentNo: '',
},
// 加载更多状态
loadMoreStatus: 'loadmore',
loadMoreText: {
loadmore: '点击加载更多',
loading: '正在加载...',
nomore: '没有更多了',
},
// 字典数据
dictHolidayType: [],
dictStayReasonType: [],
};
},
async created() {
await this.loadDictData();
await this.getList();
},
methods: {
// 加载字典数据
async loadDictData() {
try {
this.dictHolidayType = await getDict('xg_holiday_type');
this.dictStayReasonType = await getDict('xg_stay_reason_type');
} catch (error) {
console.error('加载字典数据失败:', error);
}
},
// 获取列表数据
async getList() {
this.loading = true;
try {
// 根据搜索关键字设置查询参数
const params = {
...this.queryParams,
studentName: this.searchKeyword,
studentNo: this.searchKeyword,
};
const res = await request({
url: '/studentDynamics/holidayStayList',
method: 'GET',
params: params,
});
if (res && res.code === 200) {
if (this.queryParams.pageNum === 1) {
this.dataList = res.rows || [];
} else {
this.dataList = [...this.dataList, ...(res.rows || [])];
}
this.total = res.total || 0;
// 更新加载更多状态
if (this.dataList.length >= this.total) {
this.loadMoreStatus = 'nomore';
} else {
this.loadMoreStatus = 'loadmore';
}
} else {
uni.showToast({
title: res?.msg || '获取数据失败',
icon: 'none',
});
}
} catch (error) {
console.error('获取假期留宿信息列表失败:', error);
uni.showToast({
title: '获取数据失败',
icon: 'none',
});
} finally {
this.loading = false;
}
},
// 加载更多
handleLoadMore() {
if (this.loadMoreStatus === 'nomore' || this.loadMoreStatus === 'loading') {
return;
}
this.loadMoreStatus = 'loading';
this.queryParams.pageNum++;
this.getList();
},
// 搜索操作
handleQuery() {
this.queryParams.pageNum = 1;
this.dataList = [];
this.getList();
},
// 清空搜索
handleClear() {
this.searchKeyword = '';
this.handleQuery();
},
// 获取假期类型文本
getHolidayTypeText(value) {
return getDictLabel(this.dictHolidayType, value) || '-';
},
// 获取假期类型标签类型
getHolidayTypeTagType(value) {
const fallbackMap = {
1: 'primary', // 寒假
2: 'warning', // 暑假
3: 'success', // 国庆
4: 'info', // 其他
};
return getDictTagType(this.dictHolidayType, value, fallbackMap);
},
// 获取留宿原因类型文本
getStayReasonTypeText(value) {
return getDictLabel(this.dictStayReasonType, value) || '-';
},
// 获取留宿原因类型标签类型
getStayReasonTypeTagType(value) {
const fallbackMap = {
1: 'primary', // 学习
2: 'warning', // 实习
3: 'success', // 工作
4: 'info', // 其他
};
return getDictTagType(this.dictStayReasonType, value, fallbackMap);
},
// 查看详情 - 暂不开发
// handleView(row) {
// console.log('查看详情:', row);
// },
},
};
</script>
<style lang="scss" scoped>
.holiday-stay-tab {
background-color: #f5f6fa;
min-height: calc(100vh - 176rpx);
}
.search-container {
padding: 20rpx;
background-color: #fff;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.content-wrapper {
padding: 20rpx;
}
.empty-container {
padding: 100rpx 20rpx;
}
.scroll-view {
height: calc(100vh - 176rpx - 88rpx); // 减去搜索栏高度
}
.list-container {
display: flex;
flex-direction: column;
gap: 20rpx;
padding-bottom: 20rpx;
}
.list-card {
background-color: #fff;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
border-bottom: 1px solid #f0f0f0;
}
.student-info {
display: flex;
align-items: center;
gap: 16rpx;
}
.student-name {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.student-no {
font-size: 26rpx;
color: #909399;
}
.card-content {
padding: 24rpx;
}
.info-row {
display: flex;
align-items: center;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
}
.label {
font-size: 28rpx;
color: #606266;
min-width: 140rpx;
margin-right: 16rpx;
}
.value {
font-size: 28rpx;
color: #303133;
flex: 1;
word-break: break-all;
}
.card-actions {
display: flex;
justify-content: flex-end;
align-items: center;
padding: 20rpx 24rpx;
border-top: 1px solid #f0f0f0;
}
.load-more-wrapper {
padding: 20rpx 0;
}
// uview组件样式调整
::v-deep .u-search {
background-color: #f5f6fa;
}
</style>
@@ -0,0 +1,435 @@
<template>
<view class="student-leave-tab">
<!-- 搜索区域 -->
<view class="search-container">
<u-search
v-model="searchKeyword"
placeholder="请输入姓名或学号"
:show-action="true"
action-text="搜索"
:animation="true"
@search="handleQuery"
@custom="handleQuery"
@clear="handleClear"
></u-search>
</view>
<!-- 加载状态 -->
<u-loading-page
:loading="loading && dataList.length === 0"
loading-text="加载中..."
></u-loading-page>
<!-- 列表内容 -->
<view v-if="!loading || dataList.length > 0" class="content-wrapper">
<!-- 空状态 -->
<view v-if="dataList.length === 0" class="empty-container">
<u-empty text="暂无学生请假记录" mode="data"></u-empty>
</view>
<!-- 列表 -->
<scroll-view
v-else
scroll-y
class="scroll-view"
:lower-threshold="50"
@scrolltolower="handleLoadMore"
>
<view class="list-container">
<view
v-for="(item, index) in dataList"
:key="item.id || index"
class="list-card"
>
<!-- 卡片头部 -->
<view class="card-header">
<view class="student-info">
<text class="student-name">{{ item.studentName || '-' }}</text>
<text class="student-no">{{ item.studentNo || '-' }}</text>
</view>
<u-tag
:text="item.leaveTypeName || '-'"
type="primary"
size="mini"
></u-tag>
</view>
<!-- 卡片内容 -->
<view class="card-content">
<view class="info-row">
<text class="label">学院:</text>
<text class="value">{{ item.collegeName || '-' }}</text>
</view>
<view class="info-row">
<text class="label">专业:</text>
<text class="value">{{ item.majorName || '-' }}</text>
</view>
<view class="info-row">
<text class="label">班级:</text>
<text class="value">{{ item.className || '-' }}</text>
</view>
<view class="info-row">
<text class="label">请假时间:</text>
<text class="value">
{{ item.startTime }} 至 {{ item.endTime }}
</text>
</view>
<view class="info-row">
<text class="label">请假时长:</text>
<text class="value">{{ item.leaveDuration || 0 }}天</text>
</view>
<view class="info-row">
<text class="label">是否离校:</text>
<u-tag
:text="getIsLeaveSchoolText(item.isLeaveSchool)"
:type="getIsLeaveSchoolTagType(item.isLeaveSchool)"
size="mini"
></u-tag>
</view>
<view class="info-row">
<text class="label">审批状态:</text>
<u-tag
:text="getApprovalStatusText(item.approvalStatus)"
:type="getApprovalStatusTagType(item.approvalStatus)"
size="mini"
></u-tag>
</view>
<view class="info-row">
<text class="label">销假状态:</text>
<u-tag
:text="getCancelStatusText(item.cancelStatus)"
:type="getCancelStatusTagType(item.cancelStatus)"
size="mini"
></u-tag>
</view>
</view>
<!-- 操作按钮 -->
<view class="card-actions">
<u-button
type="primary"
size="small"
plain
@click="handleViewApproval(item)"
>
<u-icon
name="list"
size="16"
color="#fff"
style="margin-right: 4px"
></u-icon>
审批记录
</u-button>
</view>
</view>
</view>
<!-- 加载更多提示 -->
<view class="load-more-wrapper">
<u-loadmore :status="loadMoreStatus" :load-text="loadMoreText" />
</view>
</scroll-view>
</view>
<!-- 审批记录组件 -->
<approval-record ref="approvalRecordRef"></approval-record>
</view>
</template>
<script>
import { request } from '@/common/request.js';
import { getDict, getDictLabel, getDictTagType } from '@/common/dict.js';
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
export default {
name: 'StudentLeaveTab',
components: {
ApprovalRecord,
},
data() {
return {
loading: false,
dataList: [],
total: 0,
searchKeyword: '', // 搜索关键字(姓名或学号)
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
studentName: '',
studentNo: '',
},
// 加载更多状态
loadMoreStatus: 'loadmore',
loadMoreText: {
loadmore: '点击加载更多',
loading: '正在加载...',
nomore: '没有更多了',
},
// 字典数据
dictYesNo: [],
dictApprovalStatus: [],
dictCancelStatus: [],
};
},
async created() {
await this.loadDictData();
await this.getList();
},
methods: {
// 加载字典数据
async loadDictData() {
try {
this.dictYesNo = await getDict('sys_yesno');
this.dictApprovalStatus = await getDict('student_leave_approval_status');
this.dictCancelStatus = await getDict('xg_xsqj_cancel_status');
} catch (error) {
console.error('加载字典数据失败:', error);
}
},
// 获取列表数据
async getList() {
this.loading = true;
try {
// 根据搜索关键字设置查询参数
const params = {
...this.queryParams,
studentName: this.searchKeyword,
studentNo: this.searchKeyword,
};
const res = await request({
url: '/studentDynamics/studentLeaveList',
method: 'GET',
params: params,
});
if (res && res.code === 200) {
if (this.queryParams.pageNum === 1) {
this.dataList = res.rows || [];
} else {
this.dataList = [...this.dataList, ...(res.rows || [])];
}
this.total = res.total || 0;
// 更新加载更多状态
if (this.dataList.length >= this.total) {
this.loadMoreStatus = 'nomore';
} else {
this.loadMoreStatus = 'loadmore';
}
} else {
uni.showToast({
title: res?.msg || '获取数据失败',
icon: 'none',
});
}
} catch (error) {
console.error('获取学生请假信息列表失败:', error);
uni.showToast({
title: '获取数据失败',
icon: 'none',
});
} finally {
this.loading = false;
}
},
// 加载更多
handleLoadMore() {
if (this.loadMoreStatus === 'nomore' || this.loadMoreStatus === 'loading') {
return;
}
this.loadMoreStatus = 'loading';
this.queryParams.pageNum++;
this.getList();
},
// 搜索操作
handleQuery() {
this.queryParams.pageNum = 1;
this.dataList = [];
this.getList();
},
// 清空搜索
handleClear() {
this.searchKeyword = '';
this.handleQuery();
},
// 获取是否离校文本
getIsLeaveSchoolText(value) {
return getDictLabel(this.dictYesNo, value) || '-';
},
// 获取是否离校标签类型
getIsLeaveSchoolTagType(value) {
const fallbackMap = {
Y: 'warning', // 是
N: 'info', // 否
};
return getDictTagType(this.dictYesNo, value, fallbackMap);
},
// 获取审批状态文本
getApprovalStatusText(value) {
return getDictLabel(this.dictApprovalStatus, value) || '-';
},
// 获取审批状态标签类型
getApprovalStatusTagType(value) {
const fallbackMap = {
0: 'warning', // 待审批
1: 'success', // 已通过
2: 'error', // 已驳回
};
return getDictTagType(this.dictApprovalStatus, value, fallbackMap);
},
// 获取销假状态文本
getCancelStatusText(value) {
return getDictLabel(this.dictCancelStatus, value) || '-';
},
// 获取销假状态标签类型
getCancelStatusTagType(value) {
const fallbackMap = {
0: 'warning', // 未销假
1: 'success', // 已销假
};
return getDictTagType(this.dictCancelStatus, value, fallbackMap);
},
// 查看审批记录
handleViewApproval(item) {
console.log('查看审批记录:', item);
if (!item.id) {
uni.showToast({
title: '数据异常,无法查看审批记录',
icon: 'none',
});
return;
}
// 调用审批记录组件,传入业务ID
this.$refs.approvalRecordRef.init(item.id);
},
},
};
</script>
<style lang="scss" scoped>
.student-leave-tab {
background-color: #f5f6fa;
min-height: calc(100vh - 176rpx);
}
.search-container {
padding: 20rpx;
background-color: #fff;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.content-wrapper {
padding: 20rpx;
}
.empty-container {
padding: 100rpx 20rpx;
}
.scroll-view {
height: calc(100vh - 176rpx - 88rpx); // 减去搜索栏高度
}
.list-container {
display: flex;
flex-direction: column;
gap: 20rpx;
padding-bottom: 20rpx;
}
.list-card {
background-color: #fff;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
border-bottom: 1px solid #f0f0f0;
}
.student-info {
display: flex;
align-items: center;
gap: 16rpx;
}
.student-name {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.student-no {
font-size: 26rpx;
color: #909399;
}
.card-content {
padding: 24rpx;
}
.info-row {
display: flex;
align-items: center;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
}
.label {
font-size: 28rpx;
color: #606266;
min-width: 140rpx;
margin-right: 16rpx;
}
.value {
font-size: 28rpx;
color: #303133;
flex: 1;
word-break: break-all;
}
.card-actions {
display: flex;
justify-content: flex-end;
align-items: center;
padding: 20rpx 24rpx;
border-top: 1px solid #f0f0f0;
}
.load-more-wrapper {
padding: 20rpx 0;
}
// uview组件样式调整
::v-deep .u-search {
background-color: #f5f6fa;
}
</style>
@@ -0,0 +1,99 @@
<template>
<view class="student-dynamics-container">
<!-- 顶部导航 -->
<u-navbar title="学生动向信息" :border-bottom="false" bg-color="#0051d9" title-color="#fff">
<view slot="left" @click="goBack">
<u-icon name="arrow-left" color="#fff" size="22"></u-icon>
</view>
</u-navbar>
<!-- Tab切换 -->
<u-tabs
:list="tabList"
:current="currentTab"
@change="handleTabChange"
active-color="#0051d9"
inactive-color="#606266"
line-color="#0051d9"
:line-width="60"
:line-height="4"
:scrollable="false"
></u-tabs>
<!-- Tab内容区域 -->
<view class="tab-content">
<!-- 调宿/退宿信息 -->
<view v-show="currentTab === 0" class="tab-pane">
<dorm-application-tab ref="dormApplicationTabRef"></dorm-application-tab>
</view>
<!-- 假期留宿信息 -->
<view v-show="currentTab === 1" class="tab-pane">
<holiday-stay-tab ref="holidayStayTabRef"></holiday-stay-tab>
</view>
<!-- 学生请假信息 -->
<view v-show="currentTab === 2" class="tab-pane">
<student-leave-tab ref="studentLeaveTabRef"></student-leave-tab>
</view>
</view>
</view>
</template>
<script>
import DormApplicationTab from './components/DormApplicationTab.vue';
import HolidayStayTab from './components/HolidayStayTab.vue';
import StudentLeaveTab from './components/StudentLeaveTab.vue';
export default {
name: 'StudentDynamics',
components: {
DormApplicationTab,
HolidayStayTab,
StudentLeaveTab,
},
data() {
return {
// Tab列表
tabList: [
{ name: '调宿/退宿信息' },
{ name: '假期留宿信息' },
{ name: '学生请假信息' },
],
// 当前激活的Tab
currentTab: 0,
};
},
onLoad() {
console.log('学生动向信息管理页面已加载');
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack();
},
// Tab切换处理
handleTabChange(item) {
console.log('切换到Tab页:', item);
// uview的@change事件返回的是对象,需要提取index属性
this.currentTab = item.index;
},
},
};
</script>
<style lang="scss" scoped>
.student-dynamics-container {
background-color: #f5f6fa;
min-height: 100vh;
}
.tab-content {
padding: 0;
}
.tab-pane {
min-height: calc(100vh - 88rpx - 88rpx); // 减去navbar和tabs的高度
}
</style>
@@ -17,7 +17,7 @@
:list="tabList"
:current="safeCurrentTab"
@click="handleTabClick"
active-color="#3c9cff"
active-color="#0051d9"
inactive-color="#606266"
:font-size="32"
:bar-width="60"
@@ -0,0 +1,210 @@
# 宿舍卫生检查组件说明
## 组件列表
### 1. SingleRoomCheck.vue - 单宿舍检查组件
**功能说明:**
- 移动端单个宿舍的卫生检查表单组件
- 与web端 `DormHygieneCheckSingleDialog.vue` 功能完全一致
- 用于嵌入在弹窗中进行单个宿舍的详细检查
**主要功能模块:**
#### 1.1 宿舍整体情况
- 违纪类型选择(从API加载)
- 宿舍扣分输入(0-100分)
- 卫生等级选择(字典:`xg_dorm_hygiene_grade`)
- 检查附件上传(支持图片,最多15个)
#### 1.2 宿舍学生情况
- 学生基本信息展示(姓名、学号、性别、床位、班级、专业)
- 学生评分项:
- 个人扣分(0-100分)
- 出勤状态(字典:`xg_student_attendance_status`)
- 学生评级(字典:`xg_dorm_hygiene_grade`)
- 请假信息展示(如果有请假记录)
- 违纪记录录入与管理
**Props:**
```javascript
{
roomInfo: Object, // 房间信息(必填)
checkInfo: Object, // 检查基础信息(必填)
existingData: Object // 编辑时的现有数据(可选)
}
```
**Events:**
```javascript
{
submit: (data) => {}, // 保存检查结果
cancel: () => {} // 取消操作
}
```
**API接口:**
- `GET /disciplineType/disciplineType/listByAreaType` - 加载违纪类型
- `GET /dormBedStudent/dormBedStudent/getRoomStudents` - 加载房间学生信息
- `GET /studentLeave/studentLeave/queryByCheckDate` - 查询学生请假信息
- `POST /dormHygieneCheck/dormHygieneCheck/saveCompleteData` - 保存完整检查数据
- `POST /resource/oss/upload` - 上传附件
**字典类型:**
- `xg_dorm_hygiene_grade` - 卫生等级
- `xg_student_attendance_status` - 出勤状态
---
### 2. ViolationRecordDialog.vue - 违纪记录弹窗
**功能说明:**
- 学生违纪记录的录入和编辑弹窗
- 支持新增和编辑两种模式
**主要字段:**
- 违纪类型(从API加载)
- 扣分分数(自动填入最低扣分)
- 违纪日期
- 违纪地点
- 违纪描述
- 处理意见
**Props:**
```javascript
{
visible: Boolean, // 弹窗显示状态
studentInfo: Object, // 学生信息
violationData: Object // 违纪数据(编辑模式)
}
```
**Events:**
```javascript
{
confirm: (data) => {}, // 确认保存
close: () => {} // 关闭弹窗
}
```
**API接口:**
- `GET /disciplineType/disciplineType/listByAreaType` - 加载违纪类型
---
## 使用示例
### 在 wsjcMainAdd.vue 中使用 SingleRoomCheck
```vue
<template>
<u-popup
:show="showRoomCheckPopup"
mode="bottom"
:round="20"
closeable
:safe-area-inset-bottom="true"
@close="handlePopupClose"
>
<view class="popup-container">
<view class="popup-header">
<text class="popup-title">
{{ currentRoom ? currentRoom.roomName : '' }} - 卫生检查
</text>
</view>
<scroll-view scroll-y class="popup-scroll" :style="{ height: popupHeight + 'px' }">
<SingleRoomCheck
v-if="showRoomCheckPopup && currentRoom"
:room-info="currentRoom"
:check-info="formData"
:existing-data="currentEditData"
@submit="handleSingleCheckSubmit"
@cancel="handlePopupClose"
/>
</scroll-view>
</view>
</u-popup>
</template>
<script>
import SingleRoomCheck from './components/SingleRoomCheck.vue';
export default {
components: {
SingleRoomCheck,
},
// ...
};
</script>
```
---
## 开发规范
### 1. API调用
- 统一使用 `mobile/src/common/request.js` 中的 `request` 方法
- 附件上传使用 `smartUploadFile` 方法(自动适配微信环境)
### 2. 字典数据
- 使用 `mobile/src/common/dict.js` 中的方法
- `getDict(dictType)` - 获取字典数据
- `getDictLabel(dictData, value)` - 获取字典标签
- `getDictTagType(dictData, value)` - 获取u-tag类型
### 3. 样式规范
- 使用uview-ui 2.x组件
- 字典数据用 `<u-tag>` 展示
- 遵循移动端UI设计规范
### 4. 数据提交
- 与web端API接口完全一致
- 参数名和数据结构保持一致
- 返回数据格式相同
---
## 技术栈
- Vue 2.6.14
- uni-app 2.x
- uview-ui 2.0.38
- dayjs(日期处理)
---
## 注意事项
1. **附件上传兼容性**
- 移动端附件功能兼容微信小程序
- 参考 `wsjcAdd.vue` 的附件实现
- 使用 `smartUploadFile` 方法自动适配环境
2. **字典数据加载**
- 组件初始化时自动加载字典数据
- 使用缓存机制,避免重复请求
3. **请假信息自动填充**
- 加载学生信息时自动查询请假记录
- 有请假记录的学生自动设置出勤状态为"请假"
4. **违纪类型自动扣分**
- 选择违纪类型后自动填入最低扣分
- 用户可手动修改
5. **数据验证**
- 扣分范围:0-100分
- 必填字段验证
- 表单提交前统一验证
---
## 更新日志
**2025-01-24**
- 创建 SingleRoomCheck.vue 组件
- 创建 ViolationRecordDialog.vue 组件
- 实现与web端完全一致的功能
- 适配移动端UI和交互方式
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+251 -7
View File
@@ -205,15 +205,77 @@
<view class="fab-button" @click="handleAdd">
<u-icon name="plus" size="24" color="#ffffff" />
</view>
<!-- 查看检查弹窗 -->
<u-popup
:show="showViewPopup"
mode="bottom"
:round="20"
closeable
:safe-area-inset-bottom="true"
@close="handleViewPopupClose"
>
<view class="popup-container">
<view class="popup-header">
<text class="popup-title">
{{ currentViewItem ? currentViewItem.roomName : '' }} - 查看检查
</text>
</view>
<scroll-view scroll-y class="popup-scroll" :style="{ height: popupHeight + 'px' }">
<SingleRoomCheck
v-if="showViewPopup && currentViewItem"
:room-info="currentViewRoomInfo"
:check-info="currentViewCheckInfo"
:existing-data="currentViewData"
:is-view-mode="true"
@cancel="handleViewPopupClose"
/>
</scroll-view>
</view>
</u-popup>
<!-- 修改检查弹窗 -->
<u-popup
:show="showEditPopup"
mode="bottom"
:round="20"
closeable
:safe-area-inset-bottom="true"
@close="handleEditPopupClose"
>
<view class="popup-container">
<view class="popup-header">
<text class="popup-title">
{{ currentEditItem ? currentEditItem.roomName : '' }} - 修改检查
</text>
</view>
<scroll-view scroll-y class="popup-scroll" :style="{ height: popupHeight + 'px' }">
<SingleRoomCheck
v-if="showEditPopup && currentEditItem"
:room-info="currentRoomInfo"
:check-info="currentCheckInfo"
:existing-data="currentEditData"
@submit="handleEditSubmit"
@cancel="handleEditPopupClose"
/>
</scroll-view>
</view>
</u-popup>
</view>
</template>
<script>
import { request } from '@/common/request.js';
import { getDict, getDictLabel, getDictTagType } from '@/common/dict.js';
import SingleRoomCheck from './components/SingleRoomCheck.vue';
export default {
name: 'DormHygieneCheck',
components: {
SingleRoomCheck,
},
data() {
return {
loading: false,
@@ -236,6 +298,21 @@ export default {
xg_dorm_hygiene_check_type: [],
xg_dorm_hygiene_check_level: [],
xg_dorm_hygiene_grade: [],
// 修改弹窗相关
showEditPopup: false,
currentEditItem: null,
currentEditData: null,
currentRoomInfo: null,
currentCheckInfo: null,
popupHeight: 600,
// 查看弹窗相关
showViewPopup: false,
currentViewItem: null,
currentViewData: null,
currentViewRoomInfo: null,
currentViewCheckInfo: null,
};
},
@@ -254,6 +331,12 @@ export default {
uni.$on('refreshCheckList', this.handleRefreshEvent);
},
onReady() {
// 计算弹窗高度(屏幕高度的70%)
const systemInfo = uni.getSystemInfoSync();
this.popupHeight = systemInfo.windowHeight * 0.7;
},
beforeDestroy() {
// 移除事件监听
uni.$off('refreshCheckList', this.handleRefreshEvent);
@@ -450,22 +533,162 @@ export default {
// 新增检查
handleAdd() {
uni.navigateTo({
url: '/pages/xgss/wsjc/wsjcAdd?type=add',
url: '/pages/xgss/wsjc/wsjcMainAdd',
});
},
// 查看检查
handleView(item) {
uni.navigateTo({
url: `/pages/xgss/wsjc/wsjcDetail?id=${item.id}&type=view`,
async handleView(item) {
try {
uni.showLoading({ title: '加载中...' });
// 加载检查记录详情
const res = await request({
url: `/dormHygieneCheck/dormHygieneCheck/${item.id}`,
method: 'GET',
});
uni.hideLoading();
if (res && res.code === 200 && res.data) {
this.currentViewItem = item;
this.currentViewData = res.data;
// 构造房间信息
this.currentViewRoomInfo = {
id: item.roomId,
roomName: item.roomName,
roomCode: item.roomCode,
campusId: item.campusId,
campusName: item.campusName,
areaId: item.areaId,
areaName: item.areaName,
buildingId: item.buildingId,
buildingName: item.buildingName,
};
// 构造检查基础信息
this.currentViewCheckInfo = {
checkTitle: item.checkTitle,
checkDate: item.checkDate,
checkerId: item.checkerId,
checkerName: item.checkerName,
checkType: item.checkType,
checkLevel: item.checkLevel,
campusId: item.campusId,
campusName: item.campusName,
buildingId: item.buildingId,
buildingName: item.buildingName,
};
this.showViewPopup = true;
} else {
uni.showToast({
title: res?.msg || '加载失败',
icon: 'none',
});
}
} catch (error) {
uni.hideLoading();
console.error('❌ 加载检查记录失败:', error);
uni.showToast({
title: '加载失败',
icon: 'none',
});
}
},
// 修改检查
handleUpdate(item) {
uni.navigateTo({
url: `/pages/xgss/wsjc/wsjcAdd?id=${item.id}&type=update`,
async handleUpdate(item) {
try {
uni.showLoading({ title: '加载中...' });
// 加载检查记录详情
const res = await request({
url: `/dormHygieneCheck/dormHygieneCheck/${item.id}`,
method: 'GET',
});
uni.hideLoading();
if (res && res.code === 200 && res.data) {
this.currentEditItem = item;
this.currentEditData = res.data;
// 构造房间信息
this.currentRoomInfo = {
id: item.roomId,
roomName: item.roomName,
roomCode: item.roomCode,
campusId: item.campusId,
campusName: item.campusName,
areaId: item.areaId,
areaName: item.areaName,
buildingId: item.buildingId,
buildingName: item.buildingName,
};
// 构造检查基础信息
this.currentCheckInfo = {
checkTitle: item.checkTitle,
checkDate: item.checkDate,
checkerId: item.checkerId,
checkerName: item.checkerName,
checkType: item.checkType,
checkLevel: item.checkLevel,
campusId: item.campusId,
campusName: item.campusName,
buildingId: item.buildingId,
buildingName: item.buildingName,
};
this.showEditPopup = true;
} else {
uni.showToast({
title: res?.msg || '加载失败',
icon: 'none',
});
}
} catch (error) {
uni.hideLoading();
console.error('❌ 加载检查记录失败:', error);
uni.showToast({
title: '加载失败',
icon: 'none',
});
}
},
// 修改检查提交处理
handleEditSubmit(data) {
uni.showToast({
title: '修改成功',
icon: 'success',
});
// 关闭弹窗
this.handleEditPopupClose();
// 刷新列表
this.loadCheckList();
},
// 修改检查弹窗关闭处理
handleEditPopupClose() {
this.showEditPopup = false;
this.currentEditItem = null;
this.currentEditData = null;
this.currentRoomInfo = null;
this.currentCheckInfo = null;
},
// 查看检查弹窗关闭处理
handleViewPopupClose() {
this.showViewPopup = false;
this.currentViewItem = null;
this.currentViewData = null;
this.currentViewRoomInfo = null;
this.currentViewCheckInfo = null;
},
// 删除检查
@@ -858,4 +1081,25 @@ export default {
::v-deep .u-button {
border-radius: 8rpx;
}
// 修改弹窗样式
.popup-container {
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
}
.popup-header {
padding: 30rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.popup-title {
font-size: 32rpx;
font-weight: 600;
color: #303133;
}
.popup-scroll {
overflow-y: auto;
}
</style>
+971
View File
@@ -0,0 +1,971 @@
<template>
<view class="page-container">
<!-- 顶部导航 -->
<u-navbar
title="宿舍卫生检查"
:border-bottom="false"
bg-color="#0051d9"
:titleStyle="{ color: '#fff' }"
>
<view slot="left" @click="goBack">
<u-icon name="arrow-left" color="#fff" size="22"></u-icon>
</view>
</u-navbar>
<!-- 加载状态 -->
<u-loading-page :loading="pageLoading" loading-text="加载中..."></u-loading-page>
<!-- 表单内容 -->
<view v-if="!pageLoading" class="content-container">
<!-- 基础信息卡片 -->
<view class="info-card">
<view class="card-title">检查基础信息</view>
<u-form ref="formRef" :model="formData" :rules="rules" label-width="100">
<!-- 检查标题 -->
<u-form-item label="检查标题" prop="checkTitle" required>
<u-input v-model="formData.checkTitle" placeholder="请输入检查标题" />
</u-form-item>
<!-- 检查日期 -->
<u-form-item label="检查日期" prop="checkDate" required>
<view @click="showDatePicker = true" style="width: 100%">
<u-input
v-model="checkDateDisplay"
placeholder="请选择检查日期"
readonly
border="none"
suffix-icon="arrow-down"
suffix-icon-style="color: #c8c9cc;"
style="pointer-events: none"
/>
</view>
<u-datetime-picker
:show="showDatePicker"
v-model="checkDateValue"
mode="datetime"
@confirm="handleDateConfirm"
@cancel="showDatePicker = false"
></u-datetime-picker>
</u-form-item>
<!-- 检查员 -->
<u-form-item label="检查员" prop="checkerName" required>
<u-input v-model="formData.checkerName" placeholder="检查员姓名" readonly />
</u-form-item>
<!-- 检查类型 -->
<u-form-item label="检查类型" prop="checkType" required>
<view @click="showCheckTypePicker = true" style="width: 100%">
<u-input
v-model="checkTypeDisplay"
placeholder="请选择检查类型"
readonly
border="none"
suffix-icon="arrow-down"
suffix-icon-style="color: #c8c9cc;"
style="pointer-events: none"
/>
</view>
<u-picker
:show="showCheckTypePicker && xg_dorm_hygiene_check_type.length > 0"
:columns="checkTypeColumns"
:defaultIndex="checkTypeDefaultIndex"
keyName="text"
@confirm="handleCheckTypeConfirm"
@cancel="showCheckTypePicker = false"
></u-picker>
</u-form-item>
<!-- 检查级别 -->
<u-form-item label="检查级别" prop="checkLevel" required>
<view @click="showCheckLevelPicker = true" style="width: 100%">
<u-input
v-model="checkLevelDisplay"
placeholder="请选择检查级别"
readonly
border="none"
suffix-icon="arrow-down"
suffix-icon-style="color: #c8c9cc;"
style="pointer-events: none"
/>
</view>
<u-picker
:show="showCheckLevelPicker && xg_dorm_hygiene_check_level.length > 0"
:columns="checkLevelColumns"
:defaultIndex="checkLevelDefaultIndex"
keyName="text"
@confirm="handleCheckLevelConfirm"
@cancel="showCheckLevelPicker = false"
></u-picker>
</u-form-item>
<!-- 校区 -->
<u-form-item label="校区" prop="campusId" required>
<view @click="showCampusPicker = true" style="width: 100%">
<u-input
v-model="formData.campusName"
placeholder="请选择校区"
readonly
border="none"
suffix-icon="arrow-down"
suffix-icon-style="color: #c8c9cc;"
style="pointer-events: none"
/>
</view>
<u-picker
:show="showCampusPicker"
:columns="campusColumns"
keyName="text"
@confirm="handleCampusConfirm"
@cancel="showCampusPicker = false"
></u-picker>
</u-form-item>
<!-- 宿舍楼 -->
<u-form-item label="宿舍楼" prop="buildingId" required>
<view @click="handleBuildingClick" style="width: 100%">
<u-input
v-model="formData.buildingName"
placeholder="请选择宿舍楼"
readonly
border="none"
suffix-icon="arrow-down"
suffix-icon-style="color: #c8c9cc;"
:disabled="!formData.campusId"
style="pointer-events: none"
/>
</view>
<u-picker
:show="showBuildingPicker"
:columns="buildingColumns"
keyName="text"
@confirm="handleBuildingConfirm"
@cancel="showBuildingPicker = false"
></u-picker>
</u-form-item>
</u-form>
</view>
<!-- 房间列表区域 -->
<view v-if="formData.buildingId && floorRooms.length > 0" class="rooms-section">
<view class="rooms-header">
<text class="rooms-title">宿舍房间列表</text>
<view class="rooms-stats">
<text>总房间: {{ totalRoomsCount }}</text>
<text style="margin-left: 20rpx">已检查: {{ completedRoomsCount }}</text>
</view>
</view>
<view class="hint-text">
<u-icon name="info-circle" color="#ff9900" size="16"></u-icon>
<text style="margin-left: 10rpx; color: #ff9900">点击房间卡片开始检查</text>
</view>
<!-- 楼层和房间展示 -->
<view class="floors-container">
<view
v-for="floorData in floorRooms"
:key="floorData.floor"
class="floor-section"
>
<view class="floor-header">
<view class="floor-name">{{ floorData.floor }}楼</view>
<view class="floor-stats">
已检查: {{ getFloorCompletedCount(floorData.floor) }} /
{{ floorData.rooms.length }}
</view>
</view>
<view class="rooms-grid">
<view
v-for="room in floorData.rooms"
:key="room.id"
class="room-card"
:class="{ completed: completedRooms.has(room.id) }"
@click="handleRoomClick(room)"
>
<view class="room-card-content">
<view class="room-name">{{ room.roomName }}</view>
<view class="room-code">
{{ room.roomCode || room.roomName }}
</view>
<view class="room-status">
<u-icon
v-if="completedRooms.has(room.id)"
name="checkmark-circle-fill"
color="#19be6b"
size="18"
></u-icon>
<u-icon
v-else
name="calendar"
color="#c8c9cc"
size="18"
></u-icon>
</view>
</view>
<view class="room-hint">
<text v-if="completedRooms.has(room.id)" class="completed-text">
已检查
</text>
<text v-else class="pending-text">点击检查</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view
v-if="formData.buildingId && floorRooms.length === 0 && !roomsLoading"
class="empty-container"
>
<u-empty text="该宿舍楼暂无房间信息" mode="data"></u-empty>
</view>
</view>
<!-- 单个房间检查弹窗 -->
<u-popup
:show="showRoomCheckPopup"
mode="bottom"
:round="20"
closeable
:safe-area-inset-bottom="true"
@close="handlePopupClose"
>
<view class="popup-container">
<view class="popup-header">
<text class="popup-title">
{{ currentRoom ? currentRoom.roomName : '' }} - 卫生检查
</text>
</view>
<scroll-view scroll-y class="popup-scroll" :style="{ height: popupHeight + 'px' }">
<SingleRoomCheck
v-if="showRoomCheckPopup && currentRoom"
:room-info="currentRoom"
:check-info="formData"
:existing-data="currentEditData"
@submit="handleSingleCheckSubmit"
@cancel="handlePopupClose"
/>
</scroll-view>
</view>
</u-popup>
</view>
</template>
<script>
import { mapState } from 'vuex';
import { getDict, getDictLabel, getDictTagType } from '@/common/dict.js';
import { request } from '@/common/request.js';
import dayjs from 'dayjs';
import SingleRoomCheck from './components/SingleRoomCheck.vue';
export default {
name: 'WsjcMainAdd',
components: {
SingleRoomCheck,
},
data() {
return {
pageLoading: true,
roomsLoading: false,
// 表单数据
formData: {
checkTitle: '',
checkDate: '',
checkerId: '',
checkerName: '',
checkType: '1',
checkLevel: '1',
campusId: '',
campusName: '',
buildingId: '',
buildingName: '',
},
// 验证规则
rules: {
checkTitle: [{ required: true, message: '请输入检查标题', trigger: 'blur' }],
checkDate: [{ required: true, message: '请选择检查日期', trigger: 'change' }],
checkerName: [{ required: true, message: '请选择检查员', trigger: 'change' }],
checkType: [{ required: true, message: '请选择检查类型', trigger: 'change' }],
checkLevel: [{ required: true, message: '请选择检查级别', trigger: 'change' }],
campusId: [{ required: true, message: '请选择校区', trigger: 'change' }],
buildingId: [{ required: true, message: '请选择宿舍楼', trigger: 'change' }],
},
// Picker显示状态
showDatePicker: false,
showCheckTypePicker: false,
showCheckLevelPicker: false,
showCampusPicker: false,
showBuildingPicker: false,
showRoomCheckPopup: false,
// 日期时间
checkDateValue: Date.now(),
checkDateDisplay: '',
// 字典数据
xg_dorm_hygiene_check_type: [],
xg_dorm_hygiene_check_level: [],
// 选项数据
campusOptions: [],
buildingOptions: [],
// 房间相关数据
floorRooms: [],
allRooms: [],
completedRooms: new Set(),
roomCheckIds: new Map(),
// 当前操作的房间
currentRoom: null,
currentEditData: null,
// 弹窗高度
popupHeight: 600,
};
},
computed: {
...mapState(['userInfo']),
// 检查类型列
checkTypeColumns() {
return [
this.xg_dorm_hygiene_check_type.map((item) => ({
text: item.label,
value: item.value,
})),
];
},
// 检查类型默认索引
checkTypeDefaultIndex() {
const index = this.xg_dorm_hygiene_check_type.findIndex(
(item) => item.value === this.formData.checkType,
);
return [index >= 0 ? index : 0];
},
// 检查类型显示文本
checkTypeDisplay() {
const item = this.xg_dorm_hygiene_check_type.find(
(d) => d.value === this.formData.checkType,
);
return item ? item.label : '';
},
// 检查级别列
checkLevelColumns() {
return [
this.xg_dorm_hygiene_check_level.map((item) => ({
text: item.label,
value: item.value,
})),
];
},
// 检查级别默认索引
checkLevelDefaultIndex() {
const index = this.xg_dorm_hygiene_check_level.findIndex(
(item) => item.value === this.formData.checkLevel,
);
return [index >= 0 ? index : 0];
},
// 检查级别显示文本
checkLevelDisplay() {
const item = this.xg_dorm_hygiene_check_level.find(
(d) => d.value === this.formData.checkLevel,
);
return item ? item.label : '';
},
// 校区列
campusColumns() {
return [this.campusOptions.map((item) => ({ text: item.campusName, value: item.id }))];
},
// 宿舍楼列
buildingColumns() {
return [
this.buildingOptions.map((item) => ({
text: this.getBuildingDisplayName(item),
value: item.id,
})),
];
},
// 总房间数
totalRoomsCount() {
return this.allRooms.length;
},
// 已完成房间数
completedRoomsCount() {
return this.completedRooms.size;
},
},
onLoad() {
this.initPage();
},
onReady() {
// 计算弹窗高度(屏幕高度的70%)
const systemInfo = uni.getSystemInfoSync();
this.popupHeight = systemInfo.windowHeight * 0.7;
},
methods: {
// 初始化页面
async initPage() {
try {
this.pageLoading = true;
// 初始化检查标题和日期
this.generateDefaultCheckTitle();
this.formData.checkDate = dayjs().format('YYYY-MM-DD HH:mm:ss');
this.checkDateDisplay = this.formData.checkDate;
this.checkDateValue = Date.now();
// 获取当前用户信息
await this.fetchCurrentUserInfo();
// 加载字典数据
await this.loadDicts();
// 加载校区选项
await this.loadCampusOptions();
} catch (error) {
console.error('初始化页面失败:', error);
this.$u.toast('初始化页面失败');
} finally {
this.pageLoading = false;
}
},
// 生成默认检查标题
generateDefaultCheckTitle() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
this.formData.checkTitle = `${year}${month}${day}宿舍卫生检查`;
},
// 获取当前用户信息
async fetchCurrentUserInfo() {
try {
const res = await request({
url: '/system/user/getInfo',
method: 'GET',
});
if (res.code === 200 && res.data?.user) {
const user = res.data.user;
this.formData.checkerId = user.userId;
this.formData.checkerName = user.nickName || user.userName;
}
} catch (error) {
console.warn('获取用户信息失败:', error);
}
},
// 加载字典数据
async loadDicts() {
try {
const [checkTypeData, checkLevelData] = await Promise.all([
getDict('xg_dorm_hygiene_check_type'),
getDict('xg_dorm_hygiene_check_level'),
]);
this.xg_dorm_hygiene_check_type = checkTypeData || [];
this.xg_dorm_hygiene_check_level = checkLevelData || [];
} catch (error) {
console.error('加载字典数据失败:', error);
}
},
// 加载校区选项
async loadCampusOptions() {
try {
const res = await request({
url: '/xqxx/dormCampus/list',
method: 'GET',
});
this.campusOptions = res.rows || [];
} catch (error) {
console.error('加载校区选项失败:', error);
uni.showToast({ title: '加载校区选项失败', icon: 'none' });
}
},
// 加载宿舍楼选项
async loadBuildingOptions(campusId) {
try {
const res = await request({
url: '/sslxx/dormBuilding/list',
method: 'GET',
params: { campusId },
});
this.buildingOptions = res.rows || [];
} catch (error) {
console.error('加载宿舍楼选项失败:', error);
uni.showToast({ title: '加载宿舍楼选项失败', icon: 'none' });
}
},
// 加载房间列表
async loadRoomsByFloor(buildingId) {
try {
this.roomsLoading = true;
const res = await request({
url: '/xgss/dormRoom/list',
method: 'GET',
params: { buildingId },
});
if (res.code === 200 && res.rows) {
this.allRooms = res.rows;
// 按楼层分组
const floorMap = new Map();
res.rows.forEach((room) => {
const floor = room.floorNum || '1';
if (!floorMap.has(floor)) {
floorMap.set(floor, []);
}
floorMap.get(floor).push(room);
});
// 转换为数组并排序
this.floorRooms = Array.from(floorMap.entries())
.map(([floor, rooms]) => ({ floor, rooms }))
.sort((a, b) => parseInt(a.floor) - parseInt(b.floor));
}
} catch (error) {
console.error('加载房间失败:', error);
uni.showToast({ title: '加载房间失败', icon: 'none' });
} finally {
this.roomsLoading = false;
}
},
// 日期确认处理
handleDateConfirm(e) {
this.formData.checkDate = dayjs(e.value).format('YYYY-MM-DD HH:mm:ss');
this.checkDateDisplay = this.formData.checkDate;
this.showDatePicker = false;
},
// 检查类型确认处理
handleCheckTypeConfirm(e) {
if (e && e.value && e.value.length > 0) {
this.formData.checkType = e.value[0].value;
}
this.showCheckTypePicker = false;
},
// 检查级别确认处理
handleCheckLevelConfirm(e) {
if (e && e.value && e.value.length > 0) {
this.formData.checkLevel = e.value[0].value;
}
this.showCheckLevelPicker = false;
},
// 校区确认处理
async handleCampusConfirm(e) {
if (e && e.value && e.value.length > 0) {
const campus = this.campusOptions.find((item) => item.id === e.value[0].value);
if (campus) {
this.formData.campusId = campus.id;
this.formData.campusName = campus.campusName;
// 清空下级选项
this.formData.buildingId = '';
this.formData.buildingName = '';
this.buildingOptions = [];
this.floorRooms = [];
this.allRooms = [];
this.completedRooms.clear();
this.roomCheckIds.clear();
await this.loadBuildingOptions(campus.id);
}
}
this.showCampusPicker = false;
},
// 宿舍楼点击处理
handleBuildingClick() {
if (!this.formData.campusId) {
this.$u.toast('请先选择校区');
return;
}
this.showBuildingPicker = true;
},
// 宿舍楼确认处理
async handleBuildingConfirm(e) {
if (e && e.value && e.value.length > 0) {
const building = this.buildingOptions.find((item) => item.id === e.value[0].value);
if (building) {
this.formData.buildingId = building.id;
this.formData.buildingName = building.buildingName;
// 清空房间数据
this.floorRooms = [];
this.allRooms = [];
this.completedRooms.clear();
this.roomCheckIds.clear();
await this.loadRoomsByFloor(building.id);
}
}
this.showBuildingPicker = false;
},
// 房间点击处理
async handleRoomClick(room) {
console.log('🔍 handleRoomClick - 点击的房间数据:', room);
console.log(
'🔍 房间字段 - roomName:',
room.roomName,
', roomCode:',
room.roomCode,
', name:',
room.name,
);
// 验证基础信息是否完整
if (
!this.formData.checkTitle ||
!this.formData.checkDate ||
!this.formData.checkerName ||
!this.formData.checkType ||
!this.formData.checkLevel
) {
uni.showToast({ title: '请先完善检查基础信息', icon: 'none' });
return;
}
this.currentRoom = room;
console.log('✅ currentRoom已赋值:', this.currentRoom);
// 检查是否已有记录ID
const checkId = this.roomCheckIds.get(room.id);
if (checkId) {
// 已检查过,加载数据用于编辑
try {
const response = await request({
url: `/dormHygieneCheck/dormHygieneCheck/${checkId}`,
method: 'GET',
});
if (response.code === 200 && response.data) {
this.currentEditData = response.data;
}
} catch (error) {
console.error('查询检查记录详情失败:', error);
uni.showToast({ title: '查询检查记录失败', icon: 'none' });
return;
}
} else {
this.currentEditData = null;
}
this.showRoomCheckPopup = true;
},
// 单个房间检查提交处理
handleSingleCheckSubmit(data) {
// 标记房间为已完成
this.completedRooms.add(this.currentRoom.id);
// 保存检查记录ID
if (data.id) {
this.roomCheckIds.set(this.currentRoom.id, data.id);
}
uni.showToast({ title: `${this.currentRoom.roomName} 检查完成`, icon: 'success' });
this.showRoomCheckPopup = false;
this.currentRoom = null;
this.currentEditData = null;
},
// 弹窗关闭处理
handlePopupClose() {
this.showRoomCheckPopup = false;
this.currentRoom = null;
this.currentEditData = null;
},
// 获取楼层已完成数量
getFloorCompletedCount(floor) {
const floorData = this.floorRooms.find((f) => f.floor === floor);
if (!floorData) return 0;
return floorData.rooms.filter((room) => this.completedRooms.has(room.id)).length;
},
// 宿舍楼显示名称
getBuildingDisplayName(building) {
let displayName = building.buildingName;
if (building.buildingType) {
if (building.buildingType === '1') {
displayName = `(男)${displayName}`;
} else if (building.buildingType === '2') {
displayName = `(女)${displayName}`;
}
}
return displayName;
},
// 返回
goBack() {
console.log('🔙 goBack - 点击返回按钮');
// 检查是否有未保存的数据
if (this.completedRoomsCount > 0) {
uni.showModal({
title: '提示',
content: `已检查 ${this.completedRoomsCount} 个房间,确定要返回吗?`,
success: (res) => {
if (res.confirm) {
uni.navigateBack({
delta: 1,
fail: (err) => {
console.error('❌ navigateBack失败:', err);
// 如果navigateBack失败,尝试使用redirectTo
uni.redirectTo({
url: '/pages/xgss/wsjc/wsjc',
fail: (err2) => {
console.error('❌ redirectTo也失败:', err2);
uni.showToast({ title: '返回失败', icon: 'none' });
},
});
},
});
}
},
});
} else {
uni.navigateBack({
delta: 1,
fail: (err) => {
console.error('❌ navigateBack失败:', err);
// 如果navigateBack失败,尝试使用redirectTo
uni.redirectTo({
url: '/pages/xgss/wsjc/wsjc',
fail: (err2) => {
console.error('❌ redirectTo也失败:', err2);
uni.showToast({ title: '返回失败', icon: 'none' });
},
});
},
});
}
},
},
};
</script>
<style lang="scss" scoped>
.page-container {
width: 100%;
min-height: 100vh;
background-color: #f5f6f7;
}
.content-container {
padding: 20rpx;
padding-top: 100rpx; /* 为导航栏预留空间 */
}
.info-card {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
}
.card-title {
font-size: 32rpx;
font-weight: 600;
color: #303133;
margin-bottom: 20rpx;
padding-left: 20rpx;
border-left: 6rpx solid #0051d9;
}
.rooms-section {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
}
.rooms-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.rooms-title {
font-size: 32rpx;
font-weight: 600;
color: #303133;
}
.rooms-stats {
font-size: 24rpx;
color: #909399;
display: flex;
flex-wrap: nowrap;
white-space: nowrap;
}
.hint-text {
display: flex;
align-items: center;
padding: 16rpx;
background-color: #fff8e6;
border-radius: 8rpx;
margin-bottom: 20rpx;
font-size: 24rpx;
}
.floors-container {
margin-top: 20rpx;
}
.floor-section {
margin-bottom: 40rpx;
}
.floor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 2rpx solid #3c9cff;
margin-bottom: 20rpx;
min-height: 60rpx;
}
.floor-name {
font-size: 30rpx;
font-weight: 600;
color: #3c9cff;
flex: 1;
margin-right: 20rpx;
}
.floor-stats {
font-size: 24rpx;
color: #909399;
min-width: 240rpx;
text-align: right;
white-space: nowrap;
flex-shrink: 0;
display: inline-block;
}
.rooms-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20rpx;
}
.room-card {
background-color: #fff;
border: 2rpx solid #e4e7ed;
border-radius: 12rpx;
padding: 20rpx;
position: relative;
transition: all 0.3s;
}
.room-card.completed {
border-color: #19be6b;
background-color: #f0f9ff;
}
.room-card-content {
display: flex;
flex-direction: column;
position: relative;
min-height: 80rpx;
}
.room-name {
font-size: 28rpx;
font-weight: 600;
color: #303133;
text-align: center;
padding-right: 30rpx;
display: flex;
align-items: center;
justify-content: center;
min-height: 50rpx;
}
.room-code {
font-size: 22rpx;
color: #909399;
text-align: center;
margin-top: 4rpx;
}
.room-status {
position: absolute;
top: 0;
right: 0;
height: 50rpx;
display: flex;
align-items: center;
justify-content: center;
}
.room-hint {
text-align: center;
margin-top: 8rpx;
font-size: 22rpx;
}
.completed-text {
color: #19be6b;
font-weight: 500;
}
.pending-text {
color: #909399;
}
.empty-container {
padding: 100rpx 0;
}
.popup-container {
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
}
.popup-header {
padding: 30rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.popup-title {
font-size: 32rpx;
font-weight: 600;
color: #303133;
}
.popup-scroll {
overflow-y: auto;
}
</style>
+135 -8
View File
@@ -117,16 +117,12 @@
<view class="info-section">
<text class="section-title">宿舍信息</text>
<view class="dorm-info-grid">
<!-- 第一行:校区和宿舍区 -->
<!-- 第一行:校区 -->
<view class="dorm-row">
<view class="info-item-half">
<text class="label">校区:</text>
<text class="value">{{ item.campusName || '-' }}</text>
</view>
<view class="info-item-half">
<text class="label">宿舍区:</text>
<text class="value">{{ item.areaName || '-' }}</text>
</view>
</view>
<!-- 第二行:宿舍楼和房间 -->
<view class="dorm-row">
@@ -207,6 +203,35 @@
<!-- <view class="fab-button" @click="handleAdd">
<u-icon name="plus" size="24" color="#ffffff" />
</view> -->
<!-- 查看检查弹窗 -->
<u-popup
:show="showViewPopup"
mode="bottom"
:round="20"
closeable
:safe-area-inset-bottom="true"
@close="handleViewPopupClose"
>
<view class="popup-container">
<view class="popup-header">
<text class="popup-title">
{{ currentViewItem ? currentViewItem.roomName : '' }} - 查看检查
</text>
</view>
<scroll-view scroll-y class="popup-scroll" :style="{ height: popupHeight + 'px' }">
<SingleRoomCheck
v-if="showViewPopup && currentViewItem"
:room-info="currentViewRoomInfo"
:check-info="currentViewCheckInfo"
:existing-data="currentViewData"
:is-view-mode="true"
@cancel="handleViewPopupClose"
/>
</scroll-view>
</view>
</u-popup>
</view>
</template>
@@ -214,9 +239,13 @@
import { request } from '@/common/request.js';
import { getDict, getDictLabel, getDictTagType } from '@/common/dict.js';
import { userManager } from '@/utils/user.js';
import SingleRoomCheck from '../wsjc/components/SingleRoomCheck.vue';
export default {
name: 'DormHygieneCheck',
components: {
SingleRoomCheck,
},
data() {
return {
loading: false,
@@ -239,6 +268,14 @@ export default {
xg_dorm_hygiene_check_type: [],
xg_dorm_hygiene_check_level: [],
xg_dorm_hygiene_grade: [],
// 查看弹窗相关
showViewPopup: false,
currentViewItem: null,
currentViewData: null,
currentViewRoomInfo: null,
currentViewCheckInfo: null,
popupHeight: 600,
};
},
@@ -257,6 +294,12 @@ export default {
uni.$on('refreshCheckList', this.handleRefreshEvent);
},
onReady() {
// 计算弹窗高度(屏幕高度的70%)
const systemInfo = uni.getSystemInfoSync();
this.popupHeight = systemInfo.windowHeight * 0.7;
},
beforeDestroy() {
// 移除事件监听
uni.$off('refreshCheckList', this.handleRefreshEvent);
@@ -487,10 +530,73 @@ export default {
},
// 查看检查
handleView(item) {
uni.navigateTo({
url: `/pages/xgss/xssswsjc/xssswsjcDetail?id=${item.id}&type=view`,
async handleView(item) {
try {
uni.showLoading({ title: '加载中...' });
// 加载检查记录详情
const res = await request({
url: `/dormHygieneCheck/dormHygieneCheck/${item.id}`,
method: 'GET',
});
uni.hideLoading();
if (res && res.code === 200 && res.data) {
this.currentViewItem = item;
this.currentViewData = res.data;
// 构造房间信息
this.currentViewRoomInfo = {
id: item.roomId,
roomName: item.roomName,
roomCode: item.roomCode,
campusId: item.campusId,
campusName: item.campusName,
areaId: item.areaId,
areaName: item.areaName,
buildingId: item.buildingId,
buildingName: item.buildingName,
};
// 构造检查基础信息
this.currentViewCheckInfo = {
checkTitle: item.checkTitle,
checkDate: item.checkDate,
checkerId: item.checkerId,
checkerName: item.checkerName,
checkType: item.checkType,
checkLevel: item.checkLevel,
campusId: item.campusId,
campusName: item.campusName,
buildingId: item.buildingId,
buildingName: item.buildingName,
};
this.showViewPopup = true;
} else {
uni.showToast({
title: res?.msg || '加载失败',
icon: 'none',
});
}
} catch (error) {
uni.hideLoading();
console.error('❌ 加载检查记录失败:', error);
uni.showToast({
title: '加载失败',
icon: 'none',
});
}
},
// 查看检查弹窗关闭处理
handleViewPopupClose() {
this.showViewPopup = false;
this.currentViewItem = null;
this.currentViewData = null;
this.currentViewRoomInfo = null;
this.currentViewCheckInfo = null;
},
// 修改检查
@@ -903,4 +1009,25 @@ export default {
::v-deep .u-button {
border-radius: 8rpx;
}
// 查看弹窗样式
.popup-container {
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
}
.popup-header {
padding: 30rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.popup-title {
font-size: 32rpx;
font-weight: 600;
color: #303133;
}
.popup-scroll {
overflow-y: auto;
}
</style>
+1 -1
View File
@@ -114,7 +114,7 @@
提交申请
</u-button>
<u-button
type="warning"
type="primary"
size="small"
icon="file-text"
shape="round"
+1 -1
View File
@@ -111,7 +111,7 @@
@click="handleViewRevoke(item)"
plain
>
查看撤销申请
撤销查看
</u-button>
</view>
</view>
File diff suppressed because it is too large. Load diff
+511
View File
@@ -0,0 +1,511 @@
<template>
<view class="discipline-record-container">
<!-- 顶部导航 -->
<u-navbar title="违纪记录录入" :border-bottom="false" bg-color="#3c9cff" title-color="#fff">
<view slot="left" @click="goBack">
<u-icon name="arrow-left" color="#fff" size="22"></u-icon>
</view>
</u-navbar>
<!-- 搜索栏 -->
<view class="search-bar">
<u-search
v-model="searchKeyword"
placeholder="搜索学生姓名或学号"
:show-action="false"
@search="handleSearch"
@custom="handleSearch"
bg-color="#f6f6f6"
shape="round"
/>
</view>
<!-- 加载状态 -->
<u-loading-page :loading="loading" loading-text="加载中..."></u-loading-page>
<!-- 违纪记录列表 -->
<view v-if="!loading" class="content-wrapper">
<!-- 空状态 -->
<view v-if="disciplineRecordList.length === 0" class="empty-container">
<u-empty text="暂无违纪记录" mode="data"></u-empty>
</view>
<!-- 记录卡片列表 -->
<view v-else class="record-list">
<view
v-for="(item, index) in disciplineRecordList"
:key="item.id || index"
class="record-card"
>
<!-- 卡片头部 -->
<view class="card-header">
<view class="student-info">
<text class="student-name">{{ item.studentName || '-' }}</text>
<text class="student-no">({{ item.studentNo || '-' }})</text>
</view>
<view class="record-status">
<u-tag
:text="getDictLabel(item.status)"
:type="getStatusType(item.status)"
size="mini"
></u-tag>
</view>
</view>
<!-- 卡片内容 -->
<view class="card-content">
<view class="info-item">
<text class="label">违纪类型:</text>
<text class="value">{{ item.disciplineTypeName || '-' }}</text>
</view>
<view class="info-item">
<text class="label">违纪日期:</text>
<text class="value">{{ item.disciplineDate || '-' }}</text>
</view>
<view class="info-item">
<text class="label">扣分分值:</text>
<text class="value value-score">-{{ item.scoreDeduction || '0' }}</text>
</view>
<view class="info-item">
<text class="label">学院班级:</text>
<text class="value">
{{ item.collegeName || '-' }} / {{ item.className || '-' }}
</text>
</view>
<view v-if="item.disciplineReason" class="info-item">
<text class="label">违纪原因:</text>
<text class="value">{{ item.disciplineReason }}</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="card-actions">
<!-- 查看按钮 -->
<u-button
type="info"
size="small"
class="action-btn"
@click="handleView(item)"
>
<u-icon name="eye" size="16" color="#323233"></u-icon>
<text>查看</text>
</u-button>
<!-- 编辑按钮 -->
<u-button
type="primary"
size="small"
class="action-btn"
@click="handleEdit(item)"
>
<u-icon name="edit-pen" size="16" color="#ffffff"></u-icon>
<text class="btn-text">编辑</text>
</u-button>
<!-- 删除按钮 -->
<u-button
type="error"
size="small"
class="action-btn"
@click="handleDelete(item)"
>
<u-icon name="trash" size="16" color="#ffffff"></u-icon>
<text class="btn-text">删除</text>
</u-button>
</view>
</view>
</view>
<!-- 加载更多 -->
<view v-if="disciplineRecordList.length > 0" class="load-more">
<u-loadmore :status="loadMoreStatus" @loadmore="loadMore" />
</view>
</view>
<!-- 浮动新增按钮 -->
<view class="float-add-button" @click="handleAdd">
<u-icon name="plus" color="#fff" size="28"></u-icon>
</view>
</view>
</template>
<script>
import { request } from '@/common/request.js';
import { getDictLabel, getDictTagType, getDict } from '@/common/dict.js';
export default {
name: 'DisciplineRecordEntry',
components: {},
data() {
return {
loading: false,
disciplineRecordList: [],
total: 0,
// 搜索关键词
searchKeyword: '',
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
studentName: '',
studentNo: '',
},
// 加载更多状态
loadMoreStatus: 'loadmore',
// 字典数据
statusDict: [],
};
},
async created() {
await this.loadDictData();
await this.getList();
// 监听页面刷新事件
uni.$on('refreshDisciplineRecordList', () => {
this.queryParams.pageNum = 1;
this.getList();
});
},
onUnload() {
// 移除监听
uni.$off('refreshDisciplineRecordList');
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack();
},
// 加载字典数据
async loadDictData() {
try {
this.statusDict = await getDict('student_discipline_status');
} catch (error) {
console.error('加载字典数据失败:', error);
}
},
// 获取字典标签
getDictLabel(value) {
return getDictLabel(this.statusDict, value) || value;
},
// 获取状态类型
getStatusType(status) {
const fallbackMap = {
1: 'success', // 生效
2: 'info', // 已撤销
};
return getDictTagType(this.statusDict, status, fallbackMap);
},
// 获取违纪记录列表(仅显示当前用户创建的)
async getList() {
this.loading = true;
try {
const res = await request({
url: '/disciplineRecord/disciplineRecord/myCreated',
method: 'GET',
params: this.queryParams,
});
if (res && res.code === 200) {
const newList = res.rows || [];
if (this.queryParams.pageNum === 1) {
this.disciplineRecordList = newList;
} else {
this.disciplineRecordList = [...this.disciplineRecordList, ...newList];
}
this.total = res.total || 0;
// 更新加载更多状态
if (this.disciplineRecordList.length >= this.total) {
this.loadMoreStatus = 'nomore';
} else {
this.loadMoreStatus = 'loadmore';
}
} else {
uni.showToast({
title: res?.msg || '获取数据失败',
icon: 'none',
});
}
} catch (error) {
console.error('获取违纪记录列表失败:', error);
uni.showToast({
title: '获取数据失败',
icon: 'none',
});
} finally {
this.loading = false;
}
},
// 搜索处理
handleSearch() {
// 解析搜索关键词
const keyword = this.searchKeyword.trim();
// 重置查询参数
this.queryParams.pageNum = 1;
// 如果关键词为空,清空搜索条件
if (!keyword) {
this.queryParams.studentName = '';
this.queryParams.studentNo = '';
} else {
// 同时按学生姓名和学号搜索
this.queryParams.studentName = keyword;
this.queryParams.studentNo = keyword;
}
// 重新加载列表
this.getList();
},
// 加载更多
loadMore() {
if (this.loadMoreStatus === 'loading' || this.loadMoreStatus === 'nomore') {
return;
}
this.loadMoreStatus = 'loading';
this.queryParams.pageNum += 1;
this.getList();
},
// 刷新列表
handleSuccess() {
this.queryParams.pageNum = 1;
this.getList();
},
// 新增按钮操作
handleAdd() {
uni.navigateTo({
url: '/pages/xskh/wjjlEntry/disciplineRecordForm?mode=add',
});
},
// 查看按钮操作
handleView(row) {
uni.navigateTo({
url: `/pages/xskh/wjjlEntry/disciplineRecordForm?mode=view&id=${row.id}`,
});
},
// 编辑按钮操作
handleEdit(row) {
uni.navigateTo({
url: `/pages/xskh/wjjlEntry/disciplineRecordForm?mode=edit&id=${row.id}`,
});
},
// 删除按钮操作
handleDelete(row) {
uni.showModal({
title: '提示',
content: `确定要删除学生"${row.studentName}"的违纪记录吗?`,
success: async (res) => {
if (res.confirm) {
try {
uni.showLoading({ title: '删除中...' });
await request({
url: `/disciplineRecord/disciplineRecord/${row.id}`,
method: 'DELETE',
});
uni.hideLoading();
uni.showToast({
title: '删除成功',
icon: 'success',
});
this.handleSuccess();
} catch (error) {
uni.hideLoading();
console.error('删除失败:', error);
uni.showToast({
title: '删除失败',
icon: 'error',
});
}
}
},
});
},
},
};
</script>
<style lang="scss" scoped>
.discipline-record-container {
background-color: #f5f6fa;
min-height: 100vh;
}
.search-bar {
padding: 20rpx;
background-color: #fff;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.content-wrapper {
padding: 20rpx;
padding-bottom: 120rpx;
}
.empty-container {
padding: 100rpx 20rpx;
}
.record-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.record-card {
background-color: #fff;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
border-bottom: 1px solid #f0f0f0;
background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
}
.student-info {
flex: 1;
display: flex;
align-items: center;
gap: 8rpx;
}
.student-name {
font-size: 32rpx;
font-weight: bold;
color: #303133;
}
.student-no {
font-size: 26rpx;
color: #909399;
}
.record-status {
margin-left: 16rpx;
}
.card-content {
padding: 24rpx;
}
.info-item {
display: flex;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
}
.label {
font-size: 28rpx;
color: #606266;
min-width: 140rpx;
margin-right: 16rpx;
}
.value {
font-size: 28rpx;
color: #303133;
flex: 1;
word-break: break-all;
}
.value-score {
color: #f56c6c;
font-weight: bold;
}
// 操作按钮
.card-actions {
display: flex;
justify-content: flex-end;
padding: 20rpx;
border-top: 1px solid #f0f0f0;
gap: 16rpx;
}
.action-btn {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
min-width: 120rpx;
border-radius: 20rpx;
::v-deep .u-button {
border-radius: 20rpx;
padding: 16rpx 20rpx;
min-width: 120rpx;
height: auto;
line-height: 1.2;
}
}
.btn-text {
font-size: 24rpx;
margin-left: 8rpx;
font-weight: 500;
color: #ffffff;
}
.load-more {
padding: 20rpx 0;
}
/* 浮动新增按钮 */
.float-add-button {
position: fixed;
right: 40rpx;
bottom: 100rpx;
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background: linear-gradient(135deg, #3c9cff, #0051d9);
box-shadow: 0 8rpx 24rpx rgba(60, 156, 255, 0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
transition: all 0.3s;
&:active {
transform: scale(0.95);
}
}
// uview组件样式调整
::v-deep .u-tag {
margin: 0;
}
::v-deep .u-button {
border-radius: 8rpx;
}
</style>
+308
View File
@@ -0,0 +1,308 @@
// 文件上传工具 - 移动端适配
import { request } from '@/common/request.js';
import { REQUEST_CONFIG } from '@/common/config.js';
/**
* 获取token
*/
function getToken() {
try {
const token = uni.getStorageSync('access_token');
return token ? `Bearer ${token}` : '';
} catch (e) {
console.error('❌ 获取Token失败:', e);
return '';
}
}
/**
* 构建完整的上传URL
*/
function buildUploadUrl(url) {
let fullUrl;
if (REQUEST_CONFIG.baseURL.startsWith('/')) {
// 开发环境,baseURL是'/api',需要使用完整的服务器URL
fullUrl = `${REQUEST_CONFIG.serverUrl}${url}`;
} else {
// 生产环境,直接拼接
fullUrl = REQUEST_CONFIG.baseURL + url;
}
return fullUrl;
}
/**
* 上传单个文件
* @param {string} url - 上传接口URL
* @param {Object} options - 上传选项
* @param {string} options.filePath - 文件路径
* @param {string} options.name - 文件字段名,默认'file'
* @param {Object} options.formData - 额外的表单数据
* @param {Function} options.onProgress - 进度回调
* @returns {Promise}
*/
export function uploadFile(url, options = {}) {
return new Promise((resolve, reject) => {
const { filePath, name = 'file', formData = {}, onProgress } = options;
if (!filePath) {
return reject(new Error('文件路径不能为空'));
}
const token = getToken();
const clientid = uni.getStorageSync('clientid') || REQUEST_CONFIG.clientId;
const fullUrl = buildUploadUrl(url);
console.log('🚀 开始上传文件:', {
url: fullUrl,
filePath,
name,
});
const uploadTask = uni.uploadFile({
url: fullUrl,
filePath,
name,
formData,
header: {
authorization: token,
clientid: clientid,
},
success: (res) => {
console.log('✅ 文件上传成功:', res);
if (res.statusCode === 200) {
try {
const data = JSON.parse(res.data);
if (data.code === 200) {
resolve(data);
} else {
uni.showToast({
title: data.msg || '上传失败',
icon: 'none',
});
reject(new Error(data.msg || '上传失败'));
}
} catch (error) {
console.error('❌ 解析上传响应失败:', error);
reject(new Error('解析响应数据失败'));
}
} else {
reject(new Error(`上传失败: HTTP ${res.statusCode}`));
}
},
fail: (err) => {
console.error('❌ 文件上传失败:', err);
uni.showToast({
title: '上传失败',
icon: 'none',
});
reject(new Error(err.errMsg || '上传失败'));
},
});
// 监听上传进度
if (onProgress && typeof onProgress === 'function') {
uploadTask.onProgressUpdate((res) => {
onProgress(res);
});
}
});
}
/**
* 上传多个文件
* @param {string} url - 上传接口URL
* @param {Array} files - 文件列表
* @param {Object} options - 上传选项
* @returns {Promise<Array>}
*/
export function uploadFiles(url, files, options = {}) {
const uploadPromises = files.map((file, index) => {
return uploadFile(url, {
...options,
filePath: file.tempFilePath || file.path || file.url,
onProgress: (progress) => {
if (options.onProgress) {
options.onProgress({
...progress,
fileIndex: index,
totalFiles: files.length,
});
}
},
});
});
return Promise.all(uploadPromises);
}
/**
* 选择并上传图片
* @param {string} url - 上传接口URL
* @param {Object} options - 选择和上传选项
* @returns {Promise}
*/
export function chooseAndUploadImage(url, options = {}) {
return new Promise((resolve, reject) => {
const {
count = 1,
sizeType = ['compressed'],
sourceType = ['album', 'camera'],
...uploadOptions
} = options;
uni.chooseImage({
count,
sizeType,
sourceType,
success: (chooseRes) => {
console.log('📷 选择图片成功:', chooseRes);
if (count === 1) {
// 单张图片上传
uploadFile(url, {
...uploadOptions,
filePath: chooseRes.tempFilePaths[0],
})
.then(resolve)
.catch(reject);
} else {
// 多张图片上传
uploadFiles(url, chooseRes.tempFiles, uploadOptions)
.then(resolve)
.catch(reject);
}
},
fail: (err) => {
console.error('❌ 选择图片失败:', err);
reject(new Error(err.errMsg || '选择图片失败'));
},
});
});
}
/**
* 选择并上传文件
* @param {string} url - 上传接口URL
* @param {Object} options - 选择和上传选项
* @returns {Promise}
*/
export function chooseAndUploadFile(url, options = {}) {
return new Promise((resolve, reject) => {
const { count = 1, type = 'all', extension = [], ...uploadOptions } = options;
// #ifdef MP-WEIXIN
// 微信小程序使用 wx.chooseMessageFile
uni.chooseMessageFile({
count,
type,
extension,
success: (chooseRes) => {
console.log('📁 选择文件成功:', chooseRes);
if (count === 1) {
uploadFile(url, {
...uploadOptions,
filePath: chooseRes.tempFiles[0].path,
})
.then(resolve)
.catch(reject);
} else {
uploadFiles(url, chooseRes.tempFiles, uploadOptions)
.then(resolve)
.catch(reject);
}
},
fail: (err) => {
console.error('❌ 选择文件失败:', err);
reject(new Error(err.errMsg || '选择文件失败'));
},
});
// #endif
// #ifndef MP-WEIXIN
// H5和其他平台使用普通文件选择
uni.chooseFile({
count,
type,
extension,
success: (chooseRes) => {
console.log('📁 选择文件成功:', chooseRes);
if (count === 1) {
uploadFile(url, {
...uploadOptions,
filePath: chooseRes.tempFilePaths[0],
})
.then(resolve)
.catch(reject);
} else {
uploadFiles(url, chooseRes.tempFiles, uploadOptions)
.then(resolve)
.catch(reject);
}
},
fail: (err) => {
console.error('❌ 选择文件失败:', err);
reject(new Error(err.errMsg || '选择文件失败'));
},
});
// #endif
});
}
/**
* 压缩图片
* @param {string} src - 图片路径
* @param {Object} options - 压缩选项
* @returns {Promise}
*/
export function compressImage(src, options = {}) {
return new Promise((resolve, reject) => {
const { quality = 80, compressedWidth, compressedHeight } = options;
uni.compressImage({
src,
quality,
compressedWidth,
compressedHeight,
success: (res) => {
console.log('✅ 图片压缩成功:', res);
resolve(res.tempFilePath);
},
fail: (err) => {
console.error('❌ 图片压缩失败:', err);
reject(new Error(err.errMsg || '图片压缩失败'));
},
});
});
}
/**
* 获取图片信息
* @param {string} src - 图片路径
* @returns {Promise}
*/
export function getImageInfo(src) {
return new Promise((resolve, reject) => {
uni.getImageInfo({
src,
success: (res) => {
resolve(res);
},
fail: (err) => {
reject(new Error(err.errMsg || '获取图片信息失败'));
},
});
});
}
// 默认导出
export default {
uploadFile,
uploadFiles,
chooseAndUploadImage,
chooseAndUploadFile,
compressImage,
getImageInfo,
};
-59
View File
@@ -1,59 +0,0 @@
org.dromara.system.domain.vo.SysSocialVoToSysSocialMapper
org.dromara.system.domain.vo.SysDictDataVoToSysDictDataMapper
org.dromara.system.domain.SysOssConfigToSysOssConfigVoMapper
org.dromara.system.domain.vo.SysLogininforVoToSysLogininforMapper
org.dromara.system.domain.bo.SysConfigBoToSysConfigMapper
org.dromara.system.domain.SysTenantPackageToSysTenantPackageVoMapper
org.dromara.system.domain.bo.SysPostBoToSysPostMapper
org.dromara.system.domain.vo.SysOssConfigVoToSysOssConfigMapper
org.dromara.system.domain.SysUserToSysUserVoMapper
org.dromara.system.domain.vo.SysDictTypeVoToSysDictTypeMapper
org.dromara.system.domain.SysRegionToSysRegionVoMapper
org.dromara.system.domain.SysDictDataToSysDictDataVoMapper
org.dromara.system.domain.bo.SysMenuBoToSysMenuMapper
org.dromara.system.domain.bo.SysWeChatConfigBoToSysWeChatConfigMapper
org.dromara.system.domain.vo.SysRoleVoToSysRoleMapper
org.dromara.system.domain.SysConfigToSysConfigVoMapper
org.dromara.system.domain.bo.SysOperLogBoToOperLogEventMapper
org.dromara.system.domain.vo.SysClientVoToSysClientMapper
org.dromara.system.domain.bo.SysOssConfigBoToSysOssConfigMapper
org.dromara.system.domain.vo.SysNoticeVoToSysNoticeMapper
org.dromara.system.domain.bo.SysDeptBoToSysDeptMapper
org.dromara.system.domain.bo.SysOssBoToSysOssMapper
org.dromara.system.domain.vo.SysMenuVoToSysMenuMapper
org.dromara.system.domain.vo.SysWeChatConfigVoToSysWeChatConfigMapper
org.dromara.system.domain.SysMenuToSysMenuVoMapper
org.dromara.system.domain.vo.SysTenantVoToSysTenantMapper
org.dromara.system.domain.bo.SysUserBoToSysUserMapper
org.dromara.system.domain.bo.SysRegionBoToSysRegionMapper
org.dromara.system.domain.bo.SysClientBoToSysClientMapper
org.dromara.system.domain.bo.SysDictDataBoToSysDictDataMapper
org.dromara.system.domain.vo.SysUserVoToSysUserMapper
org.dromara.system.domain.SysClientToSysClientVoMapper
org.dromara.system.domain.SysNoticeToSysNoticeVoMapper
org.dromara.system.domain.SysOssToSysOssVoMapper
org.dromara.system.domain.SysOperLogToSysOperLogVoMapper
org.dromara.system.domain.bo.SysLogininforBoToSysLogininforMapper
org.dromara.system.domain.vo.SysDeptVoToSysDeptMapper
org.dromara.system.domain.SysTenantToSysTenantVoMapper
org.dromara.system.domain.vo.SysConfigVoToSysConfigMapper
org.dromara.system.domain.bo.SysSocialBoToSysSocialMapper
org.dromara.system.domain.bo.SysRoleBoToSysRoleMapper
org.dromara.system.domain.vo.SysOssVoToSysOssMapper
org.dromara.system.domain.SysDictTypeToSysDictTypeVoMapper
org.dromara.system.domain.SysRoleToSysRoleVoMapper
org.dromara.system.domain.vo.SysTenantPackageVoToSysTenantPackageMapper
org.dromara.system.domain.bo.SysTenantBoToSysTenantMapper
org.dromara.system.domain.bo.SysTenantPackageBoToSysTenantPackageMapper
org.dromara.system.domain.vo.SysRegionVoToSysRegionMapper
org.dromara.system.domain.SysLogininforToSysLogininforVoMapper
org.dromara.system.domain.vo.SysPostVoToSysPostMapper
org.dromara.system.domain.SysSocialToSysSocialVoMapper
org.dromara.system.domain.bo.SysDictTypeBoToSysDictTypeMapper
org.dromara.common.log.event.OperLogEventToSysOperLogBoMapper
org.dromara.system.domain.SysWeChatConfigToSysWeChatConfigVoMapper
org.dromara.system.domain.bo.SysOperLogBoToSysOperLogMapper
org.dromara.system.domain.vo.SysOperLogVoToSysOperLogMapper
org.dromara.system.domain.SysDeptToSysDeptVoMapper
org.dromara.system.domain.bo.SysNoticeBoToSysNoticeMapper
org.dromara.system.domain.SysPostToSysPostVoMapper
+47
View File
@@ -0,0 +1,47 @@
import request from '@/utils/request';
import type { AxiosPromise } from 'axios';
import type { CollegeVo, CollegeQuery } from './types';
// 导出类型
export type { CollegeVo, MajorVo, CollegeQuery } from './types';
/**
* 查询学院列表(用于二级学院,parent_id = 101)
*/
export function list(query?: any): AxiosPromise<CollegeVo[]> {
return request({
url: '/xg/college/list',
method: 'get',
params: query
});
}
/**
* 获取学院和专业树形结构
*/
export function collegeTree(): AxiosPromise<CollegeVo[]> {
return request({
url: '/xg/college/tree',
method: 'get'
});
}
/**
* 根据学院ID获取专业列表
*/
export function listMajorsByCollegeId(collegeId: string | number): AxiosPromise<any[]> {
return request({
url: `/xg/college/majors/${collegeId}`,
method: 'get'
});
}
/**
* 查询学院详情
*/
export function getCollege(deptId: string | number): AxiosPromise<CollegeVo> {
return request({
url: `/xg/college/${deptId}`,
method: 'get'
});
}
+37
View File
@@ -0,0 +1,37 @@
/**
* 学院信息对象
*/
export interface CollegeVo {
deptId: number | string;
deptName: string;
parentId?: number | string;
orderNum?: number;
leader?: string;
phone?: string;
email?: string;
status?: string;
children?: CollegeVo[];
}
/**
* 专业信息对象
*/
export interface MajorVo {
deptId: number | string;
deptName: string;
parentId: number | string;
orderNum?: number;
leader?: string;
phone?: string;
email?: string;
status?: string;
}
/**
* 学院查询参数
*/
export interface CollegeQuery {
deptName?: string;
status?: string;
parentId?: number | string;
}
+117
View File
@@ -0,0 +1,117 @@
import request from '@/utils/request';
/**
* 二级学院分管领导配置对象
*/
export interface CollegeLeaderBo {
id?: number | string;
collegeId?: number | string;
collegeName?: string;
leaderId?: number | string;
leaderName?: string;
remark?: string;
}
/**
* 二级学院分管领导配置视图对象
*/
export interface CollegeLeaderVo {
id: number | string;
collegeId: number | string;
collegeName: string;
leaderId: number | string;
leaderName: string;
remark?: string;
createTime?: string;
updateTime?: string;
}
/**
* 查询二级学院分管领导配置列表
* @param query 查询参数
* @returns 返回二级学院分管领导配置列表
*/
export function listCollegeLeader(query: any) {
return request({
url: '/xg/ejxyfgld/list',
method: 'get',
params: query
});
}
/**
* 根据学院ID查询分管领导列表
* @param collegeId 学院ID
* @returns 返回分管领导列表
*/
export function listCollegeLeaderByCollege(collegeId: number | string) {
return request({
url: `/xg/ejxyfgld/listByCollege/${collegeId}`,
method: 'get'
});
}
/**
* 获取二级学院分管领导配置详情
* @param id 主键ID
* @returns 返回二级学院分管领导配置详情
*/
export function getCollegeLeader(id: number | string) {
return request({
url: `/xg/ejxyfgld/${id}`,
method: 'get'
});
}
/**
* 新增二级学院分管领导配置
* @param data 二级学院分管领导配置信息
* @returns 返回添加结果
*/
export function addCollegeLeader(data: CollegeLeaderBo) {
return request({
url: '/xg/ejxyfgld',
method: 'post',
data: data
});
}
/**
* 修改二级学院分管领导配置
* @param data 二级学院分管领导配置信息
* @returns 返回修改结果
*/
export function updateCollegeLeader(data: CollegeLeaderBo) {
return request({
url: '/xg/ejxyfgld',
method: 'put',
data: data
});
}
/**
* 删除二级学院分管领导配置
* @param ids 主键ID数组
* @returns 返回删除结果
*/
export function delCollegeLeader(ids: number | string | (number | string)[]) {
return request({
url: `/xg/ejxyfgld/${ids instanceof Array ? ids.join(',') : ids}`,
method: 'delete'
});
}
/**
* 导出二级学院分管领导配置
* @param query 查询参数
* @returns 返回导出结果
*/
export function exportCollegeLeader(query: any) {
return request({
url: '/xg/ejxyfgld/export',
method: 'post',
params: query,
responseType: 'blob'
});
}
+269
View File
@@ -0,0 +1,269 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
/**
* 学生动向信息管理(宿管员)API
* 这些接口专门用于学生动向Tab页,会自动根据宿管员权限过滤数据
*/
// ==================== 调宿退宿申请相关接口 ====================
export interface DormApplicationQuery {
pageNum?: number;
pageSize?: number;
studentName?: string;
studentNo?: string;
collegeName?: string;
majorName?: string;
className?: string;
applicationType?: string;
applicationStatus?: string;
applyTime?: string;
params?: any;
}
export interface DormApplicationVO {
id: string | number;
applicationName: string;
applicationType: string;
applicationStatus: string;
studentId: string | number;
studentNo: string;
studentName: string;
studentPhone: string;
collegeId: string | number;
collegeName: string;
majorId: string | number;
majorName: string;
classId: string | number;
className: string;
currentCampusId: string | number;
currentCampusName: string;
currentBuildingId: string | number;
currentBuildingName: string;
currentRoomId: string | number;
currentRoomNo: string;
currentBedId: string | number;
currentBedNo: string;
targetCampusId?: string | number;
targetCampusName?: string;
targetBuildingId?: string | number;
targetBuildingName?: string;
targetRoomId?: string | number;
targetRoomNo?: string;
targetBedId?: string | number;
targetBedNo?: string;
reasonDetail: string;
applyTime: string;
attachmentOssIds?: string;
remark?: string;
createTime: string;
updateTime: string;
}
/**
* 查询宿管员权限内的学生调宿退宿申请列表
*/
export function listDormApplication(query?: DormApplicationQuery): AxiosPromise<any> {
return request({
url: '/studentDynamics/dormApplicationList',
method: 'get',
params: query
});
}
/**
* 获取调宿退宿申请详细信息
*/
export function getDormApplication(id: string | number): AxiosPromise<DormApplicationVO> {
return request({
url: '/studentDynamics/dormApplication/' + id,
method: 'get'
});
}
/**
* 获取调宿退宿申请详细信息(别名)
*/
export const getDormApplicationInfo = getDormApplication;
/**
* 修改调宿退宿申请信息
*/
export function updateDormApplication(data: DormApplicationVO): AxiosPromise<any> {
return request({
url: '/studentDynamics/dormApplication',
method: 'put',
data: data
});
}
/**
* 删除调宿退宿申请
*/
export function delDormApplication(id: string | number | Array<string | number>): AxiosPromise<any> {
return request({
url: '/studentDynamics/dormApplication/' + id,
method: 'delete'
});
}
// ==================== 假期留宿相关接口 ====================
export interface HolidayStayQuery {
pageNum?: number;
pageSize?: number;
studentName?: string;
studentNo?: string;
collegeName?: string;
majorName?: string;
className?: string;
holidayType?: string;
applyStartDate?: string;
applyEndDate?: string;
status?: string;
params?: any;
}
export interface HolidayStayVO {
id: string | number;
studentId: string | number;
studentName: string;
studentNo: string;
classId: string | number;
className: string;
majorId: string | number;
majorName: string;
collegeId: string | number;
collegeName: string;
campusId: string | number;
campusName: string;
areaId?: string | number;
areaName?: string;
buildingId: string | number;
buildingName: string;
roomId: string | number;
roomName: string;
bedId: string | number;
bedName: string;
holidayType: string;
applyStartDate: string;
applyEndDate: string;
stayReason: string;
stayReasonType: string;
studentPhone: string;
emergencyContact: string;
emergencyPhone: string;
parentConsent: string;
attachmentOssIds?: string;
remark?: string;
createTime: string;
updateTime: string;
}
/**
* 查询宿管员权限内的学生假期留宿信息列表
*/
export function listHolidayStay(query?: HolidayStayQuery): AxiosPromise<any> {
return request({
url: '/studentDynamics/holidayStayList',
method: 'get',
params: query
});
}
/**
* 获取假期留宿详细信息
*/
export function getHolidayStay(id: string | number): AxiosPromise<HolidayStayVO> {
return request({
url: '/studentDynamics/holidayStay/' + id,
method: 'get'
});
}
/**
* 获取假期留宿详细信息(别名)
*/
export const getHolidayStayInfo = getHolidayStay;
// ==================== 学生请假相关接口 ====================
export interface StudentLeaveQuery {
pageNum?: number;
pageSize?: number;
studentName?: string;
studentNo?: string;
collegeName?: string;
majorName?: string;
className?: string;
leaveTypeName?: string;
approvalStatus?: string;
startTime?: string;
endTime?: string;
params?: any;
}
export interface StudentLeaveVO {
id: string | number;
studentId: string | number;
studentName: string;
studentNo: string;
classId: string | number;
className: string;
majorId: string | number;
majorName: string;
collegeId: string | number;
collegeName: string;
leaveTypeCode: string;
leaveTypeName: string;
startTime: string;
endTime: string;
leaveDuration: number;
isLeaveSchool: string;
needCancel: string;
emergencyContact: string;
emergencyPhone: string;
leaveReason: string;
leaveLocation?: string;
attachmentOssIds?: string;
approvalStatus: string;
approvalOpinion?: string;
approvalTime?: string;
approvalBy?: string | number;
approvalByName?: string;
cancelStatus?: string;
cancelTime?: string;
cancelLocation?: string;
cancelPhotoOssIds?: string;
cancelRemark?: string;
createTime: string;
updateTime: string;
}
/**
* 查询宿管员权限内的学生请假信息列表
*/
export function listStudentLeave(query?: StudentLeaveQuery): AxiosPromise<any> {
return request({
url: '/studentDynamics/studentLeaveList',
method: 'get',
params: query
});
}
/**
* 获取学生请假详细信息
*/
export function getStudentLeave(id: string | number): AxiosPromise<StudentLeaveVO> {
return request({
url: '/studentDynamics/studentLeave/' + id,
method: 'get'
});
}
/**
* 获取学生请假详细信息(别名)
*/
export const getStudentLeaveInfo = getStudentLeave;
+14 -8
View File
@@ -6,16 +6,17 @@ import { AxiosPromise } from 'axios';
*/
export interface XgSysUser {
userId: number | string;
userName: string;
userName?: string;
nickName: string;
email: string;
email?: string;
phonenumber: string;
sex: string;
status: string;
createTime: string;
updateTime: string;
deptId: number;
deptName: string;
sex?: string;
status?: string;
accountType?: string;
createTime?: string;
updateTime?: string;
deptId?: number;
deptName?: string;
}
/**
@@ -27,3 +28,8 @@ export function getActiveUserList(): AxiosPromise<XgSysUser[]> {
method: 'get'
});
}
/**
* 查询有效用户列表(别名,用于兼容)
*/
export const activeList = getActiveUserList;
@@ -106,3 +106,14 @@ export const getCollegeLeaveApprovers = (studentId: string | number): AxiosPromi
method: 'get'
});
};
/**
* 根据学生ID获取二级学院分管领导ID列表
* @param studentId 学生ID
*/
export const getCollegeLeaders = (studentId: string | number): AxiosPromise<{ data: number[] }> => {
return request({
url: `/studentMsg/studentMsg/getCollegeLeaders/${studentId}`,
method: 'get'
});
};
@@ -33,6 +33,21 @@
</template>
</el-table-column>
<el-table-column prop="message" label="审批意见" :show-overflow-tooltip="true" sortable align="center"></el-table-column>
<el-table-column prop="signature" label="手写签名" width="150" align="center">
<template #default="scope">
<el-popover v-if="scope.row.signature" placement="left" :width="350" trigger="hover">
<template #reference>
<div class="signature-thumbnail">
<img :src="scope.row.signature" alt="签名" class="signature-thumb-image" />
</div>
</template>
<div class="signature-preview-large">
<img :src="scope.row.signature" alt="签名" class="signature-large-image" />
</div>
</el-popover>
<span v-else style="color: #909399; font-size: 12px">未签名</span>
</template>
</el-table-column>
<el-table-column prop="createTime" label="开始时间" width="160" :show-overflow-tooltip="true" sortable align="center"></el-table-column>
<el-table-column prop="updateTime" label="结束时间" width="160" :show-overflow-tooltip="true" sortable align="center"></el-table-column>
<el-table-column
@@ -68,6 +83,7 @@
</div>
</template>
<script setup lang="ts">
import { ref, getCurrentInstance, toRefs, type ComponentInternalInstance } from 'vue';
import { flowHisTaskList } from '@/api/workflow/instance';
import { propTypes } from '@/utils/propTypes';
import { listByIds } from '@/api/system/oss';
@@ -198,4 +214,48 @@ defineExpose({
min-height: calc(100vh - 170px) !important;
}
}
// 列表中的签名缩略图容器
.signature-thumbnail {
display: flex;
justify-content: center;
align-items: center;
padding: 4px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
transform: scale(1.05);
}
.signature-thumb-image {
max-width: 120px;
max-height: 50px;
border: 1px solid #e4e7ed;
border-radius: 4px;
background: #ffffff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
object-fit: contain;
}
}
// 鼠标悬停时的大图预览
.signature-preview-large {
display: flex;
justify-content: center;
align-items: center;
padding: 16px;
background: #f5f7fa;
border-radius: 6px;
.signature-large-image {
max-width: 100%;
max-height: 200px;
border: 2px solid #dcdfe6;
border-radius: 4px;
background: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
object-fit: contain;
}
}
</style>
@@ -31,6 +31,9 @@
<el-form-item v-if="task.flowStatus === 'waiting'" label="审批意见">
<el-input v-model="form.message" type="textarea" resize="none" />
</el-form-item>
<el-form-item v-if="task.flowStatus === 'waiting'" label="">
<SignaturePad v-model="form.signature" :width="480" :height="180" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
@@ -107,6 +110,9 @@
<el-form-item label="审批意见">
<el-input v-model="backForm.message" type="textarea" resize="none" />
</el-form-item>
<el-form-item label="">
<SignaturePad v-model="backForm.signature" :width="450" :height="180" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer" style="float: right; padding-bottom: 20px">
@@ -147,6 +153,7 @@ import {
getNextNodeList
} from '@/api/workflow/task';
import UserSelect from '@/components/UserSelect';
import SignaturePad from '@/components/SignaturePad/index.vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { UserVO } from '@/api/system/user/types';
@@ -228,6 +235,7 @@ const deleteSignatureVisible = ref(false);
const form = ref<Record<string, any>>({
taskId: undefined,
message: undefined,
signature: undefined,
assigneeMap: {},
variables: {},
messageType: ['1'],
@@ -237,6 +245,7 @@ const backForm = ref<Record<string, any>>({
taskId: undefined,
nodeCode: undefined,
message: undefined,
signature: undefined,
variables: {},
messageType: ['1']
});
@@ -248,6 +257,7 @@ const openDialog = async (id?: string) => {
form.value.fileId = undefined;
taskId.value = id;
form.value.message = undefined;
form.value.signature = undefined;
dialog.visible = true;
loading.value = true;
buttonDisabled.value = true;
@@ -322,6 +332,7 @@ const handleCompleteTask = async () => {
const handleBackProcessOpen = async () => {
backForm.value = {};
backForm.value.messageType = ['1'];
backForm.value.signature = undefined;
backVisible.value = true;
backLoading.value = true;
backButtonDisabled.value = true;
+278
View File
@@ -0,0 +1,278 @@
<template>
<div class="signature-pad-wrapper">
<div class="signature-header">
<span class="signature-title">手写签名</span>
<el-button v-if="hasSignature" size="small" type="danger" :icon="Delete" @click="handleClear">清空</el-button>
</div>
<div class="signature-canvas-container">
<canvas
ref="canvasRef"
class="signature-canvas"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"
@mouseup="handleMouseUp"
@mouseleave="handleMouseUp"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
></canvas>
<div v-if="!hasSignature" class="signature-placeholder">请在此处签名</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
import { Delete } from '@element-plus/icons-vue';
interface Props {
modelValue?: string; // Base64签名数据
width?: number;
height?: number;
lineWidth?: number;
lineColor?: string;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: '',
width: 500,
height: 200,
lineWidth: 2,
lineColor: '#000000'
});
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void;
}>();
const canvasRef = ref<HTMLCanvasElement>();
const hasSignature = ref(false);
const isDrawing = ref(false);
let ctx: CanvasRenderingContext2D | null = null;
let resizeObserver: ResizeObserver | null = null;
onMounted(() => {
initCanvas();
// 如果有初始值,加载到画布上
if (props.modelValue) {
loadSignature(props.modelValue);
}
// 监听容器大小变化
if (canvasRef.value?.parentElement) {
resizeObserver = new ResizeObserver(() => {
handleResize();
});
resizeObserver.observe(canvasRef.value.parentElement);
}
});
onUnmounted(() => {
if (resizeObserver) {
resizeObserver.disconnect();
}
});
// 监听外部值变化
watch(
() => props.modelValue,
(newVal) => {
if (newVal && !hasSignature.value) {
loadSignature(newVal);
}
}
);
const initCanvas = () => {
if (!canvasRef.value) return;
const canvas = canvasRef.value;
const container = canvas.parentElement;
// 获取容器的实际宽度
const containerWidth = container?.clientWidth || props.width;
// 设置画布的物理尺寸(实际像素)
canvas.width = containerWidth;
canvas.height = props.height;
// 同时设置CSS样式,确保显示尺寸与物理尺寸一致
canvas.style.width = `${containerWidth}px`;
canvas.style.height = `${props.height}px`;
ctx = canvas.getContext('2d');
if (ctx) {
ctx.strokeStyle = props.lineColor;
ctx.lineWidth = props.lineWidth;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
}
};
// 处理容器大小变化
const handleResize = () => {
if (!canvasRef.value) return;
// 保存当前签名
const currentSignature = hasSignature.value ? canvasRef.value.toDataURL('image/png') : '';
// 重新初始化画布
initCanvas();
// 恢复签名
if (currentSignature && hasSignature.value) {
nextTick(() => {
loadSignature(currentSignature);
});
}
};
const startDrawing = (x: number, y: number) => {
if (!ctx || !canvasRef.value) return;
isDrawing.value = true;
const rect = canvasRef.value.getBoundingClientRect();
ctx.beginPath();
ctx.moveTo(x - rect.left, y - rect.top);
};
const draw = (x: number, y: number) => {
if (!isDrawing.value || !ctx || !canvasRef.value) return;
const rect = canvasRef.value.getBoundingClientRect();
ctx.lineTo(x - rect.left, y - rect.top);
ctx.stroke();
hasSignature.value = true;
};
const stopDrawing = () => {
if (!isDrawing.value) return;
isDrawing.value = false;
if (ctx) {
ctx.closePath();
}
// 导出签名数据
if (hasSignature.value && canvasRef.value) {
const signatureData = canvasRef.value.toDataURL('image/png');
emit('update:modelValue', signatureData);
}
};
// 鼠标事件处理
const handleMouseDown = (e: MouseEvent) => {
startDrawing(e.clientX, e.clientY);
};
const handleMouseMove = (e: MouseEvent) => {
draw(e.clientX, e.clientY);
};
const handleMouseUp = () => {
stopDrawing();
};
// 触摸事件处理(移动端支持)
const handleTouchStart = (e: TouchEvent) => {
e.preventDefault();
const touch = e.touches[0];
startDrawing(touch.clientX, touch.clientY);
};
const handleTouchMove = (e: TouchEvent) => {
e.preventDefault();
const touch = e.touches[0];
draw(touch.clientX, touch.clientY);
};
const handleTouchEnd = (e: TouchEvent) => {
e.preventDefault();
stopDrawing();
};
// 清空签名
const handleClear = () => {
if (!canvasRef.value || !ctx) return;
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
hasSignature.value = false;
emit('update:modelValue', '');
};
// 加载签名
const loadSignature = (base64Data: string) => {
if (!canvasRef.value || !ctx) return;
const img = new Image();
img.onload = () => {
ctx!.clearRect(0, 0, canvasRef.value!.width, canvasRef.value!.height);
ctx!.drawImage(img, 0, 0);
hasSignature.value = true;
};
img.src = base64Data;
};
// 获取签名数据
const getSignature = (): string => {
if (!canvasRef.value || !hasSignature.value) return '';
return canvasRef.value.toDataURL('image/png');
};
// 对外暴露方法
defineExpose({
clear: handleClear,
getSignature,
hasSignature: () => hasSignature.value
});
</script>
<style scoped lang="scss">
.signature-pad-wrapper {
width: 100%;
.signature-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.signature-title {
font-size: 14px;
color: #606266;
font-weight: 500;
}
}
.signature-canvas-container {
position: relative;
width: 100%;
border: 1px solid #dcdfe6;
border-radius: 4px;
background-color: #ffffff;
overflow: hidden;
&:hover {
border-color: #c0c4cc;
}
.signature-canvas {
display: block;
width: 100%;
cursor: crosshair;
touch-action: none;
}
.signature-placeholder {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #c0c4cc;
font-size: 14px;
pointer-events: none;
user-select: none;
}
}
}
</style>
+37 -25
View File
@@ -47,7 +47,7 @@
<img :src="menu.logoUrl" :alt="menu.title" class="menu-logo" />
</div>
<span class="action-title" :class="{ 'disabled': !menu.available }">{{ menu.title }}</span>
<span class="action-desc" :class="{ 'disabled': !menu.available }">{{ menu.description }}</span>
<!-- <span class="action-desc" :class="{ 'disabled': !menu.available }">{{ menu.description }}</span> -->
<!-- 移除层级路径显示 -->
<el-tag v-if="!menu.available" type="info" size="small" class="status-tag">开发中</el-tag>
</div>
@@ -946,24 +946,36 @@ onMounted(async () => {
}
.menu-card-wrapper {
flex: 0 0 calc(20% - 16px); // 每行5个,减去间隔
min-width: 180px;
flex: 0 0 calc((100% - 140px) / 8); // 每行8个,精确计算:(100% - 7个间隔*20px) / 8
min-width: 100px;
margin-bottom: 20px;
@media (max-width: 1600px) {
flex: 0 0 calc((100% - 120px) / 7); // 中大屏幕每行7个
}
@media (max-width: 1400px) {
flex: 0 0 calc(25% - 15px); // 中等屏幕每行4个
flex: 0 0 calc((100% - 100px) / 6); // 中等屏幕每行6个
}
@media (max-width: 1200px) {
flex: 0 0 calc(33.333% - 14px); // 小屏幕每行3个
flex: 0 0 calc((100% - 80px) / 5); // 小屏幕每行5个
}
@media (max-width: 992px) {
flex: 0 0 calc((100% - 60px) / 4); // 平板每行4个
}
@media (max-width: 768px) {
flex: 0 0 calc(50% - 10px); // 移动端每行2个
flex: 0 0 calc((100% - 40px) / 3); // 移动端每行3个
}
@media (max-width: 600px) {
flex: 0 0 calc((100% - 20px) / 2); // 小手机每行2个
}
@media (max-width: 480px) {
flex: 0 0 100%; // 超小屏幕每行1个
flex: 0 0 calc(100% - 20px); // 超小屏幕每行1个
}
}
@@ -972,7 +984,7 @@ onMounted(async () => {
border: none;
transition: all 0.3s ease;
cursor: pointer;
height: 140px;
height: 110px;
display: flex;
align-items: center;
justify-content: center;
@@ -995,7 +1007,7 @@ onMounted(async () => {
.menu-action {
text-align: center;
padding: 15px 10px;
padding: 10px 8px;
width: 100%;
}
@@ -1011,15 +1023,15 @@ onMounted(async () => {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 12px;
height: 48px;
margin-bottom: 8px;
height: 36px;
}
.menu-logo {
width: 48px;
height: 48px;
width: 36px;
height: 36px;
object-fit: contain;
border-radius: 8px;
border-radius: 6px;
transition: all 0.3s ease;
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.1));
@@ -1041,16 +1053,16 @@ onMounted(async () => {
}
.action-title {
font-size: 15px;
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 6px;
margin-bottom: 4px;
display: block;
line-height: 1.3;
}
.action-desc {
font-size: 12px;
font-size: 11px;
color: #666;
display: block;
line-height: 1.4;
@@ -1140,29 +1152,29 @@ onMounted(async () => {
}
.menu-action-card {
height: 120px;
height: 100px;
}
.menu-action {
padding: 10px 5px;
padding: 8px 5px;
}
.menu-logo-wrapper {
height: 36px;
margin-bottom: 8px;
height: 30px;
margin-bottom: 6px;
}
.menu-logo {
width: 36px;
height: 36px;
width: 30px;
height: 30px;
}
.action-title {
font-size: 14px;
font-size: 13px;
}
.action-desc {
font-size: 11px;
font-size: 10px;
}
.loading-state {
+322
View File
@@ -0,0 +1,322 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="100px" :inline="true">
<el-form-item label="学院名称" prop="collegeName">
<el-input v-model="queryParams.collegeName" placeholder="请输入学院名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="领导姓名" prop="leaderName">
<el-input v-model="queryParams.leaderName" placeholder="请输入领导姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['xg:ejxyfgld:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['xg:ejxyfgld:remove']"
>删除</el-button
>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['xg:ejxyfgld:export']">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="collegeLeaderList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="学院名称" align="center" prop="collegeName" />
<el-table-column label="领导姓名" align="center" prop="leaderName" />
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
<el-table-column label="创建时间" align="center" prop="createTime" width="180" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="180">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['xg:ejxyfgld:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['xg:ejxyfgld:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改二级学院分管领导对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="600px" append-to-body>
<el-form ref="collegeLeaderFormRef" :model="form" :rules="rules" label-width="120px">
<el-form-item label="二级学院" prop="collegeId">
<el-select v-model="form.collegeId" placeholder="请选择二级学院" clearable filterable style="width: 100%" @change="handleCollegeChange">
<el-option v-for="college in collegeList" :key="college.deptId" :label="college.deptName" :value="college.deptId" />
</el-select>
</el-form-item>
<el-form-item label="分管领导" prop="leaderId">
<el-select v-model="form.leaderId" placeholder="请选择分管领导" clearable filterable style="width: 100%" @change="handleLeaderChange">
<el-option v-for="user in teacherList" :key="user.userId" :label="`${user.nickName}(${user.userName})`" :value="user.userId" />
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注信息" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="CollegeLeader" lang="ts">
import { ref, reactive, toRefs, getCurrentInstance } from 'vue';
import type { FormInstance } from 'element-plus';
import {
listCollegeLeader,
getCollegeLeader,
addCollegeLeader,
updateCollegeLeader,
delCollegeLeader,
exportCollegeLeader,
type CollegeLeaderBo,
type CollegeLeaderVo
} from '@/api/xg/ejxyfgld';
import { list as listCollege } from '@/api/xg/college';
import { activeList as listTeacher } from '@/api/xg/sysUser';
interface ComponentInternalInstance {
proxy?: any;
}
interface DialogOption {
visible: boolean;
title: string;
}
interface QueryParams {
pageNum: number;
pageSize: number;
collegeName?: string;
leaderName?: string;
}
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const collegeLeaderList = ref<CollegeLeaderVo[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
// 学院列表和教师列表
const collegeList = ref<any[]>([]);
const teacherList = ref<any[]>([]);
const queryFormRef = ref<FormInstance>();
const collegeLeaderFormRef = ref<FormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const initFormData: CollegeLeaderBo = {
id: undefined,
collegeId: undefined,
collegeName: undefined,
leaderId: undefined,
leaderName: undefined,
remark: undefined
};
const data = reactive({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
collegeName: undefined,
leaderName: undefined
} as QueryParams,
rules: {
collegeId: [{ required: true, message: '二级学院不能为空', trigger: 'change' }],
leaderId: [{ required: true, message: '分管领导不能为空', trigger: 'change' }]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询二级学院分管领导列表 */
const getList = async () => {
loading.value = true;
try {
const res = await listCollegeLeader(queryParams.value);
collegeLeaderList.value = res.rows;
total.value = res.total;
} finally {
loading.value = false;
}
};
/** 加载学院列表 */
const loadCollegeList = async () => {
try {
const res = await listCollege();
collegeList.value = res.data || [];
} catch (error) {
console.error('加载学院列表失败:', error);
}
};
/** 加载教师列表 */
const loadTeacherList = async () => {
try {
const res = await listTeacher();
// 后端已经过滤出教职工(account_type = 1),直接使用
teacherList.value = res.data || [];
console.log('加载教师列表成功,共', teacherList.value.length, '条数据');
} catch (error) {
console.error('加载教师列表失败:', error);
}
};
/** 学院选择变更 */
const handleCollegeChange = (val: any) => {
const college = collegeList.value.find((c: any) => c.deptId === val);
if (college) {
form.value.collegeName = college.deptName;
}
};
/** 领导选择变更 */
const handleLeaderChange = (val: any) => {
const teacher = teacherList.value.find((t: any) => t.userId === val);
if (teacher) {
form.value.leaderName = teacher.nickName;
}
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
collegeLeaderFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: CollegeLeaderVo[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length !== 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = async () => {
reset();
await loadCollegeList();
await loadTeacherList();
dialog.visible = true;
dialog.title = '添加二级学院分管领导';
};
/** 修改按钮操作 */
const handleUpdate = async (row?: CollegeLeaderVo) => {
reset();
await loadCollegeList();
await loadTeacherList();
const id = row?.id || ids.value[0];
const res = await getCollegeLeader(id);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = '修改二级学院分管领导';
};
/** 提交按钮 */
const submitForm = () => {
collegeLeaderFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
try {
if (form.value.id) {
await updateCollegeLeader(form.value);
proxy?.$modal.msgSuccess('修改成功');
} else {
await addCollegeLeader(form.value);
proxy?.$modal.msgSuccess('新增成功');
}
dialog.visible = false;
await getList();
} catch (error) {
console.error('提交失败:', error);
} finally {
buttonLoading.value = false;
}
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: CollegeLeaderVo) => {
const deleteIds = row?.id ? [row.id] : ids.value;
await proxy?.$modal.confirm('是否确认删除选中的配置?');
try {
await delCollegeLeader(deleteIds);
await getList();
proxy?.$modal.msgSuccess('删除成功');
} catch (error) {
console.error('删除失败:', error);
}
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.$download(
'xg/ejxyfgld/export',
{
...queryParams.value
},
`二级学院分管领导_${new Date().getTime()}.xlsx`
);
};
// 初始化
getList();
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,256 @@
<template>
<el-dialog :title="dialogTitle" v-model="dialogOpen" width="800px" append-to-body @close="cancel">
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" v-loading="loading">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="申请名称:">
<span>{{ form.applicationName }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="申请类型:">
<dict-tag :options="dorm_application_type" :value="form.applicationType" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="学生姓名:">
<span>{{ form.studentName }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学号:">
<span>{{ form.studentNo }}</span>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="学院:">
<span>{{ form.collegeName }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="专业:">
<span>{{ form.majorName }}</span>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="班级:">
<span>{{ form.className }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话:">
<span>{{ form.studentPhone }}</span>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="当前宿舍:">
<span>{{ form.currentCampusName }} {{ form.currentBuildingName }} {{ form.currentRoomNo }} {{ form.currentBedNo }}</span>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20" v-if="form.applicationType === '1'">
<el-col :span="24">
<el-form-item label="目标宿舍:">
<span>{{ form.targetCampusName }} {{ form.targetBuildingName }} {{ form.targetRoomNo }} {{ form.targetBedNo }}</span>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="申请时间:">
<span>{{ form.applyTime }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="审批状态:">
<dict-tag :options="student_leave_approval_status" :value="form.applicationStatus" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="申请原因:" prop="reasonDetail">
<el-input
v-if="isEditable"
v-model="form.reasonDetail"
type="textarea"
:rows="3"
placeholder="请输入申请原因"
maxlength="500"
show-word-limit
/>
<span v-else>{{ form.reasonDetail || '无' }}</span>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="备注信息:" prop="remark">
<el-input
v-if="isEditable"
v-model="form.remark"
type="textarea"
:rows="2"
placeholder="请输入备注信息"
maxlength="200"
show-word-limit
/>
<span v-else>{{ form.remark || '无' }}</span>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel">{{ isEditable ? '取消' : '关闭' }}</el-button>
<el-button v-if="isEditable" type="primary" @click="submitForm" :loading="submitLoading">保存</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="DormApplicationDetail">
import { getCurrentInstance, ref, toRefs, computed, type ComponentInternalInstance } from 'vue';
import { ElMessage } from 'element-plus';
import { getDormApplicationInfo, updateDormApplication, type DormApplicationVO } from '@/api/xg/studentDynamics';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { dorm_application_type, student_leave_approval_status } = toRefs(
proxy?.useDict('dorm_application_type', 'student_leave_approval_status') || {}
);
const emit = defineEmits(['refresh']);
// 弹窗状态
const dialogOpen = ref(false);
const loading = ref(false);
const submitLoading = ref(false);
// 表单数据
const form = ref<DormApplicationVO>({} as DormApplicationVO);
// 表单引用
const formRef = ref();
// 表单校验规则
const rules = {
reasonDetail: [{ required: true, message: '请输入申请原因', trigger: 'blur' }]
};
/**
* 是否可编辑
* 草稿、已撤销、已退回状态可编辑
*/
const isEditable = computed(() => {
return ['draft', 'cancel', 'back'].includes(form.value.applicationStatus || '');
});
/**
* 弹窗标题
*/
const dialogTitle = computed(() => {
return isEditable.value ? '修改调宿/退宿申请' : '调宿/退宿申请详情';
});
/**
* 打开弹窗
*/
const openDialog = (id: number) => {
reset();
getDetail(id);
dialogOpen.value = true;
};
/**
* 获取详情数据
*/
const getDetail = async (id: number) => {
loading.value = true;
try {
const response = await getDormApplicationInfo(id);
form.value = response.data;
} catch (error) {
console.error('获取调宿退宿申请详情失败:', error);
ElMessage.error('获取详情失败');
} finally {
loading.value = false;
}
};
/**
* 重置表单
*/
const reset = () => {
form.value = {} as DormApplicationVO;
formRef.value?.resetFields();
};
/**
* 提交表单
*/
const submitForm = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return;
submitLoading.value = true;
try {
await updateDormApplication(form.value);
ElMessage.success('保存成功');
dialogOpen.value = false;
emit('refresh');
} catch (error) {
console.error('保存失败:', error);
ElMessage.error('保存失败');
} finally {
submitLoading.value = false;
}
});
};
/**
* 取消操作
*/
const cancel = () => {
dialogOpen.value = false;
reset();
};
// 暴露方法
defineExpose({
open: openDialog
});
</script>
<style scoped>
.dialog-footer {
text-align: right;
}
:deep(.el-form-item__label) {
font-weight: 600;
color: #606266;
}
:deep(.el-form-item__content) {
color: #303133;
}
</style>
@@ -0,0 +1,258 @@
<template>
<div class="page-container">
<!-- 搜索表单 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" style="margin-bottom: 10px">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="学生姓名" prop="studentName">
<el-input v-model="queryParams.studentName" placeholder="请输入学生姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学号" prop="studentNo">
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学院名称" prop="collegeName">
<el-input v-model="queryParams.collegeName" placeholder="请输入学院名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="专业名称" prop="majorName">
<el-input v-model="queryParams.majorName" placeholder="请输入专业名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="班级名称" prop="className">
<el-input v-model="queryParams.className" placeholder="请输入班级名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="申请类型" prop="applicationType">
<el-select v-model="queryParams.applicationType" placeholder="请选择申请类型" clearable style="width: 100%">
<el-option v-for="dict in dorm_application_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="申请时间" prop="applyTime">
<el-date-picker
clearable
v-model="queryParams.applyTime"
type="date"
style="width: 100%"
value-format="YYYY-MM-DD"
placeholder="请选择申请时间"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="审批状态" prop="applicationStatus">
<el-select v-model="queryParams.applicationStatus" placeholder="请选择审批状态" clearable style="width: 100%">
<el-option v-for="dict in student_leave_approval_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 数据表格 -->
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<span class="card-title">调宿/退宿申请列表</span>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" :data="dataList" @selection-change="handleSelectionChange">
<el-table-column label="学生姓名" align="center" prop="studentName" />
<el-table-column label="学号" align="center" prop="studentNo" />
<el-table-column label="学院" align="center" prop="collegeName" show-overflow-tooltip />
<el-table-column label="专业" align="center" prop="majorName" show-overflow-tooltip />
<el-table-column label="班级" align="center" prop="className" show-overflow-tooltip />
<el-table-column label="申请类型" align="center">
<template #default="scope">
<dict-tag :options="dorm_application_type" :value="scope.row.applicationType" />
</template>
</el-table-column>
<el-table-column label="当前宿舍" align="center" show-overflow-tooltip>
<template #default="scope"> {{ scope.row.currentBuildingName }}{{ scope.row.currentRoomNo }}{{ scope.row.currentBedNo }} </template>
</el-table-column>
<el-table-column label="目标宿舍" align="center" show-overflow-tooltip>
<template #default="scope">
<span v-if="scope.row.applicationType === '1'">
{{ scope.row.targetBuildingName }}{{ scope.row.targetRoomNo }}{{ scope.row.targetBedNo }}
</span>
<span v-else>—</span>
</template>
</el-table-column>
<el-table-column label="审批状态" align="center">
<template #default="scope">
<dict-tag :options="student_leave_approval_status" :value="scope.row.applicationStatus" />
</template>
</el-table-column>
<el-table-column label="申请时间" align="center" prop="applyTime" width="180" />
<el-table-column label="操作" align="center" width="150" fixed="right">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button
link
type="primary"
icon="View"
@click="handleView(scope.row)"
v-hasPermi="['studentDynamics:dormApplication:query']"
></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
</div>
</template>
<script setup lang="ts" name="DormApplicationTab">
import { getCurrentInstance, ref, onMounted, toRefs, type ComponentInternalInstance } from 'vue';
import { listDormApplication, type DormApplicationQuery } from '@/api/xg/studentDynamics';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { dorm_application_type, student_leave_approval_status } = toRefs(
proxy?.useDict('dorm_application_type', 'student_leave_approval_status') || {}
);
// 数据状态
const loading = ref(true);
const showSearch = ref(true);
const dataList = ref([]);
const total = ref(0);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
// 查询参数
const queryParams = ref<DormApplicationQuery & any>({
pageNum: 1,
pageSize: 10,
studentName: undefined,
studentNo: undefined,
collegeName: undefined,
majorName: undefined,
className: undefined,
applicationType: undefined,
applicationStatus: undefined,
applyTime: undefined
});
// 表单引用
const queryFormRef = ref();
/**
* 查询列表数据
*/
const getList = async () => {
loading.value = true;
try {
const response = await listDormApplication(queryParams.value);
dataList.value = response.rows;
total.value = response.total;
} catch (error) {
console.error('获取调宿退宿申请列表失败:', error);
proxy?.$modal.msgError('获取数据失败');
} finally {
loading.value = false;
}
};
/**
* 搜索按钮操作
*/
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/**
* 重置按钮操作
*/
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/**
* 多选框选中数据
*/
const handleSelectionChange = (selection: any) => {
ids.value = selection.map((item: any) => item.id);
single.value = selection.length !== 1;
multiple.value = !selection.length;
};
/**
* 查看详情
*/
const handleView = (row: any) => {
// 跳转到调宿退宿申请详情页面
proxy.$router.push({
path: `/xg/xsjcxx/xssssq/dialog`,
query: {
id: row.id,
type: 'update'
}
});
};
// 页面初始化
onMounted(() => {
getList();
});
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
padding: 0;
}
.mb8 {
margin-bottom: 8px;
}
/* 搜索按钮容器样式 */
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.search-btn-container .el-form-item {
margin-bottom: 0 !important;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
</style>
@@ -0,0 +1,474 @@
<template>
<el-dialog title="假期留宿详情" v-model="dialogOpen" width="1000px" append-to-body>
<el-form :model="form" label-width="120px" v-loading="loading">
<el-row :gutter="20">
<!-- 左列:学生基本信息 -->
<el-col :span="12">
<el-form-item label="学生姓名">
<el-input v-model="form.studentName" disabled />
</el-form-item>
<el-form-item label="学号">
<el-input v-model="form.studentNo" disabled />
</el-form-item>
<el-form-item label="学生联系电话">
<el-input v-model="form.studentPhone" disabled />
</el-form-item>
<el-form-item label="学院名称">
<el-input v-model="form.collegeName" disabled />
</el-form-item>
<el-form-item label="专业名称">
<el-input v-model="form.majorName" disabled />
</el-form-item>
<el-form-item label="班级名称">
<el-input v-model="form.className" disabled />
</el-form-item>
</el-col>
<!-- 右列:宿舍信息 -->
<el-col :span="12">
<el-form-item label="校区名称">
<el-input v-model="form.campusName" disabled />
</el-form-item>
<el-form-item label="宿舍区名称">
<el-input v-model="form.areaName" disabled />
</el-form-item>
<el-form-item label="宿舍楼名称">
<el-input v-model="form.buildingName" disabled />
</el-form-item>
<el-form-item label="房间名称">
<el-input v-model="form.roomName" disabled />
</el-form-item>
<el-form-item label="床位名称">
<el-input v-model="form.bedName" disabled />
</el-form-item>
</el-col>
</el-row>
<!-- 假期申请信息 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="假期类型">
<el-input v-model="holidayTypeLabel" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="留宿原因类型">
<el-input v-model="stayReasonTypeLabel" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="留宿开始日期">
<el-input v-model="form.applyStartDate" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="留宿结束日期">
<el-input v-model="form.applyEndDate" disabled />
</el-form-item>
</el-col>
</el-row>
<!-- 联系信息 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="紧急联系人姓名">
<el-input v-model="form.emergencyContact" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="紧急联系人电话">
<el-input v-model="form.emergencyPhone" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="家长同意">
<el-input v-model="parentConsentLabel" disabled />
</el-form-item>
</el-col>
</el-row>
<!-- 留宿原因 -->
<el-form-item label="留宿原因">
<el-input v-model="form.stayReason" type="textarea" :rows="3" disabled />
</el-form-item>
<!-- 附件显示 -->
<el-form-item label="附件">
<div class="attachment-view-container" v-if="attachmentList.length > 0">
<div class="attachment-grid">
<div v-for="file in attachmentList" :key="file.ossId" class="attachment-item">
<!-- 缩略图区域 -->
<div class="attachment-thumb">
<image-preview v-if="isImageFile(file)" :src="file.ossId" class="thumb-image" :isOssId="true" />
<div v-else class="file-placeholder">
<el-icon><Document /></el-icon>
<span class="file-ext">{{ getFileExt(file.name) }}</span>
</div>
</div>
<!-- 文件名称 -->
<div class="attachment-name" :title="file.name">
{{ getShortFileName(file.name) }}
</div>
<!-- 操作按钮 -->
<div class="attachment-actions">
<el-button type="success" size="small" @click="handleDownload(file)" title="下载">
<el-icon><Download /></el-icon>
</el-button>
</div>
</div>
</div>
</div>
<div v-else class="no-attachment">
<el-empty description="暂无附件" :image-size="60" />
</div>
</el-form-item>
<!-- 备注信息 -->
<el-form-item label="备注信息">
<el-input v-model="form.remark" type="textarea" :rows="3" disabled />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogOpen = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="HolidayStayDetail">
import { ref, computed, toRefs, getCurrentInstance, type ComponentInternalInstance } from 'vue';
import ElMessage from 'element-plus/es/components/message/index';
import ElLoading from 'element-plus/es/components/loading/index';
import { Document, Download } from '@element-plus/icons-vue';
import { getHolidayStay, type HolidayStayVO } from '@/api/xg/studentDynamics';
import ImagePreview from '@/components/ImagePreview/index.vue';
import request from '@/utils/request';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// 字典数据
const { xg_holiday_type, xg_stay_reason_type, sys_yesno } = toRefs<any>(proxy?.useDict('xg_holiday_type', 'xg_stay_reason_type', 'sys_yesno'));
// 对话框状态
const dialogOpen = ref(false);
const loading = ref(false);
// 表单数据
const form = ref<Partial<HolidayStayVO>>({
id: undefined,
studentId: undefined,
studentName: '',
studentNo: '',
classId: undefined,
className: '',
majorId: undefined,
majorName: '',
collegeId: undefined,
collegeName: '',
campusId: undefined,
campusName: '',
areaId: undefined,
areaName: '',
buildingId: undefined,
buildingName: '',
roomId: undefined,
roomName: '',
bedId: undefined,
bedName: '',
holidayType: '',
applyStartDate: '',
applyEndDate: '',
stayReason: '',
stayReasonType: '',
studentPhone: '',
emergencyContact: '',
emergencyPhone: '',
parentConsent: '',
attachmentOssIds: '',
remark: ''
});
// 附件列表
const attachmentList = ref<any[]>([]);
// 字典标签计算属性
const holidayTypeLabel = computed(() => {
if (!form.value.holidayType || !xg_holiday_type.value) return '';
const dict = xg_holiday_type.value.find((item: any) => item.value === form.value.holidayType);
return dict ? dict.label : form.value.holidayType;
});
const stayReasonTypeLabel = computed(() => {
if (!form.value.stayReasonType || !xg_stay_reason_type.value) return '';
const dict = xg_stay_reason_type.value.find((item: any) => item.value === form.value.stayReasonType);
return dict ? dict.label : form.value.stayReasonType;
});
const parentConsentLabel = computed(() => {
if (!form.value.parentConsent || !sys_yesno.value) return '';
const dict = sys_yesno.value.find((item: any) => item.value === form.value.parentConsent);
return dict ? dict.label : form.value.parentConsent;
});
/**
* 打开详情弹窗
*/
const open = async (id: string | number) => {
loading.value = true;
dialogOpen.value = true;
try {
// 获取详情数据
const response = await getHolidayStay(id);
form.value = response.data;
// 处理附件数据
await handleAttachmentData();
} catch (error) {
console.error('获取假期留宿详情失败:', error);
ElMessage.error('获取详情失败');
dialogOpen.value = false;
} finally {
loading.value = false;
}
};
/**
* 处理附件数据
*/
const handleAttachmentData = async () => {
try {
if (form.value.attachmentOssIds && form.value.attachmentOssIds.toString().trim()) {
// 将逗号分隔的字符串转换为数组
const ossIdsArray = form.value.attachmentOssIds
.toString()
.split(',')
.map((id) => id.trim())
.filter((id) => id);
// 调用接口获取附件信息
const response = await request({
url: `/resource/oss/listByIds/${ossIdsArray.join(',')}`,
method: 'get'
});
if (response.code === 200 && response.data) {
// 将附件信息转换为显示需要的格式
attachmentList.value = response.data.map((item: any) => ({
ossId: item.ossId,
name: item.originalName || '附件',
url: item.url,
type: item.contentType || ''
}));
} else {
attachmentList.value = [];
}
} else {
attachmentList.value = [];
}
} catch (error) {
console.error('处理附件数据失败:', error);
attachmentList.value = [];
}
};
/**
* 判断是否为图片文件
*/
const isImageFile = (file: any) => {
const imageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/webp'];
return imageTypes.includes(file.type) || /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(file.name);
};
/**
* 获取文件扩展名
*/
const getFileExt = (fileName: string) => {
const ext = fileName.split('.').pop()?.toUpperCase();
return ext || 'FILE';
};
/**
* 获取缩短的文件名
*/
const getShortFileName = (fileName: string) => {
if (!fileName) return '';
const lastDotIndex = fileName.lastIndexOf('.');
if (lastDotIndex === -1) {
return fileName.length > 12 ? fileName.substring(0, 12) + '...' : fileName;
}
const name = fileName.substring(0, lastDotIndex);
const ext = fileName.substring(lastDotIndex);
if (name.length <= 8) {
return fileName;
}
return name.substring(0, 8) + '...' + ext;
};
/**
* 下载附件
*/
const handleDownload = async (file: any) => {
try {
const loading = ElLoading.service({
text: '正在下载文件...',
target: document.body
});
const response = await request({
url: '/resource/oss/download/' + file.ossId,
method: 'get',
responseType: 'blob',
timeout: 300000
});
// 验证响应数据
if (!response) {
throw new Error('响应数据为空');
}
// 检查response的结构
let blobData: Blob;
if (response instanceof Blob) {
blobData = response;
} else if (response.data instanceof Blob) {
blobData = response.data;
} else {
throw new Error('无效的响应数据格式');
}
const url = window.URL.createObjectURL(blobData);
const link = document.createElement('a');
link.href = url;
link.download = file.name;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
loading?.close();
ElMessage.success('下载成功');
} catch (error) {
console.error('下载文件失败:', error);
ElMessage.error('下载文件失败,请稍后重试');
}
};
defineExpose({ open });
</script>
<style scoped>
/* 附件查看容器样式 */
.attachment-view-container {
width: 100%;
padding: 12px;
border: 1px solid #e4e7ed;
border-radius: 6px;
background: #fafafa;
}
.attachment-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
}
.attachment-item {
background: #fff;
border-radius: 6px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.3s;
}
.attachment-item:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.attachment-thumb {
position: relative;
width: 100%;
height: 110px;
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.thumb-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.file-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #909399;
font-size: 14px;
}
.file-placeholder .el-icon {
font-size: 28px;
margin-bottom: 4px;
}
.file-ext {
font-size: 12px;
text-transform: uppercase;
}
.attachment-name {
padding: 6px 8px;
font-size: 11px;
color: #303133;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
background: #fafafa;
border-top: 1px solid #f0f0f0;
line-height: 1.2;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.attachment-actions {
display: flex;
justify-content: center;
align-items: center;
padding: 6px 8px;
background: #fafafa;
border-top: 1px solid #f0f0f0;
height: 38px;
}
.attachment-actions .el-button {
padding: 4px 12px;
font-size: 12px;
border-radius: 4px;
}
.no-attachment {
text-align: center;
padding: 20px;
color: #909399;
}
</style>
@@ -0,0 +1,263 @@
<template>
<div class="page-container">
<!-- 搜索表单 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="学生姓名" prop="studentName">
<el-input v-model="queryParams.studentName" placeholder="请输入学生姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学号" prop="studentNo">
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学院名称" prop="collegeName">
<el-input v-model="queryParams.collegeName" placeholder="请输入学院名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="专业名称" prop="majorName">
<el-input v-model="queryParams.majorName" placeholder="请输入专业名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="班级名称" prop="className">
<el-input v-model="queryParams.className" placeholder="请输入班级名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="假期类型" prop="holidayType">
<el-select v-model="queryParams.holidayType" placeholder="请选择假期类型" clearable style="width: 100%">
<el-option v-for="dict in xg_holiday_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="开始日期" prop="applyStartDate">
<el-date-picker
clearable
v-model="queryParams.applyStartDate"
type="date"
style="width: 100%"
value-format="YYYY-MM-DD"
placeholder="请选择开始日期"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="结束日期" prop="applyEndDate">
<el-date-picker
clearable
v-model="queryParams.applyEndDate"
type="date"
style="width: 100%"
value-format="YYYY-MM-DD"
placeholder="请选择结束日期"
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 数据表格 -->
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<span class="card-title">假期留宿信息列表</span>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" :data="dataList" @selection-change="handleSelectionChange">
<el-table-column label="学生姓名" align="center" prop="studentName" />
<el-table-column label="学号" align="center" prop="studentNo" />
<el-table-column label="学院" align="center" prop="collegeName" show-overflow-tooltip />
<el-table-column label="专业" align="center" prop="majorName" show-overflow-tooltip />
<el-table-column label="班级" align="center" prop="className" show-overflow-tooltip />
<el-table-column label="宿舍信息" align="center" show-overflow-tooltip>
<template #default="scope"> {{ scope.row.buildingName }}{{ scope.row.roomName }} </template>
</el-table-column>
<el-table-column label="假期类型" align="center" prop="holidayType">
<template #default="scope">
<dict-tag :options="xg_holiday_type" :value="scope.row.holidayType" />
</template>
</el-table-column>
<el-table-column label="留宿时间" align="center" show-overflow-tooltip>
<template #default="scope"> {{ scope.row.applyStartDate }} 至 {{ scope.row.applyEndDate }} </template>
</el-table-column>
<el-table-column label="留宿原因类型" align="center" prop="stayReasonType">
<template #default="scope">
<dict-tag :options="xg_stay_reason_type" :value="scope.row.stayReasonType" />
</template>
</el-table-column>
<el-table-column label="联系电话" align="center" prop="studentPhone" />
<el-table-column label="紧急联系人" align="center" prop="emergencyContact" />
<el-table-column label="操作" align="center" width="150" fixed="right">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button
link
type="primary"
icon="View"
@click="handleView(scope.row)"
v-hasPermi="['studentDynamics:holidayStay:query']"
></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 查看详情弹窗 -->
<HolidayStayDetail ref="detailRef" @refresh="getList" />
</div>
</template>
<script setup lang="ts" name="HolidayStayTab">
import { getCurrentInstance, ref, onMounted, toRefs } from 'vue';
import { ElMessage } from 'element-plus';
import { listHolidayStay, type HolidayStayQuery } from '@/api/xg/studentDynamics';
import HolidayStayDetail from './HolidayStayDetail.vue';
const { proxy } = getCurrentInstance();
// 字典数据
const { xg_holiday_type, xg_stay_reason_type } = toRefs(proxy?.useDict('xg_holiday_type', 'xg_stay_reason_type') || {});
// 数据状态
const loading = ref(true);
const showSearch = ref(true);
const dataList = ref([]);
const total = ref(0);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
// 查询参数
const queryParams = ref<HolidayStayQuery & any>({
pageNum: 1,
pageSize: 10,
studentName: undefined,
studentNo: undefined,
collegeName: undefined,
majorName: undefined,
className: undefined,
holidayType: undefined,
applyStartDate: undefined,
applyEndDate: undefined,
status: undefined
});
// 表单引用
const queryFormRef = ref();
const detailRef = ref();
/**
* 查询列表数据
*/
const getList = async () => {
loading.value = true;
try {
const response = await listHolidayStay(queryParams.value);
dataList.value = response.rows;
total.value = response.total;
} catch (error) {
console.error('获取假期留宿信息列表失败:', error);
ElMessage.error('获取数据失败');
} finally {
loading.value = false;
}
};
/**
* 搜索按钮操作
*/
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/**
* 重置按钮操作
*/
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/**
* 多选框选中数据
*/
const handleSelectionChange = (selection: any) => {
ids.value = selection.map((item: any) => item.id);
single.value = selection.length !== 1;
multiple.value = !selection.length;
};
/**
* 查看详情
*/
const handleView = (row: any) => {
detailRef.value?.open(row.id);
};
// 页面初始化
onMounted(() => {
getList();
});
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
padding: 0;
}
.mb-[10px] {
margin-bottom: 10px;
}
.mb8 {
margin-bottom: 8px;
}
/* 搜索按钮容器样式 */
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.search-btn-container .el-form-item {
margin-bottom: 0 !important;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
</style>
@@ -0,0 +1,21 @@
<template>
<el-dialog title="学生请假详情" v-model="dialogOpen" width="800px" append-to-body>
<div>学生请假详情组件待实现</div>
<template #footer>
<el-button @click="dialogOpen = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="StudentLeaveDetail">
import { ref } from 'vue';
const dialogOpen = ref(false);
const open = (id: number) => {
console.log('打开学生请假详情,ID:', id);
dialogOpen.value = true;
};
defineExpose({ open });
</script>
@@ -0,0 +1,271 @@
<template>
<div class="page-container">
<!-- 搜索表单 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" style="margin-bottom: 10px">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="学生姓名" prop="studentName">
<el-input v-model="queryParams.studentName" placeholder="请输入学生姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学号" prop="studentNo">
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学院名称" prop="collegeName">
<el-input v-model="queryParams.collegeName" placeholder="请输入学院名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="专业名称" prop="majorName">
<el-input v-model="queryParams.majorName" placeholder="请输入专业名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="班级名称" prop="className">
<el-input v-model="queryParams.className" placeholder="请输入班级名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="请假类型" prop="leaveTypeName">
<el-input v-model="queryParams.leaveTypeName" placeholder="请输入请假类型名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="开始时间" prop="startTime">
<el-date-picker
clearable
v-model="queryParams.startTime"
type="date"
style="width: 100%"
value-format="YYYY-MM-DD"
placeholder="请选择开始时间"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="结束时间" prop="endTime">
<el-date-picker
clearable
v-model="queryParams.endTime"
type="date"
style="width: 100%"
value-format="YYYY-MM-DD"
placeholder="请选择结束时间"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="审批状态" prop="approvalStatus">
<el-select v-model="queryParams.approvalStatus" placeholder="请选择审批状态" clearable style="width: 100%">
<el-option v-for="dict in student_leave_approval_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 数据表格 -->
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<span class="card-title">学生请假信息列表</span>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" :data="dataList" @selection-change="handleSelectionChange">
<el-table-column label="学生姓名" align="center" prop="studentName" />
<el-table-column label="学号" align="center" prop="studentNo" />
<el-table-column label="学院" align="center" prop="collegeName" show-overflow-tooltip />
<el-table-column label="专业" align="center" prop="majorName" show-overflow-tooltip />
<el-table-column label="班级" align="center" prop="className" show-overflow-tooltip />
<el-table-column label="请假类型" align="center" prop="leaveTypeName" />
<el-table-column label="请假时间" align="center" show-overflow-tooltip>
<template #default="scope"> {{ scope.row.startTime }} 至 {{ scope.row.endTime }} </template>
</el-table-column>
<el-table-column label="请假时长" align="center">
<template #default="scope"> {{ scope.row.leaveDuration }}天 </template>
</el-table-column>
<el-table-column label="是否离校" align="center">
<template #default="scope">
<dict-tag :options="sys_yesno" :value="scope.row.isLeaveSchool" />
</template>
</el-table-column>
<el-table-column label="审批状态" align="center">
<template #default="scope">
<dict-tag :options="student_leave_approval_status" :value="scope.row.approvalStatus" />
</template>
</el-table-column>
<el-table-column label="销假状态" align="center">
<template #default="scope">
<dict-tag :options="xg_xsqj_cancel_status" :value="scope.row.cancelStatus" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="150" fixed="right">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button
link
type="primary"
icon="View"
@click="handleView(scope.row)"
v-hasPermi="['studentDynamics:studentLeave:query']"
></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
</div>
</template>
<script setup lang="ts" name="StudentLeaveTab">
import { getCurrentInstance, ref, onMounted, toRefs } from 'vue';
import { listStudentLeave, type StudentLeaveQuery } from '@/api/xg/studentDynamics';
const { proxy } = getCurrentInstance();
const { sys_yesno, student_leave_approval_status, xg_xsqj_cancel_status } = toRefs(
proxy?.useDict('sys_yesno', 'student_leave_approval_status', 'xg_xsqj_cancel_status') || {}
);
// 数据状态
const loading = ref(true);
const showSearch = ref(true);
const dataList = ref([]);
const total = ref(0);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
// 查询参数
const queryParams = ref<StudentLeaveQuery & any>({
pageNum: 1,
pageSize: 10,
studentName: undefined,
studentNo: undefined,
collegeName: undefined,
majorName: undefined,
className: undefined,
leaveTypeName: undefined,
approvalStatus: undefined,
startTime: undefined,
endTime: undefined
});
// 表单引用
const queryFormRef = ref();
/**
* 查询列表数据
*/
const getList = async () => {
loading.value = true;
try {
const response = await listStudentLeave(queryParams.value);
dataList.value = response.rows;
total.value = response.total;
} catch (error) {
console.error('获取学生请假信息列表失败:', error);
proxy?.$modal.msgError('获取数据失败');
} finally {
loading.value = false;
}
};
/**
* 搜索按钮操作
*/
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/**
* 重置按钮操作
*/
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/**
* 多选框选中数据
*/
const handleSelectionChange = (selection: any) => {
ids.value = selection.map((item: any) => item.id);
single.value = selection.length !== 1;
multiple.value = !selection.length;
};
/**
* 查看详情
*/
const handleView = (row: any) => {
// 跳转到学生请假详情页面
proxy.$router.push({
path: `/xg/xsjcxx/xsqj/leaveEdit/index`,
query: {
id: row.id,
type: 'update'
}
});
};
// 页面初始化
onMounted(() => {
getList();
});
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
padding: 0;
}
.mb8 {
margin-bottom: 8px;
}
/* 搜索按钮容器样式 */
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.search-btn-container .el-form-item {
margin-bottom: 0 !important;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
</style>
@@ -0,0 +1,80 @@
<template>
<div class="p-2">
<!-- Tab页组件 -->
<el-tabs v-model="activeTab" @tab-click="handleTabClick" class="mb-4">
<!-- 调宿/退宿信息Tab页 -->
<el-tab-pane label="调宿/退宿信息" name="dormApplication">
<DormApplicationTab v-if="activeTab === 'dormApplication'" ref="dormApplicationTabRef" />
</el-tab-pane>
<!-- 假期留宿信息Tab页 -->
<el-tab-pane label="假期留宿信息" name="holidayStay">
<HolidayStayTab v-if="activeTab === 'holidayStay'" ref="holidayStayTabRef" />
</el-tab-pane>
<!-- 请假信息Tab页 -->
<el-tab-pane label="请假信息" name="studentLeave">
<StudentLeaveTab v-if="activeTab === 'studentLeave'" ref="studentLeaveTabRef" />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts" name="StudentDynamics">
import { ref, onMounted } from 'vue';
import DormApplicationTab from './components/DormApplicationTab.vue';
import HolidayStayTab from './components/HolidayStayTab.vue';
import StudentLeaveTab from './components/StudentLeaveTab.vue';
// Tab页状态
const activeTab = ref('dormApplication');
// 子组件引用
const dormApplicationTabRef = ref();
const holidayStayTabRef = ref();
const studentLeaveTabRef = ref();
/**
* Tab页切换处理
*/
const handleTabClick = (tab: any) => {
console.log('切换到Tab页:', tab.name);
// 可以在这里添加Tab切换的逻辑
};
/**
* 页面初始化
*/
onMounted(() => {
console.log('学生动向信息管理页面已加载');
});
</script>
<style scoped>
.p-2 {
padding: 8px;
}
.mb-4 {
margin-bottom: 16px;
}
/* Tab页样式优化 */
:deep(.el-tabs__header) {
margin-bottom: 20px;
}
:deep(.el-tabs__item) {
font-size: 14px;
font-weight: 500;
}
:deep(.el-tabs__item.is-active) {
/* color: #409eff; */
font-weight: 600;
}
:deep(.el-tabs__content) {
padding: 0;
}
</style>
@@ -183,7 +183,7 @@
<el-table-column label="校区ID,关联xg_dorm_campus.id" align="center" prop="campusId" v-if="false" />
<el-table-column label="校区" align="center" prop="campusName" />
<el-table-column label="宿舍区ID,关联xg_dorm_area.id" align="center" prop="areaId" v-if="false" />
<el-table-column label="宿舍区" align="center" prop="areaName" />
<el-table-column label="宿舍区" align="center" prop="areaName" v-if="false" />
<el-table-column label="宿舍楼ID,关联xg_dorm_building.id" align="center" prop="buildingId" v-if="false" />
<el-table-column label="宿舍楼" align="center" prop="buildingName" />
<el-table-column label="房间ID,关联xg_dorm_room.id" align="center" prop="roomId" v-if="false" />
@@ -180,7 +180,7 @@
<el-table-column label="校区ID,关联xg_dorm_campus.id" align="center" prop="campusId" v-if="false" />
<el-table-column label="校区" align="center" prop="campusName" />
<el-table-column label="宿舍区ID,关联xg_dorm_area.id" align="center" prop="areaId" v-if="false" />
<el-table-column label="宿舍区" align="center" prop="areaName" />
<el-table-column label="宿舍区" align="center" prop="areaName" v-if="false" />
<el-table-column label="宿舍楼ID,关联xg_dorm_building.id" align="center" prop="buildingId" v-if="false" />
<el-table-column label="宿舍楼" align="center" prop="buildingName" />
<el-table-column label="房间ID,关联xg_dorm_room.id" align="center" prop="roomId" v-if="false" />
+23 -4
View File
@@ -307,7 +307,7 @@ import ApprovalButton from '@/components/Process/approvalButton.vue';
import { AxiosResponse } from 'axios';
import { StartProcessBo } from '@/api/workflow/workflowCommon/types';
import { addStudentLeave, getStudentLeave, updateStudentLeave } from '@/api/workflow/studentLeave/studentLeave';
import { getStudentMsg, getClassTeacherId, getCollegeLeaveApprovers } from '@/api/xg/xsjcxx/studentMsg/studentMsg';
import { getStudentMsg, getClassTeacherId, getCollegeLeaveApprovers, getCollegeLeaders } from '@/api/xg/xsjcxx/studentMsg/studentMsg';
import { startWorkFlow } from '@/api/workflow/task';
import { StudentLeaveForm, StudentLeaveVO } from '@/api/workflow/studentLeave/studentLeave/types';
import { StudentMsgVO } from '@/api/xg/xsjcxx/studentMsg/studentMsg/types';
@@ -890,7 +890,7 @@ const handleStartWorkFlow = async (data: StudentLeaveForm) => {
// 1. 调用接口获取班主任ID
let classTeacherId: number | null = null;
try {
const classTeacherResponse = await getClassTeacherId(data.studentId);
const classTeacherResponse: any = await getClassTeacherId(data.studentId);
if (classTeacherResponse && classTeacherResponse.data) {
classTeacherId = classTeacherResponse.data;
console.log('获取到班主任ID:', classTeacherId);
@@ -908,7 +908,7 @@ const handleStartWorkFlow = async (data: StudentLeaveForm) => {
// 2. 调用接口获取学院请假审批人列表
let collegeApprovers: number[] = [];
try {
const collegeApproversResponse = await getCollegeLeaveApprovers(data.studentId);
const collegeApproversResponse: any = await getCollegeLeaveApprovers(data.studentId);
if (collegeApproversResponse && collegeApproversResponse.data) {
collegeApprovers = collegeApproversResponse.data;
console.log('获取到学院请假审批人列表:', collegeApprovers);
@@ -923,6 +923,24 @@ const handleStartWorkFlow = async (data: StudentLeaveForm) => {
return; // 停止后续操作
}
// 3. 调用接口获取二级学院分管领导ID列表
let collegeLeaders: number[] = [];
try {
const collegeLeadersResponse: any = await getCollegeLeaders(data.studentId);
if (collegeLeadersResponse && collegeLeadersResponse.data) {
collegeLeaders = collegeLeadersResponse.data;
console.log('获取到二级学院分管领导ID列表:', collegeLeaders);
} else {
console.warn('获取二级学院分管领导ID列表失败');
ElMessage.error('获取二级学院分管领导信息失败,请稍后重试');
return; // 停止后续操作
}
} catch (error) {
console.error('调用获取二级学院分管领导接口失败:', error);
ElMessage.error('获取二级学院分管领导信息失败,请稍后重试');
return; // 停止后续操作
}
submitFormData.value.flowCode = flowCode.value;
submitFormData.value.businessId = data.id;
// 流程变量
@@ -935,7 +953,8 @@ const handleStartWorkFlow = async (data: StudentLeaveForm) => {
// userList: ['1', '3', '4'],
// 新增的流程变量
bzrspr: classTeacherId,
ejxyspr: collegeApprovers
ejxyspr: collegeApprovers,
ejxyfgld: collegeLeaders
};
submitFormData.value.variables = taskVariables.value;
const resp = await startWorkFlow(submitFormData.value);