fix(leave/dorm): 打回重提、审批摘要与待办标题优化

修复请假/销假打回后空 flowCode 导致启动失败;审批弹窗展示请假摘要并去掉附件;待办标题短显请假/销假;点名 todaySummary 与楼栋关联脚本一并同步。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
“wuzhen”andCursor committed 2026-10-21 11:24:48 +08:00
1 parent de223a1f5a
commit 15ed3ec9fb
20 files changed
+1136 -320

No files matched your search

+4 -3
View File
@@ -8,11 +8,12 @@ export const listDormRollCall = (query) => {
});
};
/** 当前账号当天点名汇总 */
export const getTodayRollCallSummary = () => {
/** 当天点名人数汇总:不传=全校,传 buildingId=本楼 */
export const getTodayRollCallSummary = (buildingId) => {
return request({
url: '/dormRollCall/dormRollCall/todaySummary',
method: 'GET'
method: 'GET',
params: buildingId ? { buildingId } : undefined
});
};
+210 -86
View File
@@ -17,47 +17,31 @@
</view>
<scroll-view scroll-y class="content" :show-scrollbar="false">
<!-- 消息提醒 -->
<!-- <view class="form-item">
<text class="label">消息提醒</text>
<u-checkbox-group
v-model="form.messageType"
@change="handleMessageTypeChange"
>
<view class="checkbox-row">
<u-checkbox
name="1"
:checked="true"
:disabled="true"
activeColor="#2979ff"
>
站内信
</u-checkbox>
<u-checkbox name="2" activeColor="#2979ff">邮件</u-checkbox>
<u-checkbox name="3" activeColor="#2979ff">短信</u-checkbox>
<!-- 请假业务摘要(待办直接审批时展示) -->
<view v-if="showLeaveSummary" class="form-item leave-summary">
<text class="label">请假信息</text>
<view class="leave-summary-card">
<view class="leave-summary-row">
<text class="leave-summary-label">请假类型</text>
<text class="leave-summary-value">{{ leaveInfo.leaveTypeName || '-' }}</text>
</view>
</u-checkbox-group>
</view> -->
<!-- 附件上传 -->
<view class="form-item">
<text class="label">附件</text>
<FileUpload
v-model="form.fileId"
:file-type="[
'png',
'jpg',
'jpeg',
'doc',
'docx',
'xlsx',
'xls',
'ppt',
'txt',
'pdf',
]"
:file-size="20"
/>
<view class="leave-summary-row">
<text class="leave-summary-label">开始时间</text>
<text class="leave-summary-value">{{ formatLeaveDateTime(leaveInfo.startTime) }}</text>
</view>
<view class="leave-summary-row">
<text class="leave-summary-label">结束时间</text>
<text class="leave-summary-value">{{ formatLeaveDateTime(leaveInfo.endTime) }}</text>
</view>
<view class="leave-summary-row">
<text class="leave-summary-label">是否离校</text>
<text class="leave-summary-value">{{ formatLeaveSchool(leaveInfo.isLeaveSchool) }}</text>
</view>
<view class="leave-summary-row leave-summary-row--reason">
<text class="leave-summary-label">请假原因</text>
<text class="leave-summary-value">{{ leaveInfo.leaveReason || '-' }}</text>
</view>
</view>
</view>
<!-- 抄送 -->
@@ -248,14 +232,14 @@
/>
</u-popup>
<!-- 退回弹窗 -->
<!-- 退回弹窗:必须高于审批弹窗(20090),否则打开后被盖住表现为“点了没反应” -->
<u-popup
:show="backVisible"
mode="center"
width="80%"
height="70%"
border-radius="12"
z-index="20080"
:z-index="20100"
@close="closeBackDialog"
:custom-style="{ width: '90vw' }"
>
@@ -365,14 +349,14 @@
</view>
</u-popup>
<!-- 减签弹窗 -->
<!-- 减签弹窗:高于审批弹窗 -->
<u-popup
:show="deleteSignatureVisible"
mode="center"
width="90%"
height="60%"
border-radius="12"
z-index="20080"
:z-index="20100"
@close="deleteSignatureVisible = false"
>
<view class="delete-signature-container">
@@ -480,6 +464,7 @@ export default {
title: '提示',
},
deleteSignatureVisible: false,
leaveInfo: null,
form: {
taskId: undefined,
message: undefined,
@@ -499,6 +484,11 @@ export default {
},
};
},
computed: {
showLeaveSummary() {
return !!(this.leaveInfo && (this.leaveInfo.leaveTypeName || this.leaveInfo.leaveReason));
},
},
methods: {
// 打开弹窗
async openDialog(id) {
@@ -507,6 +497,7 @@ export default {
this.form.fileId = undefined;
this.taskId = id;
this.form.message = undefined;
this.leaveInfo = null;
this.visible = true;
this.loading = true;
this.buttonDisabled = true;
@@ -539,6 +530,8 @@ export default {
if (nextData.code === 200) {
this.nestNodeList = nextData.data;
}
await this.loadLeaveSummaryIfNeeded();
}
} catch (error) {
console.error('获取任务信息失败:', error);
@@ -551,12 +544,56 @@ export default {
}
},
isLeaveFlowTask() {
const flowCode = (this.task && this.task.flowCode) || '';
const formPath = (this.task && this.task.formPath) || '';
return flowCode === 'hzdyjs-qj' || formPath.indexOf('xsqj') !== -1 || formPath.indexOf('studentLeave') !== -1;
},
async loadLeaveSummaryIfNeeded() {
if (!this.isLeaveFlowTask() || !this.task.businessId) {
this.leaveInfo = null;
return;
}
try {
const response = await request({
url: `/studentLeave/studentLeave/${this.task.businessId}`,
method: 'GET',
});
if (response.code === 200 && response.data) {
this.leaveInfo = response.data;
} else {
this.leaveInfo = null;
}
} catch (error) {
console.warn('加载请假摘要失败(不影响审批):', error);
this.leaveInfo = null;
}
},
formatLeaveDateTime(value) {
if (!value) return '-';
const date = value instanceof Date ? value : new Date(value);
if (Number.isNaN(date.getTime())) {
return String(value);
}
const pad = (n) => (n < 10 ? `0${n}` : `${n}`);
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
},
formatLeaveSchool(value) {
if (value === '1' || value === 1) return '离校';
if (value === '0' || value === 0) return '不离校';
return value || '-';
},
// 关闭弹窗
cancel() {
this.visible = false;
this.buttonDisabled = false;
this.nickName = {};
this.form.assigneeMap = {};
this.leaveInfo = null;
this.$emit('cancelCallback');
},
@@ -648,29 +685,34 @@ export default {
}
},
// 退回弹窗打开
// 点击退回:仅一个可退回节点时直接确认提交;多个节点时再打开选择弹窗
async handleBackProcessOpen() {
this.backForm = {};
this.backForm.messageType = ['1'];
this.backVisible = true;
this.backLoading = true;
this.backButtonDisabled = true;
if (!this.task || !this.task.definitionId || !this.task.nodeCode) {
uni.showToast({ title: '任务信息不完整,无法退回', icon: 'none' });
return;
}
uni.showLoading({ title: '加载中', mask: true });
let nodes = [];
try {
const response = await request({
url: `/workflow/task/getBackTaskNode/${this.task.definitionId}/${this.task.nodeCode}`,
method: 'GET',
});
if (response.code === 200) {
this.taskNodeList = response.data;
this.backLoading = false;
this.backButtonDisabled = false;
if (this.taskNodeList.length > 0) {
this.selectedBackNodeIndex = 0;
this.backForm.nodeCode = this.taskNodeList[0].nodeCode;
this.selectedBackNodeName = this.taskNodeList[0].nodeName;
}
if (response.code !== 200) {
uni.showToast({
title: response.msg || '获取退回节点失败',
icon: 'none',
});
return;
}
nodes = response.data || [];
this.taskNodeList = nodes;
if (nodes.length === 0) {
uni.showToast({ title: '暂无可退回节点', icon: 'none' });
return;
}
} catch (error) {
console.error('获取退回节点失败:', error);
@@ -678,7 +720,44 @@ export default {
title: '获取退回节点失败',
icon: 'none',
});
return;
} finally {
// 必须先关掉 loading,再弹确认框;否则 mask 会挡住「确定/取消」
uni.hideLoading();
}
// 常见请假场景:只有「申请人」一个节点,直接确认,不再进二级驳回页
if (nodes.length === 1) {
const node = nodes[0];
const opinion = (this.form && this.form.message) || '';
const result = await this.showConfirm(
`确认退回到【${node.nodeName || '申请人'}】?`,
);
if (!result.confirm) {
return;
}
await this.doBackProcess({
taskId: this.taskId,
nodeCode: node.nodeCode,
message: opinion,
messageType: ['1'],
fileId: this.form && this.form.fileId,
});
return;
}
// 多个可退回节点:打开选择弹窗
this.backForm = {
messageType: ['1'],
message: (this.form && this.form.message) || '',
fileId: this.form && this.form.fileId,
};
this.selectedBackNodeIndex = 0;
this.backForm.nodeCode = nodes[0].nodeCode;
this.selectedBackNodeName = nodes[0].nodeName;
this.backButtonDisabled = false;
this.backLoading = false;
this.backVisible = true;
},
// 确认选择驳回节点
@@ -703,43 +782,50 @@ export default {
this.selectedBackNodeName = '';
},
// 退回流程
// 退回弹窗内点「确认」
async handleBackProcess() {
if (!this.backForm || !this.backForm.nodeCode) {
uni.showToast({ title: '请选择驳回节点', icon: 'none' });
return;
}
this.backForm.taskId = this.taskId;
const result = await this.showConfirm('是否确认驳回?');
if (!result.confirm) {
return;
}
await this.doBackProcess(this.backForm);
},
// 实际调用退回接口并关闭弹窗、刷新列表
async doBackProcess(payload) {
this.loading = true;
this.backLoading = true;
this.backButtonDisabled = true;
try {
const result = await this.showConfirm('是否确认驳回?');
if (result.confirm) {
this.loading = true;
this.backLoading = true;
this.backButtonDisabled = true;
const response = await request({
url: '/workflow/task/backProcess',
method: 'POST',
data: payload,
});
const response = await request({
url: '/workflow/task/backProcess',
method: 'POST',
data: this.backForm,
if (response.code === 200) {
this.visible = false;
this.closeBackDialog();
this.$emit('submit-callback');
uni.showToast({
title: '退回成功',
icon: 'success',
});
} else {
uni.showToast({
title: response.msg || '退回失败',
icon: 'none',
});
if (response.code === 200) {
this.visible = false;
this.closeBackDialog();
console.log('驳回成功');
this.$emit('submit-callback');
uni.showToast({
title: '操作成功',
icon: 'success',
});
} else {
uni.showToast({
title: response.msg || '操作失败',
icon: 'none',
});
}
}
} catch (error) {
console.error('退回失败:', error);
uni.showToast({
title: '操作失败',
title: '退回失败',
icon: 'none',
});
} finally {
@@ -1168,6 +1254,44 @@ export default {
margin-bottom: 16rpx;
}
.leave-summary-card {
background: #f7f9fc;
border: 1rpx solid #e8eef7;
border-radius: 12rpx;
padding: 8rpx 24rpx;
}
.leave-summary-row {
display: flex;
align-items: flex-start;
padding: 16rpx 0;
border-bottom: 1rpx solid #edf1f7;
}
.leave-summary-row:last-child {
border-bottom: none;
}
.leave-summary-label {
width: 160rpx;
flex-shrink: 0;
font-size: 26rpx;
color: #667085;
line-height: 1.5;
}
.leave-summary-value {
flex: 1;
font-size: 26rpx;
color: #1f2937;
line-height: 1.5;
word-break: break-all;
}
.leave-summary-row--reason .leave-summary-value {
white-space: pre-wrap;
}
.checkbox-row {
display: flex;
gap: 40rpx;
+63 -1
View File
@@ -74,7 +74,10 @@
<view class="card-header">
<view class="task-title">
<text class="title-index">{{ index + 1 }}.</text>
{{ item.flowName || '-' }}
<text
class="title-short"
:class="getTaskTitleClass(item)"
>{{ getTaskShortTitle(item) }}</text>
</view>
<view class="task-status">
<u-tag
@@ -341,6 +344,48 @@ export default {
},
// 获取流程状态显示文本
/**
* 卡片标题短文案:请假 / 销假(避免「学生请假流程-杭州第一技师」过长)
*/
getTaskShortTitle(item) {
const kind = this.resolveLeaveTaskKind(item);
if (kind === 'leave') return '请假';
if (kind === 'cancel') return '销假';
return (item && item.flowName) || '-';
},
getTaskTitleClass(item) {
const kind = this.resolveLeaveTaskKind(item);
if (kind === 'leave') return 'title-leave';
if (kind === 'cancel') return 'title-cancel';
return '';
},
resolveLeaveTaskKind(item) {
if (!item) return '';
const code = String(item.flowCode || '').toLowerCase();
const name = String(item.flowName || '');
const category = String(item.categoryName || '');
// 销假优先(名称/编码里同时含「请假」字样时避免误判)
if (
code.includes('xj') ||
code.includes('cancel') ||
name.includes('销假') ||
category.includes('销假')
) {
return 'cancel';
}
if (
code.includes('qj') ||
code.includes('leave') ||
name.includes('请假') ||
category.includes('请假')
) {
return 'leave';
}
return '';
},
getFlowStatusText(status) {
if (!this.flowStatusDict || this.flowStatusDict.length === 0) {
// 字典未加载时的降级处理
@@ -870,6 +915,7 @@ export default {
color: #000000;
font-weight: bold;
margin-right: 8rpx;
font-size: 36rpx;
}
.task-title {
@@ -877,6 +923,22 @@ export default {
font-weight: bold;
color: #303133;
flex: 1;
display: flex;
align-items: center;
}
.title-short {
font-size: 40rpx;
font-weight: 700;
line-height: 1.2;
}
.title-leave {
color: #2979ff;
}
.title-cancel {
color: #e6a23c;
}
.task-status {
@@ -1624,18 +1624,15 @@ export default {
return;
}
// 正式提交:选流程或直接发起
if (!this.selectedFlowCode) {
// 首次触发,弹出流程选择
this.selectedFlowCode = this.flowCodeOptions[0].value;
this.showFlowSelectionPopup = true;
return;
}
// 正式提交:确保有 flowCode(打回/撤销重提不再卡在空编码)
this.ensureSelectedFlowCode();
await this.startWorkflow();
} catch (e) {
console.error('提交失败:', e);
uni.showToast({ title: '提交失败', icon: 'none' });
uni.showToast({
title: (e && (e.msg || e.message)) || '提交失败',
icon: 'none',
});
} finally {
this.buttonLoading = false;
}
@@ -1653,8 +1650,19 @@ export default {
await this.startWorkflow();
},
ensureSelectedFlowCode() {
if (this.selectedFlowCode) {
return this.selectedFlowCode;
}
this.selectedFlowCode =
(this.flowCodeOptions && this.flowCodeOptions[0] && this.flowCodeOptions[0].value) ||
'hzdyjs-xj';
return this.selectedFlowCode;
},
async startWorkflow() {
try {
this.ensureSelectedFlowCode();
// 获取班主任ID
const classTeacherResp = await request({
url: `/studentMsg/studentMsg/getClassTeacherId/${this.leaveInfo.studentId}`,
@@ -1678,10 +1686,20 @@ export default {
if (workflowResponse.code === 200) {
this.$refs.submitVerifyRef.openDialog(workflowResponse.data.taskId);
} else {
uni.showToast({
title: (workflowResponse && workflowResponse.msg) || '启动工作流失败',
icon: 'none',
duration: 2500,
});
}
} catch (error) {
console.error('启动工作流失败:', error);
uni.showToast({ title: '启动工作流失败', icon: 'none' });
uni.showToast({
title: (error && (error.msg || error.message)) || '启动工作流失败',
icon: 'none',
duration: 2500,
});
}
},
@@ -1072,6 +1072,13 @@ export default {
const leaveInfo = response.data;
Object.assign(this.form, leaveInfo);
this.form.status = leaveInfo.approvalStatus;
// 打回/撤销/草稿打开编辑时预填流程编码,避免重提时空 flowCode
if (
!this.selectedFlowCode &&
['draft', 'back', 'cancel'].includes(leaveInfo.approvalStatus)
) {
this.ensureSelectedFlowCode(leaveInfo);
}
await this.ensureApprovalCapability(id);
// 同步时间值到选择器
@@ -3106,11 +3113,8 @@ export default {
'this.needShowFlowSelection(response.data) =',
this.needShowFlowSelection(response.data),
);
// 直接使用第一个流程代码,无需显示选择弹窗
if (this.needShowFlowSelection(response.data)) {
this.selectedFlowCode = this.flowCodeOptions[0].value; // 默认选择第一个
console.log('selectedFlowCode', this.selectedFlowCode);
}
// 草稿/新建/打回/撤销重提:都必须带上有效 flowCode(后端入参曾因此校验失败)
this.ensureSelectedFlowCode(response.data);
// 启动工作流
await this.startWorkflow(response.data);
@@ -3220,6 +3224,8 @@ export default {
// 启动工作流
async startWorkflow(data) {
try {
this.ensureSelectedFlowCode(data);
// 获取班主任ID、学院审批人、二级学院分管领导
const [classTeacherResponse, collegeApproversResponse, collegeLeadersResponse] = await Promise.all([
request({
@@ -3284,13 +3290,23 @@ export default {
if (workflowResponse.code === 200) {
this.$refs.submitVerifyRef.openDialog(workflowResponse.data.taskId);
} else {
uni.showToast({
title: (workflowResponse && workflowResponse.msg) || '启动工作流失败',
icon: 'none',
duration: 2500,
});
}
} catch (error) {
console.error('启动工作流失败:', error);
// request 拦截器可能已 toast 过;这里优先展示后端真实原因,避免盖成笼统文案
const msg =
(error && (error.msg || error.message)) ||
'启动工作流失败';
uni.showToast({
title: '启动工作流失败',
title: msg,
icon: 'none',
duration: 2000,
duration: 2500,
});
}
},
@@ -3356,6 +3372,26 @@ export default {
);
},
/**
* 确保提交前一定有 flowCode。
* 打回(back)/撤销(cancel)重提时原先不会走流程选择,selectedFlowCode 为空,
* 导致 /workflow/task/startWorkFlow 入参校验失败并提示「启动工作流失败」。
*/
ensureSelectedFlowCode(data) {
if (this.selectedFlowCode) {
return this.selectedFlowCode;
}
const defaultCode =
(this.flowCodeOptions && this.flowCodeOptions[0] && this.flowCodeOptions[0].value) ||
'hzdyjs-qj';
this.selectedFlowCode = defaultCode;
console.log('ensureSelectedFlowCode', {
approvalStatus: data && data.approvalStatus,
selectedFlowCode: this.selectedFlowCode,
});
return this.selectedFlowCode;
},
// 提交回调
handleSubmitCallback() {
console.log('handleSubmitCallback 回调成功');
+38 -13
View File
@@ -63,17 +63,20 @@
<!-- 当天点名汇总 -->
<view class="today-summary-card">
<view class="summary-header">
<text class="summary-title">今日点名汇总</text>
<view class="summary-title-wrap">
<text class="summary-title">今日点名汇总</text>
<text class="summary-scope">全校</text>
</view>
<text class="summary-date">{{ todayLabel }}</text>
</view>
<view class="summary-grid">
<view class="summary-item">
<text class="summary-num">{{ todaySummary.checkedRoomCount || 0 }}</text>
<text class="summary-label">已点名寝室</text>
<text class="summary-num">{{ todaySummary.totalStudentCount || 0 }}</text>
<text class="summary-label">总人数</text>
</view>
<view class="summary-item">
<text class="summary-num">{{ todaySummary.uncheckedRoomCount || 0 }}</text>
<text class="summary-label">未点名寝室</text>
<text class="summary-num">{{ todaySummary.checkedStudentCount || 0 }}</text>
<text class="summary-label">已点名人数</text>
</view>
<view class="summary-item clickable" @click="openDetailPopup('leave')">
<text class="summary-num summary-num--leave">{{ todaySummary.leaveCount || 0 }}</text>
@@ -243,8 +246,8 @@ export default {
boxSizing: 'border-box',
},
todaySummary: {
checkedRoomCount: 0,
uncheckedRoomCount: 0,
totalStudentCount: 0,
checkedStudentCount: 0,
leaveCount: 0,
holidayStayCount: 0,
absentCount: 0,
@@ -281,17 +284,23 @@ export default {
},
onShow() {
this.loadBuildingOptions();
this.getList();
this.loadTodaySummary();
this.refreshPageData();
},
methods: {
/** 列表 + 全校汇总一起刷新 */
refreshPageData() {
this.getList();
this.loadTodaySummary();
},
async loadTodaySummary() {
try {
// 图1:始终全校汇总,不按筛选楼栋收窄
const res = await getTodayRollCallSummary();
if (res && res.code === 200 && res.data) {
this.todaySummary = {
checkedRoomCount: res.data.checkedRoomCount || 0,
uncheckedRoomCount: res.data.uncheckedRoomCount || 0,
totalStudentCount: res.data.totalStudentCount || 0,
checkedStudentCount: res.data.checkedStudentCount || 0,
leaveCount: res.data.leaveCount || 0,
holidayStayCount: res.data.holidayStayCount || 0,
absentCount: res.data.absentCount || 0,
@@ -344,13 +353,13 @@ export default {
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
this.refreshPageData();
},
resetQuery() {
this.queryParams = { pageNum: 1, pageSize: 20, batchNo: '', buildingId: '' };
this.selectedBuildingName = '';
this.getList();
this.refreshPageData();
},
handleBuildingConfirm(e) {
@@ -359,6 +368,9 @@ export default {
if (selected) {
this.queryParams.buildingId = selected.value;
this.selectedBuildingName = selected.text;
// 选楼后立即刷新该楼今日汇总
this.queryParams.pageNum = 1;
this.refreshPageData();
}
this.showBuildingPicker = false;
},
@@ -462,15 +474,28 @@ export default {
align-items: center;
margin-bottom: 20rpx;
.summary-title-wrap {
display: flex;
flex-direction: column;
gap: 4rpx;
min-width: 0;
}
.summary-title {
font-size: 30rpx;
font-weight: 600;
color: #303133;
}
.summary-scope {
font-size: 22rpx;
color: #909399;
}
.summary-date {
font-size: 24rpx;
color: #909399;
flex-shrink: 0;
}
}
+180 -8
View File
@@ -40,8 +40,17 @@
<view class="rooms-header">
<text class="rooms-title">宿舍房间列表</text>
<view class="rooms-stats">
<text>总: {{ rooms.length }}</text>
<text style="margin-left: 16rpx">已点名: {{ completedRooms.length }}</text>
<text>总人数: {{ buildingSummary.totalStudentCount || 0 }}</text>
<text>已点: {{ buildingSummary.checkedStudentCount || 0 }}</text>
<text class="stat-clickable" @click.stop="openBuildingDetail('leave')">
请假: {{ buildingSummary.leaveCount || 0 }}
</text>
<text class="stat-clickable" @click.stop="openBuildingDetail('holidayStay')">
留宿: {{ buildingSummary.holidayStayCount || 0 }}
</text>
<text class="stat-clickable" @click.stop="openBuildingDetail('absent')">
异常: {{ buildingSummary.absentCount || 0 }}
</text>
</view>
</view>
@@ -110,13 +119,49 @@
</scroll-view>
</view>
</u-popup>
<!-- 本楼请假/留宿/异常名单 -->
<u-popup
:show="detailPopupVisible"
mode="bottom"
:round="16"
:closeable="true"
:safe-area-inset-bottom="true"
@close="detailPopupVisible = false"
>
<view class="detail-popup">
<view class="detail-popup-title">{{ detailPopupTitle }}</view>
<scroll-view scroll-y class="detail-popup-scroll">
<view v-if="detailPopupList.length === 0" class="detail-empty">
<u-empty text="暂无明细" mode="data"></u-empty>
</view>
<view v-for="(row, index) in detailPopupList" :key="row.studentId || index" class="detail-row">
<view class="detail-row-main">
<text class="detail-name">{{ row.studentName || '-' }}</text>
<text class="detail-no">{{ row.studentNo || '-' }}</text>
</view>
<view class="detail-row-sub">
<text>{{ row.roomName || '-' }}</text>
<text v-if="row.bedCode"> · {{ row.bedCode }}床</text>
</view>
<view class="detail-row-sub" v-if="row.className">班级:{{ row.className }}</view>
<view class="detail-row-sub" v-if="detailPopupType === 'leave' && row.leaveTypeName">
请假类型:{{ row.leaveTypeName }}
</view>
<view class="detail-row-sub" v-if="detailPopupType === 'holidayStay' && row.holidayType">
假期类型:{{ row.holidayType }}
</view>
</view>
</scroll-view>
</view>
</u-popup>
</view>
</template>
<script>
import { mapState } from 'vuex';
import { request } from '@/common/request.js';
import { generateBatchNo, getBuildingRoomsStatus } from '@/api/dormRollCall.js';
import { generateBatchNo, getBuildingRoomsStatus, getTodayRollCallSummary } from '@/api/dormRollCall.js';
import dayjs from 'dayjs';
import RoomRollCallPopup from './components/RoomRollCallPopup.vue';
@@ -149,6 +194,19 @@ export default {
buildingOptions: [],
rooms: [],
completedRooms: [], // 已完成点名的房间ID列表
buildingSummary: {
totalStudentCount: 0,
checkedStudentCount: 0,
leaveCount: 0,
holidayStayCount: 0,
absentCount: 0,
leaveDetails: [],
holidayStayDetails: [],
absentDetails: [],
},
detailPopupVisible: false,
detailPopupType: '',
detailPopupList: [],
currentRoom: null,
popupHeight: 600,
@@ -163,6 +221,15 @@ export default {
buildingColumns() {
return [this.buildingOptions.map((item) => ({ text: item.buildingName, value: item.id }))];
},
detailPopupTitle() {
const prefix = this.formData.buildingName || '本楼';
const map = {
leave: `${prefix} · 请假名单`,
holidayStay: `${prefix} · 假期留宿名单`,
absent: `${prefix} · 异常名单`,
};
return map[this.detailPopupType] || '明细';
},
},
onLoad() {
this.initPage();
@@ -245,6 +312,7 @@ export default {
// 标记已点名房间
this.completedRooms = res.data.filter((r) => r.isCompleted).map((r) => r.roomId);
}
await this.loadBuildingSummary();
} catch (error) {
console.error('加载房间失败:', error);
uni.showToast({ title: '加载房间失败', icon: 'none' });
@@ -253,6 +321,39 @@ export default {
}
},
async loadBuildingSummary() {
if (!this.formData.buildingId) return;
try {
const res = await getTodayRollCallSummary(this.formData.buildingId);
if (res && res.code === 200 && res.data) {
this.buildingSummary = {
totalStudentCount: res.data.totalStudentCount || 0,
checkedStudentCount: res.data.checkedStudentCount || 0,
leaveCount: res.data.leaveCount || 0,
holidayStayCount: res.data.holidayStayCount || 0,
absentCount: res.data.absentCount || 0,
leaveDetails: res.data.leaveDetails || [],
holidayStayDetails: res.data.holidayStayDetails || [],
absentDetails: res.data.absentDetails || [],
};
}
} catch (error) {
console.error('加载本楼点名汇总失败:', error);
}
},
openBuildingDetail(type) {
this.detailPopupType = type;
if (type === 'leave') {
this.detailPopupList = this.buildingSummary.leaveDetails || [];
} else if (type === 'holidayStay') {
this.detailPopupList = this.buildingSummary.holidayStayDetails || [];
} else {
this.detailPopupList = this.buildingSummary.absentDetails || [];
}
this.detailPopupVisible = true;
},
isRoomCompleted(room) {
return this.completedRooms.includes(room.roomId);
},
@@ -270,6 +371,7 @@ export default {
this.rooms = [];
this.completedRooms = [];
this.hasLoadedRooms = false;
this.resetBuildingSummary();
this.loadBuildingOptions(campus.id);
}
}
@@ -294,11 +396,25 @@ export default {
this.rooms = [];
this.completedRooms = [];
this.hasLoadedRooms = false;
this.resetBuildingSummary();
}
}
this.showBuildingPicker = false;
},
resetBuildingSummary() {
this.buildingSummary = {
totalStudentCount: 0,
checkedStudentCount: 0,
leaveCount: 0,
holidayStayCount: 0,
absentCount: 0,
leaveDetails: [],
holidayStayDetails: [],
absentDetails: [],
};
},
handleRoomClick(room) {
this.currentRoom = room;
this.showRoomPopup = true;
@@ -325,6 +441,7 @@ export default {
uni.showToast({ title: `${room?.roomName || ''} 点名完成`, icon: 'success' });
this.showRoomPopup = false;
this.currentRoom = null;
this.loadBuildingSummary();
},
handlePopupClose() {
@@ -387,8 +504,9 @@ export default {
.rooms-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: column;
align-items: flex-start;
gap: 12rpx;
margin-bottom: 20rpx;
}
@@ -399,11 +517,65 @@ export default {
}
.rooms-stats {
width: 100%;
font-size: 22rpx;
color: #606266;
display: flex;
flex-wrap: wrap;
gap: 8rpx 16rpx;
.stat-clickable {
color: #0051d9;
text-decoration: underline;
}
}
.detail-popup {
padding: 32rpx 24rpx 24rpx;
max-height: 70vh;
}
.detail-popup-title {
font-size: 32rpx;
font-weight: 600;
color: #303133;
margin-bottom: 20rpx;
}
.detail-popup-scroll {
max-height: 58vh;
}
.detail-empty {
padding: 40rpx 0;
}
.detail-row {
padding: 20rpx 0;
border-bottom: 1rpx solid #ebeef5;
}
.detail-row-main {
display: flex;
justify-content: space-between;
margin-bottom: 8rpx;
}
.detail-name {
font-size: 28rpx;
font-weight: 600;
color: #303133;
}
.detail-no {
font-size: 24rpx;
color: #909399;
display: flex;
flex-wrap: nowrap;
white-space: nowrap;
}
.detail-row-sub {
font-size: 24rpx;
color: #606266;
line-height: 1.6;
}
.hint-text {