wuzhen update

This commit is contained in:
wuzhen committed 2025-08-15 17:45:09 +08:00
1 parent 7a4881592a
commit b0d47046c4
15 files changed
+897 -211

No files matched your search

@@ -82,4 +82,10 @@ public class ZsjyInternshipDailyQueryBo extends BaseEntity {
@Schema(description = "公司名称") @Schema(description = "公司名称")
private String companyName; private String companyName;
/**
* 班主任ID(用于权限控制)
*/
@Schema(description = "班主任ID")
private Long headTeacherId;
} }
@@ -61,4 +61,12 @@ public interface ZsjyInternshipDailyMapper extends BaseMapper<ZsjyInternshipDail
boolean existsByStudentAndDate(@Param("studentId") Long studentId, boolean existsByStudentAndDate(@Param("studentId") Long studentId,
@Param("checkDate") Date checkDate); @Param("checkDate") Date checkDate);
/**
* 统计班主任管理的班级数量
*
* @param teacherId 班主任ID
* @return 班级数量
*/
Long countClassesByTeacherId(@Param("teacherId") Long teacherId);
} }
@@ -48,8 +48,19 @@ public class ZsjyInternshipDailyServiceImpl implements IZsjyInternshipDailyServi
*/ */
@Override @Override
public TableDataInfo<ZsjyInternshipDailyVo> queryPageList(ZsjyInternshipDailyQueryBo queryBo, PageQuery pageQuery) { public TableDataInfo<ZsjyInternshipDailyVo> queryPageList(ZsjyInternshipDailyQueryBo queryBo, PageQuery pageQuery) {
log.info("=== 实习日常查询后端调试日志 ===");
log.info("1. 查询参数: {}", queryBo);
log.info("2. 分页参数: pageNum={}, pageSize={}", pageQuery.getPageNum(), pageQuery.getPageSize());
// 设置数据权限
setDataPermission(queryBo);
log.info("3. 设置数据权限后的查询参数: {}", queryBo);
log.info("3.1. 权限控制参数: headTeacherId={}", queryBo.getHeadTeacherId());
Page<ZsjyInternshipDailyVo> page = pageQuery.build(); Page<ZsjyInternshipDailyVo> page = pageQuery.build();
List<ZsjyInternshipDailyVo> list = baseMapper.selectDailyList(page, queryBo); List<ZsjyInternshipDailyVo> list = baseMapper.selectDailyList(page, queryBo);
log.info("4. 查询到的数据条数: {}", list != null ? list.size() : "null");
return TableDataInfo.build(list); return TableDataInfo.build(list);
} }
@@ -58,6 +69,9 @@ public class ZsjyInternshipDailyServiceImpl implements IZsjyInternshipDailyServi
*/ */
@Override @Override
public List<ZsjyInternshipDailyVo> queryList(ZsjyInternshipDailyQueryBo queryBo) { public List<ZsjyInternshipDailyVo> queryList(ZsjyInternshipDailyQueryBo queryBo) {
// 设置数据权限
setDataPermission(queryBo);
return baseMapper.selectDailyList(null, queryBo); return baseMapper.selectDailyList(null, queryBo);
} }
@@ -202,4 +216,46 @@ public class ZsjyInternshipDailyServiceImpl implements IZsjyInternshipDailyServi
} }
} }
/**
* 设置数据权限(班主任只能查看自己班级的学生数据)
*/
private void setDataPermission(ZsjyInternshipDailyQueryBo queryBo) {
try {
// 获取当前登录用户ID
Long currentUserId = LoginHelper.getUserId();
log.info("=== 权限控制调试 ===");
log.info("当前登录用户ID: {}", currentUserId);
if (ObjectUtil.isNotNull(currentUserId)) {
// 检查该用户是否是班主任
Long classTeacherCount = baseMapper.countClassesByTeacherId(currentUserId);
log.info("用户ID {}作为班主任管理的班级数量: {}", currentUserId, classTeacherCount);
if (classTeacherCount > 0) {
// 是班主任,设置权限限制
queryBo.setHeadTeacherId(currentUserId);
log.info("✅ 权限判断:用户是班主任,设置headTeacherId={}", currentUserId);
} else {
// 不是班主任,设置不存在的班主任ID,使其无法查看任何数据
log.info("❌ 权限判断:用户不是班主任,设置权限限制");
// 设置一个不存在的班主任ID(-1),这样SQL查询结果为空
queryBo.setHeadTeacherId(-1L);
log.info("🔒 设置数据权限:非班主任用户无法查看数据,headTeacherId=-1");
}
// 输出最终的权限设置
log.info("🎯 最终权限设置:queryBo.headTeacherId = {}", queryBo.getHeadTeacherId());
} else {
log.warn("⚠️ 未获取到当前用户ID,设置权限限制");
queryBo.setHeadTeacherId(-1L);
}
} catch (Exception e) {
log.error("❌ 设置数据权限失败:{}", e.getMessage(), e);
// 异常情况下,设置权限限制以确保数据安全
queryBo.setHeadTeacherId(-1L);
log.warn("🔒 异常情况下设置权限限制,headTeacherId=-1");
}
}
} }
@@ -104,6 +104,15 @@
<if test="queryBo.companyName != null and queryBo.companyName != ''"> <if test="queryBo.companyName != null and queryBo.companyName != ''">
AND i.company_name LIKE CONCAT('%', #{queryBo.companyName}, '%') AND i.company_name LIKE CONCAT('%', #{queryBo.companyName}, '%')
</if> </if>
<!-- 权限控制:班主任只能查看自己班级的学生数据 -->
<if test="queryBo.headTeacherId != null">
AND EXISTS (
SELECT 1 FROM xg_student_msg s
WHERE s.id = d.student_id
AND s.class_teacher_id = #{queryBo.headTeacherId}
AND s.deleted = 0
)
</if>
</where> </where>
ORDER BY d.check_date DESC, d.create_time DESC ORDER BY d.check_date DESC, d.create_time DESC
</select> </select>
@@ -184,4 +193,13 @@
AND deleted = 0 AND deleted = 0
</select> </select>
<!-- 统计班主任管理的班级数量 -->
<select id="countClassesByTeacherId" resultType="java.lang.Long">
SELECT COUNT(DISTINCT s.class_id)
FROM xg_student_msg s
WHERE s.class_teacher_id = #{teacherId}
AND s.deleted = 0
AND s.class_id IS NOT NULL
</select>
</mapper> </mapper>
+297 -63
View File
@@ -92,66 +92,112 @@
<!-- 位置信息卡片 --> <!-- 位置信息卡片 -->
<view class="location-card" v-if="currentLocation"> <view class="location-card" v-if="currentLocation">
<view class="card-header"> <view class="card-header">
<u-icon name="map-pin" size="18" color="#f56c6c"></u-icon> <view class="header-left">
<text class="card-title">当前位置</text> <u-icon name="map-pin" size="18" color="#f56c6c"></u-icon>
<text class="card-title">当前位置</text>
</view>
<u-button <u-button
type="info" type="info"
size="mini" size="mini"
text="重新定位" text="重新定位"
@click="refreshLocation" @click="refreshLocation"
:loading="locationLoading" :loading="locationLoading"
class="refresh-btn"
></u-button> ></u-button>
</view> </view>
<view class="location-info">
<view class="location-text">{{ currentLocation.address }}</view> <!-- 地址信息 -->
<view class="coordinates"> <view class="location-address">
<text class="coord-text">经度:{{ currentLocation.longitude.toFixed(6) }}</text> <u-icon name="location" size="14" color="#909399" class="address-icon"></u-icon>
<text class="coord-text">纬度:{{ currentLocation.latitude.toFixed(6) }}</text> <text class="address-text">{{ currentLocation.address }}</text>
</view>
<!-- 坐标信息 -->
<view class="coordinates-section">
<view class="coord-row">
<view class="coord-item">
<text class="coord-label">经度</text>
<text class="coord-value">{{ currentLocation.longitude.toFixed(6) }}</text>
</view>
<view class="coord-divider"></view>
<view class="coord-item">
<text class="coord-label">纬度</text>
<text class="coord-value">{{ currentLocation.latitude.toFixed(6) }}</text>
</view>
</view> </view>
<view class="location-accuracy" v-if="currentLocation.accuracy"> </view>
<text class="accuracy-text">定位精度:{{ currentLocation.accuracy.toFixed(0) }}米</text>
<!-- 精度信息 -->
<view class="accuracy-section" v-if="currentLocation.accuracy">
<view class="accuracy-info">
<u-icon name="target" size="14" color="#67c23a" class="accuracy-icon"></u-icon>
<text class="accuracy-text">定位精度 {{ currentLocation.accuracy.toFixed(0) }}米</text>
<u-tag <u-tag
:text="getAccuracyStatus(currentLocation.accuracy).text" :text="getAccuracyStatus(currentLocation.accuracy).text"
:type="getAccuracyStatus(currentLocation.accuracy).type" :type="getAccuracyStatus(currentLocation.accuracy).type"
size="mini" size="mini"
style="margin-left: 8px;" class="accuracy-tag"
></u-tag> ></u-tag>
</view> </view>
<view class="location-actions"> </view>
<u-button
type="primary" <!-- 操作按钮 -->
size="mini" <view class="location-actions">
plain <u-button
@click="openLocationInMap" type="primary"
style="margin-top: 8px;" size="default"
> plain
<u-icon name="map" size="16" color="#2979ff" /> @click="openLocationInMap"
<text style="margin-left: 4px;">在地图中查看</text> class="map-btn"
</u-button> >
</view> <u-icon name="map" size="16" color="#2979ff" />
<text class="btn-text">在地图中查看</text>
</u-button>
</view> </view>
</view> </view>
<!-- 位置获取失败提示 --> <!-- 位置获取失败提示 -->
<view class="location-error-card" v-else-if="locationError"> <view class="location-error-card" v-else-if="locationError">
<view class="card-header"> <view class="card-header">
<u-icon name="map-pin" size="18" color="#f56c6c"></u-icon> <view class="header-left">
<text class="card-title">位置信息</text> <u-icon name="map-pin" size="18" color="#f56c6c"></u-icon>
<text class="card-title">位置信息</text>
</view>
<u-button <u-button
type="primary" type="primary"
size="mini" size="mini"
text="重新定位" text="重新定位"
@click="refreshLocation" @click="refreshLocation"
:loading="locationLoading" :loading="locationLoading"
class="refresh-btn"
></u-button> ></u-button>
</view> </view>
<view class="error-content"> <view class="error-content">
<u-icon name="warning" size="24" color="#e6a23c"></u-icon> <view class="error-header">
<text class="error-text">{{ locationError }}</text> <u-icon name="warning-circle" size="28" color="#e6a23c" class="error-icon"></u-icon>
<text class="error-tips">签到需要位置信息,请确保:</text> <text class="error-title">无法获取位置信息</text>
<text class="error-tips">• 已开启位置权限</text> </view>
<text class="error-tips">• GPS/网络定位已开启</text>
<text class="error-tips">• 网络连接正常</text> <text class="error-message">{{ locationError }}</text>
<view class="error-tips-section">
<text class="tips-title">签到需要位置信息,请确保:</text>
<view class="tips-list">
<view class="tip-item">
<u-icon name="checkmark-circle" size="14" color="#67c23a" class="tip-icon"></u-icon>
<text class="tip-text">已开启位置权限</text>
</view>
<view class="tip-item">
<u-icon name="checkmark-circle" size="14" color="#67c23a" class="tip-icon"></u-icon>
<text class="tip-text">GPS/网络定位已开启</text>
</view>
<view class="tip-item">
<u-icon name="checkmark-circle" size="14" color="#67c23a" class="tip-icon"></u-icon>
<text class="tip-text">网络连接正常</text>
</view>
</view>
</view>
</view> </view>
</view> </view>
@@ -997,19 +1043,29 @@ export default {
.card-header { .card-header {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 12px; justify-content: space-between;
padding-bottom: 8px; margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0;
} }
.header-left {
display: flex;
align-items: center;
flex: 1;
}
.card-title { .card-title {
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
margin-left: 8px; margin-left: 8px;
flex: 1;
color: #303133; color: #303133;
} }
.refresh-btn {
flex-shrink: 0;
}
.date-text { .date-text {
font-size: 12px; font-size: 12px;
color: #909399; color: #909399;
@@ -1099,43 +1155,112 @@ export default {
} }
/* 位置信息 */ /* 位置信息 */
.location-info { .location-address {
padding-top: 8px; display: flex;
align-items: flex-start;
margin-bottom: 16px;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
border-left: 3px solid #67c23a;
} }
.location-text { .address-icon {
margin-right: 8px;
margin-top: 2px;
flex-shrink: 0;
}
.address-text {
font-size: 14px; font-size: 14px;
color: #303133; color: #303133;
margin-bottom: 8px;
line-height: 1.5; line-height: 1.5;
flex: 1;
} }
.coordinates { /* 坐标信息 */
display: flex; .coordinates-section {
justify-content: space-between; margin-bottom: 16px;
} }
.coord-text { .coord-row {
font-size: 12px;
color: #909399;
}
.location-accuracy {
display: flex; display: flex;
align-items: center; align-items: center;
margin-top: 8px; background: #f8f9fa;
margin-bottom: 8px; border-radius: 8px;
padding: 12px;
}
.coord-item {
flex: 1;
text-align: center;
}
.coord-divider {
width: 1px;
height: 32px;
background: #e4e7ed;
margin: 0 16px;
}
.coord-label {
display: block;
font-size: 12px;
color: #909399;
margin-bottom: 4px;
}
.coord-value {
display: block;
font-size: 14px;
color: #303133;
font-weight: 500;
font-family: monospace;
}
/* 精度信息 */
.accuracy-section {
margin-bottom: 16px;
}
.accuracy-info {
display: flex;
align-items: center;
padding: 10px 12px;
background: #f0f9ff;
border-radius: 8px;
border: 1px solid #b3e5fc;
}
.accuracy-icon {
margin-right: 8px;
flex-shrink: 0;
} }
.accuracy-text { .accuracy-text {
font-size: 12px; font-size: 13px;
color: #909399; color: #67c23a;
margin-right: 8px; font-weight: 500;
flex: 1;
} }
.accuracy-tag {
margin-left: 8px;
flex-shrink: 0;
}
/* 操作按钮 */
.location-actions { .location-actions {
display: flex; display: flex;
justify-content: flex-end; justify-content: center;
}
.map-btn {
min-width: 140px;
}
.btn-text {
margin-left: 6px;
} }
/* 位置获取失败提示 */ /* 位置获取失败提示 */
@@ -1149,24 +1274,73 @@ export default {
} }
.error-content { .error-content {
display: flex; padding-top: 8px;
flex-direction: column;
align-items: center;
padding-top: 12px;
} }
.error-text { .error-header {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
}
.error-icon {
margin-right: 8px;
}
.error-title {
font-size: 16px;
font-weight: bold;
color: #e6a23c;
}
.error-message {
display: block;
font-size: 14px; font-size: 14px;
color: #faad14; color: #faad14;
margin-top: 8px;
text-align: center; text-align: center;
margin-bottom: 20px;
line-height: 1.5;
} }
.error-tips { .error-tips-section {
font-size: 12px; background: #fff7e6;
color: #faad14; border-radius: 8px;
margin-top: 4px; padding: 16px;
text-align: center; border: 1px solid #ffd591;
}
.tips-title {
display: block;
font-size: 14px;
font-weight: bold;
color: #d48806;
margin-bottom: 12px;
}
.tips-list {
padding-left: 0;
}
.tip-item {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.tip-item:last-child {
margin-bottom: 0;
}
.tip-icon {
margin-right: 8px;
flex-shrink: 0;
}
.tip-text {
font-size: 13px;
color: #d48806;
line-height: 1.4;
} }
/* 统计数据 */ /* 统计数据 */
@@ -1245,4 +1419,64 @@ export default {
color: #909399; color: #909399;
margin-top: 8px; margin-top: 8px;
} }
/* 响应式设计 */
@media screen and (max-width: 375px) {
.coord-row {
padding: 10px;
}
.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);
}
}
</style> </style>
+6 -6
View File
@@ -1,4 +1,4 @@
import request from '@/utils/request' import request from '@/utils/request';
// 查询实习日常记录列表 // 查询实习日常记录列表
export function listInternshipDaily(query) { export function listInternshipDaily(query) {
@@ -6,7 +6,7 @@ export function listInternshipDaily(query) {
url: '/zsjy/internship/daily/list', url: '/zsjy/internship/daily/list',
method: 'get', method: 'get',
params: query params: query
}) });
} }
// 学生签到打卡 // 学生签到打卡
@@ -15,7 +15,7 @@ export function checkIn(data) {
url: '/zsjy/internship/daily/checkin', url: '/zsjy/internship/daily/checkin',
method: 'post', method: 'post',
data: data data: data
}) });
} }
// 新增实习日常记录 // 新增实习日常记录
@@ -24,7 +24,7 @@ export function addInternshipDaily(data) {
url: '/zsjy/internship/daily', url: '/zsjy/internship/daily',
method: 'post', method: 'post',
data: data data: data
}) });
} }
// 获取学生考勤日历 // 获取学生考勤日历
@@ -33,7 +33,7 @@ export function getCalendar(studentId, params) {
url: `/zsjy/internship/daily/calendar/${studentId}`, url: `/zsjy/internship/daily/calendar/${studentId}`,
method: 'get', method: 'get',
params: params params: params
}) });
} }
// 获取考勤统计数据 // 获取考勤统计数据
@@ -42,5 +42,5 @@ export function getStatistics(query) {
url: '/zsjy/internship/daily/statistics', url: '/zsjy/internship/daily/statistics',
method: 'get', method: 'get',
params: query params: query
}) });
} }
+290
View File
@@ -1195,6 +1195,16 @@ export interface InternshipInfoQuery {
* 实习开始日期-结束 * 实习开始日期-结束
*/ */
internshipStartDateEnd?: string; internshipStartDateEnd?: string;
/**
* 班主任ID(用于数据权限控制)
*/
classTeacherId?: string | number;
/**
* 辅导员ID(用于数据权限控制)
*/
counselorId?: string | number;
} }
/** /**
@@ -1261,3 +1271,283 @@ export interface InternshipStatisticsVO {
*/ */
internshipRate?: string; internshipRate?: string;
} }
/**
* 班主任走访记录 VO
*/
export interface InternshipVisitVO {
/**
* 走访记录ID
*/
visitId?: string | number;
/**
* 实习记录ID
*/
internshipId?: string | number;
/**
* 学生ID
*/
studentId?: string | number;
/**
* 学生姓名
*/
studentName?: string;
/**
* 学号
*/
studentNumber?: string;
/**
* 班级名称
*/
className?: string;
/**
* 专业名称
*/
majorName?: string;
/**
* 学院名称
*/
collegeName?: string;
/**
* 企业名称
*/
companyName?: string;
/**
* 企业地址
*/
companyAddress?: string;
/**
* 走访日期
*/
visitDate?: string;
/**
* 走访人员
*/
visitorName?: string;
/**
* 走访人员职务
*/
visitorPosition?: string;
/**
* 走访目的
*/
visitPurpose?: string;
/**
* 企业情况
*/
companySituation?: string;
/**
* 学生表现
*/
studentPerformance?: string;
/**
* 发现问题
*/
problemsFound?: string;
/**
* 改进建议
*/
suggestions?: string;
/**
* 走访总结
*/
visitSummary?: string;
/**
* 现场照片(JSON格式)
*/
visitImages?: string;
/**
* 走访文件附件(JSON格式)
*/
visitFiles?: string;
/**
* 状态(0-草稿 1-已提交 2-已审核)
*/
status?: string;
/**
* 备注
*/
remark?: string;
/**
* 创建时间
*/
createTime?: string;
/**
* 创建者
*/
createBy?: number;
/**
* 更新时间
*/
updateTime?: string;
/**
* 更新者
*/
updateBy?: number;
}
/**
* 班主任走访记录查询参数
*/
export interface InternshipVisitQuery extends PageQuery {
/**
* 学生姓名
*/
studentName?: string;
/**
* 学号
*/
studentNumber?: string;
/**
* 企业名称
*/
companyName?: string;
/**
* 走访人员
*/
visitorName?: string;
/**
* 走访日期-开始
*/
visitDateBegin?: string;
/**
* 走访日期-结束
*/
visitDateEnd?: string;
/**
* 状态
*/
status?: string;
}
/**
* 班主任走访记录新增参数
*/
export interface InternshipVisitAddBO {
/**
* 实习记录ID
*/
internshipId?: string | number;
/**
* 学生ID
*/
studentId?: string | number;
/**
* 企业名称
*/
companyName?: string;
/**
* 企业地址
*/
companyAddress?: string;
/**
* 走访日期
*/
visitDate?: string;
/**
* 走访人员
*/
visitorName?: string;
/**
* 走访人员职务
*/
visitorPosition?: string;
/**
* 走访目的
*/
visitPurpose?: string;
/**
* 企业情况
*/
companySituation?: string;
/**
* 学生表现
*/
studentPerformance?: string;
/**
* 发现问题
*/
problemsFound?: string;
/**
* 改进建议
*/
suggestions?: string;
/**
* 走访总结
*/
visitSummary?: string;
/**
* 现场照片(JSON格式)
*/
visitImages?: string;
/**
* 走访文件附件(JSON格式)
*/
visitFiles?: string;
/**
* 状态
*/
status?: string;
/**
* 备注
*/
remark?: string;
}
/**
* 班主任走访记录编辑参数
*/
export interface InternshipVisitEditBO extends InternshipVisitAddBO {
/**
* 走访记录ID
*/
visitId?: string | number;
}
+88 -4
View File
@@ -455,14 +455,38 @@ const handleBeforeUpload = (file: any) => {
} }
} }
// 设置超时处理 - 如果60秒后仍在loading状态,自动关闭并提示
const timeoutId = setTimeout(() => {
if (number.value > 0) {
console.warn('上传超时,自动关闭loading');
(proxy as any)?.$modal.closeLoading();
(proxy as any)?.$modal.msgError('上传超时,请检查网络连接或重试');
number.value = 0; // 重置计数器
}
}, 60000); // 60秒超时
//压缩图片,开启压缩并且大于指定的压缩大小时才压缩 //压缩图片,开启压缩并且大于指定的压缩大小时才压缩
if (props.compressSupport && file.size / 1024 > props.compressTargetSize) { if (props.compressSupport && file.size / 1024 > props.compressTargetSize) {
(proxy as any)?.$modal.loading('正在上传图片,请稍候...'); (proxy as any)?.$modal.loading('正在上传图片,请稍候...');
number.value++; number.value++;
return compressAccurately(file, props.compressTargetSize);
// 压缩图片时的错误处理
try {
return compressAccurately(file, props.compressTargetSize);
} catch (compressionError) {
console.error('图片压缩失败:', compressionError);
clearTimeout(timeoutId);
(proxy as any)?.$modal.closeLoading();
(proxy as any)?.$modal.msgError('图片压缩失败,请重试');
number.value--;
return false;
}
} else { } else {
(proxy as any)?.$modal.loading('正在上传图片,请稍候...'); (proxy as any)?.$modal.loading('正在上传图片,请稍候...');
number.value++; number.value++;
// 保存timeout ID以便在上传完成时清除
(file as any).uploadTimeoutId = timeoutId;
} }
}; };
@@ -476,6 +500,11 @@ const handleUploadSuccess = (res: any, file: any) => {
console.log('handleUploadSuccess:res', res); console.log('handleUploadSuccess:res', res);
console.log('handleUploadSuccess:file', file); console.log('handleUploadSuccess:file', file);
// 清除上传超时处理
if ((file as any).uploadTimeoutId) {
clearTimeout((file as any).uploadTimeoutId);
}
if (res.code === 200) { if (res.code === 200) {
const uploadItem = { const uploadItem = {
name: res.data.fileName, name: res.data.fileName,
@@ -488,7 +517,9 @@ const handleUploadSuccess = (res: any, file: any) => {
} else { } else {
number.value--; number.value--;
(proxy as any)?.$modal.closeLoading(); (proxy as any)?.$modal.closeLoading();
(proxy as any)?.$modal.msgError(res.msg); // 提供更详细的错误信息
const errorMsg = res.msg || '上传失败,请重试';
(proxy as any)?.$modal.msgError(errorMsg);
imageUploadRef.value?.handleRemove(file); imageUploadRef.value?.handleRemove(file);
uploadedSuccessfully(); uploadedSuccessfully();
} }
@@ -657,9 +688,62 @@ const uploadedSuccessfully = async () => {
}; };
// 上传失败 // 上传失败
const handleUploadError = () => { const handleUploadError = (error: any, file: any) => {
(proxy as any)?.$modal.msgError('上传图片失败'); console.error('上传失败详细信息:', error);
// 清除上传超时处理
if (file && (file as any).uploadTimeoutId) {
clearTimeout((file as any).uploadTimeoutId);
}
// 确保关闭loading
(proxy as any)?.$modal.closeLoading(); (proxy as any)?.$modal.closeLoading();
// 减少计数器
number.value--;
// 根据错误类型提供不同的错误信息
let errorMessage = '上传图片失败';
if (error.response) {
const status = error.response.status;
const responseData = error.response.data;
switch (status) {
case 403:
errorMessage = '没有文件上传权限,请联系管理员';
break;
case 413:
errorMessage = '文件太大,超过服务器限制';
break;
case 500:
errorMessage = '服务器内部错误,请稍后重试';
break;
case 504:
errorMessage = '上传超时,请检查网络连接';
break;
default:
if (responseData && responseData.msg) {
errorMessage = responseData.msg;
} else if (responseData && typeof responseData === 'string') {
errorMessage = `上传失败: ${responseData}`;
} else {
errorMessage = `上传失败,错误代码: ${status}`;
}
}
} else if (error.request) {
// 网络错误
if (error.code === 'ECONNABORTED' || error.message.includes('timeout')) {
errorMessage = '上传超时,请检查网络连接或文件大小';
} else {
errorMessage = '网络连接错误,请检查网络设置';
}
} else if (error.message) {
errorMessage = `上传失败: ${error.message}`;
}
console.error('上传错误信息:', errorMessage);
(proxy as any)?.$modal.msgError(errorMessage);
}; };
// 预览 // 预览
@@ -7,12 +7,14 @@
## 功能特性 ## 功能特性
### 1. 统计概览 ### 1. 统计概览
- 待审核申请数量 - 待审核申请数量
- 已通过申请数量 - 已通过申请数量
- 已拒绝申请数量 - 已拒绝申请数量
- 总处理数量 - 总处理数量
### 2. 待审核申请管理 ### 2. 待审核申请管理
- 查看待审核申请列表 - 查看待审核申请列表
- 支持多条件搜索(岗位名称、学生姓名、学号、学院名称、申请日期) - 支持多条件搜索(岗位名称、学生姓名、学号、学院名称、申请日期)
- 单个申请审核(通过/拒绝) - 单个申请审核(通过/拒绝)
@@ -20,6 +22,7 @@
- 查看申请详情 - 查看申请详情
### 3. 所有申请查看 ### 3. 所有申请查看
- 查看所有申请记录 - 查看所有申请记录
- 支持按状态筛选 - 支持按状态筛选
- 导出申请数据 - 导出申请数据
@@ -40,16 +43,19 @@ xgreview/
## 技术特点 ## 技术特点
### 1. 响应式设计 ### 1. 响应式设计
- 使用Vue 3 Composition API - 使用Vue 3 Composition API
- TypeScript类型支持 - TypeScript类型支持
- Element Plus UI组件库 - Element Plus UI组件库
### 2. 数据管理 ### 2. 数据管理
- 基于学工处审核API - 基于学工处审核API
- 支持分页查询 - 支持分页查询
- 实时数据更新 - 实时数据更新
### 3. 用户体验 ### 3. 用户体验
- 加载状态提示 - 加载状态提示
- 操作反馈消息 - 操作反馈消息
- 响应式布局 - 响应式布局
@@ -57,6 +63,7 @@ xgreview/
## API接口 ## API接口
### 主要接口 ### 主要接口
- `listPendingApplications` - 获取待审核申请列表 - `listPendingApplications` - 获取待审核申请列表
- `listStudentAffairsApplications` - 获取所有申请列表 - `listStudentAffairsApplications` - 获取所有申请列表
- `getStudentAffairsApplication` - 获取申请详情 - `getStudentAffairsApplication` - 获取申请详情
@@ -68,15 +75,18 @@ xgreview/
## 使用说明 ## 使用说明
### 1. 访问路径 ### 1. 访问路径
``` ```
/jcbt/workstudy/xgreview /jcbt/workstudy/xgreview
``` ```
### 2. 权限要求 ### 2. 权限要求
- 需要学工处审核权限 - 需要学工处审核权限
- 支持批量操作权限 - 支持批量操作权限
### 3. 操作流程 ### 3. 操作流程
1. 查看统计概览,了解待处理申请数量 1. 查看统计概览,了解待处理申请数量
2. 在"待审核申请"标签页中查看待处理申请 2. 在"待审核申请"标签页中查看待处理申请
3. 使用搜索功能筛选特定申请 3. 使用搜索功能筛选特定申请
@@ -100,16 +110,19 @@ xgreview/
## 开发说明 ## 开发说明
### 1. 组件复用 ### 1. 组件复用
本模块复用了学院审核模块的组件结构,但使用不同的API接口和字段。 本模块复用了学院审核模块的组件结构,但使用不同的API接口和字段。
### 2. 样式规范 ### 2. 样式规范
遵循项目的前端开发规范,使用统一的颜色主题和组件样式。 遵循项目的前端开发规范,使用统一的颜色主题和组件样式。
### 3. 错误处理 ### 3. 错误处理
包含完整的错误处理机制,提供用户友好的错误提示。 包含完整的错误处理机制,提供用户友好的错误提示。
## 更新日志 ## 更新日志
- **2025-01-03**:创建学工处审核模块 - **2025-01-03**:创建学工处审核模块
- **2025-01-03**:实现基础审核功能 - **2025-01-03**:实现基础审核功能
- **2025-01-03**:添加统计概览和批量操作 - **2025-01-03**:添加统计概览和批量操作
@@ -7,12 +7,14 @@
## ✅ 已完成功能 ## ✅ 已完成功能
### 1. 主页面 (index.vue) ### 1. 主页面 (index.vue)
- ✅ 统计卡片展示(待审核、已通过、已拒绝、总计) - ✅ 统计卡片展示(待审核、已通过、已拒绝、总计)
- ✅ 标签页切换(待审核申请、所有申请) - ✅ 标签页切换(待审核申请、所有申请)
- ✅ 响应式布局和美观的UI设计 - ✅ 响应式布局和美观的UI设计
- ✅ 与后勤管理模块保持一致的样式风格 - ✅ 与后勤管理模块保持一致的样式风格
### 2. 待审核申请组件 (PendingApplications.vue) ### 2. 待审核申请组件 (PendingApplications.vue)
- ✅ 多条件搜索表单(岗位名称、学生姓名、学号、学院名称、申请日期) - ✅ 多条件搜索表单(岗位名称、学生姓名、学号、学院名称、申请日期)
- ✅ 批量操作按钮(批量通过、批量拒绝) - ✅ 批量操作按钮(批量通过、批量拒绝)
- ✅ 数据表格展示(包含学院审核状态) - ✅ 数据表格展示(包含学院审核状态)
@@ -22,6 +24,7 @@
- ✅ 分页功能 - ✅ 分页功能
### 3. 所有申请组件 (AllApplications.vue) ### 3. 所有申请组件 (AllApplications.vue)
- ✅ 扩展搜索条件(包含学工处审核状态) - ✅ 扩展搜索条件(包含学工处审核状态)
- ✅ 完整申请列表展示 - ✅ 完整申请列表展示
- ✅ 学院审核和学工处审核状态对比 - ✅ 学院审核和学工处审核状态对比
@@ -29,6 +32,7 @@
- ✅ 审核操作(针对待审核状态) - ✅ 审核操作(针对待审核状态)
### 4. 申请详情组件 (ApplicationDetail.vue) ### 4. 申请详情组件 (ApplicationDetail.vue)
- ✅ 完整的申请信息展示 - ✅ 完整的申请信息展示
- ✅ 学院审核信息展示 - ✅ 学院审核信息展示
- ✅ 学工处审核信息展示 - ✅ 学工处审核信息展示
@@ -36,23 +40,27 @@
- ✅ 立即审核功能 - ✅ 立即审核功能
### 5. 路由配置 (jcbt.ts) ### 5. 路由配置 (jcbt.ts)
- ✅ 创建jcbt模块路由配置 - ✅ 创建jcbt模块路由配置
- ✅ 包含学工处审核模块路由 - ✅ 包含学工处审核模块路由
- ✅ 支持懒加载 - ✅ 支持懒加载
### 6. 文档说明 ### 6. 文档说明
- ✅ README.md - 详细的功能说明和使用指南 - ✅ README.md - 详细的功能说明和使用指南
- ✅ 开发总结.md - 开发过程和成果总结 - ✅ 开发总结.md - 开发过程和成果总结
## 🏗️ 技术架构 ## 🏗️ 技术架构
### 前端技术栈 ### 前端技术栈
- **Vue 3.5.13** + **TypeScript 5.8.3** - **Vue 3.5.13** + **TypeScript 5.8.3**
- **Element Plus** UI组件库 - **Element Plus** UI组件库
- **Composition API** 响应式编程 - **Composition API** 响应式编程
- **SCSS** 样式预处理器 - **SCSS** 样式预处理器
### 组件结构 ### 组件结构
``` ```
xgreview/ xgreview/
├── index.vue # 主页面 ├── index.vue # 主页面
@@ -66,6 +74,7 @@ xgreview/
``` ```
### API接口 ### API接口
- 基于 `@/api/jcbt/workStudyApplicationStudentAffairsReview.ts` - 基于 `@/api/jcbt/workStudyApplicationStudentAffairsReview.ts`
- 支持加密请求(AES+RSA) - 支持加密请求(AES+RSA)
- 完整的CRUD操作接口 - 完整的CRUD操作接口
@@ -73,16 +82,19 @@ xgreview/
## 🎨 设计特点 ## 🎨 设计特点
### 1. 视觉一致性 ### 1. 视觉一致性
- 与学院审核模块保持完全一致的视觉风格 - 与学院审核模块保持完全一致的视觉风格
- 使用相同的颜色主题和组件样式 - 使用相同的颜色主题和组件样式
- 统一的布局结构和交互模式 - 统一的布局结构和交互模式
### 2. 用户体验 ### 2. 用户体验
- 响应式设计,支持不同屏幕尺寸 - 响应式设计,支持不同屏幕尺寸
- 加载状态提示和操作反馈 - 加载状态提示和操作反馈
- 直观的操作流程和清晰的界面布局 - 直观的操作流程和清晰的界面布局
### 3. 功能完整性 ### 3. 功能完整性
- 覆盖完整的审核业务流程 - 覆盖完整的审核业务流程
- 支持单个和批量操作 - 支持单个和批量操作
- 提供详细的数据展示和操作记录 - 提供详细的数据展示和操作记录
@@ -90,53 +102,61 @@ xgreview/
## 🔧 开发规范遵循 ## 🔧 开发规范遵循
### 1. 前端开发规范 ### 1. 前端开发规范
- ✅ 遵循 `.cursor/cursorrules-frontend.md` 规范 - ✅ 遵循 `.cursor/cursorrules-frontend.md` 规范
- ✅ 使用Vue 3 Composition API - ✅ 使用Vue 3 Composition API
- ✅ TypeScript类型定义完整 - ✅ TypeScript类型定义完整
- ✅ 组件化开发,代码复用 - ✅ 组件化开发,代码复用
### 2. 项目结构规范 ### 2. 项目结构规范
- ✅ 页面代码存放在 `web/src/views/jcbt/workstudy/xgreview/` - ✅ 页面代码存放在 `web/src/views/jcbt/workstudy/xgreview/`
- ✅ API代码存放在 `web/src/api/jcbt/` - ✅ API代码存放在 `web/src/api/jcbt/`
- ✅ 路由配置存放在 `web/src/router/` - ✅ 路由配置存放在 `web/src/router/`
### 3. 代码质量 ### 3. 代码质量
- ✅ 完整的错误处理机制 - ✅ 完整的错误处理机制
- ✅ 清晰的代码注释和文档 - ✅ 清晰的代码注释和文档
- ✅ 遵循Vue 3最佳实践 - ✅ 遵循Vue 3最佳实践
## 📊 功能对比 ## 📊 功能对比
| 功能模块 | 学院审核 | 学工处审核 | 状态 | | 功能模块 | 学院审核 | 学工处审核 | 状态 |
|---------|---------|-----------|------| | ---------- | -------- | ---------- | -------- |
| 统计概览 | ✅ | ✅ | 完全一致 | | 统计概览 | ✅ | ✅ | 完全一致 |
| 待审核列表 | ✅ | ✅ | 完全一致 | | 待审核列表 | ✅ | ✅ | 完全一致 |
| 所有申请 | ✅ | ✅ | 完全一致 | | 所有申请 | ✅ | ✅ | 完全一致 |
| 申请详情 | ✅ | ✅ | 完全一致 | | 申请详情 | ✅ | ✅ | 完全一致 |
| 审核操作 | ✅ | ✅ | 完全一致 | | 审核操作 | ✅ | ✅ | 完全一致 |
| 批量操作 | ✅ | ✅ | 完全一致 | | 批量操作 | ✅ | ✅ | 完全一致 |
| 导出功能 | ✅ | ✅ | 完全一致 | | 导出功能 | ✅ | ✅ | 完全一致 |
## 🚀 部署说明 ## 🚀 部署说明
### 1. 文件位置 ### 1. 文件位置
所有文件已正确放置在指定目录: 所有文件已正确放置在指定目录:
- 主页面:`web/src/views/jcbt/workstudy/xgreview/index.vue` - 主页面:`web/src/views/jcbt/workstudy/xgreview/index.vue`
- 组件:`web/src/views/jcbt/workstudy/xgreview/components/` - 组件:`web/src/views/jcbt/workstudy/xgreview/components/`
- 路由:`web/src/router/jcbt.ts` - 路由:`web/src/router/jcbt.ts`
### 2. 访问路径 ### 2. 访问路径
``` ```
/jcbt/workstudy/xgreview /jcbt/workstudy/xgreview
``` ```
### 3. 权限要求 ### 3. 权限要求
- 需要学工处审核权限 - 需要学工处审核权限
- 支持批量操作权限 - 支持批量操作权限
## 🔍 测试建议 ## 🔍 测试建议
### 1. 功能测试 ### 1. 功能测试
- 统计数据展示 - 统计数据展示
- 搜索和筛选功能 - 搜索和筛选功能
- 审核操作流程 - 审核操作流程
@@ -144,11 +164,13 @@ xgreview/
- 导出功能 - 导出功能
### 2. 兼容性测试 ### 2. 兼容性测试
- 不同浏览器兼容性 - 不同浏览器兼容性
- 不同屏幕尺寸响应式 - 不同屏幕尺寸响应式
- 权限控制验证 - 权限控制验证
### 3. 性能测试 ### 3. 性能测试
- 大数据量加载性能 - 大数据量加载性能
- 操作响应速度 - 操作响应速度
- 内存使用情况 - 内存使用情况
@@ -156,16 +178,19 @@ xgreview/
## 📝 后续优化建议 ## 📝 后续优化建议
### 1. 功能增强 ### 1. 功能增强
- 添加审核历史记录 - 添加审核历史记录
- 支持审核意见模板 - 支持审核意见模板
- 增加数据统计图表 - 增加数据统计图表
### 2. 性能优化 ### 2. 性能优化
- 实现虚拟滚动 - 实现虚拟滚动
- 添加数据缓存 - 添加数据缓存
- 优化API请求 - 优化API请求
### 3. 用户体验 ### 3. 用户体验
- 添加操作引导 - 添加操作引导
- 支持快捷键操作 - 支持快捷键操作
- 增加个性化设置 - 增加个性化设置
@@ -186,4 +211,4 @@ xgreview/
**开发完成时间**:2025-01-03 **开发完成时间**:2025-01-03
**开发状态**:✅ 完成 **开发状态**:✅ 完成
**代码质量**:⭐⭐⭐⭐⭐ **代码质量**:⭐⭐⭐⭐⭐
**文档完整性**:⭐⭐⭐⭐⭐ **文档完整性**:⭐⭐⭐⭐⭐
@@ -216,8 +216,8 @@
</template> </template>
<script setup name="InternshipCheckin"> <script setup name="InternshipCheckin">
import { checkIn } from "@/api/zsjy/internshipDaily"; import { checkIn } from '@/api/zsjy/internshipDaily';
import { parseTime } from "@/utils/ruoyi"; import { parseTime } from '@/utils/ruoyi';
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
@@ -273,7 +273,7 @@ const getRecentRecords = () => {
workDuration: 8, workDuration: 8,
status: '1', status: '1',
workContent: '参与项目开发,完成用户模块设计' workContent: '参与项目开发,完成用户模块设计'
}, }
// 更多记录... // 更多记录...
]; ];
}; };
+44 -85
View File
@@ -212,13 +212,7 @@
<!-- 分页组件 --> <!-- 分页组件 -->
<div class="pagination-container"> <div class="pagination-container">
<pagination <pagination v-show="total > 0" :total="total" :page="queryParams.pageNum" :limit="queryParams.pageSize" @pagination="getList" />
v-show="total > 0"
:total="total"
:page="queryParams.pageNum"
:limit="queryParams.pageSize"
@pagination="getList"
/>
</div> </div>
</el-card> </el-card>
@@ -244,10 +238,10 @@
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="签到地点" :span="2">{{ currentRecord.checkInLocation || '-' }}</el-descriptions-item> <el-descriptions-item label="签到地点" :span="2">{{ currentRecord.checkInLocation || '-' }}</el-descriptions-item>
<el-descriptions-item label="工作内容" :span="2"> <el-descriptions-item label="工作内容" :span="2">
<div style="white-space: pre-wrap;">{{ currentRecord.workContent || '-' }}</div> <div style="white-space: pre-wrap">{{ currentRecord.workContent || '-' }}</div>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="备注" :span="2"> <el-descriptions-item label="备注" :span="2">
<div style="white-space: pre-wrap;">{{ currentRecord.remark || '-' }}</div> <div style="white-space: pre-wrap">{{ currentRecord.remark || '-' }}</div>
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
</div> </div>
@@ -277,10 +271,7 @@
<div class="calendar-day"> <div class="calendar-day">
<div class="day-number">{{ data.day.split('-').slice(2).join('') }}</div> <div class="day-number">{{ data.day.split('-').slice(2).join('') }}</div>
<div v-if="getAttendanceStatus(data.day)" class="attendance-status"> <div v-if="getAttendanceStatus(data.day)" class="attendance-status">
<el-tag <el-tag :type="getStatusTagType(getAttendanceStatus(data.day))" size="small">
:type="getStatusTagType(getAttendanceStatus(data.day))"
size="small"
>
{{ getStatusText(getAttendanceStatus(data.day)) }} {{ getStatusText(getAttendanceStatus(data.day)) }}
</el-tag> </el-tag>
</div> </div>
@@ -350,25 +341,13 @@
<el-col :span="12"> <el-col :span="12">
<el-form-item label="签到状态" prop="status"> <el-form-item label="签到状态" prop="status">
<el-select v-model="addForm.status" placeholder="请选择签到状态" style="width: 100%"> <el-select v-model="addForm.status" placeholder="请选择签到状态" style="width: 100%">
<el-option <el-option v-for="item in attendanceStatusOptions" :key="item.value" :label="item.label" :value="item.value" />
v-for="item in attendanceStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="工作时长" prop="workDuration"> <el-form-item label="工作时长" prop="workDuration">
<el-input-number <el-input-number v-model="addForm.workDuration" :min="0" :max="24" :precision="1" placeholder="工作时长(小时)" style="width: 100%" />
v-model="addForm.workDuration"
:min="0"
:max="24"
:precision="1"
placeholder="工作时长(小时)"
style="width: 100%"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
@@ -376,24 +355,10 @@
<el-input v-model="addForm.checkInLocation" placeholder="请输入签到地点" /> <el-input v-model="addForm.checkInLocation" placeholder="请输入签到地点" />
</el-form-item> </el-form-item>
<el-form-item label="工作内容" prop="workContent"> <el-form-item label="工作内容" prop="workContent">
<el-input <el-input v-model="addForm.workContent" type="textarea" :rows="3" placeholder="请输入工作内容" maxlength="500" show-word-limit />
v-model="addForm.workContent"
type="textarea"
:rows="3"
placeholder="请输入工作内容"
maxlength="500"
show-word-limit
/>
</el-form-item> </el-form-item>
<el-form-item label="备注" prop="remark"> <el-form-item label="备注" prop="remark">
<el-input <el-input v-model="addForm.remark" type="textarea" :rows="2" placeholder="请输入备注" maxlength="200" show-word-limit />
v-model="addForm.remark"
type="textarea"
:rows="2"
placeholder="请输入备注"
maxlength="200"
show-word-limit
/>
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
@@ -447,24 +412,12 @@ const addForm = ref({
// 新增表单验证规则 // 新增表单验证规则
const addFormRules = ref({ const addFormRules = ref({
studentName: [ studentName: [{ required: true, message: '请输入学生姓名', trigger: 'blur' }],
{ required: true, message: '请输入学生姓名', trigger: 'blur' } studentNumber: [{ required: true, message: '请输入学号', trigger: 'blur' }],
], className: [{ required: true, message: '请输入班级', trigger: 'blur' }],
studentNumber: [ companyName: [{ required: true, message: '请输入实习单位', trigger: 'blur' }],
{ required: true, message: '请输入学号', trigger: 'blur' } checkInTime: [{ required: true, message: '请选择签到时间', trigger: 'change' }],
], status: [{ required: true, message: '请选择签到状态', trigger: 'change' }]
className: [
{ required: true, message: '请输入班级', trigger: 'blur' }
],
companyName: [
{ required: true, message: '请输入实习单位', trigger: 'blur' }
],
checkInTime: [
{ required: true, message: '请选择签到时间', trigger: 'change' }
],
status: [
{ required: true, message: '请选择签到状态', trigger: 'change' }
]
}); });
// 查询参数 // 查询参数
@@ -500,7 +453,7 @@ const getList = () => {
queryParams.value.checkTimeEnd = ''; queryParams.value.checkTimeEnd = '';
} }
listInternshipDaily(queryParams.value).then(response => { listInternshipDaily(queryParams.value).then((response) => {
internshipDailyList.value = response.rows; internshipDailyList.value = response.rows;
total.value = response.total; total.value = response.total;
loading.value = false; loading.value = false;
@@ -510,13 +463,15 @@ const getList = () => {
// 获取统计数据 // 获取统计数据
const getStatisticsData = () => { const getStatisticsData = () => {
console.log('正在获取统计数据...'); console.log('正在获取统计数据...');
getStatistics().then(response => { getStatistics()
console.log('统计数据响应:', response); .then((response) => {
statistics.value = response.data || {}; console.log('统计数据响应:', response);
console.log('设置的统计数据:', statistics.value); statistics.value = response.data || {};
}).catch(error => { console.log('设置的统计数据:', statistics.value);
console.error('获取统计数据失败:', error); })
}); .catch((error) => {
console.error('获取统计数据失败:', error);
});
}; };
// 搜索按钮操作 // 搜索按钮操作
@@ -528,7 +483,7 @@ const handleQuery = () => {
// 重置按钮操作 // 重置按钮操作
const resetQuery = () => { const resetQuery = () => {
daterangeCheckTime.value = []; daterangeCheckTime.value = [];
proxy.resetForm("queryFormRef"); proxy.resetForm('queryFormRef');
queryParams.value = { queryParams.value = {
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 10,
@@ -581,17 +536,19 @@ const cancelAdd = () => {
// 提交新增 // 提交新增
const submitAdd = () => { const submitAdd = () => {
proxy.$refs.addFormRef.validate(valid => { proxy.$refs.addFormRef.validate((valid) => {
if (valid) { if (valid) {
addLoading.value = true; addLoading.value = true;
addInternshipDaily(addForm.value).then(response => { addInternshipDaily(addForm.value)
proxy.$modal.msgSuccess("新增签到记录成功"); .then((response) => {
addDialogVisible.value = false; proxy.$modal.msgSuccess('新增签到记录成功');
getList(); addDialogVisible.value = false;
}).finally(() => { getList();
addLoading.value = false; })
}); .finally(() => {
addLoading.value = false;
});
} }
}); });
}; };
@@ -625,16 +582,14 @@ const loadCalendarData = () => {
month: calendarMonth.value month: calendarMonth.value
}; };
getCalendar(currentStudent.value.studentId, params).then(response => { getCalendar(currentStudent.value.studentId, params).then((response) => {
calendarData.value = response.data || []; calendarData.value = response.data || [];
}); });
}; };
// 获取考勤状态 // 获取考勤状态
const getAttendanceStatus = (date) => { const getAttendanceStatus = (date) => {
const record = calendarData.value.find(item => const record = calendarData.value.find((item) => item.checkDate === date);
item.checkDate === date
);
return record ? record.status : null; return record ? record.status : null;
}; };
@@ -662,9 +617,13 @@ const getStatusText = (status) => {
// 导出按钮操作 // 导出按钮操作
const handleExport = () => { const handleExport = () => {
proxy.download('zsjy/internship/daily/export', { proxy.download(
...queryParams.value 'zsjy/internship/daily/export',
}, `实习日常记录_${new Date().getTime()}.xlsx`); {
...queryParams.value
},
`实习日常记录_${new Date().getTime()}.xlsx`
);
}; };
// 初始化 // 初始化
@@ -1,12 +1,5 @@
<template> <template>
<el-dialog <el-dialog v-model="visible" title="新增实习信息" width="80%" :before-close="handleClose" draggable destroy-on-close>
v-model="visible"
title="新增实习信息"
width="80%"
:before-close="handleClose"
draggable
destroy-on-close
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" class="form-container"> <el-form ref="formRef" :model="form" :rules="rules" label-width="120px" class="form-container">
<el-row :gutter="24"> <el-row :gutter="24">
<!-- 基本信息 --> <!-- 基本信息 -->
@@ -32,7 +25,14 @@
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="实习津贴" prop="internshipAllowance"> <el-form-item label="实习津贴" prop="internshipAllowance">
<el-input-number v-model="form.internshipAllowance" :min="0" :precision="2" controls-position="right" placeholder="实习津贴" style="width: 100%" /> <el-input-number
v-model="form.internshipAllowance"
:min="0"
:precision="2"
controls-position="right"
placeholder="实习津贴"
style="width: 100%"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -363,4 +363,3 @@ const handleSubmit = async () => {
padding-bottom: 10px; padding-bottom: 10px;
} }
</style> </style>
@@ -1,12 +1,5 @@
<template> <template>
<el-dialog <el-dialog v-model="visible" title="编辑实习信息" width="80%" :before-close="handleClose" draggable destroy-on-close>
v-model="visible"
title="编辑实习信息"
width="80%"
:before-close="handleClose"
draggable
destroy-on-close
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" class="form-container" v-loading="loading"> <el-form ref="formRef" :model="form" :rules="rules" label-width="120px" class="form-container" v-loading="loading">
<el-row :gutter="24"> <el-row :gutter="24">
<!-- 基本信息 --> <!-- 基本信息 -->
@@ -32,7 +25,14 @@
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="实习津贴" prop="internshipAllowance"> <el-form-item label="实习津贴" prop="internshipAllowance">
<el-input-number v-model="form.internshipAllowance" :min="0" :precision="2" controls-position="right" placeholder="实习津贴" style="width: 100%" /> <el-input-number
v-model="form.internshipAllowance"
:min="0"
:precision="2"
controls-position="right"
placeholder="实习津贴"
style="width: 100%"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -277,12 +277,15 @@ const rules = reactive({
}); });
// 监听弹窗显示状态 // 监听弹窗显示状态
watch(() => props.modelValue, (newVal) => { watch(
visible.value = newVal; () => props.modelValue,
if (newVal && props.studentId) { (newVal) => {
getInternshipDetail(); visible.value = newVal;
if (newVal && props.studentId) {
getInternshipDetail();
}
} }
}); );
watch(visible, (newVal) => { watch(visible, (newVal) => {
emit('update:modelValue', newVal); emit('update:modelValue', newVal);
@@ -434,4 +437,3 @@ const handleDelete = async () => {
padding-bottom: 10px; padding-bottom: 10px;
} }
</style> </style>
@@ -191,8 +191,6 @@
</div> </div>
</el-card> </el-card>
<!-- 学生实习详情对话框 --> <!-- 学生实习详情对话框 -->
<el-dialog title="学生实习详情" v-model="dialog.visible" width="900px" append-to-body> <el-dialog title="学生实习详情" v-model="dialog.visible" width="900px" append-to-body>
<div v-if="studentDetail" class="student-detail"> <div v-if="studentDetail" class="student-detail">
@@ -226,7 +224,9 @@
<el-descriptions-item label="单位联系人">{{ studentDetail.contactPersonName || '-' }}</el-descriptions-item> <el-descriptions-item label="单位联系人">{{ studentDetail.contactPersonName || '-' }}</el-descriptions-item>
<el-descriptions-item label="联系人电话">{{ studentDetail.contactPersonPhone || '-' }}</el-descriptions-item> <el-descriptions-item label="联系人电话">{{ studentDetail.contactPersonPhone || '-' }}</el-descriptions-item>
<el-descriptions-item label="实习津贴"> <el-descriptions-item label="实习津贴">
<span v-if="studentDetail.internshipAllowance && studentDetail.internshipAllowance > 0">{{ studentDetail.internshipAllowance }}元/月</span> <span v-if="studentDetail.internshipAllowance && studentDetail.internshipAllowance > 0"
>{{ studentDetail.internshipAllowance }}元/月</span
>
<span v-else>-</span> <span v-else>-</span>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="协议状态"> <el-descriptions-item label="协议状态">
@@ -245,18 +245,10 @@
</el-dialog> </el-dialog>
<!-- 新增弹窗 --> <!-- 新增弹窗 -->
<AddDialog <AddDialog v-model="showAddDialog" :student-info="selectedStudentInfo" @success="handleDialogSuccess" />
v-model="showAddDialog"
:student-info="selectedStudentInfo"
@success="handleDialogSuccess"
/>
<!-- 编辑弹窗 --> <!-- 编辑弹窗 -->
<EditDialog <EditDialog v-model="showEditDialog" :student-id="selectedStudentId" @success="handleDialogSuccess" />
v-model="showEditDialog"
:student-id="selectedStudentId"
@success="handleDialogSuccess"
/>
</div> </div>
</template> </template>