wuzhen update 宿管相关修改

This commit is contained in:
“wuzhen” committed 2026-06-23 17:15:49 +08:00
1 parent a88d87047d
commit 4a768867df
50 files changed
+3228 -424

No files matched your search

@@ -11,15 +11,15 @@
</view>
<u-form ref="formRef" :model="formData" :rules="rules" label-width="100">
<!-- 违纪类型 -->
<u-form-item label="违纪类型" prop="disciplineTypeId">
<!-- 扣分项(多选) -->
<u-form-item label="扣分项">
<view
@click="!isViewMode && (showDisciplineTypePicker = true)"
@click="!isViewMode && openDeductionPopup()"
style="width: 100%"
>
<u-input
:value="disciplineTypeDisplay"
placeholder="请选择违纪类型"
:value="deductionSummaryDisplay"
placeholder="请选择扣分项(可多选)"
readonly
border="none"
:suffix-icon="isViewMode ? '' : 'arrow-down'"
@@ -27,38 +27,70 @@
style="pointer-events: none"
/>
</view>
<u-picker
v-if="!isViewMode"
:show="showDisciplineTypePicker && disciplineTypeOptions.length > 0"
:columns="disciplineTypeColumns"
keyName="text"
@confirm="handleDisciplineTypeConfirm"
@cancel="showDisciplineTypePicker = false"
></u-picker>
</u-form-item>
<!-- 违纪类型说明 -->
<view
v-if="formData.disciplineTypeId && selectedDisciplineTypeRemark"
class="discipline-type-remark"
>
<u-icon name="info-circle" color="#909399" size="14"></u-icon>
<text class="remark-text">
【{{ formData.disciplineTypeName }}】类型说明:{{
selectedDisciplineTypeRemark
}}
</text>
<view v-if="deductionItems.length > 0" class="deduction-list">
<view
v-for="(item, index) in deductionItems"
:key="item.disciplineTypeId"
class="deduction-item"
>
<view class="deduction-item-header">
<text class="deduction-name">{{ item.disciplineTypeName }}</text>
<u-input
v-if="!isViewMode"
v-model="item.score"
type="digit"
placeholder="扣分"
@input="handleDeductionScoreChange"
/>
<text v-else class="deduction-score">{{ item.score }}分</text>
<text class="deduction-range">({{ getDeductionMin(item) }}-{{ getDeductionMax(item) }}分)</text>
<u-icon
v-if="!isViewMode"
name="close-circle"
color="#f56c6c"
size="18"
@click="removeDeductionItem(index)"
></u-icon>
</view>
<view v-if="getDeductionRemark(item.disciplineTypeId)" class="deduction-remark">
<text>{{ getDeductionRemark(item.disciplineTypeId) }}</text>
</view>
</view>
<view class="deduction-total">
合计扣分:<text class="total-value">{{ formData.totalScore }}</text> 分
</view>
</view>
<u-popup :show="showDeductionPopup" mode="bottom" round="16" @close="showDeductionPopup = false">
<view class="deduction-popup">
<view class="deduction-popup-header">
<text class="deduction-popup-title">选择扣分项</text>
<text class="deduction-popup-action" @click="confirmDeductionSelection">确定</text>
</view>
<scroll-view scroll-y class="deduction-popup-body">
<u-checkbox-group v-model="tempSelectedTypeIds" placement="column">
<u-checkbox
v-for="item in disciplineTypeOptions"
:key="item.id"
:name="String(item.id)"
:label="`${item.typeName}(${item.minScore || 0}分起)`"
customStyle="margin-bottom: 12px;"
></u-checkbox>
</u-checkbox-group>
</scroll-view>
</view>
</u-popup>
<!-- 宿舍扣分 -->
<u-form-item label="宿舍扣分" prop="totalScore">
<u-input
v-model.number="formData.totalScore"
v-model="formData.totalScore"
type="digit"
placeholder="请输入扣分(0-100分)"
:disabled="isViewMode"
:readonly="isViewMode"
@input="handleTotalScoreChange"
placeholder="自动合计"
disabled
readonly
/>
</u-form-item>
@@ -272,25 +304,9 @@
<text class="student-name">{{ student.studentName }}</text>
<!-- 性别标签 -->
<u-tag
v-if="
student.gender !== undefined &&
student.gender !== null &&
student.gender !== ''
"
:text="
student.gender === '0'
? '男'
: student.gender === '1'
? '女'
: '未知'
"
:type="
student.gender === '0'
? 'primary'
: student.gender === '1'
? 'error'
: 'warning'
"
v-if="normalizeGender(student.gender)"
:text="normalizeGender(student.gender) === '0' ? '男' : '女'"
:type="normalizeGender(student.gender) === '0' ? 'primary' : 'error'"
size="mini"
></u-tag>
</view>
@@ -583,6 +599,8 @@ import { getDict, getDictLabel, getDictTagType } from '@/common/dict.js';
import dayjs from 'dayjs';
import weChatSDK from '@/utils/wechat.js';
import ViolationRecordDialog from './ViolationRecordDialog.vue';
import { normalizeStudentId, normalizeId } from '../utils/studentId.js';
import { calcStudentViolationScoreTotal } from '../utils/violationScore.js';
export default {
name: 'SingleRoomCheck',
@@ -622,13 +640,16 @@ export default {
attachmentIds: '',
},
deductionItems: [],
showDeductionPopup: false,
tempSelectedTypeIds: [],
// 表单验证规则
rules: {
hygieneGrade: [{ required: true, message: '请选择卫生等级', trigger: 'change' }],
},
// Picker显示状态
showDisciplineTypePicker: false,
showHygieneGradePicker: false,
// 违纪类型选项
@@ -692,32 +713,9 @@ export default {
return name;
},
// 违纪类型列
disciplineTypeColumns() {
return [
this.disciplineTypeOptions.map((item) => ({
text: item.typeName,
value: item.id,
})),
];
},
// 违纪类型显示文本
disciplineTypeDisplay() {
if (!this.formData.disciplineTypeId) return '';
const item = this.disciplineTypeOptions.find(
(d) => d.id === this.formData.disciplineTypeId,
);
return item ? item.typeName : '';
},
// 违纪类型说明
selectedDisciplineTypeRemark() {
if (!this.formData.disciplineTypeId) return '';
const item = this.disciplineTypeOptions.find(
(d) => d.id === this.formData.disciplineTypeId,
);
return item ? item.remark : '';
deductionSummaryDisplay() {
if (!this.deductionItems.length) return '';
return this.deductionItems.map((item) => item.disciplineTypeName).join('、');
},
// 卫生等级列
@@ -822,12 +820,10 @@ export default {
// 加载违纪类型
await this.loadDisciplineTypes();
// 加载房间学生信息
await this.loadRoomStudents();
// 如果有现有数据,加载编辑数据
if (this.existingData) {
this.loadEditData();
if (this.existingData?.id) {
await this.loadEditData();
} else {
await this.loadRoomStudents();
}
} catch (error) {
console.error('初始化组件失败:', error);
@@ -850,6 +846,52 @@ export default {
}
},
normalizeGender(gender) {
if (gender === null || gender === undefined || gender === '') {
return undefined;
}
const value = String(gender);
if (value === '0' || value === '男') {
return '0';
}
if (value === '1' || value === '女') {
return '1';
}
return value;
},
mergeGenderFromRoomStudents(students, roomStudents) {
if (!students.length || !roomStudents.length) {
return students;
}
const genderByStudentId = {};
const genderByStudentNo = {};
roomStudents.forEach((student) => {
const gender = this.normalizeGender(student.gender);
if (!gender) {
return;
}
if (student.studentId !== undefined && student.studentId !== null) {
genderByStudentId[String(student.studentId)] = gender;
}
if (student.studentNo) {
genderByStudentNo[student.studentNo] = gender;
}
});
return students.map((student) => {
if (this.normalizeGender(student.gender)) {
return { ...student, gender: this.normalizeGender(student.gender) };
}
const gender =
(student.studentId !== undefined && student.studentId !== null
? genderByStudentId[String(student.studentId)]
: undefined) || (student.studentNo ? genderByStudentNo[student.studentNo] : undefined);
return gender ? { ...student, gender } : student;
});
},
// 加载违纪类型
async loadDisciplineTypes() {
try {
@@ -897,6 +939,7 @@ export default {
this.roomStudents = res.data.map((student) => ({
...student,
studentId: normalizeStudentId(student.studentId),
student_score: 0,
attendance_status: '1',
student_grade: 'A',
@@ -937,10 +980,10 @@ export default {
if (res.code === 200 && res.data) {
this.studentLeaves.clear();
Object.keys(res.data).forEach((studentId) => {
this.studentLeaves.set(Number(studentId), res.data[studentId]);
// 自动设置出勤状态为请假
const normalizedId = normalizeStudentId(studentId);
this.studentLeaves.set(normalizedId, res.data[studentId]);
const student = this.roomStudents.find(
(s) => s.studentId === Number(studentId),
(s) => normalizeStudentId(s.studentId) === normalizedId,
);
if (student) {
student.attendance_status = '3';
@@ -969,6 +1012,7 @@ export default {
this.formData.hygieneGrade = this.existingData.hygieneGrade || 'A';
this.formData.checkRemarks = this.existingData.checkRemarks || '';
this.formData.attachmentIds = this.existingData.attachmentIds || '';
this.initDeductionItemsFromData(this.existingData);
console.log('✅ 表单基础数据回显完成:', this.formData);
@@ -980,7 +1024,7 @@ export default {
// 3️⃣ 从后端接口加载学生评分数据、违纪记录和请假信息(复用Web端接口)
// 注意:loadStudentScoresFromAPI 内部会自动调用 loadViolationRecordsFromAPI 和 loadStudentLeaves
if (this.existingData.id) {
await this.loadStudentScoresFromAPI(this.existingData.id);
await this.loadStudentScoresFromAPI(normalizeId(this.existingData.id));
}
console.log('✅ 所有数据回显完成');
@@ -992,17 +1036,18 @@ export default {
// 从后端API加载学生评分数据(复用Web端接口)
async loadStudentScoresFromAPI(checkId) {
if (!checkId) {
const normalizedCheckId = normalizeId(checkId);
if (!normalizedCheckId) {
console.warn('⚠️ 没有检查ID,无法加载学生评分数据');
return;
}
this.studentsLoading = true;
try {
console.log('🔄 从API加载学生评分数据,检查ID:', checkId);
console.log('🔄 从API加载学生评分数据,检查ID:', normalizedCheckId);
const res = await request({
url: `/dormHygieneStudentScore/dormHygieneStudentScore/getByCheckId/${checkId}`,
url: `/dormHygieneStudentScore/dormHygieneStudentScore/getByCheckId/${normalizedCheckId}`,
method: 'GET',
});
@@ -1010,9 +1055,9 @@ export default {
console.log('✅ 成功获取学生评分数据:', res.data.length, '条');
// 将后端返回的学生评分数据映射到roomStudents
this.roomStudents = res.data.map((studentScore) => ({
let students = res.data.map((studentScore) => ({
// 学生基本信息
studentId: studentScore.studentId,
studentId: normalizeStudentId(studentScore.studentId),
studentName: studentScore.studentName,
studentNo: studentScore.studentNo,
className: studentScore.className,
@@ -1021,7 +1066,7 @@ export default {
bedId: studentScore.bedId,
bedName: studentScore.bedCode,
bedCode: studentScore.bedCode,
gender: studentScore.gender || '1',
gender: this.normalizeGender(studentScore.gender),
// 评分数据
student_score: studentScore.studentScore || 0,
attendance_status: studentScore.attendanceStatus || '1',
@@ -1034,13 +1079,27 @@ export default {
showGradePicker: false,
}));
if (students.some((student) => !student.gender) && this.roomInfo && this.roomInfo.id) {
const roomRes = await request({
url: '/dormBedStudent/dormBedStudent/getRoomStudents',
method: 'GET',
params: { roomId: this.roomInfo.id },
});
if (roomRes.code === 200 && roomRes.data) {
students = this.mergeGenderFromRoomStudents(students, roomRes.data);
}
}
this.roomStudents = students;
console.log('✅ 学生评分数据映射完成');
// 🔴 同时加载违纪记录和请假信息(复用Web端逻辑)
await Promise.all([
this.loadViolationRecordsFromAPI(checkId),
this.loadViolationRecordsFromAPI(normalizedCheckId),
this.loadStudentLeaves(),
]);
this.syncStudentScoreFromViolations();
} else {
console.warn('⚠️ 未获取到学生评分数据,保持当前学生列表');
}
@@ -1054,16 +1113,17 @@ export default {
// 从后端API加载违纪记录数据(复用Web端接口)
async loadViolationRecordsFromAPI(checkId) {
if (!checkId) {
const normalizedCheckId = normalizeId(checkId);
if (!normalizedCheckId) {
console.warn('⚠️ 没有检查ID,无法加载违纪记录数据');
return;
}
try {
console.log('🔄 从API加载违纪记录数据,检查ID:', checkId);
console.log('🔄 从API加载违纪记录数据,检查ID:', normalizedCheckId);
const res = await request({
url: `/dormHygieneCheck/dormHygieneCheck/getViolations/${checkId}`,
url: `/dormHygieneCheck/dormHygieneCheck/getViolations/${normalizedCheckId}`,
method: 'GET',
});
@@ -1075,7 +1135,7 @@ export default {
// 按学生ID分组违纪记录
res.data.forEach((violation) => {
const studentId = violation.studentId;
const studentId = normalizeStudentId(violation.studentId);
if (!studentId) return;
const violations = this.violationRecords.get(studentId) || [];
@@ -1101,6 +1161,7 @@ export default {
'✅ 所有违纪记录数据加载完成,总学生数:',
this.violationRecords.size,
);
this.syncStudentScoreFromViolations();
} else {
console.warn('⚠️ 未获取到违纪记录数据');
}
@@ -1318,7 +1379,9 @@ export default {
console.log('👥 回显学生评分数据:', studentScores);
studentScores.forEach((scoreData) => {
const student = this.roomStudents.find((s) => s.studentId === scoreData.studentId);
const student = this.roomStudents.find(
(s) => normalizeStudentId(s.studentId) === normalizeStudentId(scoreData.studentId),
);
if (student) {
student.student_score = scoreData.studentScore || 0;
student.attendance_status = scoreData.attendanceStatus || '1';
@@ -1337,7 +1400,7 @@ export default {
this.violationRecords.clear();
violationData.forEach((violation) => {
const studentId = violation.studentId;
const studentId = normalizeStudentId(violation.studentId);
if (!studentId) return;
const violations = this.violationRecords.get(studentId) || [];
@@ -1360,6 +1423,28 @@ export default {
violation.violationAttachmentIds || violation.attachmentOssIds,
);
});
this.syncStudentScoreFromViolations();
},
// 根据违纪记录合计同步个人扣分
syncStudentScoreFromViolations(studentId) {
const syncOne = (sid) => {
const student = this.roomStudents.find(
(item) => normalizeStudentId(item.studentId) === sid,
);
if (student) {
student.student_score = calcStudentViolationScoreTotal(this.getStudentViolations(sid));
}
};
if (studentId != null && studentId !== '') {
syncOne(normalizeStudentId(studentId));
return;
}
this.roomStudents.forEach((student) => {
syncOne(normalizeStudentId(student.studentId));
});
},
// 清理blob URL,防止内存泄漏
@@ -1377,38 +1462,104 @@ export default {
});
},
// 违纪类型确认处理
handleDisciplineTypeConfirm(e) {
if (e && e.value && e.value.length > 0) {
const selectedType = this.disciplineTypeOptions.find(
(item) => item.id === e.value[0].value,
);
if (selectedType) {
this.formData.disciplineTypeId = selectedType.id;
this.formData.disciplineTypeName = selectedType.typeName;
// 自动填入最低扣分
if (selectedType.minScore !== null && selectedType.minScore !== undefined) {
this.formData.totalScore = selectedType.minScore;
uni.showToast({
title: `已自动填入最低扣分:${selectedType.minScore}分`,
icon: 'none',
});
}
}
initDeductionItemsFromData(data) {
if (data?.deductionItems && data.deductionItems.length > 0) {
this.deductionItems = data.deductionItems.map((item, index) => ({
...item,
score: item.score ?? 0,
sortOrder: item.sortOrder ?? index + 1,
}));
} else if (data?.disciplineTypeId) {
this.deductionItems = [
{
disciplineTypeId: data.disciplineTypeId,
disciplineTypeName: data.disciplineTypeName,
score: data.totalScore || 0,
sortOrder: 1,
},
];
} else {
this.deductionItems = [];
}
this.showDisciplineTypePicker = false;
this.recalcDeductionTotal();
},
// 宿舍总分变化处理
handleTotalScoreChange() {
const score = parseFloat(this.formData.totalScore);
if (!isNaN(score) && (score < 0 || score > 100)) {
uni.showToast({ title: '宿舍扣分必须在0-100分之间', icon: 'none' });
this.formData.totalScore = 0;
openDeductionPopup() {
this.tempSelectedTypeIds = this.deductionItems.map((item) => String(item.disciplineTypeId));
this.showDeductionPopup = true;
},
confirmDeductionSelection() {
const currentMap = new Map(
this.deductionItems.map((item) => [String(item.disciplineTypeId), item]),
);
const nextItems = [];
this.tempSelectedTypeIds.forEach((typeId, index) => {
const option = this.disciplineTypeOptions.find((item) => String(item.id) === String(typeId));
if (!option) return;
const existing = currentMap.get(String(typeId));
nextItems.push({
disciplineTypeId: option.id,
disciplineTypeName: option.typeName,
score: existing ? existing.score : Number(option.minScore || 0),
sortOrder: index + 1,
});
});
this.deductionItems = nextItems;
this.recalcDeductionTotal();
this.showDeductionPopup = false;
},
removeDeductionItem(index) {
this.deductionItems.splice(index, 1);
this.recalcDeductionTotal();
},
handleDeductionScoreChange() {
this.deductionItems.forEach((item) => {
let score = parseFloat(item.score);
if (isNaN(score)) {
score = this.getDeductionMin(item);
}
const min = this.getDeductionMin(item);
const max = this.getDeductionMax(item);
if (score < min) score = min;
if (score > max) score = max;
item.score = score;
});
this.recalcDeductionTotal();
if ((this.formData.totalScore || 0) > 100) {
uni.showToast({ title: '合计扣分不能超过100分', icon: 'none' });
}
},
recalcDeductionTotal() {
const total = this.deductionItems.reduce(
(sum, item) => sum + (parseFloat(item.score) || 0),
0,
);
this.formData.totalScore = Number(total.toFixed(2));
},
getDeductionOption(typeId) {
return this.disciplineTypeOptions.find((item) => String(item.id) === String(typeId));
},
getDeductionMin(item) {
const option = this.getDeductionOption(item.disciplineTypeId);
return option?.minScore ?? 0;
},
getDeductionMax(item) {
const option = this.getDeductionOption(item.disciplineTypeId);
return option?.maxScore ?? 100;
},
getDeductionRemark(typeId) {
const option = this.getDeductionOption(typeId);
return option?.remark || '';
},
// 卫生等级确认处理
handleHygieneGradeConfirm(e) {
if (e && e.value && e.value.length > 0) {
@@ -2114,7 +2265,7 @@ export default {
// 获取学生请假信息
getStudentLeave(studentId) {
return this.studentLeaves.get(studentId) || null;
return this.studentLeaves.get(normalizeStudentId(studentId)) || null;
},
// 格式化日期时间
@@ -2141,8 +2292,7 @@ export default {
// 删除违纪记录
removeViolationRecord(studentId, index) {
// 保存要删除的记录信息
this.deleteViolationStudentId = studentId;
this.deleteViolationStudentId = normalizeStudentId(studentId);
this.deleteViolationIndex = index;
// 显示确认对话框
this.showDeleteConfirm = true;
@@ -2159,6 +2309,7 @@ export default {
} else {
this.violationRecords.set(this.deleteViolationStudentId, violations);
}
this.syncStudentScoreFromViolations(this.deleteViolationStudentId);
uni.showToast({ title: '违纪记录删除成功', icon: 'success' });
}
@@ -2178,25 +2329,31 @@ export default {
// 获取学生违纪记录
getStudentViolations(studentId) {
return this.violationRecords.get(studentId) || [];
return this.violationRecords.get(normalizeStudentId(studentId)) || [];
},
// 违纪记录确认
handleViolationConfirm(violationData) {
const studentId = this.currentStudent.studentId;
const studentId = normalizeStudentId(this.currentStudent.studentId);
const violations = this.violationRecords.get(studentId) || [];
if (this.currentViolationIndex >= 0) {
// 编辑模式
if (Array.isArray(violationData)) {
violations.push(...violationData);
uni.showToast({
title: `已添加${violationData.length}条违纪记录`,
icon: 'success',
});
} else if (this.currentViolationIndex >= 0) {
violations[this.currentViolationIndex] = violationData;
uni.showToast({ title: '违纪记录保存成功', icon: 'success' });
} else {
// 新增模式
violations.push(violationData);
uni.showToast({ title: '违纪记录保存成功', icon: 'success' });
}
this.violationRecords.set(studentId, violations);
this.syncStudentScoreFromViolations(studentId);
this.violationDialogVisible = false;
uni.showToast({ title: '违纪记录保存成功', icon: 'success' });
},
// 违纪记录弹窗关闭
@@ -2210,8 +2367,15 @@ export default {
// 表单验证
await this.$refs.formRef.validate();
if ((this.formData.totalScore || 0) > 100) {
uni.showToast({ title: '宿舍合计扣分不能超过100分', icon: 'none' });
return;
}
this.submitLoading = true;
this.syncStudentScoreFromViolations();
// 处理附件数据
const attachmentOssIds = this.attachmentList
.filter((file) => file.status === 'success' && file.ossId)
@@ -2270,6 +2434,7 @@ export default {
// 宿舍整体评分
...this.formData,
deductionItems: this.deductionItems,
// 学生评分数据
studentScores,
@@ -2791,4 +2956,90 @@ export default {
padding: 20rpx 30rpx;
padding-bottom: 40rpx;
}
.deduction-list {
margin: 16rpx 0 24rpx;
padding: 20rpx;
background: #f8f9fb;
border-radius: 12rpx;
}
.deduction-item + .deduction-item {
margin-top: 16rpx;
padding-top: 16rpx;
border-top: 1rpx dashed #e4e7ed;
}
.deduction-item-header {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12rpx;
}
.deduction-name {
font-size: 26rpx;
font-weight: 500;
color: #303133;
min-width: 160rpx;
}
.deduction-score {
font-size: 26rpx;
color: #f56c6c;
font-weight: 600;
}
.deduction-range {
font-size: 22rpx;
color: #909399;
}
.deduction-remark {
margin-top: 8rpx;
font-size: 22rpx;
color: #909399;
line-height: 1.5;
}
.deduction-total {
margin-top: 16rpx;
padding-top: 16rpx;
border-top: 1rpx solid #e4e7ed;
font-size: 26rpx;
color: #606266;
}
.deduction-total .total-value {
color: #f56c6c;
font-weight: 600;
font-size: 30rpx;
}
.deduction-popup {
padding: 24rpx 30rpx 40rpx;
max-height: 70vh;
}
.deduction-popup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.deduction-popup-title {
font-size: 32rpx;
font-weight: 600;
color: #303133;
}
.deduction-popup-action {
font-size: 28rpx;
color: #3c9cff;
}
.deduction-popup-body {
max-height: 55vh;
}
</style>
@@ -36,40 +36,120 @@
</view>
</view>
<!-- 违纪类型选择 -->
<u-form-item label="违纪类型" prop="disciplineTypeId" required>
<view @click="handleDisciplineTypeClick" style="width: 100%">
<u-input
:value="disciplineTypeDisplay"
placeholder="请选择违纪类型"
readonly
suffix-icon="arrow-down"
suffix-icon-style="color: #c8c9cc;"
@click.stop="handleDisciplineTypeClick"
/>
<!-- 违纪类型:编辑模式单选 -->
<template v-if="isEditMode">
<u-form-item label="违纪类型" prop="disciplineTypeId" required>
<view @click="handleDisciplineTypeClick" style="width: 100%">
<u-input
:value="disciplineTypeDisplay"
placeholder="请选择违纪类型"
readonly
suffix-icon="arrow-down"
suffix-icon-style="color: #c8c9cc;"
@click.stop="handleDisciplineTypeClick"
/>
</view>
</u-form-item>
<u-picker
:show="showDisciplineTypePicker"
:columns="disciplineTypeColumns"
keyName="text"
@confirm="handleDisciplineTypeConfirm"
@cancel="showDisciplineTypePicker = false"
@close="showDisciplineTypePicker = false"
></u-picker>
<view
v-if="formData.disciplineTypeId && selectedDisciplineTypeRemark"
class="discipline-type-remark"
>
<u-icon name="info-circle" color="#909399" size="14"></u-icon>
<text class="remark-text">
类型说明:{{ selectedDisciplineTypeRemark }}
</text>
</view>
</u-form-item>
</template>
<!-- 违纪类型选择器 -->
<u-picker
:show="showDisciplineTypePicker"
:columns="disciplineTypeColumns"
keyName="text"
@confirm="handleDisciplineTypeConfirm"
@cancel="showDisciplineTypePicker = false"
@close="showDisciplineTypePicker = false"
></u-picker>
<!-- 违纪类型:新增模式多选 -->
<template v-else>
<u-form-item label="违纪类型">
<view @click="openViolationTypePopup" style="width: 100%">
<u-input
:value="violationSummaryDisplay"
placeholder="请选择违纪类型(可多选)"
readonly
suffix-icon="arrow-down"
suffix-icon-style="color: #c8c9cc;"
style="pointer-events: none"
/>
</view>
</u-form-item>
<!-- 违纪类型说明 -->
<view
v-if="formData.disciplineTypeId && selectedDisciplineTypeRemark"
class="discipline-type-remark"
>
<u-icon name="info-circle" color="#909399" size="14"></u-icon>
<text class="remark-text">
类型说明:{{ selectedDisciplineTypeRemark }}
</text>
</view>
<view v-if="violationItems.length > 0" class="violation-item-list">
<view
v-for="(item, index) in violationItems"
:key="item.disciplineTypeId"
class="violation-item-row"
>
<view class="violation-item-header">
<text class="violation-item-name">{{ item.disciplineTypeName }}</text>
<u-input
v-model="item.scoreDeduction"
type="digit"
placeholder="扣分"
@input="handleViolationScoreChange"
/>
<text class="score-range">
({{ getViolationMin(item) }}-{{ getViolationMax(item) }}分)
</text>
<u-icon
name="close-circle"
color="#f56c6c"
size="18"
@click="removeViolationItem(index)"
></u-icon>
</view>
<view
v-if="getViolationRemark(item.disciplineTypeId)"
class="discipline-type-remark"
>
<text>类型说明:{{ getViolationRemark(item.disciplineTypeId) }}</text>
</view>
</view>
<view class="violation-item-total">
合计扣分:<text class="total-value">{{ violationItemsTotalScore }}</text> 分
</view>
</view>
<u-popup
:show="showViolationTypePopup"
mode="bottom"
round="16"
@close="showViolationTypePopup = false"
>
<view class="violation-type-popup">
<view class="violation-type-popup-header">
<text class="violation-type-popup-title">选择违纪类型</text>
<text
class="violation-type-popup-action"
@click="confirmViolationTypeSelection"
>确定</text>
</view>
<scroll-view scroll-y class="violation-type-popup-body">
<u-checkbox-group v-model="tempSelectedTypeIds" placement="column">
<u-checkbox
v-for="item in allViolationTypes"
:key="item.id"
:name="String(item.id)"
:label="getViolationTypeLabel(item)"
customStyle="margin-bottom: 12px;"
></u-checkbox>
</u-checkbox-group>
</scroll-view>
</view>
</u-popup>
</template>
<!-- 违纪日期 -->
<u-form-item label="违纪日期" prop="disciplineDate" required>
@@ -93,8 +173,13 @@
></u-datetime-picker>
</u-form-item>
<!-- 扣分分值 -->
<u-form-item label="扣分分值" prop="scoreDeduction" required>
<!-- 扣分分值(编辑模式) -->
<u-form-item
v-if="isEditMode"
label="扣分分值"
prop="scoreDeduction"
required
>
<u-input
v-model.number="formData.scoreDeduction"
type="digit"
@@ -104,7 +189,7 @@
</u-form-item>
<!-- 违纪原因 -->
<u-form-item label="违纪原因" prop="disciplineReason" required>
<u-form-item label="违纪原因" prop="disciplineReason">
<u-textarea
v-model="formData.disciplineReason"
placeholder="请输入违纪原因"
@@ -398,11 +483,14 @@ export default {
},
},
],
disciplineReason: [{ required: true, message: '请输入违纪原因', trigger: 'blur' }],
disciplineReason: [],
},
// Picker显示状态
showDisciplineTypePicker: false,
showViolationTypePopup: false,
tempSelectedTypeIds: [],
violationItems: [],
showDatePicker: false,
// 违纪日期
@@ -435,6 +523,23 @@ export default {
};
},
computed: {
isEditMode() {
return !!this.violationData;
},
violationSummaryDisplay() {
if (!this.violationItems.length) return '';
return this.violationItems.map((item) => item.disciplineTypeName).join('、');
},
violationItemsTotalScore() {
const sum = this.violationItems.reduce(
(acc, item) => acc + (parseFloat(item.scoreDeduction) || 0),
0,
);
return Number(sum.toFixed(2));
},
// 违纪类型列(带区域标签)
disciplineTypeColumns() {
return [
@@ -768,8 +873,9 @@ export default {
}
} else {
console.log('违纪记录:新增模式,重置附件列表');
// 重置附件列表
this.attachmentList = [];
this.violationItems = [];
this.tempSelectedTypeIds = [];
}
},
@@ -919,6 +1025,11 @@ export default {
// 违纪类型点击处理
handleDisciplineTypeClick() {
if (!this.isEditMode) {
this.openViolationTypePopup();
return;
}
console.log('=== 违纪类型点击 ===');
console.log('违纪类型数据量:', this.allViolationTypes.length);
console.log('违纪类型数据:', this.allViolationTypes);
@@ -996,6 +1107,100 @@ export default {
this.showDisciplineTypePicker = false;
},
openViolationTypePopup() {
if (this.allViolationTypes.length === 0) {
uni.showToast({
title: '暂无可用的违纪类型',
icon: 'none',
});
return;
}
this.tempSelectedTypeIds = this.violationItems.map((item) => String(item.disciplineTypeId));
this.showViolationTypePopup = true;
},
confirmViolationTypeSelection() {
const currentMap = new Map(
this.violationItems.map((item) => [String(item.disciplineTypeId), item]),
);
const nextItems = [];
this.tempSelectedTypeIds.forEach((typeId) => {
const option = this.allViolationTypes.find((item) => String(item.id) === String(typeId));
if (!option) return;
const existing = currentMap.get(String(typeId));
nextItems.push({
disciplineTypeId: option.id,
disciplineTypeName: option.typeName,
scoreDeduction: existing
? existing.scoreDeduction
: parseFloat(option.minScore) || 0,
});
});
this.violationItems = nextItems;
this.showViolationTypePopup = false;
},
removeViolationItem(index) {
this.violationItems.splice(index, 1);
},
getViolationOption(typeId) {
return this.allViolationTypes.find((item) => String(item.id) === String(typeId));
},
getViolationMin(item) {
const option = this.getViolationOption(item.disciplineTypeId);
return option?.minScore ?? 0;
},
getViolationMax(item) {
const option = this.getViolationOption(item.disciplineTypeId);
return option?.maxScore ?? 100;
},
getViolationRemark(typeId) {
const option = this.getViolationOption(typeId);
return option?.remark || '';
},
handleViolationScoreChange() {
this.violationItems.forEach((item) => {
let score = parseFloat(item.scoreDeduction);
if (isNaN(score)) {
score = this.getViolationMin(item);
}
const min = this.getViolationMin(item);
const max = this.getViolationMax(item);
if (score < min) score = min;
if (score > max) score = max;
item.scoreDeduction = score;
});
},
buildSharedViolationFields() {
const attachmentOssIds = this.attachmentList
.filter((file) => file.status === 'success' && file.ossId)
.map((file) => file.ossId)
.join(',');
return {
studentId: this.formData.studentId,
studentNo: this.formData.studentNo,
studentName: this.formData.studentName,
collegeId: this.formData.collegeId,
collegeName: this.formData.collegeName,
majorId: this.formData.majorId,
majorName: this.formData.majorName,
classId: this.formData.classId,
className: this.formData.className,
disciplineDate: this.formData.disciplineDate,
disciplineReason: this.formData.disciplineReason,
disciplineContent: this.formData.disciplineContent,
attachmentOssIds,
violationSource: this.formData.violationSource || '1',
};
},
// 扣分变化处理
handleScoreChange() {
// 确保转换为数字类型
@@ -1844,18 +2049,55 @@ export default {
// 确定
async handleConfirm() {
try {
// 表单验证
await this.$refs.formRef.validate();
this.confirmLoading = true;
// 处理附件数据
const attachmentOssIds = this.attachmentList
.filter((file) => file.status === 'success' && file.ossId)
.map((file) => file.ossId);
this.formData.attachmentOssIds = attachmentOssIds.join(',');
if (this.isEditMode) {
await this.$refs.formRef.validate();
const attachmentOssIds = this.attachmentList
.filter((file) => file.status === 'success' && file.ossId)
.map((file) => file.ossId);
this.formData.attachmentOssIds = attachmentOssIds.join(',');
this.$emit('confirm', { ...this.formData });
return;
}
this.$emit('confirm', { ...this.formData });
if (!this.formData.disciplineDate) {
uni.showToast({ title: '请选择违纪日期', icon: 'none' });
return;
}
if (this.violationItems.length === 0) {
uni.showToast({ title: '请至少选择一个违纪类型', icon: 'none' });
return;
}
for (const item of this.violationItems) {
const min = this.getViolationMin(item);
const max = this.getViolationMax(item);
const score = parseFloat(item.scoreDeduction);
if (isNaN(score)) {
uni.showToast({
title: `请填写「${item.disciplineTypeName}」扣分`,
icon: 'none',
});
return;
}
if (score < min || score > max) {
uni.showToast({
title: `「${item.disciplineTypeName}」须在${min}-${max}分`,
icon: 'none',
});
return;
}
}
const sharedFields = this.buildSharedViolationFields();
const records = this.violationItems.map((item) => ({
...sharedFields,
disciplineTypeId: item.disciplineTypeId,
disciplineTypeName: item.disciplineTypeName,
scoreDeduction: item.scoreDeduction,
}));
this.$emit('confirm', records);
} catch (error) {
console.error('提交失败:', error);
uni.showToast({ title: '请检查表单填写是否完整', icon: 'none' });
@@ -1915,6 +2157,9 @@ export default {
this.currentTypeMaxScore = 100;
this.disciplineDateDisplay = dayjs().format('YYYY-MM-DD');
this.disciplineDateValue = Number(new Date());
this.violationItems = [];
this.tempSelectedTypeIds = [];
this.showViolationTypePopup = false;
},
},
};
@@ -2200,4 +2445,76 @@ export default {
padding-bottom: 40rpx;
border-top: 2rpx solid #f0f0f0;
}
.violation-item-list {
margin: 0 0 24rpx;
padding: 20rpx;
background: #f8f9fb;
border-radius: 12rpx;
}
.violation-item-row + .violation-item-row {
margin-top: 16rpx;
padding-top: 16rpx;
border-top: 1rpx dashed #e4e7ed;
}
.violation-item-header {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12rpx;
}
.violation-item-name {
font-size: 26rpx;
font-weight: 500;
color: #303133;
min-width: 160rpx;
}
.score-range {
font-size: 22rpx;
color: #909399;
}
.violation-item-total {
margin-top: 16rpx;
padding-top: 16rpx;
border-top: 1rpx solid #e4e7ed;
font-size: 26rpx;
color: #606266;
}
.violation-item-total .total-value {
color: #f56c6c;
font-weight: 600;
}
.violation-type-popup {
padding: 24rpx 30rpx 40rpx;
max-height: 70vh;
}
.violation-type-popup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.violation-type-popup-title {
font-size: 32rpx;
font-weight: 600;
color: #303133;
}
.violation-type-popup-action {
font-size: 28rpx;
color: #3c9cff;
}
.violation-type-popup-body {
max-height: 55vh;
}
</style>
@@ -0,0 +1,10 @@
/** 雪花ID统一转字符串,避免 JS Number 精度丢失导致 Map/接口匹配失败 */
export function normalizeStudentId(studentId) {
if (studentId === null || studentId === undefined || studentId === '') {
return '';
}
return String(studentId);
}
/** 通用雪花ID归一化(检查ID、学生ID等) */
export const normalizeId = normalizeStudentId;
@@ -0,0 +1,7 @@
/** 计算学生有效违纪记录的扣分合计(已撤销 status=2 不计入) */
export function calcStudentViolationScoreTotal(violations) {
const total = (violations || [])
.filter((item) => item && item.status !== '2')
.reduce((sum, item) => sum + (parseFloat(String(item.scoreDeduction ?? 0)) || 0), 0);
return parseFloat(total.toFixed(2));
}
+12 -5
View File
@@ -270,6 +270,7 @@
import { request } from '@/common/request.js';
import { getDict, getDictLabel, getDictTagType } from '@/common/dict.js';
import SingleRoomCheck from './components/SingleRoomCheck.vue';
import { normalizeId } from './utils/studentId.js';
export default {
name: 'DormHygieneCheck',
@@ -544,7 +545,7 @@ export default {
// 加载检查记录详情
const res = await request({
url: `/dormHygieneCheck/dormHygieneCheck/${item.id}`,
url: `/dormHygieneCheck/dormHygieneCheck/${normalizeId(item.id)}`,
method: 'GET',
});
@@ -552,7 +553,10 @@ export default {
if (res && res.code === 200 && res.data) {
this.currentViewItem = item;
this.currentViewData = res.data;
this.currentViewData = {
...res.data,
id: normalizeId(res.data.id),
};
// 构造房间信息
this.currentViewRoomInfo = {
@@ -605,7 +609,7 @@ export default {
// 加载检查记录详情
const res = await request({
url: `/dormHygieneCheck/dormHygieneCheck/${item.id}`,
url: `/dormHygieneCheck/dormHygieneCheck/${normalizeId(item.id)}`,
method: 'GET',
});
@@ -613,7 +617,10 @@ export default {
if (res && res.code === 200 && res.data) {
this.currentEditItem = item;
this.currentEditData = res.data;
this.currentEditData = {
...res.data,
id: normalizeId(res.data.id),
};
// 构造房间信息
this.currentRoomInfo = {
@@ -708,7 +715,7 @@ export default {
uni.showLoading({ title: '删除中...' });
const res = await request({
url: `/dormHygieneCheck/dormHygieneCheck/${item.id}`,
url: `/dormHygieneCheck/dormHygieneCheck/${normalizeId(item.id)}`,
method: 'DELETE',
});
+61 -1
View File
@@ -1309,6 +1309,52 @@ export default {
}
},
normalizeGender(gender) {
if (gender === null || gender === undefined || gender === '') {
return undefined;
}
const value = String(gender);
if (value === '0' || value === '男') {
return '0';
}
if (value === '1' || value === '女') {
return '1';
}
return value;
},
mergeGenderFromRoomStudents(students, roomStudents) {
if (!students.length || !roomStudents.length) {
return students;
}
const genderByStudentId = {};
const genderByStudentNo = {};
roomStudents.forEach((student) => {
const gender = this.normalizeGender(student.gender);
if (!gender) {
return;
}
if (student.studentId !== undefined && student.studentId !== null) {
genderByStudentId[String(student.studentId)] = gender;
}
if (student.studentNo) {
genderByStudentNo[student.studentNo] = gender;
}
});
return students.map((student) => {
if (this.normalizeGender(student.gender)) {
return { ...student, gender: this.normalizeGender(student.gender) };
}
const gender =
(student.studentId !== undefined && student.studentId !== null
? genderByStudentId[String(student.studentId)]
: undefined) || (student.studentNo ? genderByStudentNo[student.studentNo] : undefined);
return gender ? { ...student, gender } : student;
});
},
// 加载房间学生信息
async loadRoomStudents() {
if (!this.formData.roomId) {
@@ -1329,8 +1375,9 @@ export default {
studentScoresRes.data &&
studentScoresRes.data.length > 0
) {
this.roomStudents = studentScoresRes.data.map((item) => ({
let students = studentScoresRes.data.map((item) => ({
...item,
gender: this.normalizeGender(item.gender),
student_score: item.studentScore || null,
attendance_status: item.attendanceStatus || '1',
student_grade: item.studentGrade || 'A',
@@ -1339,6 +1386,19 @@ export default {
bedCode: item.bedCode,
bedName: item.bedCode,
}));
if (students.some((student) => !student.gender) && this.formData.roomId) {
const roomRes = await request({
url: '/dormBedStudent/dormBedStudent/getRoomStudents',
method: 'GET',
params: { roomId: this.formData.roomId },
});
if (roomRes.code === 200 && roomRes.data) {
students = this.mergeGenderFromRoomStudents(students, roomRes.data);
}
}
this.roomStudents = students;
return;
}
}