提交代码

This commit is contained in:
zlwu committed 2025-08-16 06:57:33 +08:00
1 parent bee68f318d
commit a884f695ea
8 files changed
+847 -107

No files matched your search

@@ -19,6 +19,8 @@ import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.xg.domain.vo.XgDormViolationVo;
import org.dromara.xg.domain.bo.XgDormViolationBo;
import org.dromara.xg.domain.vo.StudentOptionVo;
import org.dromara.xg.domain.bo.StudentOptionBo;
import org.dromara.xg.service.IXgDormViolationService;
import org.dromara.common.mybatis.core.page.TableDataInfo;
@@ -63,8 +65,7 @@ public class XgDormViolationController extends BaseController {
*/
@SaCheckPermission("dormViolation:dormViolation:query")
@GetMapping("/{id}")
public R<XgDormViolationVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
public R<XgDormViolationVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
return R.ok(xgDormViolationService.queryById(id));
}
@@ -98,8 +99,27 @@ public class XgDormViolationController extends BaseController {
@SaCheckPermission("dormViolation:dormViolation:remove")
@Log(title = "宿舍违纪记录", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
return toAjax(xgDormViolationService.deleteWithValidByIds(List.of(ids), true));
}
/**
* 获取学生选择器选项列表(支持按学号、姓名检索)
*/
@SaCheckPermission("dormViolation:dormViolation:list")
@GetMapping("/studentOptions")
public TableDataInfo<StudentOptionVo> getStudentOptions(StudentOptionBo bo, PageQuery pageQuery) {
return xgDormViolationService.getStudentOptions(bo, pageQuery);
}
/**
* 根据学生ID获取学生详细信息
*
* @param studentId 学生ID
*/
@SaCheckPermission("dormViolation:dormViolation:list")
@GetMapping("/student/{studentId}")
public R<StudentOptionVo> getStudentById(@NotNull(message = "学生ID不能为空") @PathVariable Long studentId) {
return R.ok(xgDormViolationService.getStudentById(studentId));
}
}
@@ -2,7 +2,12 @@ package org.dromara.xg.mapper;
import org.dromara.xg.domain.XgDormViolation;
import org.dromara.xg.domain.vo.XgDormViolationVo;
import org.dromara.xg.domain.vo.StudentOptionVo;
import org.dromara.xg.domain.bo.StudentOptionBo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Param;
/**
* 宿舍违纪记录Mapper接口
@@ -12,4 +17,20 @@ import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
*/
public interface XgDormViolationMapper extends BaseMapperPlus<XgDormViolation, XgDormViolationVo> {
/**
* 查询学生选择器选项列表(支持按学号、姓名检索)
*
* @param page 分页参数
* @param bo 查询条件
* @return 学生选项列表
*/
Page<StudentOptionVo> selectStudentOptions(IPage<?> page, @Param("bo") StudentOptionBo bo);
/**
* 根据学生ID获取学生详细信息
*
* @param studentId 学生ID
* @return 学生详细信息
*/
StudentOptionVo selectStudentById(@Param("studentId") Long studentId);
}
@@ -2,6 +2,8 @@ package org.dromara.xg.service;
import org.dromara.xg.domain.vo.XgDormViolationVo;
import org.dromara.xg.domain.bo.XgDormViolationBo;
import org.dromara.xg.domain.vo.StudentOptionVo;
import org.dromara.xg.domain.bo.StudentOptionBo;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
@@ -65,4 +67,21 @@ public interface IXgDormViolationService {
* @return 是否删除成功
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 获取学生选择器选项列表(支持按学号、姓名检索)
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 学生选项分页列表
*/
TableDataInfo<StudentOptionVo> getStudentOptions(StudentOptionBo bo, PageQuery pageQuery);
/**
* 根据学生ID获取学生详细信息
*
* @param studentId 学生ID
* @return 学生详细信息
*/
StudentOptionVo getStudentById(Long studentId);
}
@@ -12,6 +12,8 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.dromara.xg.domain.bo.XgDormViolationBo;
import org.dromara.xg.domain.vo.XgDormViolationVo;
import org.dromara.xg.domain.bo.StudentOptionBo;
import org.dromara.xg.domain.vo.StudentOptionVo;
import org.dromara.xg.domain.XgDormViolation;
import org.dromara.xg.mapper.XgDormViolationMapper;
import org.dromara.xg.service.IXgDormViolationService;
@@ -40,7 +42,7 @@ public class XgDormViolationServiceImpl implements IXgDormViolationService {
* @return 宿舍违纪记录
*/
@Override
public XgDormViolationVo queryById(Long id){
public XgDormViolationVo queryById(Long id) {
return baseMapper.selectVoById(id);
}
@@ -94,11 +96,15 @@ public class XgDormViolationServiceImpl implements IXgDormViolationService {
lqw.eq(bo.getBedId() != null, XgDormViolation::getBedId, bo.getBedId());
lqw.like(StringUtils.isNotBlank(bo.getBedName()), XgDormViolation::getBedName, bo.getBedName());
lqw.eq(StringUtils.isNotBlank(bo.getViolationType()), XgDormViolation::getViolationType, bo.getViolationType());
lqw.eq(StringUtils.isNotBlank(bo.getViolationLevel()), XgDormViolation::getViolationLevel, bo.getViolationLevel());
lqw.eq(StringUtils.isNotBlank(bo.getViolationLevel()), XgDormViolation::getViolationLevel,
bo.getViolationLevel());
lqw.eq(bo.getViolationTime() != null, XgDormViolation::getViolationTime, bo.getViolationTime());
lqw.like(StringUtils.isNotBlank(bo.getViolationDescription()), XgDormViolation::getViolationDescription, bo.getViolationDescription());
lqw.like(StringUtils.isNotBlank(bo.getInvolvedStudents()), XgDormViolation::getInvolvedStudents, bo.getInvolvedStudents());
lqw.like(StringUtils.isNotBlank(bo.getAttachmentOssIds()), XgDormViolation::getAttachmentOssIds, bo.getAttachmentOssIds());
lqw.like(StringUtils.isNotBlank(bo.getViolationDescription()), XgDormViolation::getViolationDescription,
bo.getViolationDescription());
lqw.like(StringUtils.isNotBlank(bo.getInvolvedStudents()), XgDormViolation::getInvolvedStudents,
bo.getInvolvedStudents());
lqw.like(StringUtils.isNotBlank(bo.getAttachmentOssIds()), XgDormViolation::getAttachmentOssIds,
bo.getAttachmentOssIds());
lqw.eq(StringUtils.isNotBlank(bo.getDeleted()), XgDormViolation::getDeleted, bo.getDeleted());
return lqw;
}
@@ -136,8 +142,8 @@ public class XgDormViolationServiceImpl implements IXgDormViolationService {
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(XgDormViolation entity){
//TODO 做一些数据校验,如唯一约束
private void validEntityBeforeSave(XgDormViolation entity) {
// TODO 做一些数据校验,如唯一约束
}
/**
@@ -149,9 +155,33 @@ public class XgDormViolationServiceImpl implements IXgDormViolationService {
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
if (isValid) {
// TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteByIds(ids) > 0;
}
/**
* 获取学生选择器选项列表(支持按学号、姓名检索)
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 学生选项分页列表
*/
@Override
public TableDataInfo<StudentOptionVo> getStudentOptions(StudentOptionBo bo, PageQuery pageQuery) {
Page<StudentOptionVo> result = baseMapper.selectStudentOptions(pageQuery.build(), bo);
return TableDataInfo.build(result);
}
/**
* 根据学生ID获取学生详细信息
*
* @param studentId 学生ID
* @return 学生详细信息
*/
@Override
public StudentOptionVo getStudentById(Long studentId) {
return baseMapper.selectStudentById(studentId);
}
}
@@ -4,4 +4,118 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.xg.mapper.XgDormViolationMapper">
<!-- 学生选项查询结果映射 -->
<resultMap id="StudentOptionResult" type="org.dromara.xg.domain.vo.StudentOptionVo">
<id property="id" column="id"/>
<result property="studentNo" column="student_no"/>
<result property="name" column="name"/>
<result property="gender" column="gender"/>
<result property="collegeId" column="college_id"/>
<result property="collegeName" column="college_name"/>
<result property="majorId" column="major_id"/>
<result property="majorName" column="major_name"/>
<result property="classId" column="class_id"/>
<result property="className" column="class_name"/>
<result property="phone" column="phone"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<association property="dormInfo" javaType="org.dromara.xg.domain.vo.StudentOptionVo$DormInfo">
<result property="campusId" column="campus_id"/>
<result property="campusName" column="campus_name"/>
<result property="areaId" column="area_id"/>
<result property="areaName" column="area_name"/>
<result property="buildingId" column="building_id"/>
<result property="buildingName" column="building_name"/>
<result property="roomId" column="room_id"/>
<result property="roomName" column="room_name"/>
<result property="bedId" column="bed_id"/>
<result property="bedName" column="bed_name"/>
</association>
</resultMap>
<!-- 查询学生选择器选项列表 -->
<select id="selectStudentOptions" resultMap="StudentOptionResult">
SELECT
s.id,
s.student_no,
s.name,
s.gender,
s.college_id,
s.college_name,
s.major_id,
s.major_name,
s.class_id,
s.class_name,
s.phone,
s.create_by,
s.create_time,
s.update_by,
s.update_time,
COALESCE(dc.id, NULL) as campus_id,
COALESCE(dc.campus_name, NULL) as campus_name,
COALESCE(da.id, NULL) as area_id,
COALESCE(da.area_name, NULL) as area_name,
COALESCE(db.id, NULL) as building_id,
COALESCE(db.building_name, NULL) as building_name,
COALESCE(dr.id, NULL) as room_id,
COALESCE(dr.room_name, NULL) as room_name,
COALESCE(bed.id, NULL) as bed_id,
COALESCE(bed.bed_name, NULL) as bed_name
FROM xg_student_msg s
LEFT JOIN xg_dorm_bed_student dbs ON s.id = dbs.student_id AND dbs.deleted = 0
LEFT JOIN xg_dorm_bed bed ON dbs.bed_id = bed.id AND bed.deleted = 0
LEFT JOIN xg_dorm_room dr ON bed.room_id = dr.id AND dr.deleted = 0
LEFT JOIN xg_dorm_building db ON dr.building_id = db.id AND db.deleted = 0
LEFT JOIN xg_dorm_area da ON db.area_id = da.id AND da.deleted = 0
LEFT JOIN xg_dorm_campus dc ON da.campus_id = dc.id AND dc.deleted = 0
WHERE s.deleted = 0
AND s.student_status = '1'
<if test="bo.keyword != null and bo.keyword != ''">
AND (s.student_no LIKE CONCAT('%', #{bo.keyword}, '%')
OR s.name LIKE CONCAT('%', #{bo.keyword}, '%'))
</if>
ORDER BY s.student_no ASC
</select>
<!-- 根据学生ID获取学生详细信息 -->
<select id="selectStudentById" resultMap="StudentOptionResult">
SELECT
s.id,
s.student_no,
s.name,
s.gender,
s.college_id,
s.college_name,
s.major_id,
s.major_name,
s.class_id,
s.class_name,
s.phone,
s.create_by,
s.create_time,
s.update_by,
s.update_time,
COALESCE(dc.id, NULL) as campus_id,
COALESCE(dc.campus_name, NULL) as campus_name,
COALESCE(da.id, NULL) as area_id,
COALESCE(da.area_name, NULL) as area_name,
COALESCE(db.id, NULL) as building_id,
COALESCE(db.building_name, NULL) as building_name,
COALESCE(dr.id, NULL) as room_id,
COALESCE(dr.room_name, NULL) as room_name,
COALESCE(bed.id, NULL) as bed_id,
COALESCE(bed.bed_name, NULL) as bed_name
FROM xg_student_msg s
LEFT JOIN xg_dorm_bed_student dbs ON s.id = dbs.student_id AND dbs.deleted = 0
LEFT JOIN xg_dorm_bed bed ON dbs.bed_id = bed.id AND bed.deleted = 0
LEFT JOIN xg_dorm_room dr ON bed.room_id = dr.id AND dr.deleted = 0
LEFT JOIN xg_dorm_building db ON dr.building_id = db.id AND db.deleted = 0
LEFT JOIN xg_dorm_area da ON db.area_id = da.id AND da.deleted = 0
LEFT JOIN xg_dorm_campus dc ON da.campus_id = dc.id AND dc.deleted = 0
WHERE s.deleted = 0
AND s.id = #{studentId}
</select>
</mapper>
@@ -78,12 +78,14 @@ export const delDormViolation = (id: string | number | Array<string | number>) =
* @param query 查询参数
* @returns 学生选项列表
*/
export const getStudentOptions = (query?: StudentOptionQuery): AxiosPromise<{
export const getStudentOptions = (
query?: StudentOptionQuery
): AxiosPromise<{
total: number;
rows: StudentOptionVO[];
}> => {
return request({
url: '/holidayStay/holidayStay/studentOptions',
url: '/dormViolation/dormViolation/studentOptions',
method: 'get',
params: query
});
@@ -151,3 +153,15 @@ export const getBedOptions = (roomId?: string | number): AxiosPromise<any[]> =>
params: roomId ? { roomId } : undefined
});
};
/**
* 根据学生ID获取学生详细信息
* @param studentId 学生ID
* @returns 学生详细信息
*/
export const getStudentById = (studentId: string | number): AxiosPromise<StudentOptionVO> => {
return request({
url: `/dormViolation/dormViolation/student/${studentId}`,
method: 'get'
});
};
@@ -263,9 +263,9 @@ export interface DormViolationForm extends BaseEntity {
violationDescription?: string;
/**
* 涉及其他学生id(逗号分隔)
* 涉及其他学生id(逗号分隔)或数组
*/
involvedStudents?: string;
involvedStudents?: string | (string | number)[];
/**
* 附件OSS ID列表(逗号分隔)
@@ -106,7 +106,7 @@
>修改</el-button
>
</el-col>
<el-col :span="1.5">
<!-- <el-col :span="1.5">
<el-button
type="danger"
plain
@@ -116,7 +116,7 @@
v-hasPermi="['dormViolation:dormViolation:remove']"
>删除</el-button
>
</el-col>
</el-col> -->
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['dormViolation:dormViolation:export']">导出</el-button>
</el-col>
@@ -126,47 +126,47 @@
<el-table v-loading="loading" border :data="dormViolationList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="违纪记录ID" align="center" prop="id" v-if="true" />
<el-table-column label="学生ID" align="center" prop="studentId" />
<!-- <el-table-column label="违纪记录ID" align="center" prop="id" v-if="true" />
<el-table-column label="学生ID" align="center" prop="studentId" /> -->
<el-table-column label="学生姓名" align="center" prop="studentName" />
<el-table-column label="学号" align="center" prop="studentNo" />
<el-table-column label="学院ID" align="center" prop="collegeId" />
<!-- <el-table-column label="学院ID" align="center" prop="collegeId" /> -->
<el-table-column label="学院名称" align="center" prop="collegeName" />
<el-table-column label="专业ID" align="center" prop="majorId" />
<!-- <el-table-column label="专业ID" align="center" prop="majorId" /> -->
<el-table-column label="专业名称" align="center" prop="majorName" />
<el-table-column label="班级ID" align="center" prop="classId" />
<!-- <el-table-column label="班级ID" align="center" prop="classId" /> -->
<el-table-column label="班级名称" align="center" prop="className" />
<el-table-column label="校区ID" align="center" prop="campusId" />
<!-- <el-table-column label="校区ID" align="center" prop="campusId" /> -->
<el-table-column label="校区名称" align="center" prop="campusName" />
<el-table-column label="宿舍区域ID" align="center" prop="areaId" />
<!-- <el-table-column label="宿舍区域ID" align="center" prop="areaId" /> -->
<el-table-column label="宿舍区域名称" align="center" prop="areaName" />
<el-table-column label="宿舍楼ID" align="center" prop="buildingId" />
<!-- <el-table-column label="宿舍楼ID" align="center" prop="buildingId" /> -->
<el-table-column label="宿舍楼名称" align="center" prop="buildingName" />
<el-table-column label="宿舍房间ID" align="center" prop="roomId" />
<!-- <el-table-column label="宿舍房间ID" align="center" prop="roomId" /> -->
<el-table-column label="房间名称" align="center" prop="roomName" />
<el-table-column label="床位ID" align="center" prop="bedId" />
<!-- <el-table-column label="床位ID" align="center" prop="bedId" /> -->
<el-table-column label="床位名称" align="center" prop="bedName" />
<el-table-column label="违纪类型" align="center" prop="violationType">
<template #default="scope">
<dict-tag :options="dorm_violation_type" :value="scope.row.violationType" />
</template>
</el-table-column>
<el-table-column label="违纪等级:1轻微 2一般 3严重 4特别严重" align="center" prop="violationLevel">
<el-table-column label="违纪等级" align="center" prop="violationLevel">
<template #default="scope">
<dict-tag :options="dorm_violation_level" :value="scope.row.violationLevel" />
</template>
</el-table-column>
<el-table-column label="违纪发生时间" align="center" prop="violationTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.violationTime, '{y}-{m}-{d}') }}</span>
<span>{{ parseTime(scope.row.violationTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="违纪行为描述" align="center" prop="violationDescription" />
<el-table-column label="涉及其他学生id" align="center" prop="involvedStudents" />
<el-table-column label="附件OSS ID列表" align="center" prop="attachmentOssIds" />
<el-table-column label="删除标志" align="center" prop="deleted" />
<!-- <el-table-column label="涉及其他学生id" align="center" prop="involvedStudents" /> -->
<!-- <el-table-column label="附件OSS ID列表" align="center" prop="attachmentOssIds" /> -->
<!-- <el-table-column label="删除标志" align="center" prop="deleted" /> -->
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right" width="120">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button
@@ -180,7 +180,7 @@
<el-tooltip content="删除" placement="top">
<el-button
link
type="primary"
type="danger"
icon="Delete"
@click="handleDelete(scope.row)"
v-hasPermi="['dormViolation:dormViolation:remove']"
@@ -377,13 +377,89 @@
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="涉及其他学生" prop="involvedStudents">
<el-input v-model="form.involvedStudents" placeholder="请输入涉及其他学生ID" />
</el-form-item>
</el-col>
</el-row>
<!-- 涉及其他学生 - 单独一行 -->
<el-form-item label="涉及其他学生" prop="involvedStudents">
<el-select
v-model="form.involvedStudents"
placeholder="请选择涉及的其他学生"
filterable
remote
:remote-method="remoteSearchInvolvedStudents"
:loading="involvedStudentLoading"
clearable
multiple
collapse-tags
collapse-tags-tooltip
style="width: 100%"
@visible-change="handleInvolvedStudentSelectVisibleChange"
@change="handleInvolvedStudentsChange"
popper-class="involved-student-select-dropdown"
>
<el-option
v-for="student in involvedStudentOptions.filter((s) => s.id !== form.studentId)"
:key="student.id"
:label="`${student.name} (${student.studentNo})`"
:value="student.id"
>
<div class="student-option">
<div class="student-main">
<span class="student-name">{{ student.name }}</span>
<span class="student-no">{{ student.studentNo }}</span>
<span class="student-gender">{{ student.gender === '0' ? '男' : '女' }}</span>
</div>
<div class="student-info" v-if="student.collegeName || student.majorName || student.className">
<span v-if="student.collegeName && student.collegeName.trim()" class="info-item">{{ student.collegeName }}</span>
<span v-if="student.majorName && student.majorName.trim()" class="info-item">{{ student.majorName }}</span>
<span v-if="student.className && student.className.trim()" class="info-item">{{ student.className }}</span>
</div>
</div>
</el-option>
</el-select>
<div class="form-tip">
<el-icon><InfoFilled /></el-icon>
<span>支持多选,可搜索学号或姓名,最多选择10名学生</span>
</div>
<!-- 涉及其他学生信息展示表格 -->
<div v-if="selectedInvolvedStudents.length > 0" class="involved-students-table-container">
<div class="table-header">
<span class="table-title">已选择的涉及学生信息</span>
<span class="table-count">(共 {{ selectedInvolvedStudents.length }} 人)</span>
</div>
<el-table :data="selectedInvolvedStudents" size="small" border stripe class="involved-students-table" max-height="300">
<el-table-column label="序号" type="index" width="50" align="center" />
<el-table-column label="姓名" prop="name" width="120" align="center" />
<el-table-column label="学号" prop="studentNo" width="120" align="center" />
<el-table-column label="性别" prop="gender" width="60" align="center">
<template #default="scope">
<el-tag :type="scope.row.gender === '0' ? 'primary' : 'danger'" size="small">
{{ scope.row.gender === '0' ? '男' : '女' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="学院" prop="collegeName" min-width="120" align="center" show-overflow-tooltip />
<el-table-column label="专业" prop="majorName" min-width="120" align="center" show-overflow-tooltip />
<el-table-column label="班级" prop="className" min-width="120" align="center" show-overflow-tooltip />
<!-- <el-table-column label="宿舍信息" min-width="150" align="center" show-overflow-tooltip>
<template #default="scope">
<div v-if="scope.row.dormInfo" class="dorm-info">
<div>{{ scope.row.dormInfo.campusName }}</div>
<div>{{ scope.row.dormInfo.buildingName }} - {{ scope.row.dormInfo.roomName }}</div>
</div>
<span v-else class="no-dorm">暂无宿舍信息</span>
</template>
</el-table-column> -->
<el-table-column label="操作" width="80" align="center" fixed="right">
<template #default="scope">
<el-button type="danger" size="small" link @click="removeInvolvedStudent(scope.row.id)"> 移除 </el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-form-item>
<!-- 完整宽度的字段 -->
<el-form-item label="违纪行为描述" prop="violationDescription">
<el-input v-model="form.violationDescription" type="textarea" placeholder="请输入违纪行为描述" :rows="3" />
@@ -483,7 +559,7 @@ import { ref, reactive, toRefs, onMounted, getCurrentInstance, ComponentInternal
import ElMessage from 'element-plus/es/components/message/index';
import ElMessageBox from 'element-plus/es/components/message-box/index';
import ElLoading from 'element-plus/es/components/loading/index';
import { UploadFilled, Document, Loading, Download, Delete } from '@element-plus/icons-vue';
import { UploadFilled, Document, Loading, Download, Delete, InfoFilled } from '@element-plus/icons-vue';
import { getToken } from '@/utils/auth';
import ImagePreview from '@/components/ImagePreview/index.vue';
import request from '@/utils/request';
@@ -494,6 +570,7 @@ import {
addDormViolation,
updateDormViolation,
getStudentOptions,
getStudentById,
getCampusOptions,
getAreaOptions,
getBuildingOptions,
@@ -530,6 +607,76 @@ const studentOptions = ref<StudentOptionVO[]>([]);
const studentLoading = ref(false);
const studentSelectVisible = ref(false);
// 涉及其他学生选择器相关状态
const involvedStudentOptions = ref<StudentOptionVO[]>([]);
const involvedStudentLoading = ref(false);
const involvedStudentSelectVisible = ref(false);
// 存储已选择的涉及学生完整信息
const selectedInvolvedStudentsMap = ref<Map<string | number, StudentOptionVO>>(new Map());
// 计算属性:获取已选择的涉及学生详细信息
const selectedInvolvedStudents = computed(() => {
if (!form.value.involvedStudents || !Array.isArray(form.value.involvedStudents)) {
return [];
}
console.log('计算涉及学生信息 - 当前ID列表:', form.value.involvedStudents);
console.log('当前Map大小:', selectedInvolvedStudentsMap.value.size);
console.log('当前涉及学生选项数量:', involvedStudentOptions.value.length);
const result = form.value.involvedStudents
.map((id) => {
console.log(`处理学生ID: ${id}`);
// 优先从已保存的完整信息中获取(包括预加载的占位符)
const savedStudent = selectedInvolvedStudentsMap.value.get(id);
if (savedStudent) {
console.log(`从Map中找到学生: ${savedStudent.name}`);
return savedStudent;
}
// 从涉及学生选项中查找详细信息
const student = involvedStudentOptions.value.find((s) => s.id === id);
if (student) {
console.log(`从涉及学生选项中找到: ${student.name}`);
// 保存到Map中,避免后续丢失
selectedInvolvedStudentsMap.value.set(id, student);
return student;
}
// 如果涉及学生选项中没有,从主学生选项中查找
const mainStudent = studentOptions.value.find((s) => s.id === id);
if (mainStudent) {
console.log(`从主学生选项中找到: ${mainStudent.name}`);
// 保存到Map中,避免后续丢失
selectedInvolvedStudentsMap.value.set(id, mainStudent);
return mainStudent;
}
console.log(`未找到学生 ${id} 的详细信息,创建临时占位符`);
// 如果都没有找到,创建一个临时的占位符信息
const tempPlaceholder = {
id: id,
name: `学生${id.toString().slice(-4)}`, // 使用ID后4位作为显示名称
studentNo: id.toString(),
gender: '',
collegeName: '信息加载中...',
majorName: '信息加载中...',
className: '信息加载中...',
dormInfo: null
};
// 将临时占位符也保存到Map中,避免重复创建
selectedInvolvedStudentsMap.value.set(id, tempPlaceholder);
return tempPlaceholder;
})
.filter((student) => student); // 过滤掉无效数据
console.log('最终涉及学生列表:', result);
return result;
});
// 宿舍级联选择器相关状态
const campusOptions = ref<CampusOptionVO[]>([]);
const areaOptions = ref<AreaOptionVO[]>([]);
@@ -538,6 +685,25 @@ const roomOptions = ref<RoomOptionVO[]>([]);
const bedOptions = ref<BedOptionVO[]>([]);
const dormLoading = ref(false);
// 缓存机制,避免重复请求
const dormCache = ref<Map<string, any[]>>(new Map());
const cacheExpireTime = 5 * 60 * 1000; // 5分钟缓存过期
const cacheTimestamps = ref<Map<string, number>>(new Map());
// 缓存工具函数
const getCachedData = (key: string) => {
const timestamp = cacheTimestamps.value.get(key);
if (timestamp && Date.now() - timestamp < cacheExpireTime) {
return dormCache.value.get(key);
}
return null;
};
const setCachedData = (key: string, data: any[]) => {
dormCache.value.set(key, data);
cacheTimestamps.value.set(key, Date.now());
};
// 附件上传相关状态
const attachmentList = ref<any[]>([]);
@@ -574,7 +740,7 @@ const initFormData: DormViolationForm = {
violationLevel: undefined,
violationTime: undefined,
violationDescription: undefined,
involvedStudents: undefined,
involvedStudents: [] as (string | number)[], // 初始化为空数组
attachmentOssIds: undefined,
deleted: undefined,
remark: undefined
@@ -624,7 +790,19 @@ const data = reactive<PageData<DormViolationForm, DormViolationQuery>>({
violationType: [{ required: true, message: '违纪类型不能为空', trigger: 'change' }],
violationLevel: [{ required: true, message: '违纪等级:1轻微 2一般 3严重 4特别严重不能为空', trigger: 'change' }],
violationTime: [{ required: true, message: '违纪发生时间不能为空', trigger: 'blur' }],
violationDescription: [{ required: true, message: '违纪行为描述不能为空', trigger: 'blur' }]
violationDescription: [{ required: true, message: '违纪行为描述不能为空', trigger: 'blur' }],
involvedStudents: [
{
validator: (rule: any, value: any, callback: any) => {
if (Array.isArray(value) && value.length > 10) {
callback(new Error('最多只能选择10名学生'));
} else {
callback();
}
},
trigger: 'change'
}
]
}
});
@@ -676,6 +854,9 @@ const reset = () => {
// 清空附件列表
attachmentList.value = [];
// 清空已选择的涉及学生信息Map
selectedInvolvedStudentsMap.value.clear();
};
/** 搜索按钮操作 */
@@ -706,19 +887,67 @@ const handleAdd = () => {
/** 修改按钮操作 */
const handleUpdate = async (row?: DormViolationVO) => {
reset();
const _id = row?.id || ids.value[0];
const res = await getDormViolation(_id);
Object.assign(form.value, res.data);
// 根据已有数据级联加载宿舍选项
await loadCascadeOptionsForUpdate(form.value);
// 处理附件数据
await handleAttachmentData();
// 先显示弹窗,提升用户体验
dialog.visible = true;
dialog.title = '修改宿舍违纪记录';
// 显示加载状态
const loadingInstance = ElLoading.service({
target: '.el-dialog__body',
text: '正在加载数据...',
background: 'rgba(255, 255, 255, 0.8)'
});
try {
reset();
const _id = row?.id || ids.value[0];
// 并行执行数据加载,提升性能
const [violationRes, campusRes] = await Promise.all([
getDormViolation(_id),
getCampusOptions() // 预加载校区选项
]);
Object.assign(form.value, violationRes.data);
campusOptions.value = campusRes.rows || [];
console.log('编辑违纪记录,使用原始数据:', violationRes.data);
// 处理涉及其他学生数据 - 将逗号分隔的字符串转换为数组
if (form.value.involvedStudents && typeof form.value.involvedStudents === 'string') {
form.value.involvedStudents = form.value.involvedStudents
.split(',')
.map((id) => id.trim())
.filter((id) => id) as (string | number)[];
console.log('编辑时的涉及学生ID列表:', form.value.involvedStudents);
}
// 并行执行其他数据加载任务
const loadTasks = [];
// 如果有涉及学生,异步加载学生信息
if (form.value.involvedStudents && form.value.involvedStudents.length > 0) {
loadTasks.push(
Promise.all([loadInvolvedStudentsInfo(form.value.involvedStudents), loadInvolvedStudentsForSelect(form.value.involvedStudents)])
);
}
// 异步加载宿舍级联选项
loadTasks.push(loadCascadeOptionsForUpdate(form.value));
// 异步处理附件数据
loadTasks.push(handleAttachmentData());
// 等待所有异步任务完成
await Promise.all(loadTasks);
} catch (error) {
console.error('加载编辑数据失败:', error);
ElMessage.error('加载数据失败,请重试');
dialog.visible = false;
} finally {
loadingInstance.close();
}
};
/** 提交按钮 */
@@ -733,6 +962,11 @@ const submitForm = () => {
// 将附件ID添加到表单数据中,处理成逗号分隔的字符串
form.value.attachmentOssIds = attachmentOssIds.join(',');
// 处理涉及其他学生数据 - 将数组转换为逗号分隔的字符串
if (Array.isArray(form.value.involvedStudents)) {
form.value.involvedStudents = form.value.involvedStudents.join(',');
}
if (form.value.id) {
await updateDormViolation(form.value).finally(() => (buttonLoading.value = false));
} else {
@@ -776,7 +1010,8 @@ const remoteSearchStudents = async (query: string) => {
};
const res = await getStudentOptions(params);
console.log('学生搜索响应:', res);
studentOptions.value = res.rows || [];
// 修复:正确处理API返回的数据结构
studentOptions.value = res.data?.rows || res.rows || [];
// 调试:检查第一个学生的数据
if (studentOptions.value.length > 0) {
@@ -818,7 +1053,8 @@ const loadInitialStudents = async () => {
};
const res = await getStudentOptions(params);
console.log('初始学生数据响应:', res);
studentOptions.value = res.rows || [];
// 修复:正确处理API返回的数据结构
studentOptions.value = res.data?.rows || res.rows || [];
// 调试:检查第一个学生的数据
if (studentOptions.value.length > 0) {
@@ -839,6 +1075,171 @@ const loadInitialStudents = async () => {
}
};
/** 远程搜索涉及其他学生 */
const remoteSearchInvolvedStudents = async (query: string) => {
involvedStudentLoading.value = true;
try {
const params: StudentOptionQuery = {
keyword: query.trim(),
pageNum: 1,
pageSize: 20
};
const res = await getStudentOptions(params);
console.log('涉及学生搜索响应:', res);
// 修复:正确处理API返回的数据结构
involvedStudentOptions.value = res.data?.rows || res.rows || [];
} catch (error) {
console.error('搜索涉及学生失败:', error);
involvedStudentOptions.value = [];
} finally {
involvedStudentLoading.value = false;
}
};
/** 涉及其他学生选择器可见性变化处理 */
const handleInvolvedStudentSelectVisibleChange = async (visible: boolean) => {
involvedStudentSelectVisible.value = visible;
// 当下拉框打开且没有数据时,加载初始数据
if (visible && involvedStudentOptions.value.length === 0) {
await loadInitialInvolvedStudents();
}
};
/** 加载初始涉及其他学生数据 */
const loadInitialInvolvedStudents = async () => {
involvedStudentLoading.value = true;
try {
const params: StudentOptionQuery = {
keyword: '', // 空关键词获取初始数据
pageNum: 1,
pageSize: 20
};
const res = await getStudentOptions(params);
console.log('初始涉及学生数据响应:', res);
// 修复:正确处理API返回的数据结构
involvedStudentOptions.value = res.data?.rows || res.rows || [];
} catch (error) {
console.error('加载初始涉及学生数据失败:', error);
involvedStudentOptions.value = [];
} finally {
involvedStudentLoading.value = false;
}
};
/** 预加载涉及学生的完整信息 */
const loadInvolvedStudentsInfo = async (studentIds: (string | number)[]) => {
// 批量获取学生信息,减少接口调用次数
try {
const studentPromises = studentIds.map(async (studentId) => {
try {
const studentRes = await getStudentById(studentId);
const studentInfo = studentRes.data;
selectedInvolvedStudentsMap.value.set(studentId, studentInfo);
console.log(`预加载学生 ${studentId} 信息:`, studentInfo);
return { studentId, success: true, data: studentInfo };
} catch (error) {
console.error(`预加载学生 ${studentId} 信息失败:`, error);
// 创建临时占位符
const tempPlaceholder = {
id: studentId,
name: `学生${studentId.toString().slice(-4)}`,
studentNo: studentId.toString(),
gender: '',
collegeName: '信息加载失败',
majorName: '信息加载失败',
className: '信息加载失败',
dormInfo: null
};
selectedInvolvedStudentsMap.value.set(studentId, tempPlaceholder);
return { studentId, success: false, data: tempPlaceholder };
}
});
// 并行执行所有学生信息获取
await Promise.all(studentPromises);
} catch (error) {
console.error('批量加载涉及学生信息失败:', error);
}
};
/** 为选择框加载涉及学生信息 */
const loadInvolvedStudentsForSelect = async (studentIds: (string | number)[]) => {
try {
// 获取涉及学生的完整信息
const studentsInfo: StudentOptionVO[] = [];
for (const studentId of studentIds) {
try {
const studentRes = await getStudentById(studentId);
const studentInfo = studentRes.data;
studentsInfo.push(studentInfo);
console.log(`为选择框加载学生 ${studentId} 信息:`, studentInfo);
} catch (error) {
console.error(`为选择框加载学生 ${studentId} 信息失败:`, error);
// 创建临时占位符
const tempPlaceholder = {
id: studentId,
name: `学生${studentId.toString().slice(-4)}`,
studentNo: studentId.toString(),
gender: '',
collegeName: '信息加载失败',
majorName: '信息加载失败',
className: '信息加载失败',
dormInfo: null
};
studentsInfo.push(tempPlaceholder);
}
}
// 将学生信息添加到涉及学生选项中,确保选择框能正确显示
involvedStudentOptions.value = [...studentsInfo];
console.log('涉及学生选择框选项已更新:', involvedStudentOptions.value);
} catch (error) {
console.error('为选择框加载涉及学生信息失败:', error);
}
};
/** 涉及其他学生选择变化处理 */
const handleInvolvedStudentsChange = (selectedIds: (string | number)[]) => {
// 确保不会选择主学生
if (form.value.studentId && selectedIds.includes(form.value.studentId)) {
ElMessage.warning('不能选择主学生作为涉及其他学生');
// 移除主学生ID
form.value.involvedStudents = selectedIds.filter((id) => id !== form.value.studentId) as (string | number)[];
} else {
// 直接更新选择的学生列表
form.value.involvedStudents = selectedIds as (string | number)[];
}
// 确保新选择的学生信息被保存
selectedIds.forEach((id) => {
if (!selectedInvolvedStudentsMap.value.has(id)) {
// 从当前选项中查找并保存学生信息
const student = involvedStudentOptions.value.find((s) => s.id === id);
if (student) {
selectedInvolvedStudentsMap.value.set(id, student);
} else {
// 从主学生选项中查找
const mainStudent = studentOptions.value.find((s) => s.id === id);
if (mainStudent) {
selectedInvolvedStudentsMap.value.set(id, mainStudent);
}
}
}
});
};
/** 移除涉及学生 */
const removeInvolvedStudent = (studentId: string | number) => {
if (Array.isArray(form.value.involvedStudents)) {
form.value.involvedStudents = form.value.involvedStudents.filter((id) => id !== studentId) as (string | number)[];
// 从Map中也移除该学生信息
selectedInvolvedStudentsMap.value.delete(studentId);
ElMessage.success('已移除该学生');
}
};
/** 学生选择处理 */
const handleStudentChange = async (studentId: string | number) => {
if (!studentId) {
@@ -868,6 +1269,11 @@ const handleStudentChange = async (studentId: string | number) => {
return;
}
// 如果主学生ID在涉及其他学生列表中,则移除
if (Array.isArray(form.value.involvedStudents)) {
form.value.involvedStudents = form.value.involvedStudents.filter((id) => id !== studentId) as (string | number)[];
}
const selectedStudent = studentOptions.value.find((student) => student.id === studentId);
if (selectedStudent) {
// 填充学生基本信息
@@ -989,9 +1395,19 @@ const handleCampusChange = async (campusId: string | number) => {
/** 加载宿舍区选项 */
const loadAreaOptions = async (campusId: string | number) => {
try {
const cacheKey = `area_${campusId}`;
const cachedData = getCachedData(cacheKey);
if (cachedData) {
areaOptions.value = cachedData;
return;
}
dormLoading.value = true;
const res = await getAreaOptions(campusId);
areaOptions.value = res.rows || [];
const data = res.rows || [];
areaOptions.value = data;
setCachedData(cacheKey, data);
} catch (error) {
console.error('加载宿舍区选项失败:', error);
areaOptions.value = [];
@@ -1141,25 +1557,33 @@ const handleBedChange = (bedId: string | number) => {
/** 修改时级联加载宿舍选项数据 */
const loadCascadeOptionsForUpdate = async (formData: DormViolationForm) => {
try {
// 并行加载所有需要的宿舍选项,提升性能
const loadTasks = [];
// 如果有校区ID,加载宿舍区选项
if (formData.campusId) {
await loadAreaOptions(formData.campusId);
loadTasks.push(loadAreaOptions(formData.campusId));
// 如果有宿舍区ID,加载宿舍楼选项
if (formData.areaId) {
await loadBuildingOptions(formData.areaId);
loadTasks.push(loadBuildingOptions(formData.areaId));
// 如果有宿舍楼ID,加载房间选项
if (formData.buildingId) {
await loadRoomOptions(formData.buildingId);
loadTasks.push(loadRoomOptions(formData.buildingId));
// 如果有房间ID,加载床位选项
if (formData.roomId) {
await loadBedOptions(formData.roomId);
loadTasks.push(loadBedOptions(formData.roomId));
}
}
}
}
// 并行执行所有加载任务
if (loadTasks.length > 0) {
await Promise.all(loadTasks);
}
} catch (error) {
console.error('修改时级联加载宿舍选项失败:', error);
}
@@ -1364,56 +1788,63 @@ const handleDownloadAttachment = async (file: any) => {
// 处理附件数据
const handleAttachmentData = async () => {
try {
// 如果有附件ID,则获取附件信息
if (form.value.attachmentOssIds && form.value.attachmentOssIds.toString().trim()) {
console.log('开始处理附件数据:', form.value.attachmentOssIds);
// 将逗号分隔的字符串转换为数组
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) {
console.log('获取到的附件信息:', response.data);
// 将附件信息转换为上传组件需要的格式
const attachmentFiles = response.data.map((item: any, index: number) => ({
uid: `existing_${index}_${Date.now()}`,
name: item.originalName || `附件${index + 1}`,
url: item.url,
ossId: item.ossId,
status: 'success', // 标记为已上传成功
response: {
code: 200,
data: {
url: item.url,
ossId: item.ossId
}
}
}));
// 设置附件列表
attachmentList.value = attachmentFiles;
console.log('设置附件列表成功:', attachmentList.value);
} else {
console.warn('获取附件信息失败:', response.msg);
attachmentList.value = [];
}
} else {
// 如果没有附件ID,直接返回,避免不必要的请求
if (!form.value.attachmentOssIds || !form.value.attachmentOssIds.toString().trim()) {
console.log('没有附件数据,清空附件列表');
attachmentList.value = [];
return;
}
console.log('开始处理附件数据:', form.value.attachmentOssIds);
// 将逗号分隔的字符串转换为数组
const ossIdsArray = form.value.attachmentOssIds
.toString()
.split(',')
.map((id) => id.trim())
.filter((id) => id);
// 如果附件ID数组为空,直接返回
if (ossIdsArray.length === 0) {
attachmentList.value = [];
return;
}
// 调用接口获取附件信息
const response = await request({
url: `/resource/oss/listByIds/${ossIdsArray.join(',')}`,
method: 'get'
});
if (response.code === 200 && response.data) {
console.log('获取到的附件信息:', response.data);
// 将附件信息转换为上传组件需要的格式
const attachmentFiles = response.data.map((item: any, index: number) => ({
uid: `existing_${index}_${Date.now()}`,
name: item.originalName || `附件${index + 1}`,
url: item.url,
ossId: item.ossId,
status: 'success', // 标记为已上传成功
response: {
code: 200,
data: {
url: item.url,
ossId: item.ossId
}
}
}));
// 设置附件列表
attachmentList.value = attachmentFiles;
console.log('设置附件列表成功:', attachmentList.value);
} else {
console.warn('获取附件信息失败:', response.msg);
attachmentList.value = [];
}
} catch (error) {
console.error('处理附件数据失败:', error);
ElMessage.error('获取附件信息失败,请稍后重试');
// 不显示错误提示,避免影响用户体验
attachmentList.value = [];
}
};
@@ -1483,6 +1914,27 @@ onMounted(() => {
padding: 4px 12px !important;
}
:global(.involved-student-select-dropdown .el-select-dropdown__item) {
height: auto !important;
min-height: 32px !important;
padding: 4px 12px !important;
}
/* 表单提示样式 */
.form-tip {
display: flex;
align-items: center;
gap: 4px;
margin-top: 4px;
font-size: 12px;
color: #909399;
}
.form-tip .el-icon {
font-size: 14px;
color: #409eff;
}
/* 附件上传容器样式 */
.attachment-upload-container {
width: 100%;
@@ -1692,4 +2144,74 @@ onMounted(() => {
cursor: not-allowed;
background: #f5f5f5;
}
/* 涉及其他学生表格样式 */
.involved-students-table-container {
margin-top: 12px;
border: 1px solid #e4e7ed;
border-radius: 6px;
background: #fafafa;
overflow: hidden;
}
.table-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
}
.table-title {
font-size: 13px;
font-weight: 500;
color: #303133;
}
.table-count {
font-size: 12px;
color: #909399;
}
.involved-students-table {
background: #fff;
}
.involved-students-table :deep(.el-table__header) {
background: #f5f7fa;
}
.involved-students-table :deep(.el-table__header th) {
background: #f5f7fa;
color: #606266;
font-weight: 500;
font-size: 12px;
}
.involved-students-table :deep(.el-table__body td) {
font-size: 12px;
padding: 6px 0;
}
.dorm-info {
font-size: 11px;
line-height: 1.3;
}
.dorm-info div:first-child {
color: #303133;
font-weight: 500;
}
.dorm-info div:last-child {
color: #606266;
margin-top: 2px;
}
.no-dorm {
color: #c0c4cc;
font-size: 11px;
font-style: italic;
}
</style>